1use super::fabric_rtl::{AluOp, CmpKind, FuncUnit, LoopCell, LoopFabric, Src};
13use crate::tensor_recognize::{Axis, DagNode, Fill, Stop, TermDag, Ty, Scalar, ZipOp, ZipTerm};
14use std::collections::HashMap;
15
16fn lit(c: i64) -> String {
19 if i32::try_from(c).is_ok() {
20 if c < 0 {
21 format!("({c}).to[Long]")
22 } else {
23 format!("{c}.to[Long]")
24 }
25 } else {
26 format!("({c}L: Long)")
27 }
28}
29
30fn alu(op: AluOp, a: &str, b: &str) -> String {
31 match op {
32 AluOp::Add => format!("{a} + {b}"),
33 AluOp::Sub => format!("{a} - {b}"),
34 AluOp::Mul => format!("{a} * {b}"),
35 AluOp::And => format!("{a} & {b}"),
36 AluOp::Or => format!("{a} | {b}"),
37 AluOp::Xor => format!("{a} ^ {b}"),
38 AluOp::Shl => format!("{a} << ({b} & 63.to[Long]).to[Int]"),
39 AluOp::Shr => format!("{a} >> ({b} & 63.to[Long]).to[Int]"),
40 AluOp::UShr => format!("{a} >>> ({b} & 63.to[Long]).to[Int]"),
41 }
42}
43
44fn cmp(c: CmpKind) -> &'static str {
45 match c {
46 CmpKind::Lt => "<",
47 CmpKind::Gt => ">",
48 CmpKind::Le => "<=",
49 CmpKind::Ge => ">=",
50 CmpKind::Eq => "===",
51 CmpKind::Ne => "=!=",
52 }
53}
54
55fn accel_block(
58 lf: &LoopFabric,
59 pad: &str,
60 arg_in: &dyn Fn(usize) -> String,
61 const_params: Option<&[i64]>,
62) -> Result<String, String> {
63 if lf.seq_input.is_some() || lf.seq_heap_ptr.is_some() {
64 return Err("sequence loops (`Repeat for x in xs`) need the DRAM → PMU tile-load stage, which this emitter does not spell yet".into());
65 }
66 if !lf.epilogue.is_empty() || !lf.prologue_heap.is_empty() {
67 return Err(
68 "a post-loop epilogue / prologue heap island is not spelled by this emitter yet".into(),
69 );
70 }
71 let body_in = |k: usize| -> String {
72 if k < lf.n_state {
73 format!("c{k}")
74 } else if k == lf.n_state {
75 "iv".to_string()
76 } else {
77 format!("inv{}", k - lf.n_state - 1)
78 }
79 };
80 let over_circuit = |s: &Src| match s {
81 Src::In(k) => arg_in(*k),
82 Src::Val(v) => format!("v{v}"),
83 Src::Const(c) => lit(*c),
84 };
85 let over_body = |s: &Src| match s {
86 Src::In(k) => body_in(*k),
87 Src::Val(v) => format!("v{v}"),
88 Src::Const(c) => lit(*c),
89 };
90 let mut body = Vec::new();
91 for (i, cell) in lf.body.iter().enumerate() {
92 let rhs = match cell {
93 LoopCell::Arith { op, a, b } => alu(*op, &over_body(a), &over_body(b)),
94 LoopCell::Select {
95 cmp: c,
96 ca,
97 cb,
98 t,
99 e,
100 } => {
101 format!(
102 "mux({} {} {}, {}, {})",
103 over_body(ca),
104 cmp(*c),
105 over_body(cb),
106 over_body(t),
107 over_body(e)
108 )
109 }
110 LoopCell::Func {
111 unit: FuncUnit::DivS,
112 a,
113 b,
114 } => format!("{} / {}", over_body(a), over_body(b)),
115 LoopCell::Func {
116 unit: FuncUnit::ModS,
117 a,
118 b,
119 } => format!("{} % {}", over_body(a), over_body(b)),
120 LoopCell::Func { unit, .. } => {
121 return Err(format!(
122 "functional unit `{unit:?}` has no Spatial spelling in this emitter yet"
123 ));
124 }
125 LoopCell::MemRead { .. }
126 | LoopCell::HeapRead { .. }
127 | LoopCell::HeapWrite { .. }
128 | LoopCell::HeapAlloc { .. } => {
129 return Err("memory tiles (sequence / heap) need a PMU-resident buffer this emitter does not spell yet".into());
130 }
131 LoopCell::VectorArith { .. } => {
132 return Err("packed-SIMD tiles are not spelled by this emitter yet".into());
133 }
134 };
135 body.push(format!("{pad} val v{i} = {rhs}"));
136 }
137 let bound = |s: &Src| match s {
141 Src::Const(c) => c.to_string(),
142 Src::In(k) if const_params.is_some() => const_params.unwrap()[*k].to_string(),
143 other => format!("({}).to[Int]", over_circuit(other)),
144 };
145 let lo = over_circuit(&lf.lo);
152 let mut out = String::new();
153 out.push_str(&format!("{pad}Accel {{\n"));
154 for (j, inv) in lf.invariants.iter().enumerate() {
155 out.push_str(&format!("{pad} val inv{j} = {}\n", over_circuit(inv)));
156 }
157 for k in 0..lf.n_state {
158 out.push_str(&format!("{pad} val s{k} = Reg[Long]\n"));
159 }
160 out.push_str(&format!(
161 "{pad} Foreach({} until {} + 1) {{ i =>\n{pad} val iv = i.to[Long]\n",
162 bound(&lf.lo),
163 bound(&lf.hi)
164 ));
165 for (k, init) in lf.init.iter().enumerate() {
166 out.push_str(&format!(
167 "{pad} val c{k} = mux(iv === {lo}, {}, s{k}.value)\n",
168 over_circuit(init)
169 ));
170 }
171 for line in &body {
172 out.push_str(line);
173 out.push('\n');
174 }
175 for (k, next) in lf.next.iter().enumerate() {
176 out.push_str(&format!("{pad} val n{k} = {}\n", over_body(next)));
177 }
178 for k in 0..lf.n_state {
179 out.push_str(&format!("{pad} s{k} := n{k}\n"));
180 }
181 out.push_str(&format!("{pad} }}\n"));
182 out.push_str(&format!("{pad} out := s{}.value\n", lf.result_state));
183 out.push_str(&format!("{pad}}}\n"));
184 Ok(out)
185}
186
187pub fn emit_spatial_counted_loop_test(
193 lf: &LoopFabric,
194 class_name: &str,
195 params: &[i64],
196 gold: i64,
197) -> Result<String, String> {
198 if lf.n_in != params.len() {
199 return Err(format!(
200 "{} parameters but {} runtime args",
201 lf.n_in,
202 params.len()
203 ));
204 }
205 let accel = accel_block(lf, " ", &|k| format!("in{k}.value"), Some(params))?;
206 let mut out = String::new();
207 out.push_str(&format!(
208 "// Generated by LOGOS (logicaffeine_compile::codegen_sva::spatial_emit) — Spatial regression app for the\n// counted loop `{class_name}`: runtime args {params:?} (baked into the loop bounds), expected {gold} (the LOGOS VM's answer).\n"
209 ));
210 out.push_str("import spatial.dsl._\n\n");
211 out.push_str(&format!(
212 "@spatial class {class_name} extends SpatialTest {{\n"
213 ));
214 out.push_str(&format!(
215 " override def runtimeArgs: Args = \"{}\"\n\n",
216 params
217 .iter()
218 .map(|p| p.to_string())
219 .collect::<Vec<_>>()
220 .join(" ")
221 ));
222 out.push_str(" def main(args: Array[String]): Unit = {\n");
223 for k in 0..lf.n_in {
224 out.push_str(&format!(
225 " val in{k} = ArgIn[Long]\n setArg(in{k}, args({k}).to[Long])\n"
226 ));
227 }
228 out.push_str(" val out = ArgOut[Long]\n");
229 out.push_str(&accel);
230 out.push_str(&format!(" val gold = {}\n", lit(gold)));
231 out.push_str(" val result = getArg(out)\n");
232 out.push_str(" val cksum = result == gold\n");
233 out.push_str(&format!(" println(r\"result $result gold $gold\")\n println(\"PASS: \" + cksum + \" ({class_name})\")\n assert(cksum)\n"));
234 out.push_str(" }\n}\n");
235 Ok(out)
236}
237
238pub fn emit_spatial_counted_loop(lf: &LoopFabric, fn_name: &str) -> Result<String, String> {
241 let accel = accel_block(lf, " ", &|k| format!("in{k}.value"), None)?;
242 let mut out = String::new();
243 out.push_str(&format!(
244 "// Generated by LOGOS (logicaffeine_compile::codegen_sva::spatial_emit) from `{fn_name}` — Spatial source\n// (Koeplinger et al., PLDI 2018) for the open Plasticine toolchain. LOGOS `Int` is a 64-bit `Long`.\n"
245 ));
246 out.push_str("import spatial.dsl._\n\n");
247 out.push_str(&format!(
248 "@spatial object Logos_{fn_name} extends SpatialApp {{\n"
249 ));
250 out.push_str(" def main(args: Array[String]): Unit = {\n");
251 for k in 0..lf.n_in {
252 out.push_str(&format!(
253 " val in{k} = ArgIn[Long]\n setArg(in{k}, args({k}).to[Long])\n"
254 ));
255 }
256 out.push_str(" val out = ArgOut[Long]\n");
257 out.push_str(&accel);
258 out.push_str(&format!(" println(r\"{fn_name} = ${{getArg(out)}}\")\n"));
259 out.push_str(" }\n}\n");
260 Ok(out)
261}
262
263#[derive(Clone, Copy, Debug, PartialEq, Eq)]
272pub struct GemmUnroll {
273 pub ii: usize,
275 pub jj: usize,
276 pub kk: usize,
278 pub i: usize,
280 pub j: usize,
281}
282
283impl Default for GemmUnroll {
284 fn default() -> Self {
286 GemmUnroll {
287 ii: 1,
288 jj: 1,
289 kk: 1,
290 i: 1,
291 j: 1,
292 }
293 }
294}
295
296impl GemmUnroll {
297 pub const MAX_PER_LOOP: usize = 3;
319 pub const MAX_TOTAL_UNROLL: usize = 4;
320
321 pub const PLACEABLE_UNROLL: usize = Self::MAX_TOTAL_UNROLL;
323
324 fn divisors(extent: usize, cap: usize) -> Vec<usize> {
326 (1..=extent.min(cap).max(1))
327 .rev()
328 .filter(|f| extent % f == 0)
329 .collect()
330 }
331
332 pub fn search(ts: usize, its: usize, k_chunks: usize, budget: usize) -> GemmUnroll {
349 Self::search_nest(ts, its, k_chunks, 1, 1, budget)
350 }
351
352 pub fn search_nest(
355 ts: usize,
356 its: usize,
357 k_chunks: usize,
358 m_tiles: usize,
359 n_tiles: usize,
360 budget: usize,
361 ) -> GemmUnroll {
362 let budget = budget.max(1);
363 let per = Self::MAX_PER_LOOP;
364 let mut best = GemmUnroll::default();
365 let mut best_key = (1usize, 0i32);
366 for &j in &Self::divisors(ts, per) {
367 for &i in &Self::divisors(its, per) {
368 for &kk in &Self::divisors(k_chunks, per) {
369 for &jj in &Self::divisors(n_tiles, per) {
370 for &ii in &Self::divisors(m_tiles, per) {
371 let product = j * i * kk * jj * ii;
372 if product > budget {
373 continue;
374 }
375 let pref = 4 * (j > 1) as i32
382 + 3 * (i > 1) as i32
383 + 2 * (kk > 1) as i32
384 + (jj > 1) as i32;
385 if (product, pref) > best_key {
386 best_key = (product, pref);
387 best = GemmUnroll { ii, jj, kk, i, j };
388 }
389 }
390 }
391 }
392 }
393 }
394 best
395 }
396
397 pub fn choose(ts: usize, its: usize, ip: usize) -> GemmUnroll {
399 let _ = ip;
400 Self::search(ts, its, 1, Self::MAX_TOTAL_UNROLL)
401 }
402}
403
404pub fn emit_spatial_gemm_test(
405 m: usize,
406 n: usize,
407 k: usize,
408 ts: usize,
409 its: usize,
410 ip: usize,
411 class_name: &str,
412) -> Result<String, String> {
413 let u = GemmUnroll::search_nest(
416 ts,
417 its,
418 k / ts.max(1),
419 m / its.max(1),
420 n / ts.max(1),
421 GemmUnroll::MAX_TOTAL_UNROLL,
422 );
423 emit_spatial_gemm_test_with(m, n, k, ts, its, ip, u, class_name)
424}
425
426pub fn emit_spatial_gemm_test_with(
429 m: usize,
430 n: usize,
431 k: usize,
432 ts: usize,
433 its: usize,
434 ip: usize,
435 u: GemmUnroll,
436 class_name: &str,
437) -> Result<String, String> {
438 if ts == 0 || its == 0 || ip == 0 {
439 return Err("tiles and lanes are at least 1".into());
440 }
441 if m % its != 0 || n % ts != 0 || k % ts != 0 {
442 return Err(format!("the Spatial GEMM template streams whole tiles: its={its} must divide m={m}, ts={ts} must divide n={n} and k={k}"));
443 }
444 if ts % ip != 0 {
445 return Err(format!(
446 "the {ip}-lane reduce must divide the k-chunk ts={ts}"
447 ));
448 }
449 let mut o = String::new();
450 o.push_str(&format!(
451 "// Generated by LOGOS (logicaffeine_compile::codegen_sva::spatial_emit) from a LOGOS matmul recognized as\n// C[{m}x{n}] = A[{m}x{k}] · B[{k}x{n}]: Spatial's GEMM structure (regression/GEMM.scala) with our shape, tiles\n// its={its} ts={ts} and {ip}-lane reduce. Gold on the host; the harness's PASS line is what the ledger parses.\n"
452 ));
453 o.push_str("import spatial.dsl._\n\n");
454 o.push_str(&format!(
455 "@spatial class {class_name} extends SpatialTest {{\n"
456 ));
457 o.push_str(" type T = Int\n");
458 o.push_str(&format!(" val M = {m}\n val N = {n}\n val K = {k}\n val ts = {ts}\n val its = {its}\n val ip = {ip}\n\n"));
459 o.push_str(" def gemm(a_data: Matrix[T], b_data: Matrix[T], c_init: Matrix[T]) = {\n");
460 o.push_str(" val a_dram = DRAM[T](M, K)\n val b_dram = DRAM[T](K, N)\n val c_dram = DRAM[T](M, N)\n");
461 o.push_str(
462 " setMem(a_dram, a_data)\n setMem(b_dram, b_data)\n setMem(c_dram, c_init)\n",
463 );
464 o.push_str(" Accel {\n");
465 let par = |f: usize| {
471 if f > 1 {
472 format!(" par {f}")
473 } else {
474 String::new()
475 }
476 };
477 o.push_str(&format!(
478 " Foreach(M by its{}, N by ts{}) {{ (ii, jj) =>\n",
479 par(u.ii),
480 par(u.jj)
481 ));
482 o.push_str(" val c_col = SRAM[T](its, ts)\n");
483 o.push_str(&format!(
484 " MemReduce(c_col par ip)(K by ts{}) {{ kk =>\n",
485 par(u.kk)
486 ));
487 o.push_str(" val c_col_partial = SRAM[T](its, ts)\n");
488 o.push_str(" val b_sram = SRAM[T](ts, ts)\n");
489 o.push_str(" b_sram load b_dram(kk::kk+ts, jj::jj+ts par ip)\n");
490 o.push_str(&format!(
491 " Foreach(its by 1{}) {{ i =>\n",
492 par(u.i)
493 ));
494 o.push_str(" val a_sram = SRAM[T](ts)\n");
495 o.push_str(" a_sram load a_dram(ii+i, kk::kk+ts par ip)\n");
496 o.push_str(&format!(
497 " Foreach(ts by 1{}) {{ j =>\n",
498 par(u.j)
499 ));
500 o.push_str(" val dot = Reduce(Reg[T])(ts by 1 par ip) { k =>\n");
501 o.push_str(" b_sram(k, j) * a_sram(k)\n");
502 o.push_str(" } { _ + _ }\n");
503 o.push_str(" Pipe { c_col_partial(i, j) = dot }\n");
504 o.push_str(" }\n");
505 o.push_str(" }\n");
506 o.push_str(" c_col_partial\n");
507 o.push_str(" } { _ + _ }\n");
508 o.push_str(" c_dram(ii::ii+its, jj::jj+ts par ip) store c_col\n");
509 o.push_str(" }\n");
510 o.push_str(" }\n");
511 o.push_str(" getMatrix(c_dram)\n");
512 o.push_str(" }\n\n");
513 o.push_str(" def main(args: Array[String]): Unit = {\n");
514 o.push_str(" val a_data = (0::M, 0::K) { (i, j) => ((i * K + j) % 7).to[T] }\n");
515 o.push_str(" val b_data = (0::K, 0::N) { (i, j) => ((i * N + j) % 5).to[T] }\n");
516 o.push_str(" val c_init = (0::M, 0::N) { (i, j) => 0.to[T] }\n");
517 o.push_str(" val c_gold = (0::M, 0::N) { (i, j) => Array.tabulate(K) { k => a_data(i, k) * b_data(k, j) }.reduce { _ + _ } }\n");
518 o.push_str(" val c_result = gemm(a_data, b_data, c_init)\n");
519 o.push_str(" val cksum = approxEql(c_gold, c_result, margin = 0.05)\n");
520 o.push_str(&format!(
521 " println(\"PASS: \" + cksum + \" ({class_name})\")\n assert(cksum)\n"
522 ));
523 o.push_str(" }\n}\n");
524 Ok(o)
525}
526
527#[derive(Clone, Copy, Debug, PartialEq, Eq)]
531pub struct ZipPar {
532 pub ts: usize,
533 pub ip: usize,
534 pub op: usize,
535}
536
537#[derive(Clone, Copy, Debug, PartialEq, Eq)]
541pub struct OuterPar {
542 pub ts1: usize,
543 pub ts2: usize,
544 pub ip: usize,
545 pub ip1: usize,
546 pub ip2: usize,
547 pub op1: usize,
548 pub op2: usize,
549}
550
551fn at_least_one(knobs: &[(&str, usize)]) -> Result<(), String> {
552 match knobs.iter().find(|(_, v)| *v == 0) {
553 Some((k, _)) => Err(format!("`{k}` is at least 1")),
554 None => Ok(()),
555 }
556}
557
558#[derive(Clone, Debug, PartialEq, Eq)]
561pub struct ZipColumn {
562 pub name: String,
563 pub fill: Fill,
564}
565
566const ZIP_RESERVED: &[&str] = &[
568 "N", "ts", "ip", "op", "T", "out", "accO", "part", "gold", "cksum", "args", "i", "ii", "zipsum", "main",
569];
570
571fn host_fill(f: &Fill) -> Result<String, String> {
573 match f {
574 Fill::Const(c) if i32::try_from(*c).is_ok() => Ok(format!("{c}.to[T]")),
575 Fill::Pattern { modulus, shift: 0 } => Ok(format!("(i % {modulus}).to[T]")),
576 Fill::Pattern { modulus, shift } => Ok(format!("((i % {modulus}) + {shift}).to[T]")),
577 other => Err(format!("a column filled as {other:?} has no host data the app can print")),
578 }
579}
580
581fn fill_at(f: &Fill, t: i64) -> i64 {
583 match f {
584 Fill::Const(c) => *c,
585 Fill::Pattern { modulus, shift } => t % modulus + shift,
586 _ => unreachable!("refused by host_fill"),
587 }
588}
589
590fn op_symbol(op: ZipOp) -> &'static str {
592 match op {
593 ZipOp::Add => "+",
594 ZipOp::Sub => "-",
595 ZipOp::Mul => "*",
596 ZipOp::Div => "/",
597 ZipOp::Pow => "**",
598 ZipOp::Lt => "<",
599 ZipOp::Gt => ">",
600 ZipOp::Le => "<=",
601 ZipOp::Ge => ">=",
602 ZipOp::Eq => "===",
603 ZipOp::Ne => "=!=",
604 ZipOp::And => "&&",
605 ZipOp::Or => "||",
606 }
607}
608
609fn whole_exponent(x: f64) -> Option<i64> {
613 (x >= 0.0 && x.fract() == 0.0 && x <= f64::from(i32::MAX)).then_some(x as i64)
614}
615
616fn stop_reason(stop: Stop) -> &'static str {
618 match stop {
619 Stop::IntRange => "leaves the 32-bit Int the Spatial app computes in (the VM's Int is 64-bit)",
620 Stop::RealRange => "leaves the range of the 32-bit Float the Spatial app computes in (the VM's Real is a double)",
621 Stop::ZeroDivisor => "divides by zero",
622 Stop::NegativeExponent => "raises to a negative power",
623 Stop::Mistyped => "is not a value the app computes",
624 }
625}
626
627fn zip_term(t: &ZipTerm, read: &dyn Fn(usize) -> String, host: bool, top: bool) -> String {
630 let wrap = |s: String| if top { s } else { format!("({s})") };
631 match t {
632 ZipTerm::Read(k) => read(*k),
633 ZipTerm::Const(c) => format!("{c}.to[T]"),
634 ZipTerm::Real(b) => format!("{:?}.to[T]", f64::from_bits(*b)),
635 ZipTerm::Abs(x) => format!("abs({})", zip_term(x, read, host, true)),
636 ZipTerm::Op(op, l, r) => {
637 let whole = match (op, &**r) {
638 (ZipOp::Pow, ZipTerm::Const(c)) => whole_exponent(*c as f64),
639 (ZipOp::Pow, ZipTerm::Real(b)) => whole_exponent(f64::from_bits(*b)),
640 _ => None,
641 };
642 let rhs = whole.map_or_else(|| zip_term(r, read, host, false), |n| n.to_string());
643 wrap(format!("{} {} {rhs}", zip_term(l, read, host, false), op_symbol(*op)))
644 }
645 ZipTerm::Select(c, a, b) => {
646 let (c, a, b) = (zip_term(c, read, host, true), zip_term(a, read, host, true), zip_term(b, read, host, true));
647 if host {
648 wrap(format!("if ({c}) {a} else {b}"))
649 } else {
650 format!("mux({c}, {a}, {b})")
651 }
652 }
653 }
654}
655
656pub fn emit_spatial_zip_sum_test(
664 n: usize,
665 columns: &[ZipColumn],
666 term: &ZipTerm,
667 p: ZipPar,
668 class_name: &str,
669) -> Result<String, String> {
670 at_least_one(&[("ts", p.ts), ("ip", p.ip), ("op", p.op)])?;
671 if n % p.ts != 0 {
672 return Err(format!("the Spatial reduce-over-tiles template loads whole tiles: ts={} must divide N={n}", p.ts));
673 }
674 if p.ip > p.ts {
675 return Err(format!("the reduce is {} lanes wide over a tile of {}", p.ip, p.ts));
676 }
677 for (k, c) in columns.iter().enumerate() {
678 let ident = c.name.chars().next().map_or(false, |h| h.is_ascii_alphabetic())
679 && c.name.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_');
680 if !ident || ZIP_RESERVED.contains(&c.name.as_str()) || columns[..k].iter().any(|o| o.name == c.name) {
681 return Err(format!("the column `{}` is not a name the app can declare", c.name));
682 }
683 }
684 if term.ty(&vec![Ty::Int; columns.len()]) != Some(Ty::Int) {
685 return Err("the reduce-over-tiles app sums an Int term of Int columns".to_string());
686 }
687 let fills = columns.iter().map(|c| host_fill(&c.fill)).collect::<Result<Vec<_>, _>>()?;
688 let (lo, hi) = (i32::MIN as i128, i32::MAX as i128);
689 let mut magnitude: i128 = 0;
690 for t in 0..n as i64 {
691 let row: Vec<Scalar> = columns.iter().map(|c| Scalar::Int(fill_at(&c.fill, t))).collect();
692 let v = match term.value(&row, (lo, hi), f64::MAX) {
693 Ok(Scalar::Int(v)) => v,
694 Ok(_) => return Err(format!("element {t} {}", stop_reason(Stop::Mistyped))),
695 Err(stop) => return Err(format!("element {t} {}", stop_reason(stop))),
696 };
697 magnitude += (v as i128).abs();
698 if magnitude > hi {
699 return Err(format!("the sum leaves the 32-bit Int the Spatial app computes in by element {t} (the VM's Int is 64-bit)"));
700 }
701 }
702 let ZipPar { ts, ip, op } = p;
703 let accel = zip_term(term, &|k| format!("{}Blk(ii)", columns[k].name), false, true);
704 let host = zip_term(term, &|k| format!("{}(i)", columns[k].name), true, true);
705 let mut o = String::new();
706 o.push_str(&format!(
707 "// Generated by LOGOS (logicaffeine_compile::codegen_sva::spatial_emit) from a LOGOS loop recognized as the zip\n// sum Σ_t {host} over N={n}: Spatial's reduce-over-tiles structure (regression/DotProduct.scala,\n// regression/TPCHQ6.scala) at ts={ts} ip={ip} op={op}, over the program's own data. The PASS line is what the ledger parses.\n"
708 ));
709 o.push_str("import spatial.dsl._\n\n");
710 o.push_str(&format!("@spatial class {class_name} extends SpatialTest {{\n"));
711 o.push_str(" type T = Int\n");
712 o.push_str(&format!(" val N = {n}\n val ts = {ts}\n val ip = {ip}\n val op = {op}\n\n"));
713 let params: Vec<String> = columns.iter().map(|c| format!("{}In: Array[T]", c.name)).collect();
714 o.push_str(&format!(" def zipsum({}) = {{\n", params.join(", ")));
715 for c in columns {
716 o.push_str(&format!(" val {} = DRAM[T](N)\n", c.name));
717 }
718 o.push_str(" val out = ArgOut[T]\n");
719 for c in columns {
720 o.push_str(&format!(" setMem({0}, {0}In)\n", c.name));
721 }
722 o.push_str(" Accel {\n");
723 o.push_str(" val accO = Reg[T](0.to[T])\n");
724 o.push_str(" out := Reduce(accO)(N by ts par op) { i =>\n");
725 for c in columns {
726 o.push_str(&format!(" val {}Blk = SRAM[T](ts)\n", c.name));
727 }
728 o.push_str(" Parallel {\n");
729 for c in columns {
730 o.push_str(&format!(" {0}Blk load {0}(i::i+ts par ip)\n", c.name));
731 }
732 o.push_str(" }\n");
733 o.push_str(" val part = Reg[T]\n");
734 o.push_str(&format!(" Reduce(part)(ts par ip) {{ ii => {accel} }} {{ _ + _ }}\n"));
735 o.push_str(" part.value\n");
736 o.push_str(" } { _ + _ }\n");
737 o.push_str(" }\n");
738 o.push_str(" out\n");
739 o.push_str(" }\n\n");
740 o.push_str(" def main(args: Array[String]): Unit = {\n");
741 for (c, f) in columns.iter().zip(&fills) {
742 o.push_str(&format!(" val {} = Array.tabulate(N) {{ i => {f} }}\n", c.name));
743 }
744 let names: Vec<&str> = columns.iter().map(|c| c.name.as_str()).collect();
745 o.push_str(&format!(" val out = zipsum({})\n", names.join(", ")));
746 o.push_str(&format!(" val gold = Array.tabulate(N) {{ i => {host} }}.reduce {{ _ + _ }}\n"));
747 o.push_str(" val cksum = checkGold(out, gold)\n");
748 o.push_str(&format!(" println(\"PASS: \" + cksum + \" ({class_name})\")\n assert(cksum)\n"));
749 o.push_str(" }\n}\n");
750 Ok(o)
751}
752
753pub fn emit_spatial_outer_test(m: usize, n: usize, p: OuterPar, class_name: &str) -> Result<String, String> {
759 let OuterPar { ts1, ts2, ip, ip1, ip2, op1, op2 } = p;
760 at_least_one(&[("ts1", ts1), ("ts2", ts2), ("ip", ip), ("ip1", ip1), ("ip2", ip2), ("op1", op1), ("op2", op2)])?;
761 if m % ts1 != 0 || n % ts2 != 0 {
762 return Err(format!(
763 "the Spatial OuterProduct template streams whole tiles: ts1={ts1} must divide M={m} and ts2={ts2} must divide N={n}"
764 ));
765 }
766 if ip2 > ts1 || ip1 > ts2 {
767 return Err(format!("the tile loops run {ip2} x {ip1} lanes over a {ts1} x {ts2} tile"));
768 }
769 let mut o = String::new();
770 o.push_str(&format!(
771 "// Generated by LOGOS (logicaffeine_compile::codegen_sva::spatial_emit) from a LOGOS nest recognized as the outer\n// product out[{m}x{n}] = a[i]·b[j]: Spatial's OuterProduct structure (regression/OuterProduct.scala) with tiles\n// ts1={ts1} ts2={ts2}, ip={ip} ip1={ip1} ip2={ip2} op1={op1} op2={op2}. Gold on the host; the PASS line is what the ledger parses.\n"
772 ));
773 o.push_str("import spatial.dsl._\n\n");
774 o.push_str(&format!("@spatial class {class_name} extends SpatialTest {{\n"));
775 o.push_str(" type T = Int\n");
776 o.push_str(&format!(
777 " val M = {m}\n val N = {n}\n val ts1 = {ts1}\n val ts2 = {ts2}\n val ip = {ip}\n val ip1 = {ip1}\n val ip2 = {ip2}\n val op1 = {op1}\n val op2 = {op2}\n\n"
778 ));
779 o.push_str(" def outerproduct(a: Array[T], b: Array[T]) = {\n");
780 o.push_str(" val vec1 = DRAM[T](M)\n val vec2 = DRAM[T](N)\n val out = DRAM[T](M, N)\n");
781 o.push_str(" setMem(vec1, a)\n setMem(vec2, b)\n");
782 o.push_str(" Accel {\n");
783 o.push_str(" Foreach(M by ts1 par op1) { i =>\n");
784 o.push_str(" val b1 = SRAM[T](ts1)\n");
785 o.push_str(" b1 load vec1(i::i+ts1 par ip)\n");
786 o.push_str(" Foreach(N by ts2 par op2) { j =>\n");
787 o.push_str(" val b2 = SRAM[T](ts2)\n");
788 o.push_str(" b2 load vec2(j::j+ts2 par ip)\n");
789 o.push_str(" val outTile = SRAM[T](ts1, ts2)\n");
790 o.push_str(" Foreach(ts1 par ip2, ts2 par ip1) { (ii, jj) => outTile(ii, jj) = b1(ii) * b2(jj) }\n");
791 o.push_str(" out(i::i+ts1, j::j+ts2 par ip) store outTile\n");
792 o.push_str(" }\n");
793 o.push_str(" }\n");
794 o.push_str(" }\n");
795 o.push_str(" out\n");
796 o.push_str(" }\n\n");
797 o.push_str(" def main(args: Array[String]): Unit = {\n");
798 o.push_str(" val a = Array.tabulate(M) { i => (i % 7).to[T] }\n");
799 o.push_str(" val b = Array.tabulate(N) { i => (i % 5).to[T] }\n");
800 o.push_str(" val out = outerproduct(a, b)\n");
801 o.push_str(" val gold = (0::M, 0::N) { (i, j) => a(i) * b(j) }\n");
802 o.push_str(" val cksum = checkGold(out, gold, 0)\n");
803 o.push_str(&format!(" println(\"PASS: \" + cksum + \" ({class_name})\")\n assert(cksum)\n"));
804 o.push_str(" }\n}\n");
805 Ok(o)
806}
807
808#[derive(Clone, Copy, Debug, PartialEq, Eq)]
812pub struct GdaPar {
813 pub ts: usize,
814 pub op: usize,
815 pub mp1: usize,
816 pub mp2: usize,
817 pub ip: usize,
818}
819
820#[derive(Clone, Debug, PartialEq, Eq)]
822pub struct GdaColumn {
823 pub name: String,
824 pub axis: Axis,
825 pub fill: Fill,
826}
827
828pub fn emit_spatial_gda_test(
835 rows: usize,
836 cols: usize,
837 columns: &[GdaColumn],
838 term: &ZipTerm,
839 p: GdaPar,
840 class_name: &str,
841) -> Result<String, String> {
842 let GdaPar { ts, op, mp1, mp2, ip } = p;
843 at_least_one(&[("ts", ts), ("op", op), ("mp1", mp1), ("mp2", mp2), ("ip", ip)])?;
844 if rows % ts != 0 {
845 return Err(format!("the Spatial GDA template loads whole tiles: ts={ts} must divide R={rows}"));
846 }
847 for (k, c) in columns.iter().enumerate() {
848 let ident = c.name.chars().next().map_or(false, |h| h.is_ascii_alphabetic())
849 && c.name.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_');
850 let reserved = ["R", "C", "ts", "op", "mp1", "mp2", "ip", "T", "sigma", "sigmaOut", "sigmaBlk", "sigmaTile",
851 "subTile", "gold", "result", "cksum", "args", "r", "rr", "cc", "ii", "jj", "i", "j", "gda", "main"];
852 if !ident || reserved.contains(&c.name.as_str()) || columns[..k].iter().any(|o| o.name == c.name) {
853 return Err(format!("the array `{}` is not a name the app can declare", c.name));
854 }
855 }
856 if term.ty(&vec![Ty::Int; columns.len()]) != Some(Ty::Int) {
857 return Err("the GDA app centres Int rows on Int means".to_string());
858 }
859 let fills = columns.iter().map(|c| host_fill(&c.fill)).collect::<Result<Vec<_>, _>>()?;
860 let (lo, hi) = (i32::MIN as i128, i32::MAX as i128);
861 let d = |r: usize, c: usize| -> Result<i64, String> {
862 let row: Vec<Scalar> = columns
863 .iter()
864 .map(|col| {
865 let t = match col.axis {
866 Axis::Row => r,
867 Axis::Col => c,
868 Axis::RowCol => r * cols + c,
869 };
870 Scalar::Int(fill_at(&col.fill, t as i64))
871 })
872 .collect::<Vec<_>>();
873 match term.value(&row, (lo, hi), f64::MAX) {
874 Ok(Scalar::Int(v)) => Ok(v),
875 Ok(_) => Err(format!("row {r} column {c} {}", stop_reason(Stop::Mistyped))),
876 Err(stop) => Err(format!("row {r} column {c} {}", stop_reason(stop))),
877 }
878 };
879 let dr: Vec<Vec<i64>> = (0..rows).map(|r| (0..cols).map(|c| d(r, c)).collect::<Result<_, _>>()).collect::<Result<_, _>>()?;
880 for i in 0..cols {
881 for j in 0..cols {
882 let magnitude: i128 = dr.iter().map(|v| (v[i] as i128 * v[j] as i128).abs()).sum();
883 if magnitude > hi {
884 return Err(format!("covariance [{i}][{j}] leaves the 32-bit Int the Spatial app computes in (the VM's Int is 64-bit)"));
885 }
886 }
887 }
888 let tile = |c: &GdaColumn| format!("{}Tile", c.name);
889 let accel = zip_term(term, &|k| match columns[k].axis {
890 Axis::Row => format!("{}(rr)", tile(&columns[k])),
891 Axis::Col => format!("{}(cc)", tile(&columns[k])),
892 Axis::RowCol => format!("{}(rr, cc)", tile(&columns[k])),
893 }, false, true);
894 let host = |at: &str| zip_term(term, &|k| match columns[k].axis {
895 Axis::Row => format!("{}(r)", columns[k].name),
896 Axis::Col => format!("{}({at})", columns[k].name),
897 Axis::RowCol => format!("{}(r * C + {at})", columns[k].name),
898 }, true, false);
899 let loads = |lines: Vec<String>, pad: &str, o: &mut String| {
900 if lines.len() > 1 {
901 o.push_str(&format!("{pad}Parallel {{\n"));
902 for l in lines {
903 o.push_str(&format!("{pad} {l}\n"));
904 }
905 o.push_str(&format!("{pad}}}\n"));
906 } else {
907 for l in lines {
908 o.push_str(&format!("{pad}{l}\n"));
909 }
910 }
911 };
912 let mut o = String::new();
913 o.push_str(&format!(
914 "// Generated by LOGOS (logicaffeine_compile::codegen_sva::spatial_emit) from a LOGOS nest recognized as the scatter\n// accumulation S = Σ_r d_r d_rᵀ over R={rows} rows of C={cols}, d_r[c] = {}: Spatial's GDA structure\n// (regression/GDA.scala) at ts={ts} op={op} mp1={mp1} mp2={mp2} ip={ip}, over the program's own data. The PASS line is what the ledger parses.\n",
915 host("c")
916 ));
917 o.push_str("import spatial.dsl._\n\n");
918 o.push_str(&format!("@spatial class {class_name} extends SpatialTest {{\n"));
919 o.push_str(" type T = Int\n");
920 o.push_str(&format!(" val R = {rows}\n val C = {cols}\n val ts = {ts}\n val op = {op}\n val mp1 = {mp1}\n val mp2 = {mp2}\n val ip = {ip}\n\n"));
921 let params: Vec<String> = columns.iter().map(|c| format!("{}In: Array[T]", c.name)).collect();
922 o.push_str(&format!(" def gda({}) = {{\n", params.join(", ")));
923 for c in columns {
924 let dims = match c.axis {
925 Axis::Row => "R",
926 Axis::Col => "C",
927 Axis::RowCol => "R, C",
928 };
929 o.push_str(&format!(" val {} = DRAM[T]({dims})\n", c.name));
930 }
931 o.push_str(" val sigma = DRAM[T](C, C)\n");
932 for c in columns {
933 o.push_str(&format!(" setMem({0}, {0}In)\n", c.name));
934 }
935 o.push_str(" Accel {\n");
936 let by_axis = |a: Axis| columns.iter().filter(move |c| c.axis == a);
937 for c in by_axis(Axis::Col) {
938 o.push_str(&format!(" val {} = SRAM[T](C)\n", tile(c)));
939 }
940 loads(by_axis(Axis::Col).map(|c| format!("{} load {}(0 :: C par ip)", tile(c), c.name)).collect(), " ", &mut o);
941 o.push_str(" val sigmaOut = SRAM[T](C, C)\n");
942 o.push_str(" MemReduce(sigmaOut par ip)(R by ts par op) { r =>\n");
943 for c in columns.iter().filter(|c| c.axis != Axis::Col) {
944 let dims = if c.axis == Axis::Row { "ts" } else { "ts, C" };
945 o.push_str(&format!(" val {} = SRAM[T]({dims})\n", tile(c)));
946 }
947 loads(
948 columns
949 .iter()
950 .filter(|c| c.axis != Axis::Col)
951 .map(|c| match c.axis {
952 Axis::Row => format!("{} load {}(r :: r + ts par ip)", tile(c), c.name),
953 _ => format!("{} load {}(r :: r + ts, 0 :: C par ip)", tile(c), c.name),
954 })
955 .collect(),
956 " ",
957 &mut o,
958 );
959 o.push_str(" val sigmaBlk = SRAM[T](C, C)\n");
960 o.push_str(" MemReduce(sigmaBlk par ip)(ts par mp1) { rr =>\n");
961 o.push_str(" val subTile = SRAM[T](C)\n val sigmaTile = SRAM[T](C, C)\n");
962 o.push_str(&format!(" Foreach(C par ip) {{ cc => subTile(cc) = {accel} }}\n"));
963 o.push_str(" Foreach(C by 1 par mp2) { ii =>\n");
964 o.push_str(" Foreach(C par ip) { jj => sigmaTile(ii, jj) = subTile(ii) * subTile(jj) }\n");
965 o.push_str(" }\n sigmaTile\n } { _ + _ }\n } { _ + _ }\n");
966 o.push_str(" sigma(0 :: C, 0 :: C par ip) store sigmaOut\n }\n getMem(sigma)\n }\n\n");
967 o.push_str(" def main(args: Array[String]): Unit = {\n");
968 for (c, f) in columns.iter().zip(&fills) {
969 let n = match c.axis {
970 Axis::Row => "R",
971 Axis::Col => "C",
972 Axis::RowCol => "R * C",
973 };
974 o.push_str(&format!(" val {} = Array.tabulate({n}) {{ i => {f} }}\n", c.name));
975 }
976 let names: Vec<&str> = columns.iter().map(|c| c.name.as_str()).collect();
977 o.push_str(&format!(" val result = gda({})\n", names.join(", ")));
978 o.push_str(&format!(
979 " val gold = Array.tabulate(C) {{ i => Array.tabulate(C) {{ j => Array.tabulate(R) {{ r => {} * {} }}.reduce {{ _ + _ }} }} }}.flatten\n",
980 host("i"),
981 host("j")
982 ));
983 o.push_str(" val cksum = approxEql(gold, result)\n");
984 o.push_str(&format!(" println(\"PASS: \" + cksum + \" ({class_name})\")\n assert(cksum)\n }}\n}}\n"));
985 Ok(o)
986}
987
988#[derive(Clone, Copy, Debug, PartialEq, Eq)]
992pub struct MapPar {
993 pub ts: usize,
994 pub op: usize,
995 pub ip: usize,
996}
997
998#[derive(Clone, Debug, PartialEq, Eq)]
1001pub struct MapColumn {
1002 pub name: String,
1003 pub ty: Ty,
1004 pub fill: Fill,
1005}
1006
1007fn spatial_ty(t: Ty) -> &'static str {
1009 match t {
1010 Ty::Int => "Int",
1011 Ty::Real => "Float",
1012 Ty::Bool => "Bit",
1013 }
1014}
1015
1016const MAP_RESERVED: &[&str] = &[
1018 "N", "ts", "op", "ip", "i", "j", "r", "g", "gold", "result", "cksum", "zipmap", "main", "args", "abs", "mux", "min",
1019 "max", "pow", "sqrt", "exp", "ln", "Int", "Float", "Bit", "Array", "DRAM", "FIFO", "SRAM", "Reg", "Foreach",
1020 "Reduce", "Accel", "val", "var", "def", "type", "if", "else", "for", "while", "do", "new", "class", "object",
1021 "trait", "match", "case", "return", "true", "false", "null", "this", "super", "import", "package", "with",
1022 "extends", "override", "lazy", "yield", "try", "catch", "finally", "throw",
1023];
1024
1025fn literal(v: Scalar, at: Ty) -> String {
1028 let real = |x: f64| if x.is_sign_negative() { format!("({x:?}).to[Float]") } else { format!("{x:?}.to[Float]") };
1029 match (v, at) {
1030 (Scalar::Int(c), Ty::Real) => real(c as f64),
1031 (Scalar::Int(c), _) if c < 0 => format!("({c}).to[Int]"),
1032 (Scalar::Int(c), _) => format!("{c}.to[Int]"),
1033 (Scalar::Real(x), _) => real(x),
1034 (Scalar::Bool(b), _) => format!("{b}.to[Bit]"),
1035 }
1036}
1037
1038fn host_data(f: &Fill, ty: Ty, at: &str) -> Result<String, String> {
1040 let int = |c: i64| i32::try_from(c).is_ok();
1041 let at = if at.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { at.to_string() } else { format!("({at})") };
1042 match (f, ty) {
1043 (Fill::Const(c), Ty::Int) if int(*c) => Ok(literal(Scalar::Int(*c), Ty::Int)),
1044 (Fill::Pattern { modulus, shift: 0 }, Ty::Int) if int(*modulus) => Ok(format!("({at} % {modulus}).to[Int]")),
1045 (Fill::Pattern { modulus, shift }, Ty::Int) if int(*modulus) && int(*shift) && int(modulus + shift) => {
1046 Ok(format!("(({at} % {modulus}) + {shift}).to[Int]"))
1047 }
1048 (Fill::ConstF(b), Ty::Real) => Ok(literal(Scalar::Real(f64::from_bits(*b)), Ty::Real)),
1049 (Fill::RealPattern { modulus, shift, divisor }, Ty::Real) if int(*modulus) && int(*shift) && int(modulus + shift) => {
1050 Ok(format!(
1051 "((({at} % {modulus}) + {shift}).to[Float] / {})",
1052 literal(Scalar::Real(f64::from_bits(*divisor)), Ty::Real)
1053 ))
1054 }
1055 (other, ty) => Err(format!("a column of {ty:?} filled as {other:?} has no host data the app can print")),
1056 }
1057}
1058
1059struct DagPrinter<'d> {
1064 dag: &'d TermDag,
1065 tys: &'d [Ty],
1066 read: &'d dyn Fn(usize) -> String,
1067 host: bool,
1068 names: HashMap<usize, String>,
1069 lines: Vec<String>,
1070}
1071
1072impl<'d> DagPrinter<'d> {
1073 fn print(dag: &'d TermDag, tys: &'d [Ty], read: &'d dyn Fn(usize) -> String, host: bool) -> (Vec<String>, String) {
1074 let mut p = DagPrinter { dag, tys, read, host, names: HashMap::new(), lines: Vec::new() };
1075 let root = p.at(dag.root, tys[dag.root]);
1076 (p.lines, root)
1077 }
1078
1079 fn at(&mut self, i: usize, want: Ty) -> String {
1081 let own = self.tys[i];
1082 let text = match self.dag.nodes[i] {
1083 DagNode::Const(c) => return literal(Scalar::Int(c), want),
1084 DagNode::Real(b) => return literal(Scalar::Real(f64::from_bits(b)), want),
1085 DagNode::Read(k) => (self.read)(k),
1086 _ if self.names.contains_key(&i) => self.names[&i].clone(),
1087 node => {
1088 let expr = self.node(node, own);
1089 if self.dag.uses[i] > 1 {
1090 let name = format!("t{}", self.names.len());
1091 self.lines.push(format!("val {name} = {expr}"));
1092 self.names.insert(i, name.clone());
1093 name
1094 } else {
1095 expr
1096 }
1097 }
1098 };
1099 if own == Ty::Int && want == Ty::Real {
1100 format!("{text}.to[Float]")
1101 } else {
1102 text
1103 }
1104 }
1105
1106 fn node(&mut self, node: DagNode, own: Ty) -> String {
1107 match node {
1108 DagNode::Abs(x) => format!("abs({})", self.at(x, own)),
1109 DagNode::Op(op, l, r) => {
1110 let operand = if op.connects() {
1111 Ty::Bool
1112 } else if !op.compares() {
1113 own
1114 } else if self.tys[l] == Ty::Real || self.tys[r] == Ty::Real {
1115 Ty::Real
1116 } else {
1117 Ty::Int
1118 };
1119 let whole = match (op, self.dag.nodes[r]) {
1120 (ZipOp::Pow, DagNode::Const(c)) => whole_exponent(c as f64),
1121 (ZipOp::Pow, DagNode::Real(b)) => whole_exponent(f64::from_bits(b)),
1122 _ => None,
1123 };
1124 let a = self.at(l, operand);
1125 let b = whole.map_or_else(|| self.at(r, operand), |n| n.to_string());
1126 format!("({a} {} {b})", op_symbol(op))
1127 }
1128 DagNode::Select(c, a, b) => {
1129 let c = self.at(c, Ty::Bool);
1130 let (a, b) = (self.at(a, own), self.at(b, own));
1131 if self.host {
1132 format!("(if ({c}) {a} else {b})")
1133 } else {
1134 format!("mux({c}, {a}, {b})")
1135 }
1136 }
1137 DagNode::Read(_) | DagNode::Const(_) | DagNode::Real(_) => unreachable!("leaves are printed in place"),
1138 }
1139 }
1140}
1141
1142#[allow(clippy::too_many_arguments)]
1154pub fn emit_spatial_zip_map_test(
1155 n: usize,
1156 columns: &[MapColumn],
1157 declared: &[usize],
1158 term: &ZipTerm,
1159 out: &str,
1160 p: MapPar,
1161 pir_args: Option<&str>,
1162 class_name: &str,
1163) -> Result<String, String> {
1164 let MapPar { ts, op, ip } = p;
1165 at_least_one(&[("ts", ts), ("op", op), ("ip", ip)])?;
1166 if n % ts != 0 {
1167 return Err(format!("the Spatial BlackScholes template streams whole tiles: ts={ts} must divide N={n}"));
1168 }
1169 if ip > ts {
1170 return Err(format!("the map is {ip} lanes wide over a tile of {ts}"));
1171 }
1172 let mut order = declared.to_vec();
1173 order.sort_unstable();
1174 if order != (0..columns.len()).collect::<Vec<_>>() {
1175 return Err("the declaration order is not an order of the columns".to_string());
1176 }
1177 let reads: Vec<Ty> = columns.iter().map(|c| c.ty).collect();
1178 let dag = TermDag::of(term);
1179 let tys = dag.types(&reads).ok_or("the term is ill-typed over its columns")?;
1180 let out_ty = tys[dag.root];
1181 if out_ty == Ty::Bool || reads.contains(&Ty::Bool) {
1182 return Err("the app maps Int and Float columns to an Int or Float column".to_string());
1183 }
1184 let mut taken: Vec<String> = Vec::new();
1185 for name in columns.iter().map(|c| c.name.as_str()).chain([out]) {
1186 let ident = name.chars().next().map_or(false, |h| h.is_ascii_lowercase())
1187 && name.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_');
1188 if !ident || MAP_RESERVED.contains(&name) {
1189 return Err(format!("`{name}` is not a name the app can declare"));
1190 }
1191 taken.extend([name.to_string(), format!("{name}In"), format!("{name}Blk"), format!("{name}V")]);
1192 }
1193 let shared = (0..dag.nodes.len()).filter(|i| dag.uses[*i] > 1).count();
1194 taken.extend((0..shared).map(|k| format!("t{k}")));
1195 let mut seen = std::collections::HashSet::new();
1196 if let Some(clash) = taken.iter().find(|t| !seen.insert(t.as_str()) || MAP_RESERVED.contains(&t.as_str())) {
1197 return Err(format!("the app would declare `{clash}` twice"));
1198 }
1199 for node in &dag.nodes {
1200 if let DagNode::Const(c) = node {
1201 if i32::try_from(*c).is_err() {
1202 return Err(format!("the constant {c} {}", stop_reason(Stop::IntRange)));
1203 }
1204 }
1205 }
1206 let fills = columns.iter().map(|c| host_data(&c.fill, c.ty, "i")).collect::<Result<Vec<_>, _>>()?;
1207 let int_divisors: Vec<usize> = dag
1208 .nodes
1209 .iter()
1210 .filter_map(|node| match *node {
1211 DagNode::Op(ZipOp::Div, l, r) if tys[l] == Ty::Int && tys[r] == Ty::Int => Some(r),
1212 _ => None,
1213 })
1214 .collect();
1215 let (ints, reals) = ((i32::MIN as i128, i32::MAX as i128), f32::MAX as f64);
1216 for t in 0..n as i64 {
1217 let row: Vec<Scalar> = columns.iter().map(|c| c.fill.element(t).expect("host data has elements")).collect();
1218 dag.value(&row, ints, reals).map_err(|stop| format!("element {t} {}", stop_reason(stop)))?;
1219 for d in &int_divisors {
1220 if dag.value_of(*d, &row, ints, reals) == Ok(Scalar::Int(0)) {
1221 return Err(format!("element {t} {} on a path the program does not take, which the app's host would still evaluate", stop_reason(Stop::ZeroDivisor)));
1222 }
1223 }
1224 }
1225 let accel_read = |k: usize| format!("{}V", columns[k].name);
1226 let (accel_lines, accel_root) = DagPrinter::print(&dag, &tys, &accel_read, false);
1227 let host_read = |k: usize| format!("{}(i)", columns[k].name);
1228 let (host_lines, host_root) = DagPrinter::print(&dag, &tys, &host_read, true);
1229 let (oty, decl) = (spatial_ty(out_ty), declared.iter().map(|k| &columns[*k]).collect::<Vec<_>>());
1230 let mut o = String::new();
1231 o.push_str(&format!(
1232 "// Generated by LOGOS (logicaffeine_compile::codegen_sva::spatial_emit) from a LOGOS loop recognized as the zip\n// map {out}[t] = f({}) over N={n}: Spatial's BlackScholes structure (regression/BlackScholes.scala) at ts={ts} op={op}\n// ip={ip}, over the program's own data. The PASS line is what the ledger parses.\n",
1233 columns.iter().map(|c| format!("{}[t]", c.name)).collect::<Vec<_>>().join(", ")
1234 ));
1235 o.push_str("import spatial.dsl._\n\n");
1236 o.push_str(&format!("@spatial class {class_name} extends SpatialTest {{\n"));
1237 if let Some(args) = pir_args {
1238 o.push_str(&format!(" override def pirArgs = super.pirArgs + {args:?}\n"));
1239 }
1240 o.push_str(&format!(" val N = {n}\n val ts = {ts}\n val op = {op}\n val ip = {ip}\n\n"));
1241 let params: Vec<String> = decl.iter().map(|c| format!("{}In: Array[{}]", c.name, spatial_ty(c.ty))).collect();
1242 o.push_str(&format!(" def zipmap({}) = {{\n", params.join(", ")));
1243 for c in &decl {
1244 o.push_str(&format!(" val {} = DRAM[{}](N)\n", c.name, spatial_ty(c.ty)));
1245 }
1246 o.push_str(&format!(" val {out} = DRAM[{oty}](N)\n"));
1247 for c in &decl {
1248 o.push_str(&format!(" setMem({0}, {0}In)\n", c.name));
1249 }
1250 o.push_str(" Accel {\n");
1251 o.push_str(" Foreach(N by ts par op) { i =>\n");
1252 for c in &decl {
1253 o.push_str(&format!(" val {}Blk = FIFO[{}](ts)\n", c.name, spatial_ty(c.ty)));
1254 }
1255 o.push_str(&format!(" val {out}Blk = FIFO[{oty}](ts)\n"));
1256 for c in &decl {
1257 o.push_str(&format!(" {0}Blk load {0}(i::i+ts par ip)\n", c.name));
1258 }
1259 o.push_str(" Foreach(ts par ip) { j =>\n");
1260 for c in columns {
1261 o.push_str(&format!(" val {0}V = {0}Blk.deq\n", c.name));
1262 }
1263 for l in &accel_lines {
1264 o.push_str(&format!(" {l}\n"));
1265 }
1266 o.push_str(&format!(" {out}Blk.enq({accel_root})\n"));
1267 o.push_str(" }\n");
1268 o.push_str(&format!(" {out}(i::i+ts par ip) store {out}Blk\n"));
1269 o.push_str(" }\n }\n");
1270 o.push_str(&format!(" getMem({out})\n }}\n\n"));
1271 o.push_str(" def main(args: Array[String]): Unit = {\n");
1272 for k in declared {
1273 o.push_str(&format!(" val {} = Array.tabulate(N) {{ i => {} }}\n", columns[*k].name, fills[*k]));
1274 }
1275 o.push_str(&format!(
1276 " val result = zipmap({})\n",
1277 decl.iter().map(|c| c.name.as_str()).collect::<Vec<_>>().join(", ")
1278 ));
1279 o.push_str(" val gold = Array.tabulate(N) { i =>\n");
1280 for l in &host_lines {
1281 o.push_str(&format!(" {l}\n"));
1282 }
1283 o.push_str(&format!(" {host_root}\n }}\n"));
1284 if out_ty == Ty::Real {
1285 o.push_str(" val cksum = result.zip(gold) { (r, g) => abs(r - g) <= 0.001.to[Float] * abs(g) + 0.001.to[Float] }.reduce { _ & _ }\n");
1286 } else {
1287 o.push_str(" val cksum = result.zip(gold) { (r, g) => r === g }.reduce { _ & _ }\n");
1288 }
1289 o.push_str(&format!(" println(\"PASS: \" + cksum + \" ({class_name})\")\n assert(cksum)\n"));
1290 o.push_str(" }\n}\n");
1291 Ok(o)
1292}
1293
1294#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1298pub struct KmeansPar {
1299 pub ts: usize,
1300 pub ip: usize,
1301 pub op: usize,
1302 pub mp1: usize,
1303 pub mp2: usize,
1304 pub mp3: usize,
1305}
1306
1307pub fn emit_spatial_kmeans_test(
1320 spec: &crate::tensor_recognize::KmeansSpec,
1321 points: &Fill,
1322 p: KmeansPar,
1323 class_name: &str,
1324) -> Result<String, String> {
1325 use crate::tensor_recognize::Ties;
1326 let KmeansPar { ts, ip, op, mp1, mp2, mp3 } = p;
1327 at_least_one(&[("ts", ts), ("ip", ip), ("op", op), ("mp1", mp1), ("mp2", mp2), ("mp3", mp3)])?;
1328 let (n, k, f, iters) = (spec.n, spec.k, spec.f, spec.iters);
1329 if n % ts != 0 {
1330 return Err(format!("the Spatial Kmeans template loads whole tiles: ts={ts} must divide N={n}"));
1331 }
1332 if n < k || k == 0 || f == 0 {
1333 return Err(format!("{n} points cannot seed {k} centroids of {f} features"));
1334 }
1335 let data = host_data(points, Ty::Int, "i * DM1 + d")?;
1336 let values: Vec<i64> = (0..(n * f) as i64)
1337 .map(|t| match points.element(t) {
1338 Some(Scalar::Int(v)) => Ok(v),
1339 _ => Err("the points have no Int data the app can print".to_string()),
1340 })
1341 .collect::<Result<_, _>>()?;
1342 spec.run(&values, (i32::MIN as i128, i32::MAX as i128))
1343 .map_err(|stop| format!("the clustering {}", stop_reason(stop)))?;
1344 let dag = TermDag::of(&spec.distance);
1345 let tys = dag.types(&[Ty::Int, Ty::Int]).ok_or("the distance is ill-typed")?;
1346 let (accel_lines, accel_term) = DagPrinter::print(&dag, &tys, &|r| ["pts(pt,d)", "cts(ct,d)"][r].to_string(), false);
1347 let (host_lines, host_term) = DagPrinter::print(&dag, &tys, &|r| ["p1(i)", "p2(i)"][r].to_string(), true);
1348 let block = |lines: &[String], pad: &str, root: &str| -> String {
1349 if lines.is_empty() {
1350 root.to_string()
1351 } else {
1352 let body: String = lines.iter().map(|l| format!("{pad} {l}\n")).collect();
1353 format!("{{\n{body}{pad} {root}\n{pad}}}")
1354 }
1355 };
1356 let (sentinel, reduce, prefer) = match spec.ties {
1359 Ties::Last => ("-1", "max(a,b)", "if (a._1 < b._1 || (a._1 == b._1 && a._2 > b._2)) a else b"),
1360 Ties::First => ("K", "min(a,b)", "if (a._1 < b._1 || (a._1 == b._1 && a._2 < b._2)) a else b"),
1361 };
1362 let ties = if spec.ties == Ties::Last { "last" } else { "first" };
1363 let mut o = format!(
1364 "// Generated by LOGOS (logicaffeine_compile::codegen_sva::spatial_emit) from a LOGOS loop nest recognized as k-means\n// clustering ({n} points of {f} features, {k} centroids, {iters} epochs, the {ties} of equally near centroids):\n// Spatial's Kmeans structure (regression/Kmeans.scala) at ts={ts} ip={ip} op={op} mp1={mp1} mp2={mp2} mp3={mp3}, over the\n// program's own data. The PASS line is what the ledger parses.\nimport spatial.dsl._\n\n"
1365 );
1366 o.push_str(&format!("@spatial class {class_name} extends SpatialTest {{ self =>\n type X = Int\n\n"));
1367 o.push_str(&format!(
1368 " val K = {k}\n val D = {}\n val ts = {ts}\n val ip = {ip}\n val op = {op}\n val mp1 = {mp1}\n val mp2 = {mp2}\n val mp3 = {mp3}\n\n",
1369 f + 1
1370 ));
1371 o.push_str(&format!(" override def runtimeArgs = \"{iters} {n}\"\n\n def DM1 = D - 1\n\n"));
1372 o.push_str(&format!(
1373 r#" def kmeans(points_in: Array[X], I:Int, N:Int) = {{
1374
1375 val iters = ArgIn[Int]
1376 val numPoints = ArgIn[Int]
1377
1378 setArg(iters, I)
1379 setArg(numPoints, N)
1380
1381 val points = DRAM[X](numPoints, D)
1382 val centroids = DRAM[X](K, D)
1383 setMem(points, points_in)
1384
1385 Accel {{
1386 val cts = SRAM[X](K, D)
1387
1388 cts load points(0::K, 0::D par ip)
1389
1390 Sequential.Foreach(iters by 1){{ epoch =>
1391 val newCents = SRAM[X](K,D)
1392 if (ip == 1) {{
1393 newCents.onlyblockcyclic.blockcyclic_Bs(Seq(63))
1394 ()
1395 }}
1396 MemReduce(newCents par ip)(numPoints by ts par op){{i =>
1397 val pts = SRAM[X](ts, D)
1398 pts load points(i::i+ts, 0::D par ip)
1399
1400 MemReduce(SRAM[X](K,D) par ip)(ts par mp1){{pt =>
1401 val dists = SRAM[X](K)
1402 Foreach(K by 1 par mp2) {{ ct =>
1403 val dist = Reduce(Reg[X])(D par ip){{d =>
1404 mux(d==DM1, 0, {accel})
1405 }}{{_+_}}
1406 dists(ct) = dist.value
1407 }}
1408 val minDist = Reduce(Reg[X])(K by 1 par ip) {{ ct => dists(ct) }} {{ (a,b) => min(a, b) }}
1409 val minCent = Reduce(Reg[I32])(K by 1 par ip) {{ ct =>
1410 mux(dists(ct) == minDist.value, ct, {sentinel})
1411 }} {{ (a,b) => {reduce} }}
1412
1413 val localCent = SRAM[X](K,D)
1414 Foreach(K by 1, D par ip){{(ct,d) =>
1415 localCent(ct, d) = mux(ct == minCent.value, pts(pt,d), 0.to[X])
1416 }}
1417 localCent
1418 }}{{_+_}}
1419 }}{{_+_}}
1420
1421 Foreach(K by 1 par mp3){{ ct =>
1422 Foreach(D-1 par ip){{ d =>
1423 val centCount = newCents(ct,DM1)
1424 val cond = centCount == 0.to[X]
1425 cts(ct, d) = mux(cond, 0.to[X], newCents(ct,d) / mux(cond,1.to[X], centCount))
1426 }}
1427 }}
1428 }}
1429
1430 centroids(0::K, D par ip) store cts
1431 }}
1432
1433 getMem(centroids)
1434 }}
1435
1436 def main(args: Array[String]): Unit = {{
1437 val I = args(0).to[Int]
1438 val N = args(1).to[Int]
1439
1440 val pts = Array.tabulate(N){{i => Array.tabulate(D){{d => if (d == DM1) 1.to[X] else {data} }}}}
1441
1442 val result = kmeans(pts.flatten, I, N)
1443
1444 val cts = Array.tabulate(K) {{ i => Array.empty[X](D) }}
1445 for (k <- 0 until K) {{
1446 for (d <- 0 until D) {{
1447 val cent = cts(k)
1448 val point = pts(k)
1449 cent(d) = point(d)
1450 }}
1451 }}
1452
1453 def dist(p1: Array[X], p2: Array[X]) = {{
1454 Array.tabulate(D) {{ i =>
1455 if (i==DM1) 0.to[X] else {host}
1456 }}.reduce(_+_)
1457 }}
1458
1459 val ii = Array.tabulate(K){{i => i}}
1460 for(epoch <- 0 until I) {{
1461 val accum = Array.tabulate(K) {{ i => Array.empty[X](D) }}
1462 for (k <- 0 until K) {{
1463 for (d <- 0 until D) {{
1464 val acc = accum(k)
1465 acc(d) = 0.to[X]
1466 }}
1467 }}
1468 pts.foreach {{ pt =>
1469 val dists = cts.map{{ct => dist(pt, ct) }}
1470 val idx = dists.zip(ii){{(a,b) => pack(a,b) }}.reduce{{(a,b) => {prefer}}}._2
1471 for (d <- 0 until D) {{
1472 val acc = accum(idx)
1473 acc(d) = acc(d) + pt(d)
1474 }}
1475 }}
1476 for (k <- 0 until K) {{
1477 val acc = accum(k)
1478 val n = acc(DM1)
1479 for (d <- 0 until DM1) {{
1480 val cent = cts(k)
1481 cent(d) = if (n == 0) 0.to[X] else acc(d)/n
1482 }}
1483 }}
1484 }}
1485
1486 val gold = cts.flatten
1487 val cksum = gold.zip(result) {{ (g, r) => g === r }}.reduce {{ _ & _ }}
1488 println("PASS: " + cksum + " ({class_name})")
1489 assert(cksum)
1490 }}
1491}}
1492"#,
1493 accel = block(&accel_lines, " ", &accel_term),
1494 host = block(&host_lines, " ", &host_term),
1495 ));
1496 Ok(o)
1497}