5. Binary Format#

The Tile IR bytecode is a versioned binary representation of a Tile IR module. A file consists of a fixed header, a sequence of named sections, and an end marker. References to strings, types, constants, SSA values, and debug information are integer indices into the corresponding table or value scope.

Warning

This chapter specifies the common bytecode envelope, the general section layouts, and the public type-tag registry, but it is not yet a complete independent implementation specification. Details not reproduced in this chapter include the complete opcode and per-operation payload registry, exact constant-pool entry encodings, several attribute payloads and enum values, and enum mappings for optional type attributes. The public source repository contains the bytecode operation, type, and attribute opcode registries, along with the bytecode reader and writer interfaces and implementations. Those interfaces and implementations are authoritative for encoding details not defined here. In particular, Operation Encoding is informative.

5.1. Primitive Encodings#

5.1.1. Bytes and Fixed-Width Integers#

A byte is an unsigned 8-bit value. Every fixed-width integer wider than one byte is stored in little-endian byte order. This includes the version tag, offset-table entries, entries in the debug-reference array, and the elements of fixed-width arrays. Other debug references are varint fields as specified below.

5.1.2. Unsigned Variable-Width Integers#

An unsigned varint is the canonical unsigned LEB128 encoding of a 64-bit value. Each byte contributes seven payload bits, least-significant group first. Bit 7 is one when another byte follows and zero in the final byte.

The canonical encoding uses the shortest possible representation. It is between one and ten bytes long; in a ten-byte encoding the payload of the tenth byte is at most one. Thus 0 is 00 and 2^64 - 1 is nine ff bytes followed by 01.

encode_uvarint(value):
  do:
    byte = value & 0x7f
    value = value >> 7
    if value != 0:
      byte = byte | 0x80
    emit(byte)
  while value != 0

Unless a field is explicitly described as fixed-width, it is an unsigned varint value. This includes integer counts, indices, tags, enumerators, opcodes, and bit fields. A field type that names an integer width, such as uint16 or int32, is fixed-width; uint types are unsigned and int types are signed.

5.1.3. Signed Variable-Width Integers#

A signed_varint first maps a signed 64-bit integer to an unsigned integer with ZigZag encoding, then encodes that integer as an unsigned varint:

zigzag(n) = 2 * n          if n >= 0
            -2 * n - 1    if n < 0

For example, 0, -1, 1, and -2 map to 0, 1, 2, and 3, respectively.

5.1.4. Counted Fixed-Width Arrays#

The generated bytecode implementation uses the following representation for an array of fixed-width integer elements:

le_array<T> {
  count: varint
  elements: T[count]       // fixed-width, little-endian
}

The count is therefore present even when a field is described by its logical name, such as shape or strides. Signed array elements use their two’s-complement fixed-width representation. In an int64 tensor-view shape or stride array, the dynamic value ? is the signed value -1 (ff ff ff ff ff ff ff ff).

5.2. File Header#

The header is exactly 12 bytes:

header {
  magic:   byte[8] = 7f 54 69 6c 65 49 52 00  // "\x7fTileIR\x00"
  major:   uint8
  minor:   uint8
  tag:     uint16                         // little-endian
}

The three version fields identify the bytecode version targeted by the producer. A reader rejects a version outside its supported range. Fields whose representation changed across versions are interpreted according to this header; the version is not a varint.

5.3. Sections#

Ordinary sections use IDs 1 through 7. ID 0 is reserved for the end marker and is not an ordinary section. An ordinary section has this envelope:

section {
  id_and_alignment: byte   // bits 0..6: section ID; bit 7: has alignment
  payload_length: varint   // excludes alignment and padding
  alignment: varint?       // present when bit 7 is set
  padding: byte[]          // 0xcb until the payload address is aligned
  payload: byte[payload_length]
}

When present, alignment is a nonzero power of two. Padding is computed from the absolute file offset after the alignment field. A conforming file contains at most one section with each ID. Although ordinary section payloads are length-delimited, unknown section IDs and duplicate section IDs are invalid in the current format.

The end marker is the single byte 00. It has no length, alignment, or payload and must be the final byte of the file. EOF without this marker, an alignment bit on the marker, or data following it is invalid.

The public section IDs are:

ID

Name

Purpose

0x01

String

Interned UTF-8 byte strings. Required by the current reader.

0x02

Function

Function metadata and length-delimited function bodies. Required by the current reader.

0x03

Debug

Optional location sequences and debug attributes.

0x04

Constant

Optional out-of-line constant data.

0x05

Type

Interned type definitions. The current writer always emits this section.

0x06

Global

Optional module-level globals.

0x07

