Skip to main content

jit_lang/
lib.rs

1//! # jit_lang
2//!
3//! Lower IR to machine code in executable memory and run it now.
4//!
5//! jit-lang takes a function in the [`ir-lang`](ir_lang) intermediate representation,
6//! compiles it to native machine code for the machine it is running on, places that
7//! code in executable memory, and hands back a callable handle. It is the end of the
8//! pipeline a front-end follows: parse, type-check, lower to IR, and then — instead of
9//! writing an object file — run the function immediately.
10//!
11//! The surface is small. [`Jit`] is the engine; [`Jit::compile`] turns an
12//! [`ir_lang::Function`] into a [`Compiled`]; and [`Compiled::entry`] reinterprets the
13//! compiled code as a function pointer you can call. For a single compile, the free
14//! function [`compile`] does the same in one step.
15//!
16//! ## How it works
17//!
18//! A compile runs three stages:
19//!
20//! 1. **Validate.** The function is checked with
21//!    [`Function::validate`](ir_lang::Function::validate), so only well-formed SSA is
22//!    ever lowered.
23//! 2. **Translate and generate.** The IR is translated to [Cranelift](https://cranelift.dev)
24//!    IR — an almost one-to-one mapping, since both are SSA control-flow graphs whose
25//!    values cross blocks as block parameters — and Cranelift generates optimized
26//!    machine code for the host.
27//! 3. **Place.** The emitted bytes are copied into a guard-flanked memory region from
28//!    [`pager-lang`](pager_lang), which is then flipped from writable to read-execute.
29//!    The functions compiled here are leaf functions with no outgoing calls, so the
30//!    code is self-contained and needs no runtime relocation.
31//!
32//! ## Type and calling convention
33//!
34//! The IR's four machine types map onto the host C ABI: `int` is a 64-bit integer,
35//! `float` is an `f64`, `bool` is a byte holding `0` or `1`, and a `unit` return means
36//! the function yields no value. A `unit`-typed *parameter* has no machine
37//! representation and is refused. The compiled function uses the host's C calling
38//! convention, which is what Rust's `extern "C"` denotes, so the two agree when you
39//! call it.
40//!
41//! ## Example
42//!
43//! Compile `fn sum_to(n: int) -> int { let mut acc = 0; while n > 0 { acc += n; n -= 1 } acc }`
44//! — a loop with a back-edge carrying two values — and run it:
45//!
46//! ```
47//! use jit_lang::compile;
48//! use ir_lang::{Builder, BinOp, Type};
49//!
50//! let mut b = Builder::new("sum_to", &[Type::Int], Type::Int);
51//! let n0 = b.block_params(b.entry())[0];
52//! let header = b.create_block(&[Type::Int, Type::Int]); // (n, acc)
53//! let body = b.create_block(&[]);
54//! let exit = b.create_block(&[]);
55//!
56//! let zero = b.iconst(0);
57//! b.jump(header, &[n0, zero]);
58//!
59//! b.switch_to(header);
60//! let n = b.block_params(header)[0];
61//! let acc = b.block_params(header)[1];
62//! let z = b.iconst(0);
63//! let more = b.bin(BinOp::Gt, n, z);
64//! b.branch(more, body, &[], exit, &[]);
65//!
66//! b.switch_to(body);
67//! let acc2 = b.bin(BinOp::Add, acc, n);
68//! let one = b.iconst(1);
69//! let n2 = b.bin(BinOp::Sub, n, one);
70//! b.jump(header, &[n2, acc2]);
71//!
72//! b.switch_to(exit);
73//! b.ret(Some(acc));
74//!
75//! let f = compile(&b.finish()).expect("sum_to is well-formed");
76//!
77//! // SAFETY: the signature is `fn(int) -> int` and `f` outlives every call below.
78//! let sum_to: extern "C" fn(i64) -> i64 = unsafe { f.entry() };
79//! assert_eq!(sum_to(5), 15); // 5 + 4 + 3 + 2 + 1
80//! assert_eq!(sum_to(0), 0);
81//! ```
82//!
83//! ## Safety
84//!
85//! Generating code and jumping into it cannot be checked by the compiler: the type you
86//! transmute the entry point to has to match what was compiled, and the [`Compiled`]
87//! that owns the code has to outlive every call. Those obligations are concentrated in
88//! the single `unsafe` method [`Compiled::entry`], whose contract spells them out.
89//! Everything up to that point — building the IR, compiling it, inspecting the result
90//! — is safe.
91//!
92//! ## Platforms
93//!
94//! Code generation targets the host through Cranelift, and executable memory is managed
95//! by pager-lang, so the supported targets are their intersection: Linux, macOS, and
96//! Windows on x86-64 and ARM64. Freshly written code is made coherent before it runs —
97//! nothing to do on x86-64, where the caches are unified, and an instruction-cache
98//! synchronization on ARM64, where they are not. The crate links the standard library
99//! and reaches the operating system for memory; it is not `no_std`.
100//!
101//! ## Stability
102//!
103//! The public surface is frozen and stable as of `1.0.0`: it follows Semantic
104//! Versioning, with no breaking changes before `2.0`. [`JitError`] is
105//! `#[non_exhaustive]`, so a new failure variant is an additive, non-breaking change.
106//! The full surface and the SemVer promise are catalogued in
107//! [`docs/API.md`](https://github.com/jamesgober/jit-lang/blob/main/docs/API.md#semver-promise).
108
109#![cfg_attr(docsrs, feature(doc_cfg))]
110#![deny(missing_docs)]
111#![deny(unsafe_op_in_unsafe_fn)]
112#![deny(clippy::undocumented_unsafe_blocks)]
113#![deny(
114    clippy::unwrap_used,
115    clippy::expect_used,
116    clippy::panic,
117    clippy::todo,
118    clippy::unimplemented,
119    clippy::unreachable,
120    clippy::dbg_macro,
121    clippy::print_stdout,
122    clippy::print_stderr
123)]
124
125mod compiled;
126mod engine;
127mod error;
128mod icache;
129mod translate;
130
131pub use compiled::Compiled;
132pub use engine::{Jit, compile};
133pub use error::JitError;