libjay 0.2.1__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.1 → libjay-0.3.0}/Cargo.lock +6 -5
  2. {libjay-0.2.1 → libjay-0.3.0}/Cargo.toml +1 -1
  3. {libjay-0.2.1 → libjay-0.3.0}/PKG-INFO +1 -1
  4. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/array.rs +167 -8
  5. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/complex.rs +19 -2
  6. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/device/codegen.rs +28 -1
  7. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/device/mod.rs +11 -3
  8. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/error.rs +13 -0
  9. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/explain.rs +35 -3
  10. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/fmt.rs +192 -9
  11. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/frontend/apl.rs +469 -141
  12. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/frontend/j.rs +60 -25
  13. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/frontend/mod.rs +174 -21
  14. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/fuse.rs +107 -33
  15. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/ir.rs +72 -3
  16. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/lib.rs +2 -1
  17. libjay-0.3.0/crates/libjay/src/sparse.rs +388 -0
  18. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/verb.rs +3092 -583
  19. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay-python/src/data.rs +9 -0
  20. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay-python/src/lib.rs +96 -1
  21. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay-testkit/src/corpus.rs +53 -2
  22. libjay-0.3.0/crates/libjay-testkit/src/dialect.rs +243 -0
  23. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay-testkit/src/eval.rs +28 -3
  24. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay-testkit/src/lib.rs +1 -0
  25. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay-testkit/src/replay.rs +23 -7
  26. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay-testkit/src/snapshot.rs +2 -2
  27. {libjay-0.2.1 → libjay-0.3.0}/python/jay/__init__.py +7 -0
  28. {libjay-0.2.1 → libjay-0.3.0}/python/jay/_cli.py +14 -1
  29. libjay-0.3.0/python/jay/lang.py +145 -0
  30. libjay-0.2.1/python/jay/lang.py +0 -85
  31. {libjay-0.2.1 → libjay-0.3.0}/LICENSE +0 -0
  32. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/Cargo.toml +0 -0
  33. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/LICENSE +0 -0
  34. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/README.md +0 -0
  35. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/device/gpu.rs +0 -0
  36. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/dtype.rs +0 -0
  37. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/exact.rs +0 -0
  38. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/gerund.rs +0 -0
  39. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/limits.rs +0 -0
  40. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/par.rs +0 -0
  41. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/rng.rs +0 -0
  42. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/simd.rs +0 -0
  43. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay/src/symbol.rs +0 -0
  44. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay-python/Cargo.toml +0 -0
  45. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay-testkit/Cargo.toml +0 -0
  46. {libjay-0.2.1 → libjay-0.3.0}/crates/libjay-testkit/src/compare.rs +0 -0
  47. {libjay-0.2.1 → libjay-0.3.0}/pyproject.toml +0 -0
  48. {libjay-0.2.1 → libjay-0.3.0}/python/README.md +0 -0
  49. {libjay-0.2.1 → 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.1"
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.1"
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.1"
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.1"
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.1"
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.1"
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.1
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
@@ -793,20 +793,32 @@ pub enum Layout {
793
793
  /// honour [`Array::layout`] or take [`Array::to_row_major`] first; the
794
794
  /// runtime's rule is that a value reaching a verb has already been made
795
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.
796
802
  #[derive(Clone, Debug)]
797
803
  pub struct Array {
798
804
  pub shape: Vec<usize>,
799
805
  pub data: Data,
800
806
  layout: Layout,
807
+ sparse: Option<crate::sparse::Handle>,
808
+ proto: Option<std::sync::Arc<Array>>,
801
809
  }
802
810
 
803
811
  /// Two arrays are equal when they hold the same elements at the same
804
- /// indices, whatever buffer order each of them keeps.
812
+ /// indices, whatever buffer order — or storage kind — each of them keeps.
805
813
  impl PartialEq for Array {
806
814
  fn eq(&self, other: &Array) -> bool {
807
815
  if self.shape != other.shape {
808
816
  return false;
809
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
+ }
810
822
  if self.layout == other.layout {
811
823
  return self.data == other.data;
812
824
  }
@@ -817,7 +829,52 @@ impl PartialEq for Array {
817
829
  impl Array {
818
830
  pub fn new(shape: Vec<usize>, data: Data) -> Array {
819
831
  debug_assert_eq!(shape.iter().product::<usize>(), data.len());
820
- 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
+ }
821
878
  }
822
879
 
823
880
  /// An array whose buffer holds its first axis fastest — the columns of
@@ -825,7 +882,7 @@ impl Array {
825
882
  pub fn col_major(shape: Vec<usize>, data: Data) -> Array {
826
883
  debug_assert_eq!(shape.iter().product::<usize>(), data.len());
827
884
  let layout = if shape.len() < 2 { Layout::RowMajor } else { Layout::ColMajor };
828
- Array { shape, data, layout }
885
+ Array { shape, data, layout, sparse: None, proto: None }
829
886
  }
830
887
 
831
888
  /// The same buffer read the other way round. The caller is asserting
@@ -964,7 +1021,16 @@ impl Array {
964
1021
  /// Widen the elements. A cast reads and writes the buffer as it lies,
965
1022
  /// so the layout comes through untouched.
966
1023
  pub fn cast(&self, to: DType) -> Option<Array> {
967
- 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
+ })
968
1034
  }
969
1035
 
970
1036
  /// Split into cells: the trailing `cell_rank` axes form the cell shape,
@@ -1062,9 +1128,20 @@ impl Array {
1062
1128
  Data::Ext(v) => Some(v.iter().map(crate::exact::ext_to_f64).collect()),
1063
1129
  Data::Rat(v) => Some(v.iter().map(Rat::to_f64).collect()),
1064
1130
  Data::F64(v) => Some(v.to_vec()),
1065
- // A complex value is not a real one, even when its imaginary
1066
- // part is zero: the caller wants a real and must ask for it.
1067
- Data::Complex(_) | Data::Char(_) | Data::Symbol(_) | 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,
1068
1145
  }
1069
1146
  }
1070
1147
 
@@ -1089,8 +1166,90 @@ impl Array {
1089
1166
  }
1090
1167
  Some(out)
1091
1168
  }
1092
- Data::Complex(_) | Data::Char(_) | Data::Symbol(_) | 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);
1093
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)
1094
1253
  }
1095
1254
  }
1096
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
  }
@@ -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);
@@ -433,10 +459,12 @@ fn verb_lines(v: &Verb, depth: usize, p: &Program, tr: &Trace, out: &mut String)
433
459
  }
434
460
  Verb::UserDerived { def, alpha, omega } => {
435
461
  head(out, "a user-written operator with its operands");
436
- verb_lines(def, depth + 1, p, tr, out);
437
- 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);
438
466
  if let Some(g) = omega {
439
- verb_lines(g, depth + 1, p, tr, out);
467
+ operand_lines(g, depth + 1, p, tr, out);
440
468
  }
441
469
  }
442
470
  Verb::KeyPairs(u) => {
@@ -571,6 +599,10 @@ fn verb_lines(v: &Verb, depth: usize, p: &Program, tr: &Trace, out: &mut String)
571
599
  }
572
600
  Verb::SelfRef => head(out, "self-reference (the definition it stands in)"),
573
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
+ }
574
606
  Verb::Each(u, kind) => {
575
607
  head(
576
608
  out,