1use crate::fuse::{FusedKernel, Instr};
17use crate::verb::{ScalarDyad, ScalarMonad, Tol};
18
19pub(crate) const WORKGROUP: usize = 256;
23
24const MAX_GROUPS: usize = 1024;
27
28pub(crate) const MAP: &str = "map";
30pub(crate) const REDUCE: &str = "reduce";
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum Precision {
40 F64,
41 F32,
42}
43
44impl Precision {
45 pub fn size(self) -> usize {
47 match self {
48 Precision::F64 => 8,
49 Precision::F32 => 4,
50 }
51 }
52
53 pub fn name(self) -> &'static str {
55 match self {
56 Precision::F64 => "f64",
57 Precision::F32 => "f32",
58 }
59 }
60
61 pub fn from_name(s: &str) -> Option<Precision> {
62 match s.trim().to_ascii_lowercase().as_str() {
63 "f64" | "double" => Some(Precision::F64),
64 "f32" | "single" | "float" => Some(Precision::F32),
65 _ => None,
66 }
67 }
68
69 fn ty(self) -> &'static str {
70 self.name()
71 }
72
73 fn suffix(self) -> &'static str {
75 match self {
76 Precision::F64 => "lf",
77 Precision::F32 => "f",
78 }
79 }
80}
81
82pub(crate) fn groups_for(n: usize) -> usize {
89 (n / WORKGROUP).clamp(1, MAX_GROUPS)
90}
91
92pub(crate) fn byte_iter(v: &[f64], p: Precision) -> impl Iterator<Item = u8> {
101 v.iter().flat_map(move |&x| {
102 let mut b = [0u8; 8];
103 match p {
104 Precision::F64 => b.copy_from_slice(&x.to_ne_bytes()),
105 Precision::F32 => b[..4].copy_from_slice(&(x as f32).to_ne_bytes()),
106 }
107 b.into_iter().take(p.size())
108 })
109}
110
111pub(crate) fn from_bytes(b: &[u8], p: Precision, n: usize) -> Vec<f64> {
113 let w = p.size();
114 (0..n)
115 .map(|i| {
116 let s = &b[i * w..(i + 1) * w];
117 match p {
118 Precision::F64 => f64::from_ne_bytes(s.try_into().expect("8 bytes")),
119 Precision::F32 => f32::from_ne_bytes(s.try_into().expect("4 bytes")) as f64,
120 }
121 })
122 .collect()
123}
124
125#[derive(Default)]
131struct Needs {
132 tol_eq: bool,
133 tol_lt: bool,
134 tol_le: bool,
135 recip: bool,
136 divj: bool,
137 residue: bool,
138}
139
140pub(crate) fn wgsl(
143 k: &FusedKernel,
144 splat: &[bool],
145 p: Precision,
146) -> Result<String, &'static str> {
147 let mut needs = Needs::default();
148 let body = chain_body(k, splat, p, &mut needs)?;
149 let reduce = match k.reduce() {
150 None => None,
151 Some(op) => Some(fold_expr(op)?),
152 };
153
154 let t = p.ty();
155 let mut s = String::new();
156 s.push_str("// generated by libjay from a fused kernel\n");
157 s.push_str("struct JayGrid { n: u32, stride: u32 };\n");
158 s.push_str("@group(0) @binding(0) var<uniform> jg : JayGrid;\n");
159 s.push_str(&format!(
160 "@group(0) @binding(1) var<storage, read_write> jay_out : array<{t}>;\n"
161 ));
162 for i in 0..splat.len() {
163 s.push_str(&format!(
164 "@group(0) @binding({}) var<storage, read> jay_in{i} : array<{t}>;\n",
165 i + 2
166 ));
167 }
168 s.push('\n');
169 s.push_str(&helpers(&needs, k.tol(), p));
170 s.push_str(&format!("fn jay_chain(i: u32) -> {t} {{\n{body}}}\n\n"));
171
172 s.push_str(&format!("@compute @workgroup_size({WORKGROUP})\n"));
173 s.push_str(&format!("fn {MAP}(@builtin(global_invocation_id) gid: vec3<u32>) {{\n"));
174 s.push_str(" let i = gid.x;\n if (i >= jg.n) { return; }\n");
175 s.push_str(" jay_out[i] = jay_chain(i);\n}\n");
176
177 if let Some(fold) = reduce {
178 s.push_str(&format!("\nvar<workgroup> lane : array<{t}, {WORKGROUP}>;\n\n"));
179 s.push_str(&format!("@compute @workgroup_size({WORKGROUP})\n"));
180 s.push_str(&format!("fn {REDUCE}(\n"));
181 s.push_str(" @builtin(global_invocation_id) gid: vec3<u32>,\n");
182 s.push_str(" @builtin(local_invocation_id) lid: vec3<u32>,\n");
183 s.push_str(" @builtin(workgroup_id) wid: vec3<u32>,\n");
184 s.push_str(") {\n");
185 s.push_str(" var acc = jay_chain(gid.x);\n");
188 s.push_str(" var i = gid.x + jg.stride;\n");
189 s.push_str(" loop {\n if (i >= jg.n) { break; }\n");
190 s.push_str(&format!(" acc = {};\n", fold("acc", "jay_chain(i)")));
191 s.push_str(" i = i + jg.stride;\n }\n");
192 s.push_str(" lane[lid.x] = acc;\n workgroupBarrier();\n");
193 s.push_str(&format!(" var s = {}u;\n", WORKGROUP / 2));
196 s.push_str(" loop {\n if (s == 0u) { break; }\n");
197 s.push_str(&format!(
198 " if (lid.x < s) {{ lane[lid.x] = {}; }}\n",
199 fold("lane[lid.x]", "lane[lid.x + s]")
200 ));
201 s.push_str(" workgroupBarrier();\n s = s >> 1u;\n }\n");
202 s.push_str(" if (lid.x == 0u) { jay_out[wid.x] = lane[0]; }\n}\n");
203 }
204 Ok(s)
205}
206
207fn chain_body(
210 k: &FusedKernel,
211 splat: &[bool],
212 p: Precision,
213 needs: &mut Needs,
214) -> Result<String, &'static str> {
215 let mut out = String::new();
216 let mut stack: Vec<String> = Vec::new();
217 let mut lets: Vec<String> = Vec::new();
218 let mut temp = 0usize;
219 for ins in k.code() {
220 match ins {
221 Instr::Load(j) => {
222 let at = if *splat.get(*j).unwrap_or(&false) { "0u" } else { "i" };
223 stack.push(format!("jay_in{j}[{at}]"));
224 }
225 Instr::Let(j) => stack.push(lets.get(*j).ok_or("let")?.clone()),
226 Instr::Store(j) => {
227 let v = stack.pop().ok_or("store")?;
228 let name = format!("l{j}");
229 out.push_str(&format!(" let {name} = {v};\n"));
230 lets.push(name);
231 }
232 Instr::Monad(op) => {
233 let a = stack.pop().ok_or("monad")?;
234 let e = monad_expr(*op, &a, p, needs)?;
235 let name = format!("t{temp}");
236 temp += 1;
237 out.push_str(&format!(" let {name} = {e};\n"));
238 stack.push(name);
239 }
240 Instr::Window(..) => return Err("a moving window"),
243 Instr::Scan(_) => return Err("a running fold"),
244 Instr::Dyad(op) => {
245 let b = stack.pop().ok_or("dyad")?;
246 let a = stack.pop().ok_or("dyad")?;
247 let e = dyad_expr(*op, &a, &b, p, needs)?;
248 let name = format!("t{temp}");
249 temp += 1;
250 out.push_str(&format!(" let {name} = {e};\n"));
251 stack.push(name);
252 }
253 }
254 }
255 let root = stack.pop().ok_or("empty kernel")?;
256 out.push_str(&format!(" return {root};\n"));
257 Ok(out)
258}
259
260fn lit(v: f64, p: Precision) -> String {
262 let mut s = format!("{v:?}");
263 if !s.contains('.') && !s.contains('e') {
264 s.push_str(".0");
265 }
266 s.push_str(p.suffix());
267 s
268}
269
270fn monad_expr(
271 op: ScalarMonad,
272 a: &str,
273 p: Precision,
274 needs: &mut Needs,
275) -> Result<String, &'static str> {
276 use ScalarMonad::*;
277 let one = lit(1.0, p);
278 Ok(match op {
279 Conj => format!("({a})"),
280 Neg => format!("-({a})"),
281 Abs => format!("abs({a})"),
282 Signum => format!("sign({a})"),
283 Recip => {
284 needs.recip = true;
285 format!("recip({a})")
286 }
287 Floor => format!("floor({a})"),
288 Ceil => format!("ceil({a})"),
289 Inc => format!("({a}) + {one}"),
290 Dec => format!("({a}) - {one}"),
291 Double => format!("({a}) + ({a})"),
292 Halve => format!("({a}) / {}", lit(2.0, p)),
293 Square => format!("({a}) * ({a})"),
294 OneMinus => format!("{one} - ({a})"),
295 Exp if p == Precision::F32 => format!("exp({a})"),
299 Exp => return Err("^"),
300 _ => return Err("this monad"),
301 })
302}
303
304fn dyad_expr(
305 op: ScalarDyad,
306 a: &str,
307 b: &str,
308 p: Precision,
309 needs: &mut Needs,
310) -> Result<String, &'static str> {
311 use ScalarDyad::*;
312 let bool_to_num =
315 |c: String| format!("select({}, {}, {c})", lit(0.0, p), lit(1.0, p));
316 Ok(match op {
317 Add => format!("({a}) + ({b})"),
318 Sub => format!("({a}) - ({b})"),
319 Mul => format!("({a}) * ({b})"),
320 Min => format!("min({a}, {b})"),
321 Max => format!("max({a}, {b})"),
322 DivJ => {
323 needs.divj = true;
324 format!("divj({a}, {b})")
325 }
326 Residue => {
327 needs.residue = true;
328 format!("residue({a}, {b})")
329 }
330 Eq => {
331 needs.tol_eq = true;
332 bool_to_num(format!("teq({a}, {b})"))
333 }
334 Ne => {
335 needs.tol_eq = true;
336 bool_to_num(format!("!teq({a}, {b})"))
337 }
338 Lt => {
339 needs.tol_lt = true;
340 bool_to_num(format!("tlt({a}, {b})"))
341 }
342 Le => {
343 needs.tol_le = true;
344 bool_to_num(format!("tle({a}, {b})"))
345 }
346 Gt => {
347 needs.tol_lt = true;
348 bool_to_num(format!("tlt({b}, {a})"))
349 }
350 Ge => {
351 needs.tol_le = true;
352 bool_to_num(format!("tle({b}, {a})"))
353 }
354 _ => return Err("this dyad"),
355 })
356}
357
358fn fold_expr(op: ScalarDyad) -> Result<fn(&str, &str) -> String, &'static str> {
360 use ScalarDyad::*;
361 Ok(match op {
362 Add => |a: &str, b: &str| format!("{a} + {b}"),
363 Mul => |a: &str, b: &str| format!("{a} * {b}"),
364 Min => |a: &str, b: &str| format!("min({a}, {b})"),
365 Max => |a: &str, b: &str| format!("max({a}, {b})"),
366 _ => return Err("this reduction"),
367 })
368}
369
370fn helpers(needs: &Needs, tol: Tol, p: Precision) -> String {
374 let t = p.ty();
375 let zero = lit(0.0, p);
376 let one = lit(1.0, p);
377 let mut s = String::new();
378 if needs.tol_eq || needs.tol_lt || needs.tol_le {
379 let scale = if tol.by_smaller { "min" } else { "max" };
380 s.push_str(&format!("fn teq(a: {t}, b: {t}) -> bool {{\n"));
381 s.push_str(" if (a == b) { return true; }\n");
382 s.push_str(&format!(" let s = {scale}(abs(a), abs(b));\n"));
383 s.push_str(&format!(" return abs(a - b) < {} * s;\n}}\n", lit(tol.ct, p)));
384 }
385 if needs.tol_lt {
386 s.push_str(&format!(
387 "fn tlt(a: {t}, b: {t}) -> bool {{ return a < b && !teq(a, b); }}\n"
388 ));
389 }
390 if needs.tol_le {
391 s.push_str(&format!(
392 "fn tle(a: {t}, b: {t}) -> bool {{ return a <= b || teq(a, b); }}\n"
393 ));
394 }
395 if needs.recip {
396 s.push_str(&format!("fn recip(x: {t}) -> {t} {{\n"));
400 s.push_str(&format!(" if (x == {zero}) {{ return {one} / abs(x); }}\n"));
401 s.push_str(&format!(" return {one} / x;\n}}\n"));
402 }
403 if needs.divj {
404 s.push_str(&format!("fn divj(x: {t}, y: {t}) -> {t} {{\n"));
405 s.push_str(&format!(" if (y == {zero}) {{\n"));
406 s.push_str(&format!(" if (x == {zero}) {{ return {zero}; }}\n"));
407 s.push_str(" return sign(x) / abs(y);\n }\n");
408 s.push_str(" return x / y;\n}\n");
409 }
410 if needs.residue {
411 s.push_str(&format!("fn residue(x: {t}, y: {t}) -> {t} {{\n"));
412 s.push_str(&format!(" if (x == {zero}) {{ return y; }}\n"));
413 s.push_str(" return y - x * floor(y / x);\n}\n");
414 }
415 if !s.is_empty() {
416 s.push('\n');
417 }
418 s
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424 use crate::frontend::{compile, Dialect, Lang};
425 use crate::ir::Expr;
426
427 fn kernel(src: &str) -> FusedKernel {
429 fn find(e: &Expr) -> Option<FusedKernel> {
430 match e {
431 Expr::Fused { kernel, .. } => Some(kernel.clone()),
432 Expr::Assign { value, .. } | Expr::PrintPass { value, .. } => find(value),
433 Expr::Monad { y, .. } => find(y),
434 Expr::Dyad { x, y, .. } => find(x).or_else(|| find(y)),
435 _ => None,
436 }
437 }
438 let p = compile(Lang::J, src, &Dialect::default()).expect("compile");
439 p.stmts.iter().find_map(find).unwrap_or_else(|| panic!("{src} did not fuse"))
440 }
441
442 fn validate(src: &str, p: Precision) {
446 let module = naga::front::wgsl::parse_str(src)
447 .unwrap_or_else(|e| panic!("{}\n\n{src}", e.emit_to_string(src)));
448 let caps = match p {
449 Precision::F64 => naga::valid::Capabilities::FLOAT64,
450 Precision::F32 => naga::valid::Capabilities::empty(),
451 };
452 naga::valid::Validator::new(naga::valid::ValidationFlags::all(), caps)
453 .validate(&module)
454 .unwrap_or_else(|e| panic!("{e:?}\n\n{src}"));
455 }
456
457 const CHAINS: &[&str] = &[
458 "+/ {w} * {x}",
459 "1 + 2 * {x}",
460 "+/ ({x} - 1) * ({x} - 1)",
461 "{w} - {x} - 1",
462 "%: 1 + 2 * {x}",
463 ">./ {w} * {x}",
464 "<./ {w} + {x}",
465 "*/ 1 + {x}",
466 "+/ ({x} > 1) * {x}",
467 "+/ ({x} <: 1) * {x}",
468 "+/ (2 | {x}) * {x}",
469 "+/ ({w} % {x}) + 1",
470 "+/ (% {x}) + 1",
471 "+/ (| {x}) * -: {x}",
472 "+/ (* {x}) + >: {x}",
473 ];
474
475 #[test]
476 fn every_chain_generates_valid_f32_wgsl() {
477 for src in CHAINS {
478 let k = kernel(src);
479 let splat = vec![false; 4];
480 let s = wgsl(&k, &splat, Precision::F32).unwrap_or_else(|e| panic!("{src}: {e}"));
481 validate(&s, Precision::F32);
482 }
483 }
484
485 #[test]
486 fn every_chain_generates_valid_f64_wgsl() {
487 for src in CHAINS {
488 let k = kernel(src);
489 let splat = vec![false; 4];
490 match wgsl(&k, &splat, Precision::F64) {
491 Ok(s) => validate(&s, Precision::F64),
492 Err(op) => assert_eq!(op, "^", "{src}"),
495 }
496 }
497 }
498
499 #[test]
500 fn the_exponential_declines_in_f64_and_runs_in_f32() {
501 let k = kernel("+/ ^ {x}");
502 assert_eq!(wgsl(&k, &[false], Precision::F64), Err("^"));
503 let s = wgsl(&k, &[false], Precision::F32).expect("f32");
504 validate(&s, Precision::F32);
505 assert!(s.contains("exp("));
506 }
507
508 #[test]
509 fn a_scalar_input_is_read_at_zero() {
510 let k = kernel("+/ 2 * {x}");
511 let s = wgsl(&k, &[true, false], Precision::F32).expect("wgsl");
512 assert!(s.contains("jay_in0[0u]"), "{s}");
513 assert!(s.contains("jay_in1[i]"), "{s}");
514 }
515
516 #[test]
517 fn the_grid_never_outnumbers_the_elements() {
518 for n in [1 << 19, 1 << 20, 1 << 24, 3_000_000] {
519 assert!(groups_for(n) * WORKGROUP <= n, "{n}");
520 assert!(groups_for(n) >= 1);
521 }
522 }
523
524 #[test]
525 fn elements_survive_the_round_trip() {
526 let v = vec![1.0, -2.5, 1e300, 0.0];
527 let bytes = |p: Precision| byte_iter(&v, p).collect::<Vec<u8>>();
528 let b = bytes(Precision::F64);
529 assert_eq!(from_bytes(&b, Precision::F64, v.len()), v);
530 let b = bytes(Precision::F32);
531 let back = from_bytes(&b, Precision::F32, v.len());
532 assert_eq!(back[0], 1.0);
533 assert_eq!(back[1], -2.5);
534 assert!(back[2].is_infinite());
535 }
536
537 #[test]
538 fn precision_names_read_back() {
539 for p in [Precision::F64, Precision::F32] {
540 assert_eq!(Precision::from_name(p.name()), Some(p));
541 }
542 assert_eq!(Precision::from_name("f16"), None);
543 }
544}