This page describes the current SSA builder and backend implementation. Its unversioned esp examples are compatibility snapshots, still accepted by the reader. Start with the new text walkthrough for esper 1 syntax and the roadmap for the library-first migration boundary.

Esper BE has a deliberately small but expressive type system: a handful of primitive types plus user-defined aggregates (structs, unions, arrays, and aliases). The frontend remains responsible for type correctness; the IR carries just enough type information to drive code generation, ABI lowering, and target-specific optimizations.

TypeSizeDescription
iN⌈N/8⌉Arbitrary-width integer, N in 1..65535, in declarations only
i8 / i16 / i32 / i641/2/4/8Canonical integer widths (aliases for iN)
f324 bytes32-bit IEEE 754 float
f648 bytes64-bit IEEE 754 float
ptrtarget4 bytes (32-bit) or 8 bytes (64-bit)
v12816 bytes128-bit SIMD vector
tuplen/aMulti-value bundle from multi-return calls
void0No value (void instructions)
user typescomputedstruct, union, array, alias (see below)

Integers

Integers in Esper BE are spelled iN for any N in the range 1..65535. The type reference reserves sixteen bits for the width, and i8, i16, i32 and i64 are the canonical names for the four widths a machine has registers for.

Where a width off that list may be written is narrower than the representation allows. A declaration takes any N: a struct or union member, an array’s element type, an alias, a global’s type, and a function’s parameter and return types.

type Flags = struct {
  i1  enabled,
  i3  priority,
  i4  channel,
  i24 reserved,
}

type Words = array i17[4]
type Handle = alias i33

const c0 i32 = 0
global @packed i17 = c0

An instruction’s type operand does not. It takes i8, i16, i32, i64, f32, f64, ptr, v128, void, tuple, or the name of a user-defined aggregate, so add i17 %a, %b and ldi i3 5 are parse errors. A const is the same: there is no const c0 i17 = 5. A packed field is therefore reached by naming the machine width of the storage unit it sits in and the offset within it, which is what the byte= and bit= suffixes on load and store are for.

Signedness is encoded into the instruction (via mnemonic suffix like .s / .u, or a dedicated opcode), not into the type. A single iN value can be fed to either signed or unsigned instructions; the bit pattern is the same.

Arithmetic semantics

Planned. Arbitrary-width arithmetic is not lowered yet, since no instruction can name a width off the canonical four. What follows is the intended lowering, not what a backend emits today.

Arithmetic on iN is performed modulo 2^N. Backends round up to the nearest native representation as needed and emit explicit masks on stores and on ABI-visible boundaries:

BackendLowering for iN
VMStore in i32 or i64; mask after each arithmetic op
CRound up to uint8 / uint16 / uint32 / uint64; explicit mask
LLVMNative iN
WASMRound up to i32 / i64; explicit mask
x86_64Native register; mask on memory-store and ABI boundaries
ARM64Native register; bfi / ubfx / sbfx on bitfield access
SPIR-VRound up to i8 / i16 / i32 / i64 per Vulkan capabilities

Sub-byte members and bitfields

iN with N < 8 participates in struct layout as a packed sub-byte field. Adjacent sub-byte members share a containing byte (LSB-first packing); access goes through the annotated load / store forms described in IL & text format. The access names the machine width of the byte it reads and a bit= offset within it, so a backend lowers a load as a byte read, a shift and a mask, and a store as a read, mask, shift and write back.

Floating Point

f32 and f64 follow IEEE 754. The f16 type is not currently supported; it is planned as part of the GPU backend effort.

Pointers

The ptr type is target-sized and opaque. It is produced by allocations (alloc, salloc, halloc), by gget of pointer-typed globals, and by explicit bitcast from integers.

Pointer arithmetic is ordinary integer arithmetic; there is no dedicated address opcode and no element-size scaling. add ptr %base, %offset is the spelling to prefer, since it stays correct whatever the target’s pointer width is. A machine-width integer type is accepted in the same position, so a frontend that already knows the target may write add i64 on a 64-bit one, but it then has to change with the target.

