codegen_lang/program.rs
1//! The compiled program and the bytecode it is made of.
2
3use alloc::string::String;
4use alloc::vec::Vec;
5use core::fmt;
6
7use ir_lang::{BinOp, UnOp};
8
9/// A virtual register: a numbered slot that holds one value while a program runs.
10///
11/// The bytecode is register-based rather than stack-based. Every value the source
12/// function defines is given its own register, so an [`Op`] names its operands and its
13/// result by register instead of by a position on an operand stack. Registers are dense
14/// from zero; [`Program::register_count`] is one past the highest in use. A function's
15/// parameters occupy the first registers — read them from [`Program::params`].
16///
17/// # Examples
18///
19/// ```
20/// use codegen_lang::Reg;
21///
22/// let r = Reg(2);
23/// assert_eq!(r.0, 2);
24/// assert_eq!(r.to_string(), "r2");
25/// ```
26#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub struct Reg(pub u32);
29
30impl fmt::Display for Reg {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 write!(f, "r{}", self.0)
33 }
34}
35
36/// A jump target: a position in a program's [op stream](Program::ops) that a
37/// control-flow op transfers to.
38///
39/// Each basic block of the source function becomes a label, numbered by block index, so
40/// the entry block is always [`Label(0)`](Program::entry). Laying out a two-way branch
41/// needs one extra position for the second arm, so a backend appends a few internal
42/// labels past the block labels. Resolve a label to an op index with
43/// [`Program::label_offset`].
44///
45/// # Examples
46///
47/// ```
48/// use codegen_lang::Label;
49///
50/// assert_eq!(Label(0).to_string(), "L0");
51/// assert_eq!(Label(3).to_string(), "L3");
52/// ```
53#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
54#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
55pub struct Label(pub u32);
56
57impl fmt::Display for Label {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 write!(f, "L{}", self.0)
60 }
61}
62
63/// A constant operand loaded by [`Op::Const`].
64///
65/// The three cases mirror the IR's three constant instructions
66/// ([`Iconst`](ir_lang::Inst::Iconst), [`Fconst`](ir_lang::Inst::Fconst),
67/// [`Bconst`](ir_lang::Inst::Bconst)) and carry the same payloads, so a constant is
68/// reproduced exactly rather than widened or reinterpreted.
69///
70/// # Examples
71///
72/// ```
73/// use codegen_lang::Const;
74///
75/// assert_eq!(Const::Int(-7).to_string(), "-7");
76/// assert_eq!(Const::Bool(true).to_string(), "true");
77/// ```
78#[derive(Clone, Copy, PartialEq, Debug)]
79#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
80pub enum Const {
81 /// A signed-integer constant.
82 Int(i64),
83 /// A floating-point constant.
84 Float(f64),
85 /// A boolean constant.
86 Bool(bool),
87}
88
89impl fmt::Display for Const {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 match self {
92 Const::Int(value) => write!(f, "{value}"),
93 Const::Float(value) => write!(f, "{value}"),
94 Const::Bool(value) => write!(f, "{value}"),
95 }
96 }
97}
98
99/// One bytecode instruction.
100///
101/// An op is the unit a [`Program`] is a sequence of. The arithmetic ops
102/// ([`Const`](Op::Const), [`Bin`](Op::Bin), [`Un`](Op::Un)) write their result to a
103/// destination [`Reg`] and read their operands from registers; [`Move`](Op::Move) copies
104/// one register to another; and the control-flow ops ([`Jump`](Op::Jump),
105/// [`JumpUnless`](Op::JumpUnless), [`Return`](Op::Return)) carry a [`Label`] or a result
106/// register. The set is closed and every variant is `Copy`, so an op stream is a flat
107/// `&[Op]` an interpreter or a further pass can walk with no indirection.
108///
109/// # Examples
110///
111/// ```
112/// use codegen_lang::{Const, Op, Reg};
113///
114/// let load = Op::Const { dst: Reg(0), value: Const::Int(1) };
115/// assert_eq!(load.to_string(), "r0 = const 1");
116///
117/// let ret = Op::Return { value: Some(Reg(0)) };
118/// assert_eq!(ret.to_string(), "ret r0");
119/// ```
120#[derive(Clone, Copy, PartialEq, Debug)]
121#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
122pub enum Op {
123 /// Load a constant into `dst`.
124 Const {
125 /// Register the constant is written to.
126 dst: Reg,
127 /// The constant value.
128 value: Const,
129 },
130 /// Apply a binary operation: `dst = lhs <op> rhs`.
131 Bin {
132 /// The operation, reusing the IR's [`BinOp`].
133 op: BinOp,
134 /// Register the result is written to.
135 dst: Reg,
136 /// Left operand register.
137 lhs: Reg,
138 /// Right operand register.
139 rhs: Reg,
140 },
141 /// Apply a unary operation: `dst = <op> src`.
142 Un {
143 /// The operation, reusing the IR's [`UnOp`].
144 op: UnOp,
145 /// Register the result is written to.
146 dst: Reg,
147 /// Operand register.
148 src: Reg,
149 },
150 /// Copy a register: `dst = src`. Emitted on a control-flow edge to move a block
151 /// argument into the parameter register of the block being entered — the bytecode's
152 /// stand-in for an SSA phi.
153 Move {
154 /// Destination register.
155 dst: Reg,
156 /// Source register.
157 src: Reg,
158 },
159 /// Jump unconditionally to `target`.
160 Jump {
161 /// The label to continue at.
162 target: Label,
163 },
164 /// Jump to `target` when `cond` holds `false`; otherwise fall through to the next op.
165 JumpUnless {
166 /// Register holding the boolean condition.
167 cond: Reg,
168 /// The label taken when the condition is `false`.
169 target: Label,
170 },
171 /// Return from the function, optionally yielding the value in a register.
172 Return {
173 /// The register whose value is returned, or `None` for a unit return.
174 value: Option<Reg>,
175 },
176}
177
178impl fmt::Display for Op {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 match self {
181 Op::Const { dst, value } => write!(f, "{dst} = const {value}"),
182 Op::Bin { op, dst, lhs, rhs } => write!(f, "{dst} = {op} {lhs}, {rhs}"),
183 Op::Un { op, dst, src } => write!(f, "{dst} = {op} {src}"),
184 Op::Move { dst, src } => write!(f, "{dst} = {src}"),
185 Op::Jump { target } => write!(f, "jump {target}"),
186 Op::JumpUnless { cond, target } => write!(f, "jump_unless {cond}, {target}"),
187 Op::Return { value: Some(reg) } => write!(f, "ret {reg}"),
188 Op::Return { value: None } => write!(f, "ret"),
189 }
190 }
191}
192
193/// A lowered function: a flat bytecode program ready to be inspected, serialized, or run.
194///
195/// A program is produced by a [`Backend`](crate::Backend) — for the bytecode target, by
196/// [`Bytecode`](crate::Bytecode) or the [`compile`](crate::compile) shortcut. It owns
197/// the function's name, the registers holding its parameters, a count of every register
198/// it uses, and the [op stream](Program::ops). Control-flow ops refer to positions in
199/// that stream through [`Label`]s, which [`label_offset`](Program::label_offset)
200/// resolves to op indices. Execution begins at the first op, the [entry](Program::entry)
201/// block.
202///
203/// The [`Display`](fmt::Display) implementation renders the program as a readable
204/// disassembly, which is the easiest way to see what a backend produced.
205///
206/// # Examples
207///
208/// ```
209/// use codegen_lang::compile;
210/// use ir_lang::{Builder, BinOp, Type};
211///
212/// // fn double(x: int) -> int { x + x }
213/// let mut b = Builder::new("double", &[Type::Int], Type::Int);
214/// let x = b.block_params(b.entry())[0];
215/// let sum = b.bin(BinOp::Add, x, x);
216/// b.ret(Some(sum));
217/// let program = compile(&b.finish()).expect("double is well-formed");
218///
219/// assert_eq!(program.name(), "double");
220/// assert_eq!(program.params().len(), 1);
221/// assert_eq!(program.register_count(), 2); // x and the sum
222/// assert!(!program.is_empty());
223/// ```
224#[derive(Clone, PartialEq, Debug)]
225#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
226pub struct Program {
227 pub(crate) name: String,
228 pub(crate) params: Vec<Reg>,
229 pub(crate) registers: u32,
230 pub(crate) ops: Vec<Op>,
231 pub(crate) labels: Vec<u32>,
232}
233
234impl Program {
235 /// Returns the function's name.
236 ///
237 /// # Examples
238 ///
239 /// ```
240 /// use codegen_lang::compile;
241 /// use ir_lang::{Builder, Type};
242 ///
243 /// let mut b = Builder::new("main", &[], Type::Unit);
244 /// b.ret(None);
245 /// assert_eq!(compile(&b.finish()).unwrap().name(), "main");
246 /// ```
247 #[must_use]
248 pub fn name(&self) -> &str {
249 &self.name
250 }
251
252 /// Returns the registers holding the function's parameters, in declaration order.
253 ///
254 /// These are the registers an interpreter writes the call arguments into before it
255 /// begins executing the [op stream](Program::ops).
256 ///
257 /// # Examples
258 ///
259 /// ```
260 /// use codegen_lang::compile;
261 /// use ir_lang::{Builder, Type};
262 ///
263 /// let mut b = Builder::new("f", &[Type::Int, Type::Bool], Type::Unit);
264 /// b.ret(None);
265 /// let program = compile(&b.finish()).unwrap();
266 /// assert_eq!(program.params().len(), 2);
267 /// ```
268 #[must_use]
269 pub fn params(&self) -> &[Reg] {
270 &self.params
271 }
272
273 /// Returns the number of registers the program uses; valid register numbers are
274 /// `0..register_count`.
275 ///
276 /// # Examples
277 ///
278 /// ```
279 /// use codegen_lang::compile;
280 /// use ir_lang::{Builder, Type};
281 ///
282 /// let mut b = Builder::new("f", &[Type::Int], Type::Int);
283 /// let x = b.block_params(b.entry())[0];
284 /// let one = b.iconst(1);
285 /// let r = b.bin(ir_lang::BinOp::Add, x, one);
286 /// b.ret(Some(r));
287 /// // x, the constant, and the sum.
288 /// assert_eq!(compile(&b.finish()).unwrap().register_count(), 3);
289 /// ```
290 #[must_use]
291 pub const fn register_count(&self) -> u32 {
292 self.registers
293 }
294
295 /// Returns the program's ops, in execution order.
296 ///
297 /// # Examples
298 ///
299 /// ```
300 /// use codegen_lang::{compile, Op};
301 /// use ir_lang::{Builder, Type};
302 ///
303 /// let mut b = Builder::new("f", &[], Type::Unit);
304 /// b.ret(None);
305 /// let program = compile(&b.finish()).unwrap();
306 /// assert!(matches!(program.ops(), [Op::Return { value: None }]));
307 /// ```
308 #[must_use]
309 pub fn ops(&self) -> &[Op] {
310 &self.ops
311 }
312
313 /// Returns the number of ops in the program.
314 #[must_use]
315 pub fn len(&self) -> usize {
316 self.ops.len()
317 }
318
319 /// Returns `true` if the program has no ops. A program lowered from a valid function
320 /// is never empty: its entry block always ends in a terminator op.
321 #[must_use]
322 pub fn is_empty(&self) -> bool {
323 self.ops.is_empty()
324 }
325
326 /// Resolves a label to the index of the op it points at, or `None` if the label does
327 /// not belong to this program.
328 ///
329 /// # Examples
330 ///
331 /// ```
332 /// use codegen_lang::compile;
333 /// use ir_lang::{Builder, Type};
334 ///
335 /// let mut b = Builder::new("f", &[], Type::Unit);
336 /// b.ret(None);
337 /// let program = compile(&b.finish()).unwrap();
338 /// // Execution starts at the entry label, which is the first op.
339 /// assert_eq!(program.label_offset(program.entry()), Some(0));
340 /// ```
341 #[must_use]
342 pub fn label_offset(&self, label: Label) -> Option<usize> {
343 self.labels
344 .get(label.0 as usize)
345 .map(|&offset| offset as usize)
346 }
347
348 /// Returns the entry label, where execution begins: always `L0`, the source
349 /// function's entry block, which lowers to the first op.
350 #[must_use]
351 pub const fn entry(&self) -> Label {
352 Label(0)
353 }
354}
355
356impl fmt::Display for Program {
357 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358 write!(f, "{}(", self.name)?;
359 for (i, param) in self.params.iter().enumerate() {
360 if i != 0 {
361 f.write_str(", ")?;
362 }
363 write!(f, "{param}")?;
364 }
365 writeln!(f, ") regs={}", self.registers)?;
366
367 for (index, op) in self.ops.iter().enumerate() {
368 // A label points at exactly one op offset and no two labels share one, so at
369 // most one label prints before each op.
370 for (id, &offset) in self.labels.iter().enumerate() {
371 if offset as usize == index {
372 writeln!(f, "{}:", Label(id as u32))?;
373 }
374 }
375 writeln!(f, " {op}")?;
376 }
377 Ok(())
378 }
379}
380
381#[cfg(test)]
382#[allow(
383 clippy::unwrap_used,
384 reason = "tests build known-valid functions, so compilation cannot fail"
385)]
386mod tests {
387 use super::{Const, Label, Op, Reg};
388 use crate::compile;
389 use ir_lang::{BinOp, Builder, Type};
390
391 #[test]
392 fn test_reg_and_label_display_use_short_prefixes() {
393 assert_eq!(Reg(0).to_string(), "r0");
394 assert_eq!(Reg(41).to_string(), "r41");
395 assert_eq!(Label(0).to_string(), "L0");
396 }
397
398 #[test]
399 fn test_const_display_matches_payload() {
400 assert_eq!(Const::Int(5).to_string(), "5");
401 assert_eq!(Const::Int(-5).to_string(), "-5");
402 assert_eq!(Const::Bool(false).to_string(), "false");
403 }
404
405 #[test]
406 fn test_op_display_renders_each_form() {
407 assert_eq!(
408 Op::Const {
409 dst: Reg(0),
410 value: Const::Int(3)
411 }
412 .to_string(),
413 "r0 = const 3"
414 );
415 assert_eq!(
416 Op::Bin {
417 op: BinOp::Add,
418 dst: Reg(2),
419 lhs: Reg(0),
420 rhs: Reg(1)
421 }
422 .to_string(),
423 "r2 = add r0, r1"
424 );
425 assert_eq!(
426 Op::Move {
427 dst: Reg(1),
428 src: Reg(0)
429 }
430 .to_string(),
431 "r1 = r0"
432 );
433 assert_eq!(Op::Jump { target: Label(1) }.to_string(), "jump L1");
434 assert_eq!(
435 Op::JumpUnless {
436 cond: Reg(0),
437 target: Label(2)
438 }
439 .to_string(),
440 "jump_unless r0, L2"
441 );
442 assert_eq!(Op::Return { value: None }.to_string(), "ret");
443 }
444
445 #[test]
446 fn test_program_disassembly_prints_header_label_and_ops() {
447 let mut b = Builder::new("double", &[Type::Int], Type::Int);
448 let x = b.block_params(b.entry())[0];
449 let sum = b.bin(BinOp::Add, x, x);
450 b.ret(Some(sum));
451 let text = compile(&b.finish()).unwrap().to_string();
452
453 assert!(text.starts_with("double(r0) regs=2"));
454 assert!(text.contains("L0:"));
455 assert!(text.contains("r1 = add r0, r0"));
456 assert!(text.contains("ret r1"));
457 // The exact disassembly, as documented in the API reference and release note.
458 assert_eq!(
459 text,
460 "double(r0) regs=2\nL0:\n r1 = add r0, r0\n ret r1\n"
461 );
462 }
463
464 #[test]
465 fn test_entry_label_resolves_to_first_op() {
466 let mut b = Builder::new("f", &[], Type::Unit);
467 b.ret(None);
468 let program = compile(&b.finish()).unwrap();
469 assert_eq!(program.entry(), Label(0));
470 assert_eq!(program.label_offset(program.entry()), Some(0));
471 assert_eq!(program.label_offset(Label(999)), None);
472 }
473}