Skip to main content

logicaffeine_compile/codegen_sva/
engine_build.rs

1//! LOGOS source → what the whole-program fabric engine loads: the program lowered to an engine config, proven to
2//! answer what the VM answers on the exact arguments about to be streamed before a byte is emitted, checked against
3//! the deployed engine's profile and the flashed image's caps. One path for every caller — the `emit_wpe_soc`
4//! example the board hosts compile through, and the Fabric Terminal's library — so a program is lowered the same
5//! way wherever it runs.
6
7use std::panic::{catch_unwind, AssertUnwindSafe};
8
9use super::fabric_rtl::program_cfg_fabric;
10use std::collections::{HashMap, HashSet};
11
12use super::chip_protocol::{TAG_FLOAT, TAG_TEXT};
13use super::wprog_engine_rtl::{
14    eval_wprog_config, eval_wprog_config_run, func_code_needs_fpu, program_calls_fabric, program_main_fabric,
15    program_output, serialize_program, wprog_config_fits_engine, wprog_config_fits_hw_engine,
16    wprog_config_fits_stream_engine, wprog_config_image_unchecked, wprog_nregs, WBlock, WCell, WProgConfig, WSrc,
17    WTerm,
18};
19use crate::compile::vm_outcome;
20
21/// A way to lift a program to an engine config. A lifter panics on a program it cannot take; [`lower`] tries the
22/// function lifters in order, so a caller that runs one lifter at a time (a process or an instance per attempt)
23/// takes the same path.
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum Lifter {
26    /// The single-function CFG lifter.
27    Cfg,
28    /// The multi-function lifter: `Call` terminators and the engine's frame stack, for recursion.
29    Calls,
30    /// The whole program from its `## Main`, each `Show` a record the engine streams.
31    Main,
32}
33
34impl Lifter {
35    /// The lifters a function is tried with, in order.
36    pub const FUNCTION: [Lifter; 2] = [Lifter::Cfg, Lifter::Calls];
37
38    /// The name a host reads (`lifter=` in `emit_wpe_soc`'s facts).
39    pub fn name(self) -> &'static str {
40        match self {
41            Lifter::Cfg => "cfg",
42            Lifter::Calls => "calls",
43            Lifter::Main => "main",
44        }
45    }
46
47    /// Lift `func` of `src` — or, for [`Lifter::Main`], its `## Main`.
48    pub fn lift(self, src: &str, func: &str) -> WProgConfig {
49        match self {
50            Lifter::Cfg => serialize_program(&program_cfg_fabric(src, func)),
51            Lifter::Calls => program_calls_fabric(src, func),
52            Lifter::Main => program_main_fabric(src),
53        }
54    }
55}
56
57/// The deployed engine a program must fit. A `.wire` carries no record of the profile it was built for, and an
58/// over-cap image loads fine and returns a plausible wrong number, so this check is the guard.
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub enum Profile {
61    /// The XC7A100T engine on Board B.
62    Hw,
63    /// The XC7A200T block-RAM engine on Board A.
64    Stream,
65    /// The generic engine's caps.
66    Generic,
67}
68
69impl Profile {
70    pub const ALL: [Profile; 3] = [Profile::Hw, Profile::Stream, Profile::Generic];
71
72    pub fn name(self) -> &'static str {
73        match self {
74            Profile::Hw => "hw",
75            Profile::Stream => "stream",
76            Profile::Generic => "generic",
77        }
78    }
79
80    pub fn from_name(name: &str) -> Option<Profile> {
81        Profile::ALL.into_iter().find(|p| p.name() == name)
82    }
83
84    /// Whether `cfg`'s static dimensions fit this engine; the refusal names the cap it exceeds.
85    pub fn fits(self, cfg: &WProgConfig) -> Result<(), String> {
86        match self {
87            Profile::Hw => wprog_config_fits_hw_engine(cfg),
88            Profile::Stream => wprog_config_fits_stream_engine(cfg),
89            Profile::Generic => wprog_config_fits_engine(cfg),
90        }
91    }
92}
93
94/// Run `f` under a lowering attempt with the panic hook silenced: a lifter that cannot handle a program panics,
95/// and that is an ordinary "try the next lifter" outcome here, not a crash worth a backtrace on stderr.
96fn attempt<T>(f: impl FnOnce() -> T) -> Option<T> {
97    let hook = std::panic::take_hook();
98    std::panic::set_hook(Box::new(|_| {}));
99    let r = catch_unwind(AssertUnwindSafe(f)).ok();
100    std::panic::set_hook(hook);
101    r
102}
103
104/// The VM's answer for `func(params)` — a synthesized `## Main` showing the call, run on the reference VM (the
105/// same `vm_outcome` every execution tier is locked against). This is the golden the chip must reproduce.
106pub fn vm_golden(src: &str, func: &str, params: &[i64]) -> Result<i64, String> {
107    let al = params
108        .iter()
109        .map(|a| if *a < 0 { format!("(0 - {})", -a) } else { a.to_string() })
110        .collect::<Vec<_>>()
111        .join(", ");
112    let prog = format!("{src}## Main\n    Show {func}({al}).\n");
113    let out = vm_outcome(&prog);
114    if let Some(e) = out.error {
115        return Err(format!("the VM cannot run {func}({al}): {e}"));
116    }
117    out.output
118        .split_whitespace()
119        .next()
120        .and_then(|w| w.parse::<i64>().ok())
121        .ok_or_else(|| format!("{func}({al}) did not show an Int on the VM: {:?}", out.output))
122}
123
124/// The VM's OUTPUT for a whole program — one line per `Show` — the golden a Main program's streamed records are
125/// checked against.
126pub fn vm_output_lines(src: &str) -> Result<Vec<String>, String> {
127    let out = vm_outcome(src);
128    if let Some(e) = out.error {
129        return Err(format!("the VM cannot run the program: {e}"));
130    }
131    Ok(out.output.trim_end().lines().map(str::to_string).collect())
132}
133
134/// Lower a whole `## Main` program and PROVE the engine model prints exactly what the VM prints before a byte
135/// is emitted: the output tape rendered line by line == `vm_outcome(src).output`.
136pub fn lower_main(src: &str) -> Result<(WProgConfig, Vec<String>), String> {
137    let vm = vm_output_lines(src)?;
138    let cfg = attempt(|| Lifter::Main.lift(src, "")).ok_or_else(|| "the Main lifter refused the program".to_string())?;
139    let model = attempt(|| program_output(&cfg, &[])).ok_or_else(|| "the engine model faulted".to_string())?;
140    if model != vm {
141        return Err(format!("the engine model's output {model:?} is not the VM's {vm:?}"));
142    }
143    Ok((cfg, vm))
144}
145
146/// Lower `func` to an engine config the same way the benchmark lane does: the single-function CFG lifter when
147/// it agrees with the VM on these params, else the multi-function (call-stack) lifter. Either way the config is
148/// PROVEN ≡ VM on the exact params about to be streamed before a byte is emitted.
149pub fn lower(src: &str, func: &str, params: &[i64]) -> Result<(WProgConfig, Lifter, i64), String> {
150    let vm = vm_golden(src, func, params)?;
151    let mut tried = Vec::new();
152    for lifter in Lifter::FUNCTION {
153        let name = lifter.name();
154        match attempt(|| lifter.lift(src, func)) {
155            Some(cfg) => match attempt(|| eval_wprog_config(&cfg, params)) {
156                Some(got) if got == vm => return Ok((cfg, lifter, vm)),
157                Some(got) => tried.push(format!("{name} lifter gives {got}")),
158                None => tried.push(format!("{name} lifter's model faulted")),
159            },
160            None => tried.push(format!("{name} lifter refused the program")),
161        }
162    }
163    Err(format!("no lowering runs {func}({params:?}) ≡ VM (VM says {vm}; {})", tried.join("; ")))
164}
165
166/// Check the program against the caps of the image that is actually on the board (`NAME=N,…`, the recipe the
167/// build records beside every bitstream). The static dimensions are read off the config; DEPTH and HEAP are
168/// data-dependent, so they are MEASURED by running the program on the model with the exact params about to be
169/// streamed. An over-cap program loads fine and returns a plausible wrong number — this is the guard.
170pub fn fits_caps(cfg: &WProgConfig, params: &[i64], caps: &str) -> Result<(), String> {
171    let run = eval_wprog_config_run(cfg, params, &[]);
172    let im = wprog_config_image_unchecked(cfg);
173    let measured: Vec<(&str, usize)> = vec![
174        ("NREG", wprog_nregs(cfg)),
175        ("MAXBLK", cfg.blocks.len()),
176        ("MAXCELL", cfg.blocks.iter().map(|b| b.cells.len()).sum()),
177        ("MAXV", cfg.blocks.iter().map(|b| b.cells.len()).max().unwrap_or(0)),
178        ("MAXWR", cfg.blocks.iter().map(|b| b.writes.len()).sum()),
179        ("MAXLIV", cfg.blocks.iter().map(|b| b.livein.len()).sum()),
180        ("MAXARG", im.arg_op.len()),
181        ("MAXK", im.konst.len()),
182        ("HEAP", run.heap.len()),
183        ("DEPTH", run.peak_depth),
184    ];
185    for entry in caps.split(',').map(str::trim).filter(|e| !e.is_empty()) {
186        let (name, cap) = entry.split_once('=').ok_or_else(|| format!("bad --caps entry {entry:?} (want NAME=N)"))?;
187        let cap: usize = cap.trim().parse().map_err(|_| format!("bad --caps value in {entry:?}"))?;
188        // `FPU=0`: the image was built without the float lane — a program with any float functional unit has no
189        // datapath to run on and must be refused, not streamed to return a zero.
190        if name.trim() == "FPU" {
191            if cap == 0 {
192                if let Some(c) =
193                    cfg.blocks.iter().flat_map(|b| b.cells.iter()).find(|c| c.kind == 2 && func_code_needs_fpu(c.code))
194                {
195                    return Err(format!(
196                        "the program uses a float unit (func code {}) but the flashed image was built with FPU=0",
197                        c.code
198                    ));
199                }
200            }
201            continue;
202        }
203        let need = measured.iter().find(|(n, _)| *n == name.trim()).map(|(_, v)| *v).ok_or_else(|| {
204            format!("unknown cap {name:?} (known: NREG MAXBLK MAXCELL MAXV MAXWR MAXLIV MAXARG MAXK HEAP DEPTH)")
205        })?;
206        if need > cap {
207            return Err(format!("{name} needs {need} but the flashed image has {name}={cap}"));
208        }
209    }
210    Ok(())
211}
212
213// ── the fabric map ─────────────────────────────────────────────────────────────────────────────────────────────
214
215/// What each part of the fabric does for one compiled program, read off the config the engine runs: the labels the
216/// Fabric Terminal draws beside its activity panel, whose BLOCKS tiles are the config's blocks and whose CELLS strip
217/// is its cells in the engine's flat order.
218#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
219pub struct FabricMap {
220    pub blocks: Vec<MapBlock>,
221}
222
223/// One block of the config: a BLOCKS tile.
224#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
225pub struct MapBlock {
226    /// Its part in the control flow: `init` (where the program begins), `fn` (a function's entry, entered by a call),
227    /// `test` (it branches), `loop` (it jumps back to an earlier block), `step` (it jumps forward), `call` (it calls a
228    /// function), `end` (it returns), `idle` (no path reaches it, so it never runs).
229    pub role: &'static str,
230    /// Its first cell's place in the CELLS strip: the cells of block `b` follow those of blocks `0..b`.
231    pub first_cell: usize,
232    /// Each of its cells' operation, in order.
233    pub cells: Vec<String>,
234    /// What it does before control moves on, a statement each, over the program's variable names: what it shows
235    /// and stores, the lists and texts it builds, and the registers it writes that some block reads.
236    pub says: Vec<String>,
237    /// Where control goes next.
238    pub next: MapNext,
239}
240
241/// A block's terminator, as the map gives it.
242#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
243#[serde(tag = "kind", rename_all = "lowercase")]
244pub enum MapNext {
245    Jump { to: usize },
246    Branch { cond: String, then: usize, otherwise: usize },
247    Return { value: String },
248    Call { callee: usize, args: Vec<String>, result: String, back: usize },
249}
250
251/// The Main frame's variable names by register — the registers a [`Lifter::Main`] config's own blocks read and
252/// write. A function's blocks have registers of their own frame, which these names do not describe.
253pub fn main_register_names(src: &str) -> HashMap<u16, String> {
254    crate::ui_bridge::with_parsed_program(src, |parsed, interner| {
255        let Ok((stmts, _types, _policies)) = parsed else {
256            return HashMap::new();
257        };
258        crate::vm::Compiler::compile_for_debug(stmts, interner, None)
259            .map(|p| p.reg_names.into_iter().collect())
260            .unwrap_or_default()
261    })
262}
263
264const ARITH: [&str; 9] = ["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"];
265const COMPARE: [&str; 6] = ["<", ">", "<=", ">=", "==", "!="];
266const UNIT: [&str; 21] = [
267    "/", "%", "//", "**", "*hi", "f+", "f-", "f*", "f/", "sqrt", "floor", "~=", "*", "float", "f<", "f>", "f<=", "f>=",
268    "f==", "f!=", "popcount",
269];
270
271/// Whether functional unit `code` works on floats (its operands are f64 bit patterns).
272fn float_unit(code: u8) -> bool {
273    matches!(code, 5..=11 | 14..=19)
274}
275
276/// A cell's operation, as the CELLS strip's label for it.
277fn cell_op(c: &WCell) -> String {
278    let pick = |table: &[&str]| table.get(c.code as usize).copied().unwrap_or("?").to_string();
279    match c.kind {
280        0 => pick(&ARITH),
281        1 => pick(&COMPARE),
282        2 => pick(&UNIT),
283        3 => "ld".into(),
284        4 => "st".into(),
285        5 => "new".into(),
286        6 => "copy".into(),
287        7 => "++".into(),
288        8 => "at".into(),
289        9 => (if c.code == 0 { "==" } else { "!=" }).into(),
290        10 => "show".into(),
291        _ => "?".into(),
292    }
293}
294
295/// The map of `cfg`, with `names` for the registers of the blocks [`Lifter::Main`]'s `## Main` region runs — the
296/// blocks reached from block 0 without entering a call.
297pub fn fabric_map(cfg: &WProgConfig, names: &HashMap<u16, String>) -> FabricMap {
298    let reach = |through_calls: bool| {
299        let mut seen = HashSet::new();
300        let mut work = vec![0usize];
301        while let Some(i) = work.pop() {
302            if i >= cfg.blocks.len() || !seen.insert(i) {
303                continue;
304            }
305            match &cfg.blocks[i].term {
306                WTerm::Jump(t) => work.push(*t),
307                WTerm::Branch { then_blk, else_blk, .. } => work.extend([*then_blk, *else_blk]),
308                WTerm::Call { callee_block, ret_block, .. } => {
309                    work.push(*ret_block);
310                    if through_calls {
311                        work.push(*callee_block);
312                    }
313                }
314                WTerm::Return(_) => {}
315            }
316        }
317        seen
318    };
319    let (main, runs) = (reach(false), reach(true));
320    let callees: HashSet<usize> = cfg
321        .blocks
322        .iter()
323        .filter_map(|b| match &b.term {
324            WTerm::Call { callee_block, .. } => Some(*callee_block),
325            _ => None,
326        })
327        .collect();
328    // A register some block reads; a write to any other is the VM's scratch, with no effect on the program.
329    let read: HashSet<u16> = cfg.blocks.iter().flat_map(|b| b.livein.iter().copied()).collect();
330    // A register whose value a float unit or a float Show reads: a constant written to it is an f64's bits.
331    let float_regs: HashSet<u16> = cfg
332        .blocks
333        .iter()
334        .flat_map(|b| {
335            b.cells.iter().flat_map(move |c| {
336                let float = (c.kind == 2 && float_unit(c.code)) || (c.kind == 10 && c.code == TAG_FLOAT);
337                c.ops.iter().filter(move |s| float && s.mode == 0).filter_map(move |s| b.livein.get(s.reference as usize).copied())
338            })
339        })
340        .collect();
341    let none = HashMap::new();
342    let mut first = 0;
343    let blocks = cfg
344        .blocks
345        .iter()
346        .enumerate()
347        .map(|(i, b)| {
348            let role = if !runs.contains(&i) {
349                "idle"
350            } else if i == 0 {
351                "init"
352            } else if callees.contains(&i) {
353                "fn"
354            } else {
355                match &b.term {
356                    WTerm::Return(_) => "end",
357                    WTerm::Call { .. } => "call",
358                    WTerm::Branch { .. } => "test",
359                    WTerm::Jump(t) if *t <= i => "loop",
360                    WTerm::Jump(_) => "step",
361                }
362            };
363            let text = BlockText::new(b, if main.contains(&i) { names } else { &none }, &float_regs);
364            let block = MapBlock {
365                role,
366                first_cell: first,
367                cells: b.cells.iter().map(cell_op).collect(),
368                says: text.says(&read),
369                next: text.next(),
370            };
371            first += b.cells.len();
372            block
373        })
374        .collect();
375    FabricMap { blocks }
376}
377
378/// A list or text the block builds cell by cell: its elements, and the register it is kept in.
379struct Built {
380    items: Vec<WSrc>,
381    text: Option<String>,
382    kept_in: Option<u16>,
383}
384
385/// One block written out: its cells as expressions over its live-in registers, then its statements and terminator.
386struct BlockText<'a> {
387    b: &'a WBlock,
388    names: &'a HashMap<u16, String>,
389    float_regs: &'a HashSet<u16>,
390    built: HashMap<usize, Built>,
391    consumed: HashSet<usize>,
392}
393
394impl<'a> BlockText<'a> {
395    fn new(b: &'a WBlock, names: &'a HashMap<u16, String>, float_regs: &'a HashSet<u16>) -> Self {
396        let mut t = BlockText { b, names, float_regs, built: HashMap::new(), consumed: HashSet::new() };
397        let is_val = |s: &WSrc, i: usize| s.mode == 2 && s.reference as usize == i;
398        let konst = |s: &WSrc| (s.mode == 1).then_some(s.konst);
399        for (a, c) in b.cells.iter().enumerate() {
400            let Some(size) = (c.kind == 5).then(|| konst(&c.ops[0])).flatten() else { continue };
401            let n = size - 1;
402            let Some(len_write) =
403                b.cells.iter().position(|w| w.kind == 4 && is_val(&w.ops[0], a) && konst(&w.ops[1]) == Some(n))
404            else {
405                continue;
406            };
407            let mut used = vec![len_write];
408            let mut items = Vec::new();
409            for j in 1..=n {
410                let add = b.cells.iter().position(|x| {
411                    x.kind == 0 && x.code == 0 && is_val(&x.ops[0], a) && konst(&x.ops[1]) == Some(j)
412                });
413                let write = add.and_then(|k| b.cells.iter().position(|w| w.kind == 4 && is_val(&w.ops[0], k)));
414                match (add, write) {
415                    (Some(k), Some(w)) => {
416                        used.extend([k, w]);
417                        items.push(b.cells[w].ops[1]);
418                    }
419                    _ => break,
420                }
421            }
422            if items.len() as i64 != n {
423                continue;
424            }
425            let read_as_text = b.cells.iter().any(|u| {
426                let reads = |slot: usize| is_val(&u.ops[slot], a);
427                (u.kind == 10 && u.code == TAG_TEXT && reads(0))
428                    || (u.kind == 7 && ((u.code & 1 != 0 && reads(0)) || (u.code & 2 != 0 && reads(1))))
429                    || (matches!(u.kind, 8 | 9) && (reads(0) || (u.kind == 9 && reads(1))))
430            });
431            let bytes: Option<Vec<i64>> = items.iter().map(konst).collect();
432            let text = bytes.filter(|v| read_as_text && v.iter().all(|&x| (0x20..0x7F).contains(&x))).map(|v| {
433                format!("\"{}\"", v.iter().map(|&x| x as u8 as char).collect::<String>())
434            });
435            let kept_in = b.writes.iter().find(|(_, s)| is_val(s, a)).map(|(r, _)| *r);
436            t.built.insert(a, Built { items, text, kept_in });
437            t.consumed.extend(used);
438        }
439        t
440    }
441
442    fn reg(&self, r: u16) -> String {
443        self.names.get(&r).cloned().unwrap_or_else(|| format!("r{r}"))
444    }
445
446    fn konst(&self, k: i64, float: bool) -> String {
447        if float {
448            let f = f64::from_bits(k as u64);
449            if f.fract() == 0.0 && f.abs() < 1e15 {
450                format!("{f:.1}")
451            } else {
452                format!("{f}")
453            }
454        } else {
455            k.to_string()
456        }
457    }
458
459    fn src_as(&self, s: &WSrc, float: bool) -> String {
460        match s.mode {
461            0 => self.b.livein.get(s.reference as usize).map_or_else(|| "?".into(), |&r| self.reg(r)),
462            1 => self.konst(s.konst, float),
463            _ => self.val(s.reference as usize),
464        }
465    }
466
467    fn src(&self, s: &WSrc) -> String {
468        self.src_as(s, false)
469    }
470
471    /// The literal a built list or text is written as.
472    fn literal(&self, built: &Built) -> String {
473        built.text.clone().unwrap_or_else(|| format!("[{}]", built.items.iter().map(|s| self.src(s)).collect::<Vec<_>>().join(", ")))
474    }
475
476    /// Word `j` of the list or text at `base`, or its length (word 0), as `base[j]` / `length(base)`.
477    fn element(&self, at: &WSrc) -> (String, Option<String>) {
478        if at.mode == 2 {
479            if let Some(c) = self.b.cells.get(at.reference as usize) {
480                if c.kind == 0 && c.code == 0 {
481                    return (self.src(&c.ops[0]), Some(self.src(&c.ops[1])));
482                }
483            }
484        }
485        (self.src(at), None)
486    }
487
488    fn val(&self, i: usize) -> String {
489        if let Some(built) = self.built.get(&i) {
490            return built.kept_in.map(|r| self.reg(r)).unwrap_or_else(|| self.literal(built));
491        }
492        let Some(c) = self.b.cells.get(i) else { return format!("v{i}") };
493        let float = c.kind == 2 && float_unit(c.code);
494        let (a, b) = (self.src_as(&c.ops[0], float), self.src_as(&c.ops[1], float));
495        let op = cell_op(c);
496        let e = match c.kind {
497            0 | 7 | 9 => bin(&a, &op, &b),
498            1 => {
499                let cond = bin(&a, &op, &b);
500                if c.ops[2].mode == 1 && c.ops[2].konst == 1 && c.ops[3].mode == 1 && c.ops[3].konst == 0 {
501                    cond
502                } else {
503                    format!("({cond} ? {} : {})", self.src(&c.ops[2]), self.src(&c.ops[3]))
504                }
505            }
506            2 => match c.code {
507                9 | 10 | 13 | 20 => format!("{op}({a})"),
508                _ => bin(&a, &op, &b),
509            },
510            3 => match self.element(&c.ops[0]) {
511                (base, Some(j)) => format!("{base}[{j}]"),
512                (base, None) => format!("length({base})"),
513            },
514            4 => b,
515            5 => format!("new[{a}]"),
516            6 => format!("copy({a})"),
517            8 => format!("{a}[{b}]"),
518            10 => a,
519            _ => format!("v{i}"),
520        };
521        if e.len() > 48 {
522            format!("v{i}")
523        } else {
524            e
525        }
526    }
527
528    /// The block's statements, in the order its cells and writes take effect; `read` holds the registers some block
529    /// reads, the only writes with an effect.
530    fn says(&self, read: &HashSet<u16>) -> Vec<String> {
531        let mut out = Vec::new();
532        let mut defined = HashSet::new();
533        for (i, c) in self.b.cells.iter().enumerate() {
534            if let Some(built) = self.built.get(&i) {
535                if let Some(r) = built.kept_in {
536                    out.push(format!("{} = {}", self.reg(r), self.literal(built)));
537                    defined.insert(r);
538                }
539                continue;
540            }
541            if self.consumed.contains(&i) {
542                continue;
543            }
544            match c.kind {
545                10 => out.push(format!("show {}", self.src_as(&c.ops[0], c.code == TAG_FLOAT))),
546                4 => out.push(match self.element(&c.ops[0]) {
547                    (base, Some(j)) => format!("{base}[{j}] = {}", self.src(&c.ops[1])),
548                    (base, None) => format!("length({base}) = {}", self.src(&c.ops[1])),
549                }),
550                _ => {}
551            }
552        }
553        for (r, s) in &self.b.writes {
554            if defined.contains(r) || !read.contains(r) {
555                continue;
556            }
557            let (name, value) = (self.reg(*r), self.src_as(s, self.float_regs.contains(r)));
558            if name != value {
559                out.push(format!("{name} = {value}"));
560            }
561        }
562        out
563    }
564
565    fn next(&self) -> MapNext {
566        match &self.b.term {
567            WTerm::Jump(t) => MapNext::Jump { to: *t },
568            WTerm::Branch { cond, then_blk, else_blk } => {
569                MapNext::Branch { cond: self.src(cond), then: *then_blk, otherwise: *else_blk }
570            }
571            WTerm::Return(s) => MapNext::Return { value: self.src(s) },
572            WTerm::Call { callee_block, args, dst_reg, ret_block } => MapNext::Call {
573                callee: *callee_block,
574                args: args.iter().map(|a| self.src(a)).collect(),
575                result: self.reg(*dst_reg),
576                back: *ret_block,
577            },
578        }
579    }
580}
581
582fn bin(a: &str, op: &str, b: &str) -> String {
583    let wrap = |x: &str| {
584        if x.contains(' ') && !x.starts_with('"') && !x.starts_with('(') && !x.starts_with('[') {
585            format!("({x})")
586        } else {
587            x.to_string()
588        }
589    };
590    format!("{} {op} {}", wrap(a), wrap(b))
591}