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 |
|---|---|---|
|
String |
Interned UTF-8 byte strings. Required by the current reader. |
|
Function |
Function metadata and length-delimited function bodies. Required by the current reader. |
|
Debug |
Optional location sequences and debug attributes. |
|
Constant |
Optional out-of-line constant data. |
|
Type |
Interned type definitions. The current writer always emits this section. |
|
Global |
Optional module-level globals. |
|
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 ( |
|
13.1 |
None |
1 ( |
|
13.1 |
None |
2 ( |
|
13.1 |
None |
3 ( |
|
13.1 |
None |
4 ( |
|
13.1 |
None |
5 ( |
|
13.1 |
None |
6 ( |
|
13.1 |
None |
7 ( |
|
13.1 |
None |
8 ( |
|
13.1 |
None |
9 ( |
|
13.1 |
None |
10 ( |
|
13.1 |
None |
11 ( |
|
13.1 |
None |
12 ( |
Pointer |
13.1 |
|
13 ( |
Tile |
13.1 |
|
14 ( |
TensorView |
13.1 |
|
15 ( |
PartitionView |
13.1 |
|
16 ( |
Function |
13.1 |
|
17 ( |
Token |
13.1 |
None |
18 ( |
|
13.2 |
None |
19 ( |
|
13.3 |
None |
20 ( |
GatherScatterView |
13.3 |
|
21 ( |
StridedView |
13.3 |
|
22 ( |
|
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_flagsvarint. 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, anoptional_flagsvarintprecedes the required fields and bit 0 controls the enum afterdim_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
varintconstant-table index.In builds that support the internal string element type, a string DenseElements value stores a
varintelement 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
varintcount followed by value indices;a
varintbit 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.