Skip to main content

qcode/value/insn/
segment.rs

1//! Token-segment rendering of instructions for rich (colored, clickable) display.
2//!
3//! [`instruction_segments`] produces the *same bytes* as an instruction's
4//! [`Display`](std::fmt::Display) (`InstructionStatement`) — concatenating every
5//! returned [`Token`]'s text reproduces the canonical textual IR exactly — while
6//! additionally tagging each run with a semantic [`TokenKind`] (for coloring) and
7//! an optional [`Link`] (for click-through to the referenced value, function, or
8//! block). The textual format stays the single source of truth: a test asserts
9//! `concat(segments) == format!("{insn}")` for every mnemonic, so the two can
10//! never drift.
11
12use crate::{
13    context::{Context, Shared},
14    space::Space,
15    value::{
16        BasicBlock, FunctionBody, LocalBlockId, LocalValueId, QCodeView, ValueId,
17        block::BlockId,
18        bytes::BytesRef,
19        function::FunctionId,
20        insn::{Callee, InstructionRef, Mnemonic},
21        literal::{LiteralId, LiteralRef, SymbolicRef},
22        varnode::Varnode,
23    },
24};
25
26/// What a token *is*, semantically — drives syntax coloring in a viewer.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum TokenKind {
29    /// A type name (`i32`, `f64`), the access width of a load/store, or a cast
30    /// target width.
31    Type,
32    /// An instruction-result reference (`%name` / `%tmp…`).
33    Variable,
34    /// A block parameter (`@name`), or a parameter/argument name on the left of
35    /// `=` in a branch/call argument.
36    BlockParam,
37    /// A varnode / register reference.
38    Varnode,
39    /// A literal constant (`0x2`, `&<blk>`, `&"str"`) or a bare numeric offset.
40    Literal,
41    /// A byte-string literal (`b"…"`).
42    Bytes,
43    /// A reserved word or mnemonic (`load`, `goto`, `call fn`, `zext`, …).
44    Keyword,
45    /// An operator (`+`, ` = `, ` <- `, ` <$> `, `.`).
46    Operator,
47    /// Structural punctuation (`(`, `)`, `,`, `:`, `[`, `]`, `;`, spaces).
48    Punctuation,
49    /// A basic-block label (`<name>`).
50    Label,
51    /// An aggregate field name (`lhs`, `.val`).
52    Field,
53    /// A function reference (call/apply/map/scan target, or function used as a
54    /// value operand).
55    Function,
56    /// A memory space name (`ram`, `register`, …).
57    Space,
58}
59
60/// A click-through target carried by a token, when it names something navigable.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum Link {
63    Value(ValueId),
64    Function(FunctionId),
65    Block(BlockId),
66}
67
68/// One contiguous run of rendered text with its semantic kind and optional link.
69///
70/// Concatenating the `text` of every token of an instruction yields exactly the
71/// instruction's `Display` output.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct Token {
74    pub text: String,
75    pub kind: TokenKind,
76    pub link: Option<Link>,
77}
78
79impl Token {
80    fn new(text: impl Into<String>, kind: TokenKind, link: Option<Link>) -> Self {
81        Token {
82            text: text.into(),
83            kind,
84            link,
85        }
86    }
87}
88
89/// Accumulator with terse push helpers, kept private to this module.
90struct Seg<'ctx, 'str, R> {
91    view: R,
92    out: Vec<Token>,
93    marker: std::marker::PhantomData<&'ctx &'str ()>,
94}
95
96impl<'ctx, 'str: 'ctx, R> Seg<'ctx, 'str, R>
97where
98    R: QCodeView<'ctx, 'str>,
99{
100    fn push(&mut self, text: impl Into<String>, kind: TokenKind, link: Option<Link>) {
101        self.out.push(Token::new(text, kind, link));
102    }
103
104    fn kw(&mut self, text: &str) {
105        self.push(text, TokenKind::Keyword, None);
106    }
107
108    fn op(&mut self, text: impl Into<String>) {
109        self.push(text, TokenKind::Operator, None);
110    }
111
112    fn punct(&mut self, text: &str) {
113        self.push(text, TokenKind::Punctuation, None);
114    }
115
116    /// The `<ty> ` prefix shared by every typed operand. Mirrors
117    /// `write!(f, "{} ", type_name)` in `ValueRef`'s `Display`.
118    fn ty(&mut self, type_id: crate::types::TypeId) {
119        self.push(
120            format!("{} ", self.view.shared().types.type_name(type_id)),
121            TokenKind::Type,
122            None,
123        );
124    }
125
126    /// A value operand, rendered exactly as `ValueRef`'s `Display`: `<ty> <atom>`
127    /// for scalars (instruction, block param, literal, bytes, varnode), and bare
128    /// for functions/blocks.
129    fn value(&mut self, id: ValueId) {
130        let link = Some(Link::Value(id));
131        // Note: instruction / block-param operands are never foreign. They are
132        // stored as bare body-local ids and qualified with their reader's own
133        // `func`, so a cross-function data operand is unrepresentable — no guard
134        // is needed (or wanted: one would mask a mis-qualification). Only the
135        // *absolute* ids below (blocks, and symbolic block literals) can name
136        // another function.
137        match id {
138            ValueId::Instruction(iid) => {
139                let r = self.view.insn_ref(iid);
140                self.ty(r.type_id());
141                self.push(instruction_atom(self.view, iid), TokenKind::Variable, link);
142            }
143            ValueId::BlockParam(pid) => {
144                let r = self.view.param_ref(pid);
145                self.ty(r.type_id());
146                self.push(
147                    block_param_atom(self.view, pid),
148                    TokenKind::BlockParam,
149                    link,
150                );
151            }
152            ValueId::Literal(lid) => {
153                let r = LiteralRef::from_id(self.view.shared(), lid);
154                self.ty(r.type_id());
155                // The literal *atom* is rendered from the whole `&Context`, so a
156                // symbolic block/function literal resolves its target name (a
157                // `&Shared`-backed `LiteralRef` cannot — context-split 5b-ii #1).
158                self.push(literal_atom_view(self.view, lid), TokenKind::Literal, link);
159            }
160            ValueId::Bytes(bid) => {
161                let r = BytesRef::from_id(self.view.shared(), bid);
162                self.ty(r.type_id());
163                self.push(r.to_string(), TokenKind::Bytes, link);
164            }
165            ValueId::Varnode(vid) => {
166                let r = Varnode::from_id(self.view.shared(), vid);
167                self.push(format!("i{} ", r.size() * 8), TokenKind::Type, None);
168                self.push(r.to_string(), TokenKind::Varnode, link);
169            }
170            ValueId::Poison(pid) => {
171                let ty = self.view.shared().values.poisons[pid].type_id;
172                self.ty(ty);
173                self.push("poison".to_string(), TokenKind::Literal, link);
174            }
175            ValueId::Temp(id) => {
176                let r = self.view.temp_ref(id);
177                self.push(format!("i{} ", r.size() * 8), TokenKind::Type, None);
178                self.push(r.to_string(), TokenKind::Varnode, link);
179            }
180            ValueId::Function(fid) => {
181                let name = self.view.interface(fid).name.to_string();
182                self.push(
183                    format!("<{name}>"),
184                    TokenKind::Function,
185                    Some(Link::Function(fid)),
186                );
187            }
188            ValueId::BasicBlock(bid) => {
189                // A block used as a value renders via the block's own `Display`
190                // (never `ValueRef`'s, which routes back here — that would recurse).
191                // A block in another function (a transient during discovery) is
192                // unreadable through a function-scoped view, so render its id
193                // instead of resolving the foreign body's block text.
194                let text = if self.view.owner().is_some_and(|o| o != bid.func) {
195                    format!("<{bid}>")
196                } else {
197                    self.view.block_ref(bid).to_string()
198                };
199                self.push(text, TokenKind::Label, Some(Link::Block(bid)));
200            }
201        }
202    }
203
204    /// A value operand printed *bare* (no type prefix) for instruction results,
205    /// matching `fmt_bare_value` used by `extract`/`gep`. Non-instruction values
206    /// fall back to the regular typed rendering.
207    fn bare_value(&mut self, id: ValueId) {
208        match id {
209            ValueId::Instruction(iid) => {
210                self.push(
211                    instruction_atom(self.view, iid),
212                    TokenKind::Variable,
213                    Some(Link::Value(id)),
214                );
215            }
216            other => self.value(other),
217        }
218    }
219
220    /// A direct branch/cbranch target: `<name @p=arg …>`. Mirrors
221    /// `fmt_branch_target`. The `target` is a bare body-local index; `func` is the
222    /// terminator's owning function (strict IR locality ⇒ the target lives in that
223    /// same arena), used to recover the full [`BlockId`].
224    fn branch_target(&mut self, func: FunctionId, target: LocalBlockId, args: &[LocalValueId]) {
225        let target = BlockId::new(func, target);
226        let block = self.view.block_ref(target);
227        let name = block.name().unwrap_or("unnamed");
228        self.push(
229            format!("<{name}"),
230            TokenKind::Label,
231            Some(Link::Block(target)),
232        );
233
234        let params = block.params().collect::<Vec<_>>();
235        for (i, &arg) in args.iter().enumerate() {
236            self.punct(" ");
237            match params.get(i) {
238                Some(param) => self.push(param.to_string(), TokenKind::BlockParam, None),
239                None => self.push(format!("@arg{i}"), TokenKind::BlockParam, None),
240            }
241            self.op("=");
242            self.value(arg.qualify(func));
243        }
244
245        self.push(">", TokenKind::Label, None);
246    }
247}
248
249/// The bare atom for an instruction result: `%name` or `%tmp<id>`.
250fn instruction_atom<'ctx, 'str: 'ctx>(
251    view: impl QCodeView<'ctx, 'str>,
252    id: crate::value::InstructionId,
253) -> String {
254    match view.instruction(id).name.as_deref() {
255        Some(name) => format!("%{name}"),
256        None => format!("%tmp{:x}", usize::from(id.local)),
257    }
258}
259
260/// The bare atom for a block parameter: `@name` or `@param<id>`.
261fn block_param_atom<'ctx, 'str: 'ctx>(
262    view: impl QCodeView<'ctx, 'str>,
263    id: crate::value::BlockParamId,
264) -> String {
265    let r = view.param_ref(id);
266    match r.name() {
267        Some(name) => format!("@{name}"),
268        None => format!("@param{:x}", usize::from(id.local)),
269    }
270}
271
272/// Render an instruction as colored, linkable tokens. Concatenating the tokens'
273/// text equals the instruction's `Display` (`as_statement()`) output.
274pub fn instruction_segments<'ctx, 'str: 'ctx, R>(insn: &InstructionRef<'str, 'ctx, R>) -> Vec<Token>
275where
276    R: QCodeView<'ctx, 'str>,
277{
278    let view = insn.view;
279    let mut seg = Seg {
280        view,
281        out: Vec::new(),
282        marker: std::marker::PhantomData,
283    };
284
285    // LHS: `<ty> %name = ` (mirrors `InstructionStatement` + `InstructionRef`'s
286    // inherent `fmt`). Terminators and other size-0 instructions have no LHS.
287    if insn.size() != 0 {
288        seg.ty(insn.type_id());
289        seg.push(
290            instruction_atom(view, insn.id),
291            TokenKind::Variable,
292            Some(Link::Value(ValueId::Instruction(insn.id))),
293        );
294        seg.op(" = ");
295    }
296
297    match insn.mnemonic() {
298        Mnemonic::Tuple(t) => tuple_with_type(&mut seg, insn.id.func, t, insn.type_id()),
299        m => mnemonic_segments(&mut seg, insn.id.func, m),
300    }
301
302    seg.out
303}
304
305fn tuple_with_type<'ctx, 'str: 'ctx>(
306    seg: &mut Seg<'ctx, 'str, impl QCodeView<'ctx, 'str>>,
307    func: FunctionId,
308    t: &crate::value::insn::Tuple,
309    type_id: crate::types::TypeId,
310) {
311    seg.kw("pack");
312    seg.punct("(");
313    for (i, &field) in t.fields.iter().enumerate() {
314        if i > 0 {
315            seg.punct(", ");
316        }
317        let name = seg
318            .view
319            .shared()
320            .types
321            .field_name(type_id, i)
322            .map(str::to_owned)
323            .unwrap_or_else(|| format!("field{}", i + 1));
324        seg.push(name, TokenKind::Field, None);
325        seg.op("=");
326        seg.value(field.qualify(func));
327    }
328    seg.punct(");");
329}
330
331fn mnemonic_segments<'ctx, 'str: 'ctx>(
332    seg: &mut Seg<'ctx, 'str, impl QCodeView<'ctx, 'str>>,
333    func: FunctionId,
334    m: &Mnemonic,
335) {
336    use crate::value::insn::Unop;
337    match m {
338        Mnemonic::Load(l) => {
339            seg.kw("load");
340            seg.punct("(");
341            seg.push(space_name(seg.view, func, l.space), TokenKind::Space, None);
342            seg.punct(":");
343            seg.push(l.size.to_string(), TokenKind::Type, None);
344            seg.punct(", ");
345            seg.value(l.ptr.qualify(func));
346            seg.punct(");");
347        }
348        Mnemonic::Store(s) => {
349            seg.kw("store");
350            seg.punct("(");
351            seg.push(space_name(seg.view, func, s.space), TokenKind::Space, None);
352            seg.punct(":");
353            seg.push(s.size.to_string(), TokenKind::Type, None);
354            seg.punct(", ");
355            seg.value(s.ptr.qualify(func));
356            seg.op(" <- ");
357            seg.value(s.src.qualify(func));
358            seg.punct(");");
359        }
360        Mnemonic::Branch(b) => {
361            seg.kw("goto ");
362            seg.branch_target(func, b.target, &b.args);
363            seg.punct(";");
364        }
365        Mnemonic::BranchInd(b) => {
366            seg.kw("goto ");
367            seg.punct("[");
368            seg.value(b.ptr.qualify(func));
369            seg.punct("];");
370        }
371        Mnemonic::Switch(sw) => {
372            seg.kw("switch ");
373            seg.value(sw.scrutinee.qualify(func));
374            seg.punct(" { ");
375            for (i, case) in sw.cases.iter().enumerate() {
376                if i > 0 {
377                    seg.punct(", ");
378                }
379                seg.push(format!("{:#x}", case.value), TokenKind::Literal, None);
380                seg.op(" => ");
381                seg.branch_target(func, case.target, &case.args);
382            }
383            if let Some(default) = sw.default {
384                if !sw.cases.is_empty() {
385                    seg.punct(", ");
386                }
387                seg.kw("default");
388                seg.op(" => ");
389                seg.branch_target(func, default, &sw.default_args);
390            }
391            seg.punct(" };");
392        }
393        Mnemonic::CBranch(cb) => {
394            seg.kw("if ");
395            seg.value(cb.condition.qualify(func));
396            seg.kw(" goto ");
397            seg.branch_target(func, cb.success_block, &cb.success_args);
398            seg.kw(" else goto ");
399            seg.branch_target(func, cb.failure_block, &cb.failure_args);
400            seg.punct(";");
401        }
402        Mnemonic::Apply(a) => {
403            seg.kw("apply ");
404            let (target, link) = callee_name_link(seg.view, a.target);
405            seg.push(target, TokenKind::Function, link);
406            seg.punct("(");
407            for (i, &arg) in a.args.iter().enumerate() {
408                if i > 0 {
409                    seg.punct(", ");
410                }
411                seg.value(arg.qualify(func));
412            }
413            seg.punct(");");
414        }
415        Mnemonic::Call(c) => {
416            seg.kw("call fn ");
417            let (target, link) = callee_name_link(seg.view, c.target);
418            seg.push(target, TokenKind::Function, link);
419            seg.punct("(");
420            for (i, &arg) in c.args.iter().enumerate() {
421                if i > 0 {
422                    seg.punct(", ");
423                }
424                let arg_name = c
425                    .target
426                    .real()
427                    .map(|target| call_arg_name(seg.view, target, i))
428                    .unwrap_or_else(|| format!("@arg{i}="));
429                seg.push(arg_name, TokenKind::BlockParam, None);
430                seg.value(arg.qualify(func));
431            }
432            seg.punct(");");
433        }
434        Mnemonic::TailCall(tc) => {
435            seg.kw("tailcall fn ");
436            let (target, link) = callee_name_link(seg.view, tc.target);
437            seg.push(target, TokenKind::Function, link);
438            seg.punct("(");
439            for (i, &arg) in tc.args.iter().enumerate() {
440                if i > 0 {
441                    seg.punct(", ");
442                }
443                seg.value(arg.qualify(func));
444            }
445            seg.punct(");");
446        }
447        Mnemonic::CallInd(c) => {
448            seg.kw("call ");
449            seg.punct("[");
450            seg.value(c.ptr.qualify(func));
451            seg.punct("]");
452            if !c.args.is_empty() {
453                seg.punct("(");
454                for (i, &arg) in c.args.iter().enumerate() {
455                    if i > 0 {
456                        seg.punct(", ");
457                    }
458                    seg.value(arg.qualify(func));
459                }
460                seg.punct(")");
461            }
462            seg.punct(";");
463        }
464        // No operands: the marker alone. Kept short so a run of unlifted padding
465        // stays readable.
466        Mnemonic::BadInsn(_) => {
467            seg.kw("badinsn");
468            seg.punct(";");
469        }
470        Mnemonic::Return(r) => match r.value {
471            Some(value) => {
472                seg.kw("return ");
473                seg.value(value.qualify(func));
474                seg.kw(" at ");
475                seg.value(r.ptr.qualify(func));
476                seg.punct(";");
477            }
478            None => {
479                seg.kw("return at ");
480                seg.value(r.ptr.qualify(func));
481                seg.punct(";");
482            }
483        },
484        Mnemonic::ReturnValue(r) => {
485            seg.kw("return ");
486            seg.value(r.value.qualify(func));
487            seg.punct(";");
488        }
489        Mnemonic::Unop(u) => match u.op {
490            Unop::IntNegate | Unop::IntNot | Unop::FloatNegate => {
491                seg.op(format!("{} ", u.op));
492                seg.value(u.src.qualify(func));
493                seg.punct(";");
494            }
495            _ => {
496                seg.kw(&u.op.to_string());
497                seg.punct("(");
498                seg.value(u.src.qualify(func));
499                seg.punct(");");
500            }
501        },
502        Mnemonic::Binop(b) => {
503            seg.value(b.lhs.qualify(func));
504            seg.op(format!(" {} ", b.op));
505            seg.value(b.rhs.qualify(func));
506            seg.punct(";");
507        }
508        Mnemonic::Zext(z) => cast(seg, func, "zext", 'i', z.size, z.src),
509        Mnemonic::Sext(s) => cast(seg, func, "sext", 'i', s.size, s.src),
510        Mnemonic::IntToFloat(c) => cast(seg, func, "int2float", 'f', c.size, c.src),
511        Mnemonic::FloatToFloat(c) => cast(seg, func, "float2float", 'f', c.size, c.src),
512        Mnemonic::FloatToInt(c) => cast(seg, func, "trunc", 'i', c.size, c.src),
513        Mnemonic::Range(r) => {
514            seg.value(r.src.qualify(func));
515            seg.punct("[");
516            seg.push(r.start.to_string(), TokenKind::Literal, None);
517            seg.punct(":");
518            seg.push((r.start + r.size).to_string(), TokenKind::Literal, None);
519            seg.punct("];");
520        }
521        Mnemonic::IsFloatNaN(o) => unary_call(seg, func, "nan", o.src),
522        Mnemonic::LzCount(o) => unary_call(seg, func, "lzcount", o.src),
523        Mnemonic::PopCount(o) => unary_call(seg, func, "popcount", o.src),
524        Mnemonic::Carry(o) => binary_call(seg, func, "carry", o.lhs, o.rhs),
525        Mnemonic::SCarry(o) => binary_call(seg, func, "scarry", o.lhs, o.rhs),
526        Mnemonic::SBorrow(o) => binary_call(seg, func, "sborrow", o.lhs, o.rhs),
527        Mnemonic::Assert(a) => {
528            seg.kw("assert ");
529            seg.value(a.condition.qualify(func));
530            seg.punct(";");
531        }
532        // `Tuple` is normally routed through `tuple_with_type` (it always has a
533        // result type); this bare form mirrors `Tuple`'s own `MnemonicKind::fmt`.
534        Mnemonic::Tuple(t) => {
535            seg.kw("pack");
536            seg.punct("(");
537            for (i, &field) in t.fields.iter().enumerate() {
538                if i > 0 {
539                    seg.punct(", ");
540                }
541                seg.push(format!("field{}", i + 1), TokenKind::Field, None);
542                seg.op("=");
543                seg.value(field.qualify(func));
544            }
545            seg.punct(");");
546        }
547        Mnemonic::Extract(e) => {
548            seg.kw("extract");
549            seg.punct("(");
550            seg.bare_value(e.agg.qualify(func));
551            let name = e
552                .field_name_view(seg.view, func)
553                .map(str::to_owned)
554                .unwrap_or_else(|| format!("field{}", e.index + 1));
555            seg.push(format!(".{name}"), TokenKind::Field, None);
556            seg.punct(");");
557        }
558        Mnemonic::Gep(g) => {
559            seg.kw("gep");
560            seg.punct("(");
561            seg.bare_value(g.base.qualify(func));
562            match g.field_name_view(seg.view, func) {
563                Some(name) => seg.push(format!(".{name}"), TokenKind::Field, None),
564                None => {
565                    seg.op(" + ");
566                    seg.push(format!("{:#x}", g.offset), TokenKind::Literal, None);
567                }
568            }
569            seg.punct(");");
570        }
571        Mnemonic::Map(map) => {
572            let (body, link) = callee_name_link(seg.view, map.body);
573            if map.captures.is_empty() {
574                seg.push(body, TokenKind::Function, link);
575                seg.op(" <$> ");
576                seg.value(map.src.qualify(func));
577                seg.punct(";");
578            } else {
579                seg.punct("(");
580                seg.push(body, TokenKind::Function, link);
581                for &c in &map.captures {
582                    seg.punct(" ");
583                    seg.value(c.qualify(func));
584                }
585                seg.op(") <$> ");
586                seg.value(map.src.qualify(func));
587                seg.punct(";");
588            }
589        }
590        Mnemonic::Scan(scan) => {
591            let (body, link) = callee_name_link(seg.view, scan.body);
592            let body = match scan.body {
593                Callee::Real(_) => format!("@{body}"),
594                Callee::Minted(_) => body,
595            };
596            if scan.captures.is_empty() {
597                seg.kw("scanl ");
598                seg.push(body, TokenKind::Function, link);
599                seg.punct(" ");
600                seg.value(scan.init.qualify(func));
601                seg.punct(" ");
602                seg.value(scan.src.qualify(func));
603                seg.punct(";");
604            } else {
605                seg.kw("scanl ");
606                seg.punct("(");
607                seg.push(body, TokenKind::Function, link);
608                for &c in &scan.captures {
609                    seg.punct(" ");
610                    seg.value(c.qualify(func));
611                }
612                seg.punct(") ");
613                seg.value(scan.init.qualify(func));
614                seg.punct(" ");
615                seg.value(scan.src.qualify(func));
616                seg.punct(";");
617            }
618        }
619        Mnemonic::PCodeOp(p) => {
620            let op = seg.view.shared().pcode_ops[p.id].to_string();
621            if let Some(dst) = p.dst {
622                seg.value(dst.qualify(func));
623                seg.op(" = ");
624            }
625            seg.kw(&op);
626            seg.punct("(");
627            for (i, &arg) in p.args.iter().enumerate() {
628                if i > 0 {
629                    seg.punct(", ");
630                }
631                seg.value(arg.qualify(func));
632            }
633            seg.punct(");");
634        }
635        Mnemonic::Intrinsic(intr) => {
636            seg.kw(&format!("${}", intr.id.name()));
637            seg.punct("(");
638            for (i, &arg) in intr.args.iter().enumerate() {
639                if i > 0 {
640                    seg.punct(", ");
641                }
642                seg.value(arg.qualify(func));
643            }
644            seg.punct(");");
645        }
646    }
647}
648
649fn cast<'ctx, 'str: 'ctx>(
650    seg: &mut Seg<'ctx, 'str, impl QCodeView<'ctx, 'str>>,
651    func: FunctionId,
652    kw: &str,
653    prefix: char,
654    size: usize,
655    src: LocalValueId,
656) {
657    seg.kw(kw);
658    seg.punct("(");
659    seg.push(format!("{prefix}{}", size * 8), TokenKind::Type, None);
660    seg.punct(", ");
661    seg.value(src.qualify(func));
662    seg.punct(");");
663}
664
665fn unary_call<'ctx, 'str: 'ctx>(
666    seg: &mut Seg<'ctx, 'str, impl QCodeView<'ctx, 'str>>,
667    func: FunctionId,
668    kw: &str,
669    src: LocalValueId,
670) {
671    seg.kw(kw);
672    seg.punct("(");
673    seg.value(src.qualify(func));
674    seg.punct(");");
675}
676
677fn binary_call<'ctx, 'str: 'ctx>(
678    seg: &mut Seg<'ctx, 'str, impl QCodeView<'ctx, 'str>>,
679    func: FunctionId,
680    kw: &str,
681    lhs: LocalValueId,
682    rhs: LocalValueId,
683) {
684    seg.kw(kw);
685    seg.punct("(");
686    seg.value(lhs.qualify(func));
687    seg.punct(", ");
688    seg.value(rhs.qualify(func));
689    seg.punct(");");
690}
691
692/// The space name as printed by `load`/`store`'s `fmt`: the named space, or a
693/// `space: <id>` fallback for an unnamed space.
694fn space_name<'ctx, 'str: 'ctx>(
695    view: impl QCodeView<'ctx, 'str>,
696    func: FunctionId,
697    space: crate::space::LocalMemorySpaceId,
698) -> String {
699    match space.qualify(func) {
700        crate::space::MemorySpaceId::Shared(space) => {
701            let space_ref = Space::from_id(view.shared(), space);
702            match space_ref.name.as_deref() {
703                Some(name) => name.to_string(),
704                None => format!("space: {space}"),
705            }
706        }
707        // `$tempN` is an explicit body-local space token. The numeric local ID
708        // makes the canonical print stable even when a display name is absent,
709        // duplicated, or not a qcode identifier; lowering recreates one local
710        // space per token in first-use order.
711        crate::space::MemorySpaceId::Temp(space) => {
712            format!("$temp{}", usize::from(space.local))
713        }
714    }
715}
716
717/// The `@name=` / `@arg<i>=` prefix for a direct-call argument. Mirrors
718/// `fmt_call_arg_name`.
719fn call_arg_name<'ctx, 'str: 'ctx>(
720    view: impl QCodeView<'ctx, 'str>,
721    target: FunctionId,
722    index: usize,
723) -> String {
724    // The authoritative name is the callee's root block param, which lives in the
725    // callee's *body*. A function-scoped view (a `BodyView`, as used by the
726    // pass-fixpoint fingerprint) may not read another function's body at all, so
727    // for a foreign callee fall back to the interface-only name — the C-prototype
728    // argument name, if any, else the positional form. Purely cosmetic: only the
729    // rendered argument label changes, never the operand itself.
730    let foreign = view.owner().is_some_and(|owner| owner != target);
731    let name = if foreign {
732        view.interface(target)
733            .signature
734            .as_ref()
735            .and_then(|s| s.extern_interface.as_ref())
736            .and_then(|iface| iface.args.get(index))
737            .and_then(|a| a.name.as_ref().map(|n| n.to_string()))
738    } else {
739        view.function_ref(target).input_arg_name(index)
740    };
741    match name {
742        Some(name) => format!("@{name}="),
743        None => format!("@arg{index}="),
744    }
745}
746
747/// Render a real function symbol or an unresolved pass-local placeholder.
748/// Placeholders deliberately carry no link: they are not installed functions.
749fn callee_name_link<'ctx, 'str: 'ctx>(
750    view: impl QCodeView<'ctx, 'str>,
751    callee: Callee,
752) -> (String, Option<Link>) {
753    match callee {
754        Callee::Real(id) => (
755            view.interface(id).name.to_string(),
756            Some(Link::Function(id)),
757        ),
758        Callee::Minted(slot) => (format!("<minted:{slot}>"), None),
759    }
760}
761
762/// The token stream for a single value operand. Concatenating the token text
763/// equals [`ValueRef`](crate::value::ValueRef)'s `Display` — which is implemented
764/// by writing these.
765pub fn value_tokens(ctx: &Context<'_>, id: ValueId) -> Vec<Token> {
766    let mut seg = Seg {
767        view: crate::value::ModuleView::new(ctx),
768        out: Vec::new(),
769        marker: std::marker::PhantomData,
770    };
771    seg.value(id);
772    seg.out
773}
774
775/// Provider-generic value rendering used by immutable arena-cluster refs.
776pub fn value_tokens_view<'ctx, 'str: 'ctx, R>(view: R, id: ValueId) -> Vec<Token>
777where
778    R: QCodeView<'ctx, 'str>,
779{
780    let link = Some(Link::Value(id));
781    let shared = view.shared();
782    let mut out = Vec::new();
783    let mut typed = |type_id, text: String, kind| {
784        out.push(Token::new(
785            format!("{} ", shared.types.type_name(type_id)),
786            TokenKind::Type,
787            None,
788        ));
789        out.push(Token::new(text, kind, link));
790    };
791
792    match id {
793        ValueId::Instruction(iid) => {
794            let insn = view.instruction(iid);
795            let atom = insn.name.as_deref().map_or_else(
796                || {
797                    let local: usize = iid.local.into();
798                    format!("%tmp{local:x}")
799                },
800                |name| format!("%{name}"),
801            );
802            typed(insn.type_id, atom, TokenKind::Variable);
803        }
804        ValueId::BlockParam(pid) => {
805            let param = view.block_param(pid);
806            let atom = param.name.as_deref().map_or_else(
807                || {
808                    let local: usize = pid.local.into();
809                    format!("@param{local:x}")
810                },
811                |name| format!("@{name}"),
812            );
813            typed(param.type_id, atom, TokenKind::BlockParam);
814        }
815        ValueId::Literal(lid) => {
816            let literal = &shared.values.literals[lid];
817            let atom = literal_atom_view(view, lid);
818            typed(literal.type_id, atom, TokenKind::Literal);
819        }
820        ValueId::Bytes(id) => {
821            let value = BytesRef::from_id(shared, id);
822            typed(value.type_id(), value.to_string(), TokenKind::Bytes);
823        }
824        ValueId::Varnode(id) => {
825            let value = Varnode::from_id(shared, id);
826            out.push(Token::new(
827                format!("i{} ", value.size() * 8),
828                TokenKind::Type,
829                None,
830            ));
831            out.push(Token::new(value.to_string(), TokenKind::Varnode, link));
832        }
833        ValueId::Temp(id) => {
834            let value = view.temp_ref(id);
835            out.push(Token::new(
836                format!("i{} ", value.size() * 8),
837                TokenKind::Type,
838                None,
839            ));
840            out.push(Token::new(value.to_string(), TokenKind::Varnode, link));
841        }
842        ValueId::Function(id) => out.push(Token::new(
843            format!("<{}>", view.interface(id).name),
844            TokenKind::Function,
845            Some(Link::Function(id)),
846        )),
847        ValueId::BasicBlock(id) => out.push(Token::new(
848            view.block_ref(id).to_string(),
849            TokenKind::Label,
850            Some(Link::Block(id)),
851        )),
852        ValueId::Poison(id) => {
853            typed(
854                shared.values.poisons[id].type_id,
855                "poison".to_string(),
856                TokenKind::Literal,
857            );
858        }
859    }
860    out
861}
862
863/// The rendered *atom* (no `<ty>` prefix) of the literal `id`, resolved against
864/// the whole `&Context` so symbolic block/function literals show their target
865/// name. This is the full-context twin of `LiteralRef`'s `Display` (which, being
866/// `&Shared`-backed, cannot reach body/interface names and falls back to the
867/// numeric form). Used by the instruction renderer and the dataflow graph.
868/// Concatenating with the type prefix reproduces the pre-narrowing rendering
869/// byte-for-byte (context-split stage 5b-ii item #1).
870pub fn literal_atom(ctx: &Context<'_>, id: LiteralId) -> String {
871    let literal = &ctx.shared.values.literals[id];
872    match &literal.symbolic {
873        Some(SymbolicRef::Block(bid)) => match BasicBlock::from_id(ctx, *bid).name() {
874            Some(name) => format!("&<{}>", name),
875            None => format!("&<0x{:x}>", literal.value),
876        },
877        Some(SymbolicRef::Function(fid)) => {
878            format!("&<{}>", FunctionBody::from_id(ctx, *fid).name())
879        }
880        Some(SymbolicRef::String(s)) => format!("&{:?}", s),
881        None if ctx.shared.types.is_bool(literal.type_id) => {
882            (if literal.value != 0 { "true" } else { "false" }).to_string()
883        }
884        None => format!("0x{:x}", literal.value),
885    }
886}
887
888fn literal_atom_view<'ctx, 'str: 'ctx>(view: impl QCodeView<'ctx, 'str>, id: LiteralId) -> String {
889    let shared = view.shared();
890    let literal = &shared.values.literals[id];
891    match &literal.symbolic {
892        // A symbolic block-ref into *another* function (a transient during
893        // discovery/jump-table recovery) cannot be name-resolved through a
894        // function-scoped `BodyView` — reading the foreign body trips the locality
895        // guard — so fall back to the numeric form. A whole-module view (`owner()
896        // == None`) resolves the name normally.
897        Some(SymbolicRef::Block(id)) if view.owner().is_some_and(|o| o != id.func) => {
898            format!("&<0x{:x}>", literal.value)
899        }
900        Some(SymbolicRef::Block(id)) => view.block_ref(*id).name().map_or_else(
901            || format!("&<0x{:x}>", literal.value),
902            |name| format!("&<{name}>"),
903        ),
904        Some(SymbolicRef::Function(id)) => format!("&<{}>", view.interface(*id).name),
905        Some(SymbolicRef::String(value)) => format!("&{value:?}"),
906        None if shared.types.is_bool(literal.type_id) => {
907            (if literal.value != 0 { "true" } else { "false" }).to_string()
908        }
909        None => format!("0x{:x}", literal.value),
910    }
911}
912
913/// The token stream for a **shared-leaf** value operand (literal, bytes, varnode),
914/// rendered from only the module's [`Shared`] IR state. The `&Shared` twin of
915/// [`value_tokens`] for the operands a `&Shared`-backed [`ValueRef`](crate::value::ValueRef) can hold;
916/// symbolic block/function literals fall back to the numeric form (their names
917/// live in bodies/interfaces, out of a `&Shared`'s reach). Panics on
918/// arena-cluster ids, which a shared-leaf ref never carries.
919pub fn value_tokens_shared(shared: &Shared<'_>, id: ValueId) -> Vec<Token> {
920    let link = Some(Link::Value(id));
921    let mut out = Vec::new();
922    match id {
923        ValueId::Literal(lid) => {
924            let r = LiteralRef::from_id(shared, lid);
925            out.push(Token::new(
926                format!("{} ", shared.types.type_name(r.type_id())),
927                TokenKind::Type,
928                None,
929            ));
930            out.push(Token::new(r.to_string(), TokenKind::Literal, link));
931        }
932        ValueId::Bytes(bid) => {
933            let r = BytesRef::from_id(shared, bid);
934            out.push(Token::new(
935                format!("{} ", shared.types.type_name(r.type_id())),
936                TokenKind::Type,
937                None,
938            ));
939            out.push(Token::new(r.to_string(), TokenKind::Bytes, link));
940        }
941        ValueId::Varnode(vid) => {
942            let r = Varnode::from_id(shared, vid);
943            out.push(Token::new(
944                format!("i{} ", r.size() * 8),
945                TokenKind::Type,
946                None,
947            ));
948            out.push(Token::new(r.to_string(), TokenKind::Varnode, link));
949        }
950        ValueId::Poison(pid) => {
951            let ty = shared.values.poisons[pid].type_id;
952            out.push(Token::new(
953                format!("{} ", shared.types.type_name(ty)),
954                TokenKind::Type,
955                None,
956            ));
957            out.push(Token::new("poison".to_string(), TokenKind::Literal, link));
958        }
959        _ => panic!("value_tokens_shared: not a shared-leaf value id"),
960    }
961    out
962}