EmbIR#
This page is the reference for EmbIR, the intermediate representation
between EmbCC's front end and its backends. It describes the data
structures in src/ir/ir.h, every opcode, the textual form that
embcc inspect ir prints and reads back, and the invariants the
optimizer and the backends rely on. It is written for people who change
IR generation, add a pass, or teach a backend a new operation.
Overview#
EmbIR is linear three-address code over virtual registers ("vregs").
One struct ir_unit holds one translation unit. Each function is an
array of struct ir_ins in program order; control flow is expressed
with labels and branches inside that array. There is no separate
basic-block structure: the optimizer derives blocks and a control-flow
graph when it needs them.
EmbIR is not in SSA form. Expression temporaries are written once in
most cases, and the optimizer builds SSA on demand for mem2reg and
takes it down again before the next pass
(see The optimizer).
Values are typed by width and a few flags rather than by a type graph: an instruction says how many bytes it operates on, whether the operation is signed, and whether it is a floating-point operation. The ABI decisions that need C types (how an argument is passed, how a structure is returned) are made during IR generation and recorded on the instruction, so backends never consult the AST for them.
| File | Contents |
|---|---|
src/ir/ir.h |
The data structures and the public functions |
src/ir/irgen.c |
IR generation from the checked AST (irgen) |
src/ir/irgen_int.h |
The helpers the per-target halves of IR generation use |
src/arch/<arch>/irgen.c |
Target-specific lowering: va_arg and extended asm |
src/ir/irprint.c |
The textual form (ir_print_unit, ir_opname) |
src/ir/irparse.c |
The reader for the textual form (ir_parse) |
src/opt/opt.c |
The optimizer and the IR verifier (verify_func) |
Where EmbIR sits in the pipeline#
The driver calls irgen(u) on the unit that semantic analysis has
checked, then opt_run(iu, level), then one backend's codegen_unit*
function. The DWARF emitter (src/debug/dwarf.c) and the unwind-table
emitter (src/debug/eh.c) read the same struct ir_unit after code
generation. See Architecture.
IR generation emits a function only if it is defined in the unit and is
either external, referenced (used), or named main. At -O1 and
above, the driver removes static functions that are unreachable after
optimization before code generation; inspect ir reports the unit after
that step.
Units, symbols and strings#
struct ir_unit#
| Field | Meaning |
|---|---|
src |
The AST unit it was generated from. NULL in a unit read by ir_parse. |
syms, nsyms |
The symbol table: every function and object the IR names. |
funcs, nfuncs |
The functions, as an array of struct ir_func. |
strs, nstrs |
The string-literal pool. |
rodata_len |
The size of the pool in bytes. |
struct ir_sym#
Instructions refer to functions and objects by index into
ir_unit::syms (callee_sym, glob_sym). Symbols are interned by name
and kind, so a function named by several instructions has one entry.
ir_sym_func and ir_sym_global intern a symbol from an AST node and
return its index.
| Field | Meaning |
|---|---|
name |
The symbol name. |
is_func |
1 for a function, 0 for an object. |
defined |
A definition exists in this unit. |
is_weak |
Declared weak. |
is_varargs |
Functions: declared with .... |
sret_first |
Functions: parameter 0 is the indirect-result pointer. |
is_nothrow |
Functions: no exception propagates out of it. |
The instructions also keep the AST pointers callee (a struct func *)
and glob (a struct global *) beside the indices. Backends and the
driver use the pointers to reach symbol-table indices and section
offsets; the textual form uses the indices.
struct ir_str#
Each string literal is one entry with its bytes, its length including
the terminating NUL, and off, its offset in .rodata. Offsets are
assigned in order as strings are interned and become section offsets
unchanged. ir_intern_string adds a string and returns its index; the
driver uses it for strings that global initializers point at.
IR_STRADDR names a string by index.
Functions#
struct ir_func#
| Field | Meaning |
|---|---|
name, file, line |
The function's name and where it is defined. |
is_static, is_varargs |
Linkage and variadic-ness. |
nparams |
Number of parameters. |
nvars |
Number of frame slots, parameters included. |
param_abi |
One struct ir_arg per parameter: how the prologue receives it. |
locals |
One struct ir_local per frame slot (nvars entries). |
ret_abi |
How the function returns its value. |
pcs |
ARM only: the function's pcs attribute. |
src |
The AST function. Holds code_off/code_len, which code generation fills, and the types the DWARF emitter needs. |
nvregs |
Number of vregs; all vreg numbers are below it. |
nlabels |
Number of labels; all label numbers are below it. |
scratch_bytes |
Caller-side storage for structure return values and, for Microsoft x64, by-reference argument copies. |
outgoing_bytes |
The largest stack-argument area any call in the function needs. |
has_i128 |
The function computes with __int128 values. |
has_alloca |
The function contains IR_ALLOCA; the stack pointer moves at run time. |
ins, nins, cap |
The instruction array. |
jt, njt |
The jump tables that IR_SWITCH instructions refer to. |
lines, nlines |
-g: (code offset, line) pairs, filled by code generation. |
dbgvars, ndbgvars |
-g: the source-level parameters and locals, filled by IR generation. |
var_off |
-g: each vreg's frame offset, filled by code generation. |
var_scope_lo, var_scope_hi |
Each local's lexical scope as a half-open range of instruction indices. Backends give locals with disjoint scopes one stack slot. |
eh, neh |
Exception regions (C++). |
eh_types, neh_types |
The catch types a landing pad's selector numbers, 1-based. |
csites, ncsites |
Code generation: the code range and region of every call in a function with exception regions. |
Frame slots: struct ir_local#
Each frame slot carries the facts about its C type that the optimizer
and the backends need, decided at IR generation. ir_locals_fill
computes them from the function's slot types; the inliner calls it
again after it adds slots.
| Field | Meaning |
|---|---|
size, align |
Size and alignment in bytes. |
user_align |
__attribute__((aligned(N))) on the variable; 0 when absent. |
is_volatile |
The slot is volatile. mem2reg does not promote it, store forwarding and DCE leave its accesses alone, and the shared register allocator (ra_allocate_class) gives it no register, so every access is to memory. |
is_ldouble |
A long double wider than double (x87 or binary128). |
is_int128 |
An __int128. |
is_int_or_ptr |
An integer or a pointer of any width. |
is_scalar_int_or_ptr |
An integer or pointer of 4 or 8 bytes. |
is_scalar_float |
A float or double of 4 or 8 bytes (not a wide long double). |
Exception regions#
struct ir_eh describes one exception region, produced from the C
front end's STMT_EHREGION (which the C++ lowering writes). lo and
hi are the region's instruction range, lp_label is the landing pad's
label, parent the enclosing region or -1, and acts/nacts the catch
and cleanup actions. A call inside a region records 1 + region in
eh_region. Code generation fills lp_off and the call sites
(ir_add_csite); src/debug/eh.c turns them into .gcc_except_table.
Virtual registers#
Vregs are numbered from 0 to nvregs - 1 in three ranges:
| Range | Meaning |
|---|---|
0 .. nparams-1 |
The parameters, in order. |
nparams .. nvars-1 |
The other frame slots: locals in declaration order and the hidden slots semantic analysis creates (compound literals, VLA sizes, saved stack pointers). The slot of a local aligned beyond the stack's alignment holds a pointer to its storage (see IR_ALLOCA). |
nvars .. nvregs-1 |
Temporaries. |
A VLA's length is evaluated into its size slot where its declaration is
reached. A block-scope typedef of a variably modified type is a
STMT_DECL with is_vm_typedef set (the id its VLA nodes carry): IR
generation evaluates the typedef's own array lengths into their size
slots at that statement, and a later declaration, sizeof or array
step that uses the name reads those slots instead of evaluating the
lengths again. A typedef inside a loop is sized again on each pass.
A vreg below nvars is a frame slot. IR_LDVAR, IR_STVAR and
IR_ADDR take a slot number, printed vN. A slot number used as an
ordinary operand (%N with N < nvars) names the value in that slot;
after mem2reg, a parameter that is never assigned is read this way,
as the value the prologue stored there.
Temporaries are values. Most are written by exactly one instruction. The exceptions are deliberate:
- A merge temp receives a value on each path into a join. IR
generation writes one for
?:,&&,||and the register/stack choice inva_arg, with anIR_MOVorIR_CONSTon each path. - After optimization, phi copies from
mem2reg, partial-redundancy elimination and loop transformations write some temps more than once.
Code that treats a temp as a known value must first check that it has exactly one definition.
Widths#
Temporaries hold promoted values. An instruction's w field is the
width of the value it operates on or produces:
w |
Values |
|---|---|
| 4 | int class: everything narrower than 8 bytes |
| 8 | 8-byte integers and pointers, and double |
| 16 | __int128, a long double wider than double, and a 128-bit vector |
w is the IR's width class, not the target's register width: on AVR an
int operation still has w 4, and the backend narrows it. Pointer
arithmetic uses the target's pointer size (8 on the LP64 targets, 4 on
ARMv7-M and RV32).
A value whose C type is narrower than its class (only AVR's 2-byte
int and unsigned) is kept equal to its extension, because a
divide, a right shift, a comparison or a conversion to long reads all
four bytes. After an operation that can carry out of the type's width,
IR generation (int_wrap) emits an IR_EXT of the type's size, signed
for int: after IR_ADD, IR_SUB, IR_MUL, IR_SHL and IR_NEG, a
signed IR_DIV, and an unsigned IR_BNOT, in binary expressions,
compound assignments and unary - and ~. The optimizer removes the
extensions it can prove redundant.
Variables live in memory at their true size. IR_LDVAR and IR_LOAD
read size bytes and extend to w (sign-extending when sign is set);
IR_STVAR and IR_STORE truncate to size bytes.
Instructions#
struct ir_ins#
| Field | Meaning |
|---|---|
op |
The opcode. |
line, col |
Source location; col is 0 when unknown. |
synth |
The compiler invented the instruction; it corresponds to no source construct. |
dst, a, b, c |
Destination and operands: vreg numbers, slot numbers or -1. |
w |
Operation width class (4, 8 or 16). |
size |
Memory or source width in bytes, for the operations that have one. |
sign |
Signed variant: signed division, arithmetic shift, signed comparison, sign extension. |
flt |
Floating-point operation at width w. |
vol |
LOAD/STORE/LDVAR/STVAR: a volatile or atomic access. The optimizer never removes, merges, forwards or reorders it. The load and store of a bit-field's storage unit carry it when the bit-field is accessed through a volatile lvalue, and the LDVAR/STVAR of a volatile local carry it. |
imm |
IR_CONST's value; the folded constant when imm_b is set; the operator of IR_ARMW and IR_VBIN. |
imm_b |
Operand b is the constant imm, not a vreg. Set only by the optimizer's immediate-folding pass, on ADD, SUB, MUL, AND, OR, XOR, CMP, SHL and SHR, never on a floating-point or 16-byte operation. |
pred |
IR_CMP's predicate: B_EQ, B_NE, B_LT, B_LE, B_GT or B_GE. |
label |
The label of IR_LABEL, IR_JMP, IR_BRZ, IR_BRNZ and IR_LABELADDR; IR_SWITCH's default; IR_STRADDR's string index. |
jt |
IR_SWITCH: index into ir_func::jt. |
callee, callee_sym |
IR_CALL (direct) and IR_FADDR: the function. |
glob, glob_sym |
IR_GADDR: the object. |
indirect |
IR_CALL through a function pointer held in a. |
sret_first |
IR_CALL: argument 0 is the indirect-result pointer. |
call_varargs, call_nfixed |
IR_CALL: the callee is variadic, and how many parameters are named. |
call_pcs |
IR_CALL: the callee's ARM pcs attribute. |
memoff |
LOAD/STORE: constant byte offset added to the address. Set only by a backend's own pre-codegen pass (ra_fold_memoff); 0 everywhere else. |
natural |
LOAD/STORE: the address is known to be aligned for the access. 0 means not known. |
argv, nargs |
IR_CALL: the arguments, each a struct ir_arg. |
retsize, rety, retnclass, retcls, ret_hfa_n, ret_hfa_size, ret_byref, ret_x87, scratch |
IR_CALL returning a structure: its size, type and classification under each ABI, and the frame offset of the caller-side copy. |
ret_tybytes, ret_tysign |
IR_CALL returning a scalar: the return type's own size and signedness. w describes the return register instead. |
asm_ir |
IR_ASM: the assembled template and its operands. |
eh_region |
IR_CALL: 1 + the innermost exception region, or 0. |
emit() in src/ir/irgen.c creates an instruction with dst, a,
b and label set to -1, w and size set to 4 and sign set to 1,
and stamps it with the current source location. Fields that do not
apply to an opcode keep those defaults.
Call arguments: struct ir_arg#
Each argument of an IR_CALL, each entry of param_abi, and ret_abi
is classified during IR generation.
| Field | Meaning |
|---|---|
vreg |
The value, or the address of the value when is_struct. |
is_struct, size |
Aggregate or scalar, and its size in bytes. |
nclass, cls[2] |
System V classification by eightbyte (CLASS_INTEGER, CLASS_SSE, CLASS_NONE); nclass 0 means MEMORY. |
on_stack, stk_off |
The argument goes in the outgoing stack area, at this offset. |
align, nat_align |
Alignment, and the ARM procedure-call standards' natural alignment (ty_natural_align). |
is_float, is_int128 |
Scalar kind. |
hfa_n, hfa_size |
AAPCS64 homogeneous floating-point aggregate: member count and member size. |
byref |
Passed as a pointer to a copy (AAPCS64 stage B.3, or Microsoft x64). |
copy_off |
Microsoft x64: offset of the caller's copy in its scratch area. |
ty |
The C type, for the classifications that read members. |
Opcodes#
The enumeration enum ir_op has 63 opcodes, followed by IR_OPCOUNT,
which is not an opcode. The order of the enumeration does not group
opcodes by kind; test opcodes by name, never by numeric range.
ir_opname returns an opcode's mnemonic and ir_op_from_name the
reverse.
In the tables, Form is the textual form (see
The textual form), W is w, S is size, and
[s], [f], [v] are the flags.
Values and arithmetic#
| Opcode | Form | Semantics |
|---|---|---|
IR_CONST |
%d = const.W IMM |
dst = imm. |
IR_MOV |
%d = mov.W %a |
dst = a, a full copy of the temp. |
IR_ADD |
%d = add.W %a, %b |
dst = a + b. |
IR_SUB |
%d = sub.W %a, %b |
dst = a - b. |
IR_MUL |
%d = mul.W %a, %b |
dst = a * b. |
IR_DIV |
%d = div.W %a, %b |
dst = a / b, signed when sign. |
IR_MOD |
%d = mod.W %a, %b |
dst = a % b, signed when sign. |
IR_AND |
%d = and.W %a, %b |
dst = a & b. |
IR_OR |
%d = or.W %a, %b |
dst = a | b. |
IR_XOR |
%d = xor.W %a, %b |
dst = a ^ b. |
IR_SHL |
%d = shl.W %a, %b |
dst = a << b. |
IR_SHR |
%d = shr.W %a, %b |
dst = a >> b, arithmetic when sign, logical otherwise. |
IR_NEG |
%d = neg.W %a |
dst = -a. |
IR_BNOT |
%d = bnot.W %a |
dst = ~a. |
IR_CMP |
%d = cmp.W PRED %a, %b |
dst = (a PRED b), 0 or 1. sign selects a signed integer comparison. With flt, w is the width of the floating-point operands. |
IR_SELECT |
%d = select.W:C %a ? %b : %c |
dst = a ? b : c. Both arms are already-computed values. w and sign describe the arms; size (printed C) is the width of the condition a, 4 or 8, which the backend tests at that width. Produced only by if-conversion in the optimizer, which copies C from the w of the branch it replaces. |
IR_BSWAP |
%d = bswap.W %a |
dst is a with its size bytes reversed (2, 4 or 8); __builtin_bswapN. Produced only for x86-64, AArch64 and RV64. For ARMv7-M, RV32 and AVR, IR generation builds the swap from shifts, masks and ORs (bswap_lowered), an 8-byte one as two 4-byte swaps exchanged. |
IR_SQRT |
%d = sqrt.W[f] %a |
dst = sqrt(a) at float width w (4 or 8); __builtin_sqrt*. |
With flt set, ADD, SUB, MUL, DIV, MOV and CMP operate on
floating-point values of width w; w 16 with flt is a wide
long double. IR generation negates a float or double by flipping
its sign bit with an integer IR_XOR. A binary operation's b operand
is printed #IMM when imm_b is set.
Conversions#
| Opcode | Form | Semantics |
|---|---|---|
IR_EXT |
%d = ext.W:S %a |
The low S bytes of a, extended to width W; sign extension when sign. Also used to truncate: ext.4:1 keeps one byte. |
IR_I2F |
%d = i2f.W:S %a |
Integer of S bytes (signed when sign) to a floating-point value of width W. |
IR_F2I |
%d = f2i.W:S %a |
Floating-point value of width S to an integer of width W, signed when sign. |
IR_F2F |
%d = f2f.W:S %a |
Floating-point value of width S to width W. |
IR_BITCAST |
%d = bitcast.W:S %a |
The bits of a at the same width (W == S). sign set: the destination is an integer (float bits out); clear: the destination is a float. IR generation lowers fabs, copysign, signbit, isnan, isinf, isfinite and isnormal builtins to integer operations around it. |
Frame slots and memory#
| Opcode | Form | Semantics |
|---|---|---|
IR_LDVAR |
%d = ldvar.W:S vN |
Read S bytes of slot a, extend to W. |
IR_STVAR |
stvar:S vN, %a |
Write the low S bytes of a to slot dst. |
IR_ADDR |
%d = addr vN |
dst = the address of slot a. Taking a slot's address keeps it in memory. |
IR_LOAD |
%d = load.W:S [%a] |
Read S bytes at address a (plus memoff), extend to W. |
IR_STORE |
store:S [%a], %b |
Write the low S bytes of b at address a (plus memoff). |
IR_MEMCPY |
memcpy:S [%a], [%b] |
Copy S bytes from address b to address a. |
IR_MEMZERO |
memzero:S [%a] |
Zero S bytes at address a. IR generation clears an initialized local array or structure, and any local initialized with a brace list, this way before it stores the initializer's elements; = {} lists none and leaves only the clear. |
IR_ALLOCA |
%d = alloca %a |
dst = a fresh block of a bytes on the stack, above the outgoing-argument area, aligned as the stack is (16 bytes; 8 on ARMv7-M). Used for VLAs and __builtin_alloca, and at function entry for each aggregate local aligned beyond target_stack_align(): IR generation carves size + align - 1 bytes, rounds the address up, and stores it in the local's slot, through which every access to the local goes (local_addr). Sets has_alloca. |
IR_SPSAVE |
%d = spsave |
dst = the stack pointer. |
IR_SPRESTORE |
sprestore %a |
Set the stack pointer to a, releasing every IR_ALLOCA since the IR_SPSAVE that produced a. |
IR_FRAMEADDR |
%d = frameaddr |
dst = this function's frame pointer (rbp, x29), which points at the saved frame pointer and the return address. The base of __builtin_frame_address and __builtin_return_address. Lowered by the x86-64 and AArch64 backends only. |
Addresses of symbols#
| Opcode | Form | Semantics |
|---|---|---|
IR_STRADDR |
%d = straddr strN |
dst = the address of string label in .rodata. |
IR_GADDR |
%d = gaddr @NAME |
dst = the address of the object glob. |
IR_FADDR |
%d = faddr @NAME |
dst = the address of the function callee. |
IR_LABELADDR |
%d = labeladdr LN |
dst = the address of label label (GNU &&label). With vol set -- printed labeladdr LN data K -- it is a marker irgen plants at a function's entry for a label static data takes the address of (static void *t[] = { &&a }, &&b - &&a): it emits no code and nothing reads dst; it keeps the label address-taken and current through the passes, and the backend records where the label landed in slot imm of the function's label_pos (cg_note_labels), which the driver adds into the data. |
Calls and returns#
| Opcode | Form | Semantics |
|---|---|---|
IR_CALL |
%d = call @NAME(%x, ...) or %d = call [%a](%x, ...) |
Call callee, or the function pointer in a when indirect. Arguments are in argv. The result is in dst; IR generation allocates a dst for every call, including one that returns void. A structure result is written to the caller's scratch area at scratch. w is the width of the return register and flt marks a floating-point result. |
IR_RET |
ret %a or ret |
Return a; a is -1 for a void return. For a structure, a holds the structure's address and size its size; the function's ret_abi says how it travels. |
The end of the instruction array also returns: every backend emits an
epilogue after the last instruction, so a void function may end
without IR_RET. IR generation ends main (with an integer return
type) with ret of the constant 0, as C99 requires for a main that
reaches its closing brace; after a body whose every path returns, the
optimizer removes it as unreachable.
Control flow#
| Opcode | Form | Semantics |
|---|---|---|
IR_LABEL |
LN: |
Marks a position. Label numbers are below nlabels. |
IR_JMP |
jmp LN |
Jump to label. |
IR_BRZ |
brz.W %a -> LN |
Jump to label if a is zero; otherwise fall through. |
IR_BRNZ |
brnz.W %a -> LN |
Jump to label if a is nonzero; otherwise fall through. |
IR_SWITCH |
switch.W %a -> LDEF [N] L0 L1 ... |
If a, compared unsigned, is below the table's length N, jump to entry a of table jt; otherwise jump to the default label. a is the case value minus the lowest case. |
IR_IGOTO |
igoto [%a] |
Jump to the address in a (GNU computed goto). |
IR_UD2 |
ud2 |
The target's undefined instruction (ud2, udf #0, unimp). Used for __builtin_trap, __builtin_unreachable and failed -fsanitize checks. |
A basic block starts at the first instruction, at every IR_LABEL, and
after every IR_JMP, IR_BRZ, IR_BRNZ, IR_RET, IR_UD2,
IR_IGOTO and IR_SWITCH. IR_RET and IR_UD2 have no successor;
IR_JMP has its label; IR_BRZ and IR_BRNZ have their label and the
next block; IR_SWITCH has its default and every table entry;
IR_IGOTO has every label whose address an IR_LABELADDR in the
function takes. Any other final instruction falls through to the next
block (build_cfg in src/opt/opt.c).
A jump table (struct ir_jt) holds n label numbers. It belongs to the
function and is referred to by index, so copies of an IR_SWITCH
(made by the inliner or by switch threading) share one table.
ir_jt_add creates a table with every entry -1; ir_jt_clone copies
one with its labels renumbered. A dense switch becomes IR_SWITCH
only where target_jump_tables() allows it (not on AVR); under -Os
the density threshold is higher.
Atomics and barriers#
| Opcode | Form | Semantics |
|---|---|---|
IR_FENCE |
fence |
A full memory barrier. |
IR_XCHG |
%d = xchg.W:S [%a], %b |
Atomically: dst = *a; *a = b. |
IR_XADD |
%d = xadd.W:S [%a], %b |
Atomically: dst = *a; *a += b. Also used for subtraction with a negated operand. |
IR_ARMW |
%d = armw.W:S [%a], %b op 'X' |
Atomically: dst = *a; *a = dst X b, where imm holds X: &, |, ^, or n for NAND (~(dst & b)). |
IR_CAS |
%d = cas.W:S [%a], %b, %c |
Atomically: dst = *a; if (dst == b) *a = c. The result is the value seen (the __sync_*_compare_and_swap shape). |
IR_CMPXCHG |
%d = cmpxchg.W:S [%a], [%b], %c |
Atomically compare *a with *b; on a match store c; otherwise store the value seen to *b. dst is 1 on a match and 0 otherwise (the __atomic_compare_exchange shape). |
IR_CAS16 |
%d = cas16.W:S [%a], %b, %c |
IR_CAS on 16 bytes (__int128). IR generation builds the other 16-byte atomics as loops around it. |
Atomic loads and stores are ordinary IR_LOAD and IR_STORE with
vol set, with IR_FENCE placed around them by IR generation.
Variadic functions, inline assembly, exceptions#
| Opcode | Form | Semantics |
|---|---|---|
IR_VA_START |
va_start [%a] |
Initialize the va_list whose address is in a. On System V x86-64 and AAPCS64 the backend fills a record in the frame (System V's 24-byte __va_list_tag, AAPCS64's 32-byte record) and stores its address in the va_list. Where va_list is a plain pointer (target_va_list_is_pointer(): ARMv7-M, RISC-V, AVR, Apple arm64, Microsoft x64) it stores the address of the first variadic argument. va_arg itself is lowered to ordinary IR by the target's irgen.c. |
IR_ASM |
asm N bytes, I in, O out |
Extended asm. asm_ir holds the template already assembled to bytes and the operand list. The backend loads each input's value into its fixed register, places the bytes, and stores each output register through the output's address. |
IR_LANDING |
%d, %b = landing |
The entry of a landing pad: dst receives the exception pointer and b the selector, from the registers the unwinder sets. Defines two temps. |
struct ir_asm holds code/codelen (the bytes) and the inputs and
outputs as struct ir_asm_op: temp (the input's value, or the
output's address), reg (the fixed register), size, inout (a +
operand whose register must hold the current value first) and mem (an
m operand: the register holds the address and the template performs
the access).
Vectors#
Vector operations work on 128-bit values. S is the element width in
bytes, so a vector has 16 / S lanes. A vector temp occupies a 16-byte
slot and is never given an integer register. These opcodes are produced
only by the optimizer's vectorizer, which runs only for x86-64.
| Opcode | Form | Semantics |
|---|---|---|
IR_VLOAD |
%d = vload.W:S [%a] |
dst = the 16 bytes at address a. |
IR_VSTORE |
vstore.W:S [%a], %b |
Store vector b at address a. |
IR_VBIN |
%d = vbin.W:S OP %a, %b |
Lane-wise a OP b; imm holds +, -, &, | or ^. For < (shift left) and > (shift right) the count is the constant c, printed #N, and sign selects an arithmetic right shift. There is no multiply. |
IR_VSPLAT |
%d = vsplat.W:S %a |
Every lane set to scalar a. |
IR_VREDADD |
%d = vredadd.W:S %a |
The sum of the lanes of a, as a scalar of width W. |
IR_VWIDEN |
%d = vwiden.W:S lo|hi %a |
Half of the lanes of a (c 0 the low half, 1 the high half), each widened to twice S; sign extension when sign. |
Producers and consumers#
IR generation (irgen, with src/arch/<arch>/irgen.c for va_arg and
asm) produces every opcode except IR_SELECT and the vector opcodes,
which only the optimizer creates. The optimizer also sets imm_b, and
a backend's pre-codegen pass may set memoff.
A backend refuses an operation it cannot lower with a diagnostic that names the instruction, for example:
embcc: fa.c:1: error: the ARMv7-M backend cannot lower this operation at 64 bits yet (function fa) [frameaddr w=8 size=4]
The operand and control-flow knowledge of each opcode is spread over switches in the optimizer, the register allocator and each backend. The list of places to update when an opcode is added is in The optimizer.
Source locations#
Every instruction carries the line and column of the expression or
statement it was generated from, or synth when it corresponds to no
source construct (a prologue store, a landing pad's entry, the zero
mem2reg gives a variable read before any write). IR generation stamps
the location in emit(); a pass that replaces an instruction copies
line, col and synth from the instruction it replaces. Code
generation turns locations into the -g line table (ir_func::lines).
The textual form#
embcc inspect ir FILE.c [options] prints the IR of a C or C++ file as
it stands after optimization at the -O level given, so the same
command shows IR generation's output at -O0 and the optimizer's at
-O2. inspect must be the first argument; the rest is an ordinary
command line (--target=, -I, -D, -O, -f<pass>).
embcc inspect ir FILE.ir reads a file in the textual form with
ir_parse and prints it again. With an -O level, the optimizer runs on
the parsed unit before it is printed, which tests a pass on IR written to
provoke it. A .ir file is accepted only by inspect ir; other modes
treat it as C source.
embcc inspect ir sum.c -O2 -fno-vectorize -fno-unroll > sum.ir
embcc inspect ir sum.ir | cmp - sum.ir
embcc inspect ir -O2 case.ir
Example#
int sum(int *a, int n)
{
int s = 0;
for (int i = 0; i < n; i++)
s += a[i];
return s;
}
At -O0:
; EmbIR
func @sum nparams=2 nvars=4 vregs=23 labels=3 {
local v0 size=8 align=8 intptr scalar
local v1 size=4 align=4 intptr scalar
local v2 size=4 align=4 intptr scalar
local v3 size=4 align=4 intptr scalar
%4 = const.4s 0 ; 3:13
stvar:4s v2, %4 ; 3:9
%5 = const.4s 0 ; 4:18
stvar:4s v3, %5 ; 4:14
L0:
%6 = ldvar.4:4s v3 ; 4:21
%7 = ldvar.4:4s v1 ; 4:25
%8 = cmp.4s lt %6, %7 ; 4:21
brz.4s %8 -> L2 ; 4:14
%9 = ldvar.4:4s v2 ; 5:9
%10 = ldvar.8:8 v0 ; 5:14
%11 = ldvar.4:4s v3 ; 5:16
%12 = ext.8:4s %11 ; line 5
%13 = const.8s 4 ; 5:14
%14 = mul.8s %12, %13 ; 5:14
%15 = add.8s %10, %14 ; 5:14
%16 = load.4:4s [%15] ; 5:14
%17 = add.4s %9, %16 ; 5:9
stvar:4s v2, %17 ; 5:9
L1:
%18 = ldvar.4:4s v3 ; line 4
%19 = mov.4s %18 ; line 4
%20 = const.4s 1 ; line 4
%21 = add.4s %18, %20 ; line 4
stvar:4s v3, %21 ; line 4
jmp L0 ; 5:9
L2:
%22 = ldvar.4:4s v2 ; 6:12
ret %22 ; 6:5
}
At -O2 -fno-vectorize -fno-unroll (x86-64): the locals are promoted
to temps, the parameters are read directly as %0 and %1, the loop
is rotated and walks a pointer, and %23, %24 and %28 are merge
temps with several definitions.
; EmbIR
func @sum nparams=2 nvars=4 vregs=30 labels=5 {
local v0 size=8 align=8 intptr scalar
local v1 size=4 align=4 intptr scalar
local v2 size=4 align=4 intptr scalar
local v3 size=4 align=4 intptr scalar
%25 = const.4 0 ; compiler-synthesized
%23 = mov.4 %25 ; 4:14
%24 = mov.4 %25 ; 4:14
%8 = cmp.4s lt %24, %1 ; 4:21
brz.4s %8 -> L2 ; 4:14
%28 = mov.8 %0 ; line 5
L3:
%16 = load.4:4s [%28] ; 5:14
%17 = add.4s %16, %23 ; 5:9
%21 = add.4s %24, #1 ; line 4
%23 = mov.4 %17 ; 5:9
%24 = mov.4 %21 ; 5:9
%28 = add.8 %28, #4 ; line 4
%27 = cmp.4s lt %24, %1 ; 4:21
brnz.4s %27 -> L3 ; 4:14
L2:
ret %23 ; 6:5
}
Layout of a unit#
The printer writes, in order:
The line
; EmbIR.One line per string literal:
strN = "BYTES", with\n,\t,"and\escaped and the terminating NUL omitted. A blank line follows when there are strings.One line per symbol:
func @NAME [defined] [weak] [varargs] [sret] [nothrow] ;declordata @NAME [defined] [weak] ;decl. A blank line follows when there are symbols.Each function, separated by blank lines:
func @NAME [static] [varargs] [alloca] [i128] nparams=N nvars=N vregs=N labels=N [scratch=N] [outgoing=N] { local vI size=N align=N [user_align=N] [volatile] [ldouble] [int128] [intptr] [scalar] ...one instruction per line... }scratch=andoutgoing=are printed only when nonzero. There is onelocalline per frame slot;intptrisis_int_or_ptrandscalarisis_scalar_int_or_ptr.
Instructions#
A label is printed at column 0 as LN:. Every other instruction is
indented two spaces and printed in the form given in the opcode tables,
followed by a tab and its location:
| Trailer | Meaning |
|---|---|
; LINE:COL |
Line and column known. |
; line LINE |
Column unknown. |
; compiler-synthesized |
synth set and no line. |
Operands are spelled:
| Spelling | Meaning |
|---|---|
%N |
Vreg N. |
vN |
Frame slot N. |
[%N] |
The address held in vreg N. |
#N |
An immediate operand (imm_b), or IR_VBIN's shift count. |
LN |
Label N. |
@NAME |
A symbol, declared earlier in the unit. |
strN |
String N. |
The mnemonic carries a suffix. For operations without a memory width it
is .W followed by the flags; for those with one (ldvar, stvar,
load, store, ext, the conversions, the atomics, the vector
opcodes and select) it is .W:S followed by the flags. stvar and
store omit .W. The flags are s (sign), f (flt) and v (vol), in that
order. So ldvar.8:4s is a sign-extending 4-byte load into an 8-byte
value, and load.4:1v a volatile byte load. A width or flag that is
not set is not printed. Because IR generation sets sign by default,
s also appears on operations whose result does not depend on it
(add.4s, brz.4s, store:8s).
call prints varargs(N) when the callee is variadic, with N the
number of named parameters, and sret when argument 0 is the result
pointer.
Reading it back#
ir_parse(file, text) reads the form line by line and modifies text
in place. It builds the unit's symbol table from the func ... ;decl
and data ... ;decl lines, so a symbol must be declared before an
instruction names it. A line that begins with ; is a comment. A func
line without ;decl opens a function; } closes it.
An error stops the read with the file and line:
embcc: bad.ir:3: error: EmbIR: expected a known opcode
The round trip is tested by tests/golden/ir-roundtrip.sh: printing a
parsed unit must reproduce the text it was parsed from, byte for byte,
at -O0, -O1 and -O2, and for EmbCC's own sources at -O0 and
-O2.
What the text does not carry#
The textual form encodes what passes need to be tested text-in, text-out. It does not carry:
- the classification of call arguments and return values (
argvbeyond the vreg,ret*,scratch,call_pcs), the function'sparam_abi,ret_abiandpcs; - the width and float flag of
retand ofcall, andret's structure size; natural,memoffandeh_region;- exception regions, debug variables and variable scopes;
is_scalar_floatof a frame slot;- an
asmstatement's bytes and operands.
A parsed unit therefore has src set to NULL and is used only to be
printed: the driver does not optimize or generate code from it. Known
limits of the reader:
- An
asminstruction cannot be read back; the reader stops withEmbIR: 'asm' has no operand form here — teach irparse.c about it. - The operator of
armw(op '&') is not read back; the parsed instruction hasimm0. - A line is copied into a 2048-byte buffer before its strings and
key=valuefields are read, so a longer line (a long string literal) is truncated.
Invariants and the verifier#
The following must hold for any IR handed to the optimizer or a backend:
- Every vreg is below
nvregsand every label belownlabels. - Every temp an instruction reads has a definition in the function.
Frame slots (
< nvars) may be read before they are written. - Every label an
IR_SWITCHnames, including its default, is placed by anIR_LABELin the same function. var_scope_loandvar_scope_hiindex the current instruction array. A pass that renumbers instructions remaps or frees them.- Every instruction has
lineset or is markedsynth. IR_LANDINGdefines two temps,dstandb.
When the environment variable EMBCC_VERIFY is set (to any value), the
optimizer checks the first four of these and the location rule for each
function, once on the IR from IR generation and once after
optimization. A failure is fatal and names the function, the check and
when it failed (after irgen or after opt):
internal: NAME reads temp %N with no definition (after TAG) — an optimizer pass dropped a value that is still used
The verifier runs inside the optimizer, so it runs only at -O1 and
above. The test suite sets EMBCC_VERIFY. The full list of messages is
in The optimizer.
Related pages#
- Architecture: where IR generation and the optimizer sit in the compiler.
- The front end: the AST and type system IR generation reads.
- The optimizer: the passes over EmbIR.
- Backends: how each target lowers EmbIR to machine code.