libjay 0.2.0__tar.gz → 0.3.0__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 (49) hide show
  1. {libjay-0.2.0 → libjay-0.3.0}/Cargo.lock +6 -5
  2. {libjay-0.2.0 → libjay-0.3.0}/Cargo.toml +1 -1
  3. {libjay-0.2.0 → libjay-0.3.0}/PKG-INFO +1 -1
  4. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/array.rs +183 -9
  5. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/complex.rs +19 -2
  6. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/device/codegen.rs +28 -1
  7. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/device/mod.rs +11 -3
  8. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/dtype.rs +9 -0
  9. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/error.rs +13 -0
  10. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/explain.rs +40 -3
  11. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/fmt.rs +238 -20
  12. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/frontend/apl.rs +619 -143
  13. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/frontend/j.rs +108 -37
  14. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/frontend/mod.rs +174 -21
  15. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/fuse.rs +321 -104
  16. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/ir.rs +75 -3
  17. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/lib.rs +3 -1
  18. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/par.rs +32 -2
  19. libjay-0.3.0/crates/libjay/src/sparse.rs +388 -0
  20. libjay-0.3.0/crates/libjay/src/symbol.rs +131 -0
  21. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/verb.rs +5105 -978
  22. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay-python/src/data.rs +19 -2
  23. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay-python/src/lib.rs +102 -1
  24. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay-testkit/src/corpus.rs +53 -2
  25. libjay-0.3.0/crates/libjay-testkit/src/dialect.rs +243 -0
  26. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay-testkit/src/eval.rs +28 -3
  27. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay-testkit/src/lib.rs +1 -0
  28. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay-testkit/src/replay.rs +23 -7
  29. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay-testkit/src/snapshot.rs +2 -2
  30. {libjay-0.2.0 → libjay-0.3.0}/python/jay/__init__.py +7 -0
  31. {libjay-0.2.0 → libjay-0.3.0}/python/jay/_cli.py +14 -1
  32. libjay-0.3.0/python/jay/lang.py +145 -0
  33. libjay-0.2.0/python/jay/lang.py +0 -85
  34. {libjay-0.2.0 → libjay-0.3.0}/LICENSE +0 -0
  35. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/Cargo.toml +0 -0
  36. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/LICENSE +0 -0
  37. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/README.md +0 -0
  38. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/device/gpu.rs +0 -0
  39. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/exact.rs +0 -0
  40. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/gerund.rs +0 -0
  41. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/limits.rs +0 -0
  42. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/rng.rs +0 -0
  43. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay/src/simd.rs +0 -0
  44. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay-python/Cargo.toml +0 -0
  45. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay-testkit/Cargo.toml +0 -0
  46. {libjay-0.2.0 → libjay-0.3.0}/crates/libjay-testkit/src/compare.rs +0 -0
  47. {libjay-0.2.0 → libjay-0.3.0}/pyproject.toml +0 -0
  48. {libjay-0.2.0 → libjay-0.3.0}/python/README.md +0 -0
  49. {libjay-0.2.0 → libjay-0.3.0}/python/jay/_tstring.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.3.0"
