# codegen-lang v0.2.0 — Core

**The first release with domain logic.** v0.2.0 turns the v0.1.0 scaffold into a working backend: it lowers an `ir-lang` function in SSA form to a flat, register-based bytecode program. The release introduces the whole public surface — the `Backend` trait, the `Bytecode` reference backend, and the `Program` / `Op` model it emits — together with full rustdoc, an `API.md` reference, property tests, and Criterion benchmarks. The surface remains pre-1.0 and will be frozen at `1.0.0`.

## What is codegen-lang?

A backend abstraction for compiler back ends. A front-end produces a function in SSA form with [`ir-lang`](https://crates.io/crates/ir-lang); codegen-lang lays that control-flow graph out as a linear stream of instructions a machine can run. `Backend` is the trait a target implements; `Bytecode` is the target shipped here — a small, register-based bytecode that is enough to inspect, serialize, and run generated code without pulling in a native code generator. A backend built on LLVM or Cranelift slots in behind the same trait.

## What's new in 0.2.0

### The `Backend` abstraction

`Backend` is the trait the crate is built around. It lowers an `ir_lang::Function` to a backend-defined `Output`, so different targets produce different representations behind one interface:

```rust
use codegen_lang::{Backend, Bytecode};
use ir_lang::{Builder, Type};

let mut b = Builder::new("noop", &[], Type::Unit);
b.ret(None);

let program = Bytecode.compile(&b.finish()).expect("noop is well-formed");
assert_eq!(program.name(), "noop");
```

A backend holds no per-call state, so a single instance compiles many functions. The shipped `Bytecode` backend has `Output = Program`; a future LLVM or Cranelift backend would name its own module type.

### The bytecode and its `Program`

The `Bytecode` backend emits a `Program`: a function name, the registers holding its parameters, a register count, and a flat op stream. The lowering is direct — each IR value gets its own virtual register, each instruction becomes one `Op`, and each basic block becomes a `Label` at the op it begins on. The `Display` impl is a readable disassembly:

```rust
use codegen_lang::compile;
use ir_lang::{Builder, BinOp, Type};

// fn double(x: int) -> int { x + x }
let mut b = Builder::new("double", &[Type::Int], Type::Int);
let x = b.block_params(b.entry())[0];
let sum = b.bin(BinOp::Add, x, x);
b.ret(Some(sum));

let program = compile(&b.finish()).unwrap();
assert_eq!(
    program.to_string(),
    "double(r0) regs=2\nL0:\n    r1 = add r0, r0\n    ret r1\n",
);
```

`Op` is a closed, `Copy` set — `Const`, `Bin`, `Un`, `Move`, `Jump`, `JumpUnless`, `Return` — so the op stream is a flat slice an interpreter or a further pass walks with no indirection.

### Control flow: branches, loops, and block parameters

A value crosses a control-flow join in SSA as a block argument. The backend realizes that as a `Move` into the target block's parameter register, emitted on the edge — the bytecode's stand-in for a phi node. Because the source and destination registers of an edge are disjoint in SSA, the copies never interfere, so no parallel-move scheduling is needed. A two-way branch lowers to a `JumpUnless` plus two exclusive arms, which stays correct even when both arms target the same block with different arguments.

This covers diamonds and loops, not just straight-line code:

```rust
use codegen_lang::{compile, Op};
use ir_lang::{Builder, BinOp, Type};

// fn max(a: int, b: int) -> int { if a < b { b } else { a } }
let mut f = Builder::new("max", &[Type::Int, Type::Int], Type::Int);
let a = f.block_params(f.entry())[0];
let c = f.block_params(f.entry())[1];
let join = f.create_block(&[Type::Int]);
let then_blk = f.create_block(&[]);
let else_blk = f.create_block(&[]);
let cond = f.bin(BinOp::Lt, a, c);
f.branch(cond, then_blk, &[], else_blk, &[]);
f.switch_to(then_blk);
f.jump(join, &[c]);
f.switch_to(else_blk);
f.jump(join, &[a]);
f.switch_to(join);
let r = f.block_params(join)[0];
f.ret(Some(r));

let program = compile(&f.finish()).unwrap();
assert!(program.ops().iter().any(|op| matches!(op, Op::JumpUnless { .. })));
assert!(program.ops().iter().any(|op| matches!(op, Op::Move { .. })));
```

### Validation at the boundary

`compile` checks its input with `ir_lang::Function::validate` before lowering. A function that is not well-formed SSA is rejected with `CodegenError::InvalidIr`, which carries the precise `ValidationError`, rather than being miscompiled:

```rust
use codegen_lang::{compile, CodegenError};
use ir_lang::{Builder, Type};

let func = Builder::new("f", &[], Type::Unit).finish(); // no terminator
assert!(matches!(compile(&func), Err(CodegenError::InvalidIr(_))));
```

`CodegenError` is `#[non_exhaustive]` (a future backend may report target-specific failures) and implements `Display`, `std::error::Error`, and `From<ValidationError>`.

### `no_std` and `serde`

The crate is `#![no_std]`-capable: with `default-features = false` it needs only `alloc`. The optional `serde` feature derives `Serialize` / `Deserialize` for `Program` and its parts, so a compiled program can be cached or moved between tools. Both features forward to the matching `ir-lang` feature.

### Tests and benchmarks

The codegen workflow is exercised end to end. A reference interpreter in the integration suite runs compiled `double`, `abs`, `max`, and a countdown loop, and checks the results. Property tests generate random straight-line functions and assert that compiling and running matches an independent evaluation, and that every emitted program is structurally sound. Criterion benchmarks cover straight-line, branching, and looping shapes; lowering is a single linear pass and the measured cost is linear in function size (for example, an ~8 000-value function lowers, including validation, in roughly 31 µs on the reference machine).

## Breaking changes

**None.**

v0.1.0 had no domain logic, so the entire public surface is new and additive.

## Verification

Run on Windows x86_64 with WSL2 (Ubuntu), Rust stable 1.95.x and the 1.85 MSRV toolchain; the same commands run in the CI matrix across Linux, macOS, and Windows:

```bash
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo test --all-features
cargo build --no-default-features          # no_std build
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo bench --bench bench --all-features --no-run
cargo +1.85 build --all-features           # MSRV
cargo deny check
cargo audit
```

All green. Test counts at this tag:

- Default features: 16 unit + 14 integration (6 workflow + 5 invalid + 3 property) + 15 doctests.
- `--all-features`: identical counts; `serde` adds derives, not tests.

## What's next

- **1.0.0 — API freeze.** Stabilize and freeze the public surface, record the SemVer promise in `docs/API.md`, and confirm the full test and benchmark suite green on all three platforms.

## Installation

```toml
[dependencies]
codegen-lang = "0.2"
ir-lang = "1"

# no_std:
codegen-lang = { version = "0.2", default-features = false }

# with serialization:
codegen-lang = { version = "0.2", features = ["serde"] }
```

MSRV: Rust 1.85.

## Documentation

- [README](https://github.com/jamesgober/codegen-lang/blob/main/README.md)
- [API Reference](https://github.com/jamesgober/codegen-lang/blob/main/docs/API.md)
- [Roadmap](https://github.com/jamesgober/codegen-lang/blob/main/dev/ROADMAP.md)
- [CHANGELOG](https://github.com/jamesgober/codegen-lang/blob/main/CHANGELOG.md)

---

**Full diff:** [`v0.1.0...v0.2.0`](https://github.com/jamesgober/codegen-lang/compare/v0.1.0...v0.2.0).
**Changelog:** [`CHANGELOG.md`](https://github.com/jamesgober/codegen-lang/blob/main/CHANGELOG.md#020---2026-06-30).