Producer

Optional producer string reference, available in bytecode 13.3 and later.

The current writer emits sections in Global, Function, Constant, Debug, Type, Producer, and String order, omitting optional empty sections, and then emits the end marker. A reader first collects the sections and then processes them in dependency order, so ordinary sections need not occur in writer order.

5.3.1. Table-Based Sections#

The String, Type, and Constant sections use an offset table followed by a single data blob. Offsets are relative to the beginning of that blob, not to the beginning of the section.

string_section {
  count: varint
  padding: byte[]          // align offsets to 4 bytes
  offsets: uint32[count]
  data: byte[]             // concatenated UTF-8 byte strings
}

type_section {
  count: varint
  padding: byte[]          // align offsets to 4 bytes
  offsets: uint32[count]
  data: byte[]             // concatenated type encodings
}

constant_section {
  count: varint
  padding: byte[]          // align offsets to 8 bytes
  offsets: uint64[count]
  data: byte[]             // concatenated constant encodings
}

Entry i occupies data[offsets[i] .. offsets[i + 1]). The end of the data blob is used in place of offsets[i + 1] for the last entry. A string has no terminator; its offset interval determines its byte length.

5.3.2. Function Section#

The public function-entry envelope is:

function_section {
  count: varint
  entries: function_entry[count]
}

function_entry {
  name: varint                    // string-table index
  signature: varint               // function type-table index
  flags: byte
  location_sequence: varint       // debug sequence; 0 means unknown
  optimization_hints: attribute?  // present when flags bit 2 is set
  body_length: varint
  body: byte[body_length]
}

In the public format, flags bit 0 means private visibility, bit 1 means a kernel entry point, and bit 2 means optimization hints are present. Other bits are reserved in public bytecode. The function body is a stream of operation records and is parsed until body_length bytes have been consumed.

5.3.3. Global and Producer Sections#

global_section {
  count: varint
  entries: global_entry[count]
}

global_entry {
  name: varint             // string-table index
  value_type: varint       // type-table index
  value: varint            // constant-table index
  alignment: varint
  visibility: varint       // bytecode 13.3 and later
  constant: varint         // bytecode 13.3 and later
}

producer_section {
  producer: varint         // string-table index
}

The Global section omits visibility and constant before version 13.3. The Producer section is optional and is not emitted before version 13.3.

5.4. Type Encodings#

Each type-table entry begins with a type_tag encoded as a varint. The remainder of the entry is determined by that tag and the bytecode version. All current public tags fit in one byte, but the field remains a varint.

The public type-tag registry is frozen at the following assignments:

Tag

Type

Introduced

Payload after the tag

0 (0x00)

i1

13.1

None

1 (0x01)

i8

13.1

None

2 (0x02)

i16

13.1

None

3 (0x03)

i32

13.1

None

4 (0x04)

i64

13.1

None

5 (0x05)

f16

13.1

None

6 (0x06)

bf16

13.1

None

7 (0x07)

f32

13.1

None

8 (0x08)

tf32

13.1

None

9 (0x09)

f64

13.1

None

10 (0x0a)

f8E4M3FN

13.1

None

11 (0x0b)

f8E5M2

13.1

None

12 (0x0c)

Pointer

13.1

pointer_payload; extended in 13.4

13 (0x0d)

Tile

13.1

tile_payload

14 (0x0e)

TensorView

13.1

tensor_view_payload; extended in 13.4

15 (0x0f)

PartitionView

13.1

partition_view_payload; optional-field encoding changed in 13.3

16 (0x10)

Function

13.1

function_payload

17 (0x11)

Token

13.1

None

18 (0x12)

f8E8M0FNU

13.2

None

19 (0x13)

f4E2M1FN

13.3

None

20 (0x14)

GatherScatterView

13.3

gather_scatter_view_payload

21 (0x15)

StridedView

13.3

strided_view_payload

22 (0x16)

i4

13.3

None

Values outside this table are not public type tags. Internal and test builds may define tags in separate reserved ranges; those assignments are not part of the public bytecode registry.

The composite payload abbreviations in the registry mean:

pointer_payload {
  pointee_type: varint
}

tile_payload {
  element_type: varint
  shape: le_array<int64>
}

tensor_view_payload {
  element_type: varint
  shape: le_array<int64>
  strides: le_array<int64>
}

partition_view_payload {
  tile_shape: le_array<int32>
  tensor_view_type: varint
  dim_map: le_array<int32>
  padding_value: optional enum
}

gather_scatter_view_payload {
  optional_flags: varint
  tile_shape: le_array<int32>
  tensor_view_type: varint
  sparse_dim: varint
  padding_value: enum?      // present when optional_flags bit 0 is set
}