498
498
  dependencies = [
499
499
  "libjay-testkit",
500
500
  "multiversion",
@@ -510,22 +510,23 @@ dependencies = [
510
510
 
511
511
  [[package]]
512
512
  name = "libjay-capi"
513
- version = "0.2.0"
513
+ version = "0.3.0"
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.3.0"
521
521
  dependencies = [
522
+ "libjay",
522
523
  "libjay-testkit",
523
524
  "rayon",
524
525
  ]
525
526
 
526
527
  [[package]]
527
528
  name = "libjay-python"
528
- version = "0.2.0"
529
+ version = "0.3.0"
529
530
  dependencies = [
530
531
  "arrow-array",
531
532
  "arrow-buffer",
@@ -537,7 +538,7 @@ dependencies = [
537
538
 
538
539
  [[package]]
539
540
  name = "libjay-testkit"
540
- version = "0.2.0"
541
+ version = "0.3.0"
541
542
  dependencies = [
542
543
  "libjay",
543
544
  ]
@@ -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.3.0"
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.3.0
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),
@@ -778,20 +793,32 @@ pub enum Layout {
778
793
  /// honour [`Array::layout`] or take [`Array::to_row_major`] first; the
779
794
  /// runtime's rule is that a value reaching a verb has already been made
780
795
  /// row-major unless that verb asked for the other one.
796
+ ///
797
+ /// A SPARSE array is the one exception to "the buffer holds every element":
798
+ /// `shape` is still the logical shape, but `data` holds only the stored
799
+ /// cells and [`crate::sparse::Sparse`] says where they sit. Only `$.`, the
800
+ /// display and `":` read that form; every other reader takes
801
+ /// [`Array::densified`] first.
781
802
  #[derive(Clone, Debug)]
782
803
  pub struct Array {
783
804
  pub shape: Vec<usize>,
784
805
  pub data: Data,
785
806
  layout: Layout,
807
+ sparse: Option<crate::sparse::Handle>,
808
+ proto: Option<std::sync::Arc<Array>>,
786
809
  }
787
810
 
788
811
  /// Two arrays are equal when they hold the same elements at the same
789
- /// indices, whatever buffer order each of them keeps.
812
+ /// indices, whatever buffer order — or storage kind — each of them keeps.
790
813
  impl PartialEq for Array {
791
814
  fn eq(&self, other: &Array) -> bool {
792
815
  if self.shape != other.shape {
793
816
  return false;
794
817
  }
818
+ if self.sparse.is_some() || other.sparse.is_some() {
819
+ let (a, b) = (self.densified(), other.densified());
820
+ return a.to_row_major().data == b.to_row_major().data;
821
+ }
795
822
  if self.layout == other.layout {
796
823
  return self.data == other.data;
797
824
  }
@@ -802,7 +829,52 @@ impl PartialEq for Array {
802
829
  impl Array {
803
830
  pub fn new(shape: Vec<usize>, data: Data) -> Array {
804
831
  debug_assert_eq!(shape.iter().product::<usize>(), data.len());
805
- Array { shape, data, layout: Layout::RowMajor }
832
+ Array { shape, data, layout: Layout::RowMajor, sparse: None, proto: None }
833
+ }
834
+
835
+ /// A sparse array: the logical `shape`, the stored cells, and the
836
+ /// description of where they sit. `data` holds `entries` cells and not
837
+ /// one element per position, so this is the only constructor that does
838
+ /// not tie the buffer's length to the shape.
839
+ pub fn sparse(shape: Vec<usize>, data: Data, sparse: crate::sparse::Sparse) -> Array {
840
+ Array { shape, data, layout: Layout::RowMajor, sparse: Some(std::sync::Arc::new(sparse)), proto: None }
841
+ }
842
+
843
+ /// True while the array holds only its stored cells.
844
+ pub fn is_sparse(&self) -> bool {
845
+ self.sparse.is_some()
846
+ }
847
+
848
+ /// The item an array with no items would have held — APL's prototype.
849
+ ///
850
+ /// A simple array's type says what its fills look like, so nothing has
851
+ /// to be remembered; a nested one does, since an empty buffer of boxes
852
+ /// no longer says whether its items were pairs of numbers or of
853
+ /// characters. `0⍴⊂2 3⍴9` is such an array, and `↑` of it answers the
854
+ /// 2 by 3 table of zeros this holds. Only the operations that make an
855
+ /// empty out of a nested array set it, and only APL reads it.
856
+ pub fn proto(&self) -> Option<&Array> {
857
+ self.proto.as_deref()
858
+ }
859
+
860
+ /// The same array, remembering what its items looked like.
861
+ pub fn with_proto(mut self, proto: Array) -> Array {
862
+ self.proto = Some(std::sync::Arc::new(proto));
863
+ self
864
+ }
865
+
866
+ /// How this array is stored sparsely, or None for a dense one.
867
+ pub fn sparse_parts(&self) -> Option<&crate::sparse::Sparse> {
868
+ self.sparse.as_deref()
869
+ }
870
+
871
+ /// This array with every position materialised. A dense array is a
872
+ /// refcount bump; a sparse one is expanded here and nowhere else.
873
+ pub fn densified(&self) -> Array {
874
+ match &self.sparse {
875
+ None => self.clone(),
876
+ Some(s) => crate::sparse::densify(self, s),
877
+ }
806
878
  }
807
879
 
808
880
  /// An array whose buffer holds its first axis fastest — the columns of
@@ -810,7 +882,7 @@ impl Array {
810
882
  pub fn col_major(shape: Vec<usize>, data: Data) -> Array {
811
883
  debug_assert_eq!(shape.iter().product::<usize>(), data.len());
812
884
  let layout = if shape.len() < 2 { Layout::RowMajor } else { Layout::ColMajor };
813
- Array { shape, data, layout }
885
+ Array { shape, data, layout, sparse: None, proto: None }
814
886
  }
815
887
 
816
888
  /// The same buffer read the other way round. The caller is asserting
@@ -949,7 +1021,16 @@ impl Array {
949
1021
  /// Widen the elements. A cast reads and writes the buffer as it lies,
950
1022
  /// so the layout comes through untouched.
951
1023
  pub fn cast(&self, to: DType) -> Option<Array> {
952
- Some(Array { shape: self.shape.clone(), data: self.data.cast(to)?, layout: self.layout })
1024
+ if self.is_sparse() {
1025
+ return self.densified().cast(to);
1026
+ }
1027
+ Some(Array {
1028
+ shape: self.shape.clone(),
1029
+ data: self.data.cast(to)?,
1030
+ layout: self.layout,
1031
+ sparse: None,
1032
+ proto: self.proto.clone(),
1033
+ })
953
1034
  }
954
1035
 
955
1036
  /// Split into cells: the trailing `cell_rank` axes form the cell shape,
@@ -1035,7 +1116,7 @@ impl Array {
1035
1116
  Data::Rat(v) => Some(v.iter().map(|x| [x.to_f64(), 0.0]).collect()),
1036
1117
  Data::F64(v) => Some(v.iter().map(|&x| [x, 0.0]).collect()),
1037
1118
  Data::Complex(v) => Some(v.to_vec()),
1038
- Data::Char(_) | Data::Box(_) => None,
1119
+ Data::Char(_) | Data::Symbol(_) | Data::Box(_) => None,
1039
1120
  }
1040
1121
  }
1041
1122
 
@@ -1047,9 +1128,20 @@ impl Array {
1047
1128
  Data::Ext(v) => Some(v.iter().map(crate::exact::ext_to_f64).collect()),
1048
1129
  Data::Rat(v) => Some(v.iter().map(Rat::to_f64).collect()),
1049
1130
  Data::F64(v) => Some(v.to_vec()),
1050
- // A complex value is not a real one, even when its imaginary
1051
- // part is zero: the caller wants a real and must ask for it.
1052
- Data::Complex(_) | Data::Char(_) | Data::Box(_) => None,
1131
+ // A complex value whose imaginary part is zero IS a real one
1132
+ // wherever a real is wanted: J answers `1 <. j. 0` with 0 and
1133
+ // `i. 3j0` with `0 1 2`, while `3!:0 j. 0` still reports the
1134
+ // complex type, so the demotion is at the use and not at the
1135
+ // making.
1136
+ Data::Complex(v) => {
1137
+ v.iter().map(|z| (z[1] == 0.0).then_some(z[0])).collect()
1138
+ }
1139
+ // An EMPTY array carries no value of the wrong type, so it is
1140
+ // acceptable numeric data whatever type it was written at:
1141
+ // `#. ''` is 0 in J and `¯3⊥''` is 0 in GNU APL. An empty BOX
1142
+ // is not: J refuses `2 #. 0$<1` where it answers `2 #. ''`.
1143
+ Data::Char(_) | Data::Symbol(_) if self.count() == 0 => Some(Vec::new()),
1144
+ Data::Char(_) | Data::Symbol(_) | Data::Box(_) => None,
1053
1145
  }
1054
1146
  }
1055
1147
 
@@ -1074,8 +1166,90 @@ impl Array {
1074
1166
  }
1075
1167
  Some(out)
1076
1168
  }
1077
- Data::Complex(_) | Data::Char(_) | Data::Box(_) => None,
1169
+ // The same two readings [`Array::to_f64_vec`] gives: a complex
1170
+ // with no imaginary part is the real it displays as, and an
1171
+ // empty of a non-numeric type holds no value to refuse.
1172
+ Data::Complex(v) => v
1173
+ .iter()
1174
+ .map(|z| {
1175
+ (z[1] == 0.0 && z[0].fract() == 0.0 && z[0].abs() < i64::MAX as f64)
1176
+ .then_some(z[0] as i64)
1177
+ })
1178
+ .collect(),
1179
+ Data::Char(_) | Data::Symbol(_) if self.count() == 0 => Some(Vec::new()),
1180
+ Data::Char(_) | Data::Symbol(_) | Data::Box(_) => None,
1181
+ }
1182
+ }
1183
+
1184
+ /// Numeric contents as i64 where a COUNT, a LENGTH or an INDEX is
1185
+ /// wanted, admitting a float that is merely near a whole number.
1186
+ ///
1187
+ /// Both references round such a float to the whole number beside it
1188
+ /// rather than refusing it — `⍳2-1E¯14` is `1 2` and `(2-1e_14) {. 1 2 3`
1189
+ /// is `1 2` — and neither admission is the comparison tolerance:
1190
+ /// `⎕CT←0` and `9!:19 (0)` leave both exactly where they are. The two
1191
+ /// admissions differ in shape. [`NearInt::J`] is relative, a value
1192
+ /// within `2^-44` of a whole number's magnitude; [`NearInt::Apl`] is
1193
+ /// absolute, `1e-10`, whatever the magnitude; [`NearInt::Tolerant`] is
1194
+ /// relative and follows the comparison tolerance in force. Everything a
1195
+ /// full integer apart is still a refusal in all three.
1196
+ pub fn to_i64_vec_near(&self, near: NearInt) -> Option<Vec<i64>> {
1197
+ let Data::F64(v) = &self.data else {
1198
+ // Every other type is exact or is refused outright; only a
1199
+ // float can be near a whole number without being one.
1200
+ return self.to_i64_vec();
1201
+ };
1202
+ v.iter().map(|&x| near.round(x)).collect()
1203
+ }
1204
+ }
1205
+
1206
+ /// The near-integer admission a count, a length or an index position uses.
1207
+ ///
1208
+ /// In J and in GNU APL it is a language constant: neither lets a program
1209
+ /// move it, and neither is the comparison tolerance the same program can
1210
+ /// set. Dyalog's is the exception — relative and scaled by `⎕CT` — so the
1211
+ /// rule is a dialect setting there, and [`NearInt::Tolerant`] carries the
1212
+ /// tolerance in force with it.
1213
+ #[derive(Clone, Copy, Debug, PartialEq)]
1214
+ pub enum NearInt {
1215
+ /// J: `|x - n| ≤ 2^-44 × max(|x|, |n|)`, so the window grows with the
1216
+ /// magnitude and closes completely at zero.
1217
+ J,
1218
+ /// APL: `|x - n| < 1e-10` at every magnitude.
1219
+ Apl,
1220
+ /// Dyalog: the dialect's own tolerant equality against the whole
1221
+ /// number, so the window grows with the magnitude and `⎕CT` moves it.
1222
+ Tolerant(crate::verb::Tol),
1223
+ }
1224
+
1225
+ impl NearInt {
1226
+ /// J's relative admission, which is also the value J's comparison
1227
+ /// tolerance starts at — the two are separate settings that happen to
1228
+ /// share a number.
1229
+ pub const J_RELATIVE: f64 = 1.0 / 17_592_186_044_416.0;
1230
+ /// APL's absolute admission.
1231
+ pub const APL_ABSOLUTE: f64 = 1e-10;
1232
+
1233
+ /// The rule for a language, at that language's shipped dialect.
1234
+ pub fn of(lang: crate::Lang) -> NearInt {
1235
+ match lang {
1236
+ crate::Lang::J => NearInt::J,
1237
+ crate::Lang::Apl => NearInt::Apl,
1238
+ }
1239
+ }
1240
+
1241
+ /// The whole number `x` stands for, or None when it stands for none.
1242
+ pub fn round(self, x: f64) -> Option<i64> {
1243
+ if x.fract() == 0.0 {
1244
+ return (x.abs() < i64::MAX as f64).then_some(x as i64);
1078
1245
  }
1246
+ let n = x.round();
1247
+ let within = match self {
1248
+ NearInt::J => (x - n).abs() <= Self::J_RELATIVE * x.abs().max(n.abs()),
1249
+ NearInt::Apl => (x - n).abs() < Self::APL_ABSOLUTE,
1250
+ NearInt::Tolerant(tol) => tol.eq(x, n),
1251
+ };
1252
+ (within && n.abs() < i64::MAX as f64).then_some(n as i64)
1079
1253
  }
1080
1254
  }
1081
1255
 
@@ -39,9 +39,23 @@ pub fn conj(a: Cx) -> Cx {
39
39
  [a[0], -a[1]]
40
40
  }
41
41
 
42
+ /// A complex product is four real ones, and each of them follows J's rule
43
+ /// that a zero factor wins: `_ * 0j1` is `0j_` and `0j_ * 0j_` is `__`
44
+ /// only when `_ * 0` is 0 rather than a NaN. It is also what gives `j. _`
45
+ /// its value, because `j.` multiplies by the imaginary unit. GNU APL never
46
+ /// reaches the case — it refuses an infinite operand to `×` outright — so
47
+ /// the rule costs nothing there.
48
+ #[inline]
49
+ fn prod(x: f64, y: f64) -> f64 {
50
+ if (x == 0.0 || y == 0.0) && !(x.is_finite() && y.is_finite()) {
51
+ return 0.0;
52
+ }
53
+ x * y
54
+ }
55
+
42
56
  #[inline]
43
57
  pub fn mul(a: Cx, b: Cx) -> Cx {
44
- [a[0] * b[0] - a[1] * b[1], a[0] * b[1] + a[1] * b[0]]
58
+ [prod(a[0], b[0]) - prod(a[1], b[1]), prod(a[0], b[1]) + prod(a[1], b[0])]
45
59
  }
46
60
 
47
61
  /// Division, with J's rule for a zero divisor carried onto both parts:
@@ -156,7 +170,10 @@ pub fn pow(a: Cx, b: Cx) -> Cx {
156
170
  if a[1] == 0.0 && a[0] < 0.0 && b[1] == 0.0 {
157
171
  let m = (-a[0]).powf(b[0]);
158
172
  let (c, s) = cos_sin_pi(b[0]);
159
- return [m * c, m * s];
173
+ // `prod`, not `*`: at a half turn the cosine is an exact zero, and
174
+ // an infinite magnitude beside it is the zero-factor case again.
175
+ // `__ ^ 0.5` is `0j_` and `__ ^ 1.5` is `0j__`.
176
+ return [prod(m, c), prod(m, s)];
160
177
  }
161
178
  exp(mul(b, ln(a)))
162
179
  }
@@ -408,9 +408,36 @@ fn helpers(needs: &Needs, tol: Tol, p: Precision) -> String {
408
408
  s.push_str(" return x / y;\n}\n");
409
409
  }
410
410
  if needs.residue {
411
+ // The quotient is rounded with the dialect's tolerance, as it is on
412
+ // the host: J takes the tolerant floor and answers an exact zero
413
+ // when the product is tolerantly the dividend, GNU APL shifts the
414
+ // quotient by `⎕CT` and reads the remainder against the modulus.
415
+ let ct = lit(tol.ct, p);
411
416
  s.push_str(&format!("fn residue(x: {t}, y: {t}) -> {t} {{\n"));
412
417
  s.push_str(&format!(" if (x == {zero}) {{ return y; }}\n"));
413
- s.push_str(" return y - x * floor(y / x);\n}\n");
418
+ if tol.by_smaller {
419
+ s.push_str(" let q = y / x;\n");
420
+ s.push_str(" let c = ceil(q);\n");
421
+ s.push_str(&format!(
422
+ " let k = select(floor(q), c, abs(q - c) < {ct} * min(abs(q), abs(c)));\n"
423
+ ));
424
+ s.push_str(" let d = x * k;\n");
425
+ s.push_str(&format!(
426
+ " if (abs(y - d) < {ct} * min(abs(y), abs(d))) {{ return {zero}; }}\n"
427
+ ));
428
+ s.push_str(" return y - d;\n}\n");
429
+ } else {
430
+ s.push_str(" let q = y / x;\n let c = ceil(q);\n let gap = c - q;\n");
431
+ s.push_str(&format!(
432
+ " let k = select(floor(q), c, gap <= {ct} || gap < {ct} * max(abs(q), abs(c)));\n"
433
+ ));
434
+ s.push_str(" let r = y - x * k;\n");
435
+ s.push_str(&format!(" if (abs(r) < {ct} * abs(x)) {{ return {zero}; }}\n"));
436
+ s.push_str(&format!(
437
+ " if (r != {zero} && (r < {zero}) != (x < {zero})) {{ return r + x; }}\n"
438
+ ));
439
+ s.push_str(" return r;\n}\n");
440
+ }
414
441
  }
415
442
  if !s.is_empty() {
416
443
  s.push('\n');
@@ -145,12 +145,14 @@ impl Device {
145
145
  pub fn upload(&self, y: &Array) -> Result<Array, DeviceError> {
146
146
  let Some(backend) = self.backend() else { return Ok(y.clone()) };
147
147
  // What goes to the device is the elements in row-major order; a
148
- // column-major argument is laid out once before it leaves.
148
+ // column-major argument is laid out once before it leaves, and a
149
+ // sparse one is expanded — a device buffer is one element per
150
+ // position and nothing else.
149
151
  let laid_out;
150
- let y = if y.is_row_major() {
152
+ let y = if y.is_row_major() && !y.is_sparse() {
151
153
  y
152
154
  } else {
153
- laid_out = y.to_row_major();
155
+ laid_out = y.densified().to_row_major();
154
156
  &laid_out
155
157
  };
156
158
  // A float array is uploaded from its own buffer; anything else is
@@ -307,6 +309,9 @@ pub enum Refusal {
307
309
  Unsupported(&'static str),
308
310
  /// The kernel itself would decline these inputs, device or no device.
309
311
  Declined,
312
+ /// The shader's answer holds an infinity or a NaN, where the dialect
313
+ /// has a rule of its own and the shader has only IEEE arithmetic.
314
+ NonFinite,
310
315
  /// Too little data to pay for a dispatch.
311
316
  TooSmall,
312
317
  /// The device refused: an allocation, a shader, a queue submission.
@@ -323,6 +328,9 @@ impl Refusal {
323
328
  }
324
329
  Refusal::Unsupported(op) => format!("`{op}` has no shader form here"),
325
330
  Refusal::Declined => "the fused kernel declined these inputs".into(),
331
+ Refusal::NonFinite => {
332
+ "the answer holds an infinity or a NaN, which the dialect's rules read".into()
333
+ }
326
334
  Refusal::TooSmall => "there is too little data to pay for a dispatch".into(),
327
335
  Refusal::Failed(e) => format!("the device refused: {e}"),
328
336
  }
@@ -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
  }
@@ -30,6 +30,11 @@ pub enum ErrorKind {
30
30
  Domain,
31
31
  Type,
32
32
  Value,
33
+ /// Arithmetic with no value at all: J refuses a NaN its own arithmetic
34
+ /// made (`_ - _`, `2 | _`), and names the failure this way. A NaN the
35
+ /// program itself wrote (`_.`) travels through unrefused, so this is
36
+ /// about the operation, not the operand.
37
+ Nan,
33
38
  /// Present in the language, not implemented yet.
34
39
  NotYet,
35
40
  /// Absent from the language itself; will never exist.
@@ -52,6 +57,7 @@ impl ErrorKind {
52
57
  ErrorKind::Domain => "domain error",
53
58
  ErrorKind::Type => "type error",
54
59
  ErrorKind::Value => "value error",
60
+ ErrorKind::Nan => "NaN error",
55
61
  ErrorKind::NotYet => "not supported yet",
56
62
  ErrorKind::Language => "not in the language",
57
63
  ErrorKind::Sandbox => "closed by the sandbox",
@@ -99,6 +105,13 @@ impl Error {
99
105
  Self::new(ErrorKind::Domain, msg, Some(span))
100
106
  }
101
107
 
108
+ /// Arithmetic whose answer is a NaN nobody asked for. The message names
109
+ /// the operation and the pair that produced it, in the source language's
110
+ /// own spelling of the infinities.
111
+ pub fn nan(msg: impl Into<String>, span: Span) -> Self {
112
+ Self::new(ErrorKind::Nan, msg, Some(span))
113
+ }
114
+
102
115
  pub fn internal(msg: impl Into<String>) -> Self {
103
116
  Self::new(ErrorKind::Internal, msg, None)
104
117
  }
@@ -226,6 +226,11 @@ fn control_lines(c: &Control, depth: usize, p: &Program, tr: &Trace, out: &mut S
226
226
  let _ = writeln!(out, "{pad}branch →");
227
227
  expr_lines(target, depth + 1, p, tr, out);
228
228
  }
229
+ Control::Guard { test, body } => {
230
+ let _ = writeln!(out, "{pad}guard — the dfn's answer when it holds");
231
+ block("test", test, out);
232
+ block("body", body, out);
233
+ }
229
234
  Control::If { arms, otherwise } => {
230
235
  let _ = writeln!(out, "{pad}if — {} arm(s)", arms.len());
231
236
  for (i, arm) in arms.iter().enumerate() {
@@ -377,6 +382,23 @@ fn source_of(p: &Program, e: &Expr) -> String {
377
382
 
378
383
  // ------------------------------------------------------------------- verbs
379
384
 
385
+ /// One operand of a user-written operator: the function's own tree, or the
386
+ /// one line an array operand needs.
387
+ fn operand_lines(
388
+ o: &crate::verb::Operand,
389
+ depth: usize,
390
+ p: &Program,
391
+ tr: &Trace,
392
+ out: &mut String,
393
+ ) {
394
+ match o {
395
+ crate::verb::Operand::Func(v) => verb_lines(v, depth, p, tr, out),
396
+ crate::verb::Operand::Value(_) => {
397
+ let _ = writeln!(out, "{}an array operand", " ".repeat(depth * STEP));
398
+ }
399
+ }
400
+ }
401
+
380
402
  fn verb_lines(v: &Verb, depth: usize, p: &Program, tr: &Trace, out: &mut String) {
381
403
  let pad = " ".repeat(depth * STEP);
382
404
  let head = |out: &mut String, what: &str| {
@@ -395,6 +417,10 @@ fn verb_lines(v: &Verb, depth: usize, p: &Program, tr: &Trace, out: &mut String)
395
417
  head(out, "reduce (insert between items)");
396
418
  verb_lines(u, depth + 1, p, tr, out);
397
419
  }
420
+ Verb::NWise(u) => {
421
+ head(out, "reduce (insert between items); n-wise with a left argument");
422
+ verb_lines(u, depth + 1, p, tr, out);
423
+ }
398
424
  Verb::Fit(u, n) => {
399
425
  head(out, &format!("fit !.{n} (comparison tolerance)"));
400
426
  verb_lines(u, depth + 1, p, tr, out);
@@ -426,12 +452,19 @@ fn verb_lines(v: &Verb, depth: usize, p: &Program, tr: &Trace, out: &mut String)
426
452
  verb_lines(f, depth + 1, p, tr, out);
427
453
  verb_lines(g, depth + 1, p, tr, out);
428
454
  }
455
+ Verb::InnerProduct { u, v, .. } => {
456
+ head(out, "inner product (u . v; the matrix product for +/ . *)");
457
+ verb_lines(u, depth + 1, p, tr, out);
458
+ verb_lines(v, depth + 1, p, tr, out);
459
+ }
429
460
  Verb::UserDerived { def, alpha, omega } => {
430
461
  head(out, "a user-written operator with its operands");
431
- verb_lines(def, depth + 1, p, tr, out);
432
- verb_lines(alpha, depth + 1, p, tr, out);
462
+ if let Ok(body) = def.pick(alpha, omega.as_ref()) {
463
+ verb_lines(body, depth + 1, p, tr, out);
464
+ }
465
+ operand_lines(alpha, depth + 1, p, tr, out);
433
466
  if let Some(g) = omega {
434
- verb_lines(g, depth + 1, p, tr, out);
467
+ operand_lines(g, depth + 1, p, tr, out);
435
468
  }
436
469
  }
437
470
  Verb::KeyPairs(u) => {
@@ -566,6 +599,10 @@ fn verb_lines(v: &Verb, depth: usize, p: &Program, tr: &Trace, out: &mut String)
566
599
  }
567
600
  Verb::SelfRef => head(out, "self-reference (the definition it stands in)"),
568
601
  Verb::Named(n) => head(out, &format!("verb named {n}, resolved when it is applied")),
602
+ Verb::UnderRavel(u) => {
603
+ head(out, "under ravel (flatten, apply, put the shape back)");
604
+ verb_lines(u, depth + 1, p, tr, out);
605
+ }
569
606
  Verb::Each(u, kind) => {
570
607
  head(
571
608
  out,