Skip to main content

logicaffeine_compile/accel/
c_model.rs

1//! The executable device model as C: what the host binary links and calls when it runs on a machine that
2//! has no accelerator.
3//!
4//! The model is generated from the same objects the device artifact is: each region's control-flow graph and
5//! the lane routines its cells are, at the device's lane shape. Every lane routine becomes a C function over
6//! `uint32_t` words masked to the lane width (one statement per lane operation, a `for` per `Repeat`), and
7//! every region becomes a C function that walks its blocks with `goto`, calls a routine per cell, and returns
8//! 0 with the divergence code the moment a cell hands the call back. `logos_dev_call` is the driver the
9//! host's dispatch calls; it counts what every region did and, at exit, appends one line of accounting to the
10//! file `LOGOS_ACCEL_STATS` names. `LOGOS_ACCEL_OFFLOAD=off` makes it hand every call back. Neither ever
11//! writes to the program's standard output or standard error.
12
13use std::collections::BTreeMap;
14use std::fmt::Write;
15
16use crate::codegen_sva::fabric_rtl::Divergence;
17
18use super::cells::{routine, CellOp};
19use super::int64::FAULT_BITS;
20use super::lane::{split_u64, LInst, LOp, LStmt, LaneShape, Routine};
21use super::region::{RStmt, RTerm, RegionCfg};
22
23/// The C symbol of `op`'s routine at `shape`.
24pub fn routine_symbol(op: CellOp, shape: LaneShape) -> String {
25    format!("lr_{}_w{}m{}", op.name(), shape.word, shape.mul)
26}
27
28fn mask(shape: LaneShape) -> String {
29    format!("0x{:X}u", shape.mask())
30}
31
32fn inst_c(out: &mut String, i: &LInst, shape: LaneShape, pad: &str) {
33    let m = mask(shape);
34    let w = shape.word;
35    let d = i.dst;
36    match i.op {
37        LOp::Const(k) => writeln!(out, "{pad}r[{d}] = {k}u;"),
38        LOp::Mov(a) => writeln!(out, "{pad}r[{d}] = r[{a}];"),
39        LOp::Add { a, b, cin } => {
40            let c = i.dst2.expect("an add writes its carry");
41            writeln!(
42                out,
43                "{pad}{{ uint64_t s_ = (uint64_t)r[{a}] + (uint64_t)r[{b}] + (uint64_t)r[{cin}]; r[{d}] = (uint32_t)(s_ & {m}); r[{c}] = (uint32_t)(s_ >> {w}); }}"
44            )
45        }
46        LOp::Sub { a, b, bin } => {
47            let c = i.dst2.expect("a subtract writes its borrow");
48            writeln!(
49                out,
50                "{pad}{{ int64_t s_ = (int64_t)r[{a}] - (int64_t)r[{b}] - (int64_t)r[{bin}]; r[{d}] = (uint32_t)((uint64_t)s_ & {m}); r[{c}] = (uint32_t)(s_ < 0); }}"
51            )
52        }
53        LOp::Mul { a, b } => writeln!(out, "{pad}r[{d}] = r[{a}] * r[{b}];"),
54        LOp::And { a, b } => writeln!(out, "{pad}r[{d}] = r[{a}] & r[{b}];"),
55        LOp::Or { a, b } => writeln!(out, "{pad}r[{d}] = r[{a}] | r[{b}];"),
56        LOp::Xor { a, b } => writeln!(out, "{pad}r[{d}] = r[{a}] ^ r[{b}];"),
57        LOp::Not { a } => writeln!(out, "{pad}r[{d}] = ~r[{a}] & {m};"),
58        LOp::Shl { a, k } => writeln!(out, "{pad}r[{d}] = (r[{a}] << {k}) & {m};"),
59        LOp::Shr { a, k } => writeln!(out, "{pad}r[{d}] = r[{a}] >> {k};"),
60        LOp::Ltu { a, b } => writeln!(out, "{pad}r[{d}] = r[{a}] < r[{b}];"),
61        LOp::Eq { a, b } => writeln!(out, "{pad}r[{d}] = r[{a}] == r[{b}];"),
62        LOp::Sel { c, t, f } => writeln!(out, "{pad}r[{d}] = r[{c}] ? r[{t}] : r[{f}];"),
63    }
64    .expect("writing to a String");
65}
66
67fn body_c(out: &mut String, body: &[LStmt], shape: LaneShape, depth: usize) {
68    let pad = "    ".repeat(depth + 1);
69    for s in body {
70        match s {
71            LStmt::Inst(i) => inst_c(out, i, shape, &pad),
72            LStmt::Repeat { times, body } => {
73                writeln!(out, "{pad}for (uint32_t t{depth}_ = 0; t{depth}_ < {times}u; t{depth}_++) {{").expect("write");
74                body_c(out, body, shape, depth + 1);
75                writeln!(out, "{pad}}}").expect("write");
76            }
77        }
78    }
79}
80
81/// C for one lane routine: `static void <symbol>(const uint32_t *in, uint32_t *out)`. Its register file is a
82/// local that starts at zero on every call, as the lane interpreter's does, so a call depends on nothing but
83/// its inputs and the model is reentrant.
84pub fn routine_c(r: &Routine, symbol: &str) -> String {
85    let mut out = String::new();
86    writeln!(out, "static void {symbol}(const uint32_t *in, uint32_t *out) {{").expect("write");
87    writeln!(out, "    uint32_t r[{}] = {{0}};", r.regs.max(1)).expect("write");
88    for (k, reg) in r.inputs.iter().enumerate() {
89        writeln!(out, "    r[{reg}] = in[{k}];").expect("write");
90    }
91    body_c(&mut out, &r.body, r.shape, 0);
92    for (k, reg) in r.outputs.iter().enumerate() {
93        writeln!(out, "    out[{k}] = r[{reg}];").expect("write");
94    }
95    writeln!(out, "}}\n").expect("write");
96    out
97}
98
99/// The cell ops a region uses, selects included.
100fn ops_of(region: &RegionCfg) -> Vec<CellOp> {
101    let mut ops = Vec::new();
102    for b in &region.blocks {
103        for s in &b.stmts {
104            let op = match s {
105                RStmt::Cell { op, .. } => *op,
106                RStmt::Select { cmp, .. } => CellOp::Select(*cmp),
107                _ => continue,
108            };
109            if !ops.contains(&op) {
110                ops.push(op);
111            }
112        }
113    }
114    ops
115}
116
117/// C for a region at `shape`: `static int <symbol>(const int64_t *args, int64_t *out, uint32_t *fault)`,
118/// returning 1 with `*out` set, or 0 with `*fault` the divergence code.
119pub fn region_c(region: &RegionCfg, shape: LaneShape, symbol: &str) -> String {
120    let n = shape.words_for(64);
121    let nf = shape.words_for(FAULT_BITS);
122    let w = shape.word;
123    let mut out = String::new();
124    writeln!(out, "static int {symbol}(const int64_t *args, int64_t *out, uint32_t *fault) {{").expect("write");
125    writeln!(out, "    uint32_t v[{}][{n}] = {{{{0}}}};", region.regs.max(1)).expect("write");
126    writeln!(out, "    uint32_t in_[{}], o_[{}];", 4 * n, n + nf).expect("write");
127    for i in 0..region.params.len() {
128        for k in 0..n {
129            writeln!(out, "    v[{i}][{k}] = (uint32_t)(((uint64_t)args[{i}] >> {}) & {});", k as u32 * w, mask(shape)).expect("write");
130        }
131    }
132    writeln!(out, "    goto B0;").expect("write");
133    let copy = |out: &mut String, dst: &str, src: &str| {
134        writeln!(out, "    memcpy({dst}, {src}, sizeof(uint32_t) * {n});").expect("write");
135    };
136    for (bi, b) in region.blocks.iter().enumerate() {
137        writeln!(out, "B{bi}:;").expect("write");
138        for s in &b.stmts {
139            match *s {
140                RStmt::Const { dst, value } => {
141                    for (k, word) in split_u64(shape, value as u64, n).iter().enumerate() {
142                        writeln!(out, "    v[{dst}][{k}] = {word}u;").expect("write");
143                    }
144                }
145                RStmt::Copy { dst, src } => copy(&mut out, &format!("v[{dst}]"), &format!("v[{src}]")),
146                RStmt::Cell { dst, op, a, b } => {
147                    copy(&mut out, "in_", &format!("v[{a}]"));
148                    copy(&mut out, &format!("in_ + {n}"), &format!("v[{b}]"));
149                    writeln!(out, "    {}(in_, o_);", routine_symbol(op, shape)).expect("write");
150                    if op.faults() {
151                        let joined: Vec<String> = (0..nf).map(|k| format!("((uint32_t)o_[{}] << {})", n + k, k as u32 * w)).collect();
152                        writeln!(out, "    {{ uint32_t f_ = {}; if (f_) {{ *fault = f_; return 0; }} }}", joined.join(" | ")).expect("write");
153                    }
154                    copy(&mut out, &format!("v[{dst}]"), "o_");
155                }
156                RStmt::Select { dst, cmp, a, b, t, e } => {
157                    copy(&mut out, "in_", &format!("v[{a}]"));
158                    copy(&mut out, &format!("in_ + {n}"), &format!("v[{b}]"));
159                    copy(&mut out, &format!("in_ + {}", 2 * n), &format!("v[{t}]"));
160                    copy(&mut out, &format!("in_ + {}", 3 * n), &format!("v[{e}]"));
161                    writeln!(out, "    {}(in_, o_);", routine_symbol(CellOp::Select(cmp), shape)).expect("write");
162                    copy(&mut out, &format!("v[{dst}]"), "o_");
163                }
164            }
165        }
166        match b.term {
167            RTerm::Jump(t) => writeln!(out, "    goto B{t};"),
168            RTerm::Branch { cond, then_, else_ } => {
169                let any: Vec<String> = (0..n).map(|k| format!("v[{cond}][{k}]")).collect();
170                writeln!(out, "    if ({}) goto B{then_}; else goto B{else_};", any.join(" | "))
171            }
172            RTerm::Return(r) => {
173                let joined: Vec<String> = (0..n).map(|k| format!("((uint64_t)v[{r}][{k}] << {})", k as u32 * w)).collect();
174                writeln!(out, "    *out = (int64_t)({});\n    return 1;", joined.join(" | "))
175            }
176        }
177        .expect("write");
178    }
179    writeln!(out, "}}\n").expect("write");
180    out
181}
182
183/// The model translation unit for `regions` (each with its host-side region number) on a device whose lanes
184/// are `shape`: every routine the regions use, every region, and the `logos_dev_call` driver with its
185/// accounting. `device` names the device in the accounting line.
186pub fn model_c(regions: &[(u32, RegionCfg)], shape: LaneShape, device: &str) -> String {
187    let mut out = String::new();
188    writeln!(out, "/* The {device} device model: lane routines at {}-bit words with {}-bit multipliers, and the regions they run. */", shape.word, shape.mul)
189        .expect("write");
190    writeln!(out, "#include <stdint.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n").expect("write");
191    let mut used: BTreeMap<String, CellOp> = BTreeMap::new();
192    for (_, r) in regions {
193        for op in ops_of(r) {
194            used.insert(routine_symbol(op, shape), op);
195        }
196    }
197    for (sym, op) in &used {
198        out.push_str(&routine_c(&routine(shape, *op), sym));
199    }
200    for (id, r) in regions {
201        out.push_str(&region_c(r, shape, &format!("logos_region_{id}")));
202    }
203    let count = regions.iter().map(|(id, _)| *id as usize + 1).max().unwrap_or(0).max(1);
204    let codes = Divergence::ALL.iter().map(|d| d.code()).max().unwrap_or(0) as usize + 1;
205    writeln!(out, "static uint64_t logos_dev_done[{count}];").expect("write");
206    writeln!(out, "static uint64_t logos_dev_declined[{count}][{codes}];").expect("write");
207    writeln!(out, "static uint64_t logos_dev_off_calls[{count}];").expect("write");
208    writeln!(out, "static int logos_dev_mode = -1;\n").expect("write");
209    let names: Vec<String> = (0..count)
210        .map(|i| {
211            regions.iter().find(|(id, _)| *id as usize == i).map(|(_, r)| format!("\"{}\"", r.name.replace('\\', "\\\\").replace('"', "\\\""))).unwrap_or_else(|| "\"\"".to_string())
212        })
213        .collect();
214    writeln!(out, "static const char *logos_dev_names[{count}] = {{ {} }};\n", names.join(", ")).expect("write");
215    let reasons: Vec<String> = (0..codes)
216        .map(|c| format!("\"{}\"", Divergence::from_code(c as u32).map(|d| d.name()).unwrap_or("none")))
217        .collect();
218    writeln!(out, "static const char *logos_dev_reasons[{codes}] = {{ {} }};\n", reasons.join(", ")).expect("write");
219    writeln!(
220        out,
221        r#"static void logos_dev_account(void) {{
222    const char *path = getenv("LOGOS_ACCEL_STATS");
223    if (!path || !*path) return;
224    FILE *f = fopen(path, "a");
225    if (!f) return;
226    fprintf(f, "{{\"device\":\"{device}\",\"regions\":[");
227    for (int i = 0; i < {count}; i++) {{
228        fprintf(f, "%s{{\"region\":%d,\"name\":\"%s\",\"done\":%llu,\"offload_off\":%llu,\"declined\":{{", i ? "," : "", i, logos_dev_names[i], (unsigned long long)logos_dev_done[i], (unsigned long long)logos_dev_off_calls[i]);
229        int first = 1;
230        for (int c = 1; c < {codes}; c++) {{
231            if (!logos_dev_declined[i][c]) continue;
232            fprintf(f, "%s\"%s\":%llu", first ? "" : ",", logos_dev_reasons[c], (unsigned long long)logos_dev_declined[i][c]);
233            first = 0;
234        }}
235        fprintf(f, "}}}}");
236    }}
237    fprintf(f, "]}}\n");
238    fclose(f);
239}}
240
241int logos_dev_call(int region, const int64_t *args, int64_t *out) {{
242    if (logos_dev_mode < 0) {{
243        const char *m = getenv("LOGOS_ACCEL_OFFLOAD");
244        logos_dev_mode = (m && strcmp(m, "off") == 0) ? 0 : 1;
245        atexit(logos_dev_account);
246    }}
247    if (region < 0 || region >= {count}) return 0;
248    if (!logos_dev_mode) {{ logos_dev_off_calls[region]++; return 0; }}
249    uint32_t fault = 0;
250    int ok = 0;
251    switch (region) {{"#
252    )
253    .expect("write");
254    for (id, _) in regions {
255        writeln!(out, "        case {id}: ok = logos_region_{id}(args, out, &fault); break;").expect("write");
256    }
257    writeln!(
258        out,
259        r#"        default: return 0;
260    }}
261    if (ok) logos_dev_done[region]++;
262    else if (fault < {codes}) logos_dev_declined[region][fault]++;
263    return ok;
264}}"#
265    )
266    .expect("write");
267    out
268}