JIR — jihoo IR text format (version 21)
JIR is the contract between the Rust frontend/VM and the C++ LLVM backend.
jihoo emit-ir writes it, jihoo-llc reads it, and jihoo reads it back too
(jihoo run file.jir, jihoo build file.jir). Both sides must agree on this
document; the Rust definitions live in crates/jihoo-ir/src/lib.rs and the C++ ones
in backend-llvm/src/jir.h. The typing rules are implemented once, in
crates/jihoo-ir/src/types.rs, and checked by crates/jihoo-ir/src/verify.rs. Each
side has its reader: crates/jihoo-ir/src/parse.rs and
backend-llvm/src/jir_parser.cpp.
Versions
Section titled “Versions”A module starts with jir N, the version of this format it is written in, and
jihoo-llc compiles only the one version it was built for
(jihoo-llc --version prints it). A module of another version is refused at
that first line, so a frontend and a backend from different builds never
silently disagree about what a module means.
The version goes up with every change that lets the two sides disagree: a new or removed instruction, type or header, a change of syntax, or a change of what something means (such as division by zero going from undefined to a trap). A change to this document that only clarifies it keeps the version.
JIR_VERSION in crates/jihoo-ir/src/lib.rs, jir::kVersion in
backend-llvm/src/jir.h and the title above must agree; the version tests in
crates/jihoo-ir check that. They also keep a fingerprint of this document, so
editing it fails a test until you decide whether the edit needs a new version.
| version | change |
|---|---|
| 0 | every format before versions were kept, up to and including native division checks |
| 1 | the first version that is bumped on change; the header must come first |
| 2 | the target header |
| 3 | stringify, and hygiene marks in the code quote builds |
| 4 | float operations (sqrt, floor, ceil, trunc, round, abs, copysign) and bitcast; print in native modules; structs to and from C by value, and export; select cases separated by | |
| 5 | lists ([T]) with len, slice, repeat, push, pop, and array, elem, setelem and paths on them; on str: len, strbyte, slice, frombytes, ordering; readline |
| 6 | trap |
| 7 | atomics: atomic_load, atomic_store, atomic_swap, atomic_add, atomic_sub, atomic_and, atomic_or, atomic_xor, atomic_cas, and fence |
| 8 | spawn in native modules, starting a thread |
| 9 | lists in native modules, and clone and drop |
| 10 | str in native modules: string literals of type str, strbyte, frombytes, to_str, readline and comparisons |
| 11 | closure in native modules; native function values are {code, env} (16 bytes) |
| 12 | ref and cell in native modules, as counted boxes |
| 13 | chan, send, recv and select in native modules, on lib/channel.jh |
| 14 | codetext, code as text and text as code at compile time |
| 15 | typeof, and type holes in quote |
| 16 | take, moving a field out of a struct that is used again |
| 17 | tailcall, a call that takes the caller’s place |
| 18 | file headers and loc, source positions |
| 19 | take moves a part at a path (take %s, (field 0, elem %i)), not only a field |
| 20 | args, readfile, writefile, appendfile and parsefloat |
| 21 | own, views and moves lines: what the ownership pass decided |
Example
Section titled “Example”; jihoo IRjir 4profile freestandingtarget x86_64
struct $Node { value: i64, next: *$Node } size 16 align 8
fn @sum(*$Node) -> i64 { regs *$Node i64 i64 i64 *$Node bool *i64 i64 i64 **$Node *$Nodebb0: %1 = const 0 %2 = copy %1 jmp bb1bb1: %3 = const 0 %4 = cast %3 %5 = ne %0, %4 br %5, bb2, bb3bb2: %6 = fieldptr %0, 0 %7 = load %6 %8 = add %2, %7 %2 = copy %8 %9 = fieldptr %0, 1 %10 = load %9 %0 = copy %10 jmp bb1bb3: ret %2}Lexical rules
Section titled “Lexical rules”- The format is line-oriented: one header, label, instruction or terminator per line.
;starts a comment that runs to the end of the line (outside string literals).%Nis a register,@namea function,$Namea struct,bbNa block. Struct names that are not identifiers are quoted: instances of generic structs are$"Pair(i64)". Function names may contain.: instances of generic functions are@max.0, …- Integers are signed 64-bit decimals (
-5,42). Float constants (only infconst) are decimals with a.or an exponent (1.5,-0.0,1e-7), orinf,-inf,nan; they are read as the nearestf64. - Strings are double-quoted. Escapes:
\n,\t,\\,\",\xHH(any byte).
Module
Section titled “Module”jir 18 ; format version, must come firstprofile hosted|native|freestanding ; language profiletarget x86_64|aarch64 ; the machine, 64-bit Linuxfile N "path" ; zero or more source files, numbered from 0 in orderstruct ... ; zero or more structsenum ... ; zero or more enumsextern ... ; zero or more C functions (native only)fn ... ; zero or more functions| profile | runs on | GC | allowed builtins | pointers | C functions |
|---|---|---|---|---|---|
hosted |
VM | yes | print, to_str |
no | no |
native |
LLVM + libc | no | print, syscall, asm |
yes | extern, export |
freestanding |
LLVM, no libc | no | syscall, asm |
yes | export |
The target is the machine the module was made for: its syscall numbers and
asm are that machine’s, since the frontend picked per-target modules for it
(import sys loads sys.x86_64.jh or sys.aarch64.jh). jihoo-llc compiles
for that machine (x86_64-unknown-linux-gnu or aarch64-unknown-linux-gnu).
The frontend always writes the line; a hand-written module may leave it out,
and is then compiled for the machine jihoo-llc runs on. The VM ignores it.
Both targets are LP64, which is what size_of and struct layouts assume.
The entry point is @main for hosted and native modules and @_start for
freestanding ones. It returns unit or i64 and takes no parameters, except
that a native @main may take C’s (i32, **u8) (argc and argv).
Extern functions
Section titled “Extern functions”extern @puts(*u8) -> i32extern @printf(*u8, ...) -> i32A C function the module calls, by its symbol name, with the C calling
convention. call and funcref name it like any other function, and a module
cannot have a function and an extern function of the same name. Its parameter
and result types are C types: integers, floats, bool, pointers, function
types taking and returning those, structs whose fields are those (or arrays of
them, or such structs; at least one field), and a unit result (C’s void).
Enums and arrays go by pointer. Integers narrower than 32 bits and bools are
sign- or zero-extended as the C ABI requires, and structs are passed and
returned as the target’s C ABI says (Native ABI).
With ..., the function takes more arguments after the listed ones, like
printf. A call may pass any integer, float, bool, pointer or function value
there, and they get C’s default promotions (bools and integers narrower than 32
bits widen to 32, f32 to f64). Structs cannot go there. A variadic extern
cannot be a funcref.
| type | meaning | LLVM |
|---|---|---|
unit |
no value | {} |
bool |
true / false |
i1 |
i8 … i64 |
signed integers | i8 … i64 |
u8 … u64 |
unsigned integers | i8 … i64 |
f32, f64 |
IEEE 754 binary32 and binary64 | float, double |
str |
immutable string of bytes; hosted and native | {ptr, i64, i64} |
*T |
raw pointer to T, native and freestanding only |
ptr |
$Name |
struct or enum, by value | named struct |
expr, stmts, items |
code, inside macros; compile time only | — |
[N x T] |
array of N Ts, by value |
[N x T] |
[T] |
list of any number of Ts, by value; hosted and native |
{ptr, i64, i64} |
fn(T, ...) -> R |
function value | ptr; native {ptr, ptr} |
ref T |
immutable shared reference to a T; hosted and native |
ptr to {i64, T} |
chan T |
channel of T values between tasks; hosted and native |
ptr |
cell T |
shared mutable T; hosted and native |
ptr to {i64, T} |
str and *T are deliberately separate: GC references and raw pointers must never
mix. That separation is what will later allow GC-enabled native builds.
In the Rust IR and in the VM, an integer is stored as an i64 in canonical form:
sign-extended for signed types, zero-extended for unsigned ones (u64 keeps its bit
pattern). const values must already be canonical.
Structs
Section titled “Structs”struct $Name { field: T, field: T, ... } size S align AField names are only for readability; instructions refer to fields by index. A struct may contain other structs and arrays by value, but not itself (directly or through other structs or arrays); use a pointer for recursive data.
size S align A is the struct’s layout as computed by the frontend
(crates/jihoo-ir/src/layout.rs: C rules for 64-bit targets). It is what
size_of/align_of were folded to, so the LLVM backend rejects the module if the
target’s data layout disagrees. It is omitted for structs without a fixed layout
(those containing str, which only exist on the VM).
Structs and arrays are values: copy copies all fields or elements, and
setfield/setelem produce a new value.
enum $Name { A, B(T, U), ... } size S align AA sum type: a value is one of the variants, and each variant carries its own
payload values. Instructions refer to variants and payload values by index.
Structs and enums share one namespace. Like a struct, an enum may hold other
types by value but not itself, and size S align A is omitted when it contains
str.
The layout is the C layout of struct { uint32_t tag; union { ... } payload; },
where each variant’s payload is laid out like a struct of its values. Natively
the backend uses { i32, [N x iA] }, with A the largest payload alignment, and
reads a variant’s payload through a struct of its value types at the payload’s
address. On the VM an enum is a GC object holding the tag and the payload.
Functions
Section titled “Functions”fn @name(T1, T2, ...) -> R { regs T1 T2 ... Tnbb0: ...}- The
regsline, right after the header, gives the type of every register%0 .. %(n-1). It must start with the parameter types: arguments arrive in%0 .. %(params-1). In native modules,own,viewsandmoveslines may follow (Ownership). - Blocks are numbered
bb0, bb1, ...in order.bb0is the entry block. - Every block ends with exactly one terminator.
Exported functions
Section titled “Exported functions”fn @geo.area($Rect) -> f64 export @rect_area {In a native or freestanding module, export @sym after the result type also
defines the function as the C function sym, which C code linked with the
program can call with the C calling convention. The function is still called
from JIR by its own name. Its parameter and result types are C types, as for
extern functions; a symbol is exported once, is not also an
extern function, and is not the entry point (main natively, _start
freestanding).
Registers are not SSA: a register may be written more than once (the frontend
maps each let variable to one register), but always with values of its declared
type. The LLVM backend gives each register an alloca and relies on mem2reg.
Instructions
Section titled “Instructions”In the table, int means any integer type and %a: T means %a has type T.
The result type is what the destination register must be declared as.
Values and arithmetic
Section titled “Values and arithmetic”| syntax | operands | result | meaning |
|---|---|---|---|
%d = const N |
int or bool |
constant; bool is 0 or 1 |
|
%d = fconst X |
f32 or f64 |
float constant; for f32, X is exactly an f32 value |
|
%d = unit |
unit |
the unit value | |
%d = str "..." |
str (hosted, native) or *u8 (native, freestanding) |
string literal, of the type of %d; compiled ones are NUL-terminated constant bytes |
|
%d = copy %a |
T |
T |
copy |
%d = neg %a |
signed int or float | same | wrapping negation; for floats, flips the sign (also of 0 and NaN) |
%d = not %a |
bool or int |
same | logical not, or bitwise not of an integer |
%d = add|sub|mul %a, %b |
T, T (int) |
T |
wrapping arithmetic |
%d = div|rem %a, %b |
T, T (int) |
T |
signed or unsigned by type; truncating; dividing by zero is a runtime error on the VM and traps natively; signed MIN / -1 is MIN, and MIN % -1 is 0 |
%d = add|sub|mul|div|rem %a, %b |
T, T (float) |
T |
IEEE 754, rounded to nearest; rem is C’s fmod (exact, sign of %a) |
%d = sqrt %a |
float | same | the square root, correctly rounded (NaN below zero) |
%d = floor|ceil|trunc|round %a |
float | same | the integral value toward −∞, toward +∞, toward zero, or nearest with ties away from zero (C’s round); a zero result keeps the sign of %a |
%d = abs %a |
float | same | the magnitude: the sign cleared (also of 0 and NaN) |
%d = copysign %a, %b |
T, T (float) |
T |
the magnitude of %a with the sign of %b; a NaN has no sign, so with a NaN %b it is the magnitude of %a |
%d = bitcast %a |
f32, f64, u32 or u64 |
u32, u64, f32 or f64 |
the same bits as the other type: a float as the unsigned integer of its width, or back; every NaN reads as the quiet NaN with a clear sign (0x7fc00000, 0x7ff8000000000000) |
%d = and|or|xor %a, %b |
T, T (int or bool) |
T |
bitwise (for bool: logical, both sides evaluated) |
%d = shl|shr %a, %b |
T, T (int) |
T |
shift by %b modulo the bit width; shr is arithmetic for signed types, logical for unsigned |
%d = add %a, %b |
str, str or [T], [T] |
the same | concatenation |
%d = add|sub %a, %b |
*T, i64 |
*T |
pointer offset in elements of T |
%d = eq|ne %a, %b |
T, T: bool, int, float, str, *U |
bool |
equality (str compares contents; floats as IEEE: NaN is unequal to itself, 0.0 == -0.0); the frontend compares structs, enums, arrays, lists and refs with helper functions @fn.eq.N |
%d = lt|le|gt|ge %a, %b |
T, T: int, float, str or *U |
bool |
ordered comparison, signed or unsigned by type; pointers unsigned; str byte by byte, a prefix first; false if either float is NaN |
%d = cast %a |
see below | dst type | conversion |
%d = call @f(%a, ...) |
parameter types of @f |
return type of @f |
call |
%d = funcref @f |
fn(P...) -> R of @f |
function @f as a value |
|
%d = call %f(%a, ...) |
%f: fn(P...) -> R, then P... |
R |
call a function value |
%d = closure @f(%c, ...) |
the first parameter types of @f |
fn(P...) -> R, the rest of @f’s signature |
hosted and native: a function value that calls @f with %c, ... before its own arguments; natively the captured values move into memory the value owns |
cast allows: int → int (truncate, or sign-/zero-extend by the source type),
bool → int (0/1), int → float and float → float (to nearest), float → int
(dropping the fraction, saturating at the limits, NaN → 0), *T → *U, *T ↔
i64/u64, and any type to itself.
Structs
Section titled “Structs”| syntax | operands | result | meaning |
|---|---|---|---|
%d = struct $S(%a, %b, ...) |
every field, in order | $S |
build a struct |
%d = field %s, N |
$S |
type of field N | read a field |
%d = setfield %s, N, %v |
$S, type of field N |
$S |
copy of %s with field N replaced |
%d = getpath %s, (step, ...) |
a struct or array; each step field N or elem %i (%i: i64) |
type at the end of the path | read a nested part; elements are bounds-checked |
%d = take %s, (...) |
as getpath |
type of the part | move the part out, leaving it empty (native; see Ownership) |
%d = setpath %s, (step, ...), %v |
as getpath, then the type at the end of the path |
type of %s |
copy of %s with the nested part replaced |
setfield, setelem and setpath describe values: natively and on the VM they
update in place when %d is %s (on the VM, only if no other reference to the
object exists).
| syntax | operands | result | meaning |
|---|---|---|---|
%d = variant K(%a, %b, ...) |
the payload of variant K | the enum (type of %d) |
build variant K |
%d = tag %e |
an enum | u32 |
the index of the variant %e holds |
%d = payload %e, K, N |
an enum | type of value N of variant K | read a payload value; %e must hold variant K (the VM checks; natively it is undefined) |
References and cells (hosted and native)
Section titled “References and cells (hosted and native)”On the VM refs and cells are GC objects. Natively each points to a box
{ i64 count, T value } from malloc that its copies share: clone adds one
to the count, drop takes one away and frees the box at zero (see
Ownership). A ref’s count changes atomically, since
refs may reach other threads; a cell’s does not, since cells may not. deref
gives a view of the value; cellget gives a value the ownership pass copies,
since other holders of the cell can change it. A cell that holds itself
(through other cells) is never freed.
| syntax | operands | result | meaning |
|---|---|---|---|
%d = ref %v |
T |
ref T |
a new reference to a copy of %v |
%d = deref %r |
ref T |
T |
the value %r refers to |
A ref field breaks the rule that a struct or enum may not contain itself.
On the VM, types containing a ref or cell have no fixed layout, like those
containing str; natively each is a pointer (8 bytes).
| syntax | operands | result | meaning |
|---|---|---|---|
%d = cell %v |
T |
cell T |
a new cell holding a copy of %v |
%d = cellget %c, (step, ...) |
cell T, a path into T (may be empty) |
type at the end of the path | read what the cell holds, or a part of it |
cellset %c, (step, ...), %v |
cell T, a path, the type at its end |
replace what the cell holds, or a part of it |
Tasks and channels (hosted and native)
Section titled “Tasks and channels (hosted and native)”| syntax | operands | result | meaning |
|---|---|---|---|
%d = chan %n |
i64 |
chan T |
a new channel buffering up to %n values (0: sender and receiver meet) |
send %c, %v |
chan T, T |
send %v, waiting while the channel is full |
|
%d = recv %c |
chan T |
T |
receive a value, waiting until there is one |
spawn %f(%a, ...) |
%f: fn(P...) -> R, then P... |
call %f in a new task; the result is dropped. Also native: in a new, detached thread of the C library’s |
|
%d = select [case | ...] |
each case recv %c -> %v (%v: T) or send %c, %v, optionally a last default |
i64 |
do the first case that can go ahead, waiting until one can; %d is its index. With default it does not wait: %d is the number of cases if none can go ahead |
The VM runs tasks on one thread with a deterministic round-robin scheduler; the run ends when the entry function returns. A task is only switched out at a call, at a jump to a block with a lower or equal number (every loop has one, since blocks are numbered breadth-first), or when it waits on a channel.
In a native module, spawn copies %f and the arguments into memory from
malloc and starts a thread with pthread_create, which calls %f with
them, frees the memory and ends; the thread is detached (pthread_detach),
so nothing waits for it, and returning from main ends the program with it.
Running out of memory or threads traps. Freestanding modules have no
spawn.
Natively a chan T points to a channel of lib/channel.jh (its Raw),
which the module must contain: jihoo-llc calls the functions that module
exports as jihoo_chan_new, jihoo_chan_send, jihoo_chan_recv,
jihoo_chan_hold, jihoo_chan_release, jihoo_chan_take,
jihoo_chan_free, jihoo_select_new, jihoo_select_on and
jihoo_select_run (the loader adds the module to a native program that uses
channels). Values go in and out as bytes: send moves its value in, recv
moves one out. A channel is counted like a ref (see
Ownership), and the last copy to go frees the
values still in it. A select send case sends a copy, which is freed if
another case goes ahead; at most 16 cases. Natively a channel never carries a
cell, which would then be shared by two threads.
Arrays
Section titled “Arrays”index operands are i64. Element access is bounds-checked: the VM reports a
runtime error and native code traps.
| syntax | operands | result | meaning |
|---|---|---|---|
%d = array(%a, %b, ...) |
N values of type T |
[N x T] |
build an array |
%d = splat %v |
T |
[N x T] |
an array with every element %v |
%d = elem %a, %i |
[N x T], i64 |
T |
read an element |
%d = setelem %a, %i, %v |
[N x T], i64, T |
[N x T] |
copy of %a with element %i replaced |
Lists and strings (hosted, and lists natively)
Section titled “Lists and strings (hosted, and lists natively)”A list [T] is an array whose length is known only when the program runs.
Natively, it is a header { ptr data, i64 len, i64 cap } held by value, with
the elements in memory from malloc; see Ownership
for who frees it. A native str is the same header over bytes; one with
cap 0 is a literal’s constant bytes, which are never freed, and copying it
copies the header only.
array builds one when %d is a list (of as many elements as it lists), and
elem, setelem, getpath, setpath, cellget and cellset work on lists
as on arrays, bounds-checked against the current length. len and slice
also take a str, which is a sequence of bytes.
| syntax | operands | result | meaning |
|---|---|---|---|
%d = len %a |
str or [T] |
i64 |
the number of bytes or elements |
%d = slice %a, %lo, %hi |
str or [T], i64, i64 |
the type of %a |
bytes or elements %lo up to %hi; an error unless 0 <= lo <= hi <= len |
%d = strbyte %s, %i |
str, i64 |
u8 |
byte %i; bounds-checked |
%d = frombytes %b |
[u8] |
str |
the string of those bytes |
%d = repeat %v, %n |
T, i64 |
[T] |
%n copies of %v; an error if %n is negative |
push %r, (step, ...), %v |
a register, a path to a [T] in it (may be empty), T |
append %v to the list at the path in %r, or in what %r holds if it is a cell |
|
%d = pop %r, (step, ...) |
as push |
T |
remove the last element of that list and give it; an error if the list is empty |
%d = readline |
str |
the next line of standard input, with its \n; "" at the end of the input |
push and pop change register %r (or the cell): as for setpath %r, %r,
the list is changed in place unless something else refers to it, so a loop of
pushes takes time in proportion to the number of elements.
Arguments, files and numbers in text (hosted and native)
Section titled “Arguments, files and numbers in text (hosted and native)”| syntax | operands | result | meaning |
|---|---|---|---|
%d = args |
[str] |
the program’s arguments, without its name (natively C’s argv[1] on) |
|
%d = readfile %p |
str |
a struct of a str and an i64 |
the bytes of the file at path %p and 0, or "" and the error number |
%d = writefile %p, %s |
str, str |
i64 |
makes the file at %p hold %s, creating it (mode 0666, less the umask) if missing; 0 or the error number |
%d = appendfile %p, %s |
str, str |
i64 |
the same, but adds %s at the end of the file |
%d = parsefloat %s |
str |
f64 |
the f64 nearest to the decimal number %s (ties to even), or NaN if %s is not one |
Error numbers are Linux’s (errno), the same on both targets: what open,
read or write failed with. A path with a NUL byte in it is EINVAL (22),
which C’s open could not be asked. readfile’s struct is any whose fields
are a str and an i64, in that order (the frontend uses the tuple
(str, i64)).
A decimal number for parsefloat is [+-]digits[.digits][(e|E)[+-]digits]
and nothing else: no spaces, no inf or nan, no hex. A number too large
for an f64 is infinite, and one too small is zero, with its sign.
Natively the number is read with C’s strtod after the check, so it assumes
the C locale.
Ownership (native lists)
Section titled “Ownership (native lists)”A value owns memory if it is a list or a str, or holds one by value (in a field, an
element or a variant’s payload). Natively there is no GC, and copying a list
header copies no elements, so a native module states who frees what with two
instructions, which the frontend’s ownership pass inserts
(crates/jihoo-ir/src/ownership.rs):
| syntax | operands | result | meaning |
|---|---|---|---|
%d = clone %a |
any T |
T |
a copy: the lists and strings in %a are shared, counted once more (a closure’s captured values are copied, sharing theirs); %d may be %a |
drop %a |
any T |
frees the memory %a owns; %a must not be used again before it is written |
In a native module:
copy, building a value from parts (struct,variant,array), writing a part (setfield,setelem,setpath,push),retandspawnmove an owning operand: the operand’s register must not be used again before it is written. Writing a part frees the part it replaces.- Reading an owning part (
field,elem,getpath,payload) gives a view that shares the memory of the value it was read from; the value must outlive the view and not change while the view is used, and the view must not be dropped.clone %v, %vmakes a view a value of its own. popmoves the element out of the list, andtake %s, (path)the part at the path out of%s, which keeps the part empty: dropping%sor writing the part frees nothing of it. The pass makestakewhere the part is not read again before it, or a part holding it, is written, or%sdies; the VM reads the part.- Call arguments are borrowed: the callee must not change, move or drop its
parameters (it clones one first), and the caller still owns its argument.
The pass also makes a consuming version
@f.ownof a function that clones parameters, which owns those parameters instead; a call whose arguments for them are at their last use calls it and moves them. A thread started withspawnowns its arguments and frees them when the function returns. tailcallmoves its arguments, asretdoes its value: the caller’s frame is gone, so the callee owns them. It calls the version@g.tailof@g, which the pass makes, and which owns every parameter of@gthat owns memory (and frees one it never reads).repeat,splat,sliceandaddon lists copy the elements they take (cloning owning ones) and leave their operands as they were.- Registers that own memory start out empty, so dropping one that was never written frees nothing.
The VM ignores both: on it, clone is copy and drop does nothing.
Natively, the heap memory of a list or a string starts with a count of its
holders (an i64, 8 bytes before where the header points). clone counts
one more; drop one fewer, and the last holder frees the elements and the
memory. Strings never change in place, so their copies share their bytes for
good. Every instruction that writes into a list’s memory (push, pop,
setelem, and setpath, take and cellset through elements) first makes
each list on its path unique: memory counted more than once is copied (the
elements, each counted once more), and the copy is the value’s own. So a
write never shows through another copy, as on the VM. Counts are atomic.
The pass also writes down what it decided, after a function’s regs line,
so that the frontend can check it (crates/jihoo-ir/src/owncheck.rs):
own 0 2 views %5 %7 moves %12own: the parameters, by position, that the caller hands over (in a consuming or tail version); the others are borrowed.views: the registers that are views (of a part read, or acopyof a value still used): never dropped or moved, used only while what they share memory with lives unchanged.moves: the registers written byfieldorpayloadreads that move the part out of a value taken apart. Such a value is not dropped; its owning parts must all be moved out (the pass reads the ones it does not need and drops them).
The check follows every owning register along every path: each value is
moved or dropped exactly once and not used after, none is left at a ret
or tailcall, only the function’s own values are dropped, moved or changed
in place, a view is not used after its value changes (except in place at a
disjoint part), and a value is in the same state on every way into a block.
A failure is an internal error of the compiler. The backend reads past
these lines.
Memory (native and freestanding only)
Section titled “Memory (native and freestanding only)”| syntax | operands | result | meaning |
|---|---|---|---|
%d = load %p |
*T |
T |
read memory |
store %p, %v |
*T, T |
write memory | |
%d = addr %r |
T |
*T |
address of register %r (valid until the function returns) |
%d = fieldptr %p, N |
*$S |
*F (F = type of field N) |
address of a field |
%d = elemptr %p, %i |
*[N x T], i64 |
*T |
address of an element; bounds-checked |
Atomics (native and freestanding only)
Section titled “Atomics (native and freestanding only)”Atomics read and change memory that threads share, each as one indivisible
step. All of them are sequentially consistent, and the memory must be aligned
to its type (as everything jihoo lays out is). T is an integer, or for the
rows that say so also a pointer *U.
| syntax | operands | result | meaning |
|---|---|---|---|
%d = atomic_load %p |
*T (int or pointer) |
T |
read memory |
atomic_store %p, %v |
*T, T (int or pointer) |
write memory | |
%d = atomic_swap %p, %v |
*T, T (int or pointer) |
T |
write %v; the result is the old value |
%d = atomic_add|sub|and|or|xor %p, %v |
*T, T (int) |
T |
replace the value x by x op %v (wrapping, as add and sub do); the result is the old value |
%d = atomic_cas %p, %e, %n |
*T, T, T (int or pointer) |
bool |
if the value is %e, replace it by %n; the result says whether it did |
fence |
a full memory barrier; does nothing on the VM, which has one thread |
The VM has no pointers, so the atomics other than fence are runtime errors
there, as load is.
Builtins
Section titled “Builtins”| syntax | operands | result | meaning |
|---|---|---|---|
%d = syscall(%n, %a, ...) |
int or *T, 1 to 7 operands |
i64 |
native and freestanding only: raw Linux syscall %n |
trap |
stop the program: a runtime error on the VM, llvm.trap natively (as a failed bounds or division check); the block ends with unreachable after it |
||
loc F, L, C |
nothing: the instructions after it, up to the next loc in the block, come from line L, column C of file F (a file header). jihoo writes them only when asked (build --cost), and reads them to say where a copy is made; the backend reads past them |
||
print %a |
number, bool, str (hosted, native) or *u8 (native) |
hosted and native: print the value and a newline (floats as the shortest text that reads back exactly, ties rounded up: 0.1, 1.0, 1e100, inf, NaN; a *u8 is a NUL-terminated string); natively through the C library’s stdout |
|
%d = to_str %a |
number or bool |
str |
hosted, native and macros: the value as text, as print writes it |
%d = unique %p |
str |
str |
compile time only: p plus a number unique in this compilation |
%d = stringify %c |
expr, stmts or items |
str |
compile time only: the source text of code, with the hygiene marks of its names removed |
%d = codetext %a |
code or str |
str or code |
compile time only: the same text as the other type; code to str keeps the hygiene marks, str to code takes the text as it is |
%d = typeof %c |
expr |
str |
compile time only: the type the code would have where the macro was called, as the text of a type; the compiler supplies it and runs the macro again if it was not known |
%d = asm "tmpl", "cons"(%a, ...) |
int, bool or *T |
int, *T or unit |
native and freestanding only: inline assembly |
%d = quote ["p0", "p1", ...](kind %h, ...) |
see below | expr, stmts or items |
compile time only: code from template pieces and holes |
Each quote hole has a kind: expr (code in parentheses, or an int, bool or
str literal), ident (a str or expr that is an identifier, inserted as
is), stmts (stmts or expr code on lines of its own), items (items
code on lines of its own) or type (a str or expr with the text of a
type, inserted as is).
The template pieces carry hygiene marks: a name the template wrote is followed
by # (let tmp# = $x). The compiler numbers them per expansion (tmp#7) and
resolves them once the code is parsed (Compiler);
stringify gives the text without them.
asm passes tmpl and cons to LLVM unchanged: the template uses LLVM operand
references (${0} is the output if there is one, then the inputs, in order;
$$ is a literal $) and cons is an LLVM constraint string (=r, {rdi},
0, ~{memory}, …) with one input constraint per operand. On x86_64 the
backend uses the Intel dialect and adds ~{dirflag},~{fpsr},~{flags}, as clang
does. Bool operands are passed as i8.
Terminators
Section titled “Terminators”| syntax | meaning |
|---|---|
jmp bbN |
jump |
br %c, bbT, bbF |
%c: bool; go to bbT if true, else bbF |
ret %a |
return %a, which must have the function’s return type |
tailcall @g(%a, ...) |
return what @g returns given the arguments, without nesting: the caller’s frame is gone before @g runs |
unreachable |
control never gets here |
Returning from @_start exits the process: with the returned value if @_start
returns i64, with 0 if it returns unit.
tailcall calls a function of the module, not an extern, that returns the
caller’s type, with arguments of its parameter types; its signature may
otherwise differ from the caller’s. A chain of tail calls runs in constant
stack. The VM runs the callee in the caller’s frame. Natively the functions
that make or take tail calls get bodies with LLVM’s tailcc convention, and
a tail call is a musttail call between two of them; each keeps the C
convention under its own name, for function values, exports and main.
Aggregates go through a buffer (per thread natively) instead of the caller’s
frame, which is gone when the callee copies them, as it does on entry. From
@_start, which has no caller, it is a call followed by the exit.
Function values
Section titled “Function values”A function value is an index into the module’s functions on the VM, a code
pointer in freestanding modules (8 bytes, aligned to 8), and { code, env }
in native ones (16 bytes). There is no null function value, and function
values have no equality. A call through a function value uses the same
calling convention as a direct call, including the aggregate rules below.
A closure is a function value too, which passes the values it captured as
the first arguments. On the VM it is a GC object holding the function and the
captured values. Natively, env points to memory from malloc holding a
function that frees it, one that copies it, and the captured values; code
takes env after the sret pointer, before the arguments. A function value
that is not a closure has a null env, and a call through a function value
tests which it is. Function values own memory as lists do (see
Ownership), but may be read and written through
pointers: one read through a pointer is borrowed from that memory. C gets
code alone; passing it a closure traps. The frontend names the functions it lifts out of anonymous functions
@fn.N; fn is a keyword, so these never clash with user names.
Native ABI
Section titled “Native ABI”Between the program’s own functions, jihoo-llc passes aggregates (structs
and arrays) by pointer: the caller passes the address of its copy and the
callee copies it into its own storage, and an aggregate result is written
through a hidden first parameter. Aggregates are moved with memcpy; a
freestanding module defines weak memcpy, memmove, memset, fmod and
fmodf (which float rem becomes), and floor, ceil, trunc, round and
their f forms (which LLVM calls where the machine has no instruction for
them), because it has no libc; a native one uses libc’s and libm’s.
Scalars and pointers are passed the C way, so a jihoo function whose
parameters and result are floats, integers of at least 32 bits, pointers or
unit can be handed to C as a callback (as qsort’s comparator, say).
Functions that make or take tail calls have a second body with LLVM’s
tailcc convention, which the program’s direct calls use (see
Terminators).
The C ABI
Section titled “The C ABI”Calls to extern functions and the C functions of exported ones follow the target’s C ABI, structs included, as a C compiler would:
- x86_64 (System V): a struct of at most 16 bytes is split into
eightbytes. One holding only floats goes in an SSE register, any other in a
general register; if the registers a struct needs are not all free, the whole
struct goes on the stack. Larger structs go on the stack (
byval), and a larger result is written through a pointer the caller passes first. - aarch64 (AAPCS64): a struct of one to four floats of one type goes in as
many SIMD registers; any other struct of at most 16 bytes in one or two
general registers. A larger struct is passed as a pointer to a copy the
caller makes, and a larger result is written through a pointer in
x8.
An exported function’s C function takes its arguments that way and calls the program’s function with them.
Symbols
Section titled “Symbols”The program’s functions are internal symbols named jihoo.<name>, so they
never clash with C symbols, except a freestanding module’s _start. An
exported function is also the external symbol it exports. A native module
defines C’s int main(int argc, char **argv), which calls jihoo.main and
returns its result truncated to int (0 for unit). Native objects are
position independent, since the C compiler that links them usually makes a PIE.