strided_view_payload {
  optional_flags: varint
  tile_shape: le_array<int32>
  traversal_strides: le_array<int32>
  tensor_view_type: varint
  dim_map: le_array<int32>
  padding_value: enum?      // present when optional_flags bit 0 is set
}

function_payload {
  input_count: varint
  input_types: varint[input_count]
  result_count: varint
  result_types: varint[result_count]
}

Type parameters added in later versions change the corresponding payload:

  • In version 13.4 and later, Pointer and TensorView begin with an optional_flags varint. Their pointer-communication attribute is written after the required fields when bit 0 is set. Earlier versions omit both fields.

  • PartitionView has always had an optional padding value. Before version 13.3, a presence byte and then the optional enum follow dim_map. In version 13.3 and later, an optional_flags varint precedes the required fields and bit 0 controls the enum after dim_map.

5.5. Attribute Encodings#

An attribute embedded in a context that does not otherwise determine its kind is self-contained: it begins with an attribute tag encoded as a varint. Attributes whose kind is fixed by an operation schema omit this tag and encode only their payload.

The public self-contained tags are Integer (1), Float (2), Boolean (Bool, 3), Type (4), String (5), Array (6), DenseElements (7), DivBy (8), SameElements (9), Dictionary (10), OptimizationHints (11), and Bounded (12). Array and Dictionary payloads begin with a varint count; their elements or values are themselves self-contained attributes.

A self-contained DenseElements attribute has this prefix:

dense_elements {
  tag: varint = 7
  shaped_type: varint       // type-table index
  data: dense_data
}

There is no separate discriminator in dense_data. The shaped type’s element type selects the branch:

  • A numeric DenseElements value stores one varint constant-table index.

  • In builds that support the internal string element type, a string DenseElements value stores a varint element count followed by that many string-table indices.

This type-directed branch is unambiguous because shaped_type is decoded before data.

5.6. Debug Section#

Debug locations are not stored inline in operation records. Instead, the function entry selects a location sequence. The first element of that sequence is the function location; subsequent elements are consumed in the serialized operation order, including operations nested in regions.

Index 0 is reserved for the unknown location both for location-sequence references and for debug-attribute references. Stored sequences and stored attributes therefore use one-based references.

debug_section {
  sequence_count: varint
  padding: byte[]                    // align to 4 bytes
  sequence_offsets: uint32[sequence_count]

  debug_reference_count: varint
  padding: byte[]                    // align to 8 bytes
  debug_references: uint64[debug_reference_count]

  attribute_count: varint
  padding: byte[]                    // align to 4 bytes
  attribute_offsets: uint32[attribute_count]
  attribute_data: byte[]
}

sequence_offsets[i] selects the first element of sequence i + 1 in debug_references. The next sequence offset, or debug_reference_count for the last sequence, bounds the sequence. Likewise, attribute_offsets bounds entries in attribute_data.

Each debug-attribute entry starts with a varint tag. The public tags and their payload fields, all encoded as varint references or unsigned values, are:

Tag

Kind

Payload

0

Unknown

None; normally represented by reserved reference 0.

1

DICompileUnit

DIFile reference.

2

DIFile

Filename string index, directory string index.

3

DILexicalBlock

Scope reference, file reference, line, column.

4

DILoc

Scope reference, filename string index, line, column.

5

DISubprogram

File reference, line, name string index, linkage-name string index, compile-unit reference, scope line.

6

CallSite

Callee-location reference, caller-location reference.

5.7. Operation Encoding#

Each operation record begins with an opcode encoded as a varint. The opcode is followed immediately by its opcode-specific payload; there is no per-operation byte length and no inline location index.

For every result represented in the targeted bytecode version, the payload stores a type-table index. A fixed-result operation writes the known number of type indices without a count. A variadic-result operation first writes a varint result count and then that many type indices. Result SSA indices are not written: the reader assigns them sequentially as operations are decoded, after the function arguments and any block arguments in scope.

Other recurring payload patterns include:

  • fixed operands as sequential SSA-value indices;

  • variadic operand groups as a varint count followed by value indices;

  • a varint bit field for optional operands and attributes;

  • regions as a block count followed by blocks; and

  • blocks as an argument count and argument type indices, followed by an operation count and operation records.

The presence, order, version gating, and exact interpretation of these fields is opcode-specific. Placeholder opcode numbers and inferred-result examples are deliberately omitted here: the complete public opcode and payload tables remain to be specified. Because operation records are not length-delimited, a current reader rejects an unknown or version-incompatible opcode rather than skipping it.