Vectors

The v128 type represents a 128-bit SIMD vector. The interpretation of the lanes is determined by the instruction, not by the type itself. A v128 value has no inherent lane layout; you can operate on the same value as i32x4 in one instruction and f32x4 in the next.

SIMD instructions use lane descriptors to specify how the 128 bits are partitioned. These are not types in the Esper BE type system; they are instruction operands, written after the value operands rather than in the type position, which stays v128: vadd v128 %0, %1, i32x4.

DescriptorLanesLane width
i8x16168-bit integer
i16x8816-bit integer
i32x4432-bit integer
i64x2264-bit integer
f32x4432-bit float
f64x2264-bit float

See the Instruction Set page for the full list of SIMD instructions.

Tuples

The tuple type marks an SSA value that bundles several values produced together. It is produced by multi-return call instructions when the callee has 2+ return values. Individual components are projected using extract:

%4 = call $divmod %0, %1        // tuple result
%5 = extract i32 %4, 0          // first component
%6 = extract i32 %4, 1          // second component

Tuples are not first-class values; they cannot be stored to memory, passed as arguments, or used as operands to arithmetic instructions. Their sole purpose is to carry multi-return results from a call to the extract projections that decompose them.

Void

The void type is used in function signatures to indicate that a function does not return a value, and internally to mark SSA id slots consumed by instructions that produce no result (stores, jumps, etc.). Void-typed SSA slots must never be referenced as operands.

User-defined Aggregates

Aggregate types are declared at the unit’s top level with the type keyword. The four aggregate kinds are:

Aggregate types use bare names (no sigil), distinguishing them from function references ($name) and global references (@name).

Structs

type Point = struct {
  f32 x,
  f32 y,
}

Members are listed in declaration order. The layout helper packs members at their natural alignment; adjacent sub-byte members (iN with N < 8) share a containing byte, LSB-first:

type Flags = struct {
  i1  enabled,
  i3  priority,
  i4  channel,
  i24 reserved,
}

Flags occupies exactly 4 bytes. enabled is bit 0, priority is bits 1..3, channel is bits 4..7, and reserved fills bytes 1..3.

Unions

type Variant = union {
  i32 as_int,
  f32 as_float,
  ptr as_ptr,
}

All union members start at offset 0. Reading one member’s type from a value that was written through a different member’s type performs a bit-level reinterpretation (the IR-level equivalent of a bitcast).

Arrays

type Matrix4 = array Point[16]
type Bytes32 = array i8[32]

The element type may be any other type (primitive or aggregate). The element count is a compile-time u32 literal.

Aliases

type Handle = alias i64
type PointAlias = alias Point

alias declarations give a new name to an existing type. Aliases are transparent: they participate in type checking by their underlying type.

Debug locations

Type declarations and individual members may carry @ "file":line:column annotations that round-trip through the parser and printer:

type Point @ "geom.f":12:1 = struct {
  f32 x @ "geom.f":13:3,
  f32 y @ "geom.f":14:3,
}

Forward references

Aggregate members may reference types declared later in the same unit. The parser’s fixup queue resolves these once the full unit is parsed.

First-class SSA values

Aggregates flow through SSA the same as primitives: they can be the result of construction opcodes (struct.new, array.new), they can be projected with field / array.elem, they can be updated with field.set (producing a new aggregate value), they can travel as a branch argument, and they can be loaded from or stored to memory as a whole.

%3 = struct.new Point %x, %y
%4 = field f32 %3, 0                 // %3.x by index
%5 = field.set %3, 0, %new_x         // updated copy
%6 = array.new Bytes32 %b0, %b1, ... // const-count construction
%7 = array.elem i8 %6, 5             // const-index projection
%a = load Point %ptr
store Point %ptr, %a

The full IL surface for aggregates (including aggregate constants and the annotated load / store forms used for sub-byte and field-aware access) is described in IL & text format.