Info tables¶
AIHC uses an info table to describe each type of object in managed memory. An object header ordinarily points to one info table. Thus, an object does not contain a copy of its run-time description.
Generated info tables are immutable. The collector temporarily uses the header as a forwarding address when it moves an object. The run-time system also makes temporary tables for blackholes.
The AMD64, ARM64, LLVM, and WebAssembly backends use the same info table model.
They use the common AihcInfo layout from the native run-time system.
Table fields¶
Each info table contains these fields:
| Field | Purpose |
|---|---|
identity |
Identifies the function or the saturated constructor. |
field_is_pointer |
Points to a byte map that identifies managed pointers. |
next |
Points to the table for the next application stage. |
backend_entry |
Gives the entry address that has the backend calling convention. |
srt |
Points to the static reference table of the code, or is null. |
field_count |
Gives the number of payload fields in the object. One byte: an object has at most 255 fields. |
remaining_arity |
Gives the number of arguments that the object still requires. One byte: a function takes at most 255 arguments. |
frame_kind |
Identifies a continuation frame for stack unwind operations. |
object_kind |
Identifies a node, closure, thunk, partial constructor, or special run-time object. |
needs_eval |
Is 1 for a thunk and a blackhole, 2 for an indirection, and 0 for a value. |
The garbage collector uses field_count and field_is_pointer to find managed pointers.
The application code uses remaining_arity and next to apply one source argument.
The inline evaluation check uses only needs_eval to find a value or an indirection.
The run-time evaluation code uses object_kind and backend_entry to enter an object.
Exception code uses frame_kind to identify continuation frames.
One source argument can use more than one machine field.
For this reason, two adjacent tables can have field counts that differ by more than one.
The next table gives the correct field layout for the next source argument.
Application stages¶
A function has one info table for each required application stage.
An underapplication copies the object and its fields into a new object.
The new object header points to the next table.
A saturated closure can enter its generated code without this copy.
The table sequence records these changes:
The last table has no next value.
A saturated function can enter its generated code.
A constructor works differently.
It has two tables however many arguments it takes: one for the saturated node,
and one shared by every stage that still wants arguments.
An unsaturated constructor object stores in its first field the count of the
fields it holds, and its own fields follow from the second.
The shared table gives the saturated table as its next value, which is where
the run-time system reads the full width and the complete pointer map.
The slots an unsaturated constructor has filled are a prefix of the slots the
saturated one holds, so one pointer map serves both.
A saturated constructor is a data node, does not enter code, and stores no
count.
Suspended function example¶
Consider a suspended call that captures x and y:
AIHC can represent this suspended call as a GRIN thunk for the generated total function.
The object stores the captured values in its payload.
Its table has the following logical values:
| Field | Value |
|---|---|
identity |
The generated total function. |
field_is_pointer |
One byte for x and one byte for y. |
next |
Null. |
backend_entry |
The backend entry for total. |
srt |
Null. |
field_count |
2 |
remaining_arity |
0 |
frame_kind |
No frame. |
object_kind |
Thunk. |
needs_eval |
1 |
The pointer map depends on the run-time representation of x and y.
For example, a lifted value has a pointer byte of 1.
An Int# value has a pointer byte of 0.
When code evaluates total, it reads the needs_eval byte from the table.
The byte is 1, so the code calls the run-time evaluation code.
After the update, the object is an indirection and the byte is 2.
The code then follows the indirection to the result without a call.
That code reads the thunk kind and uses the backend entry to call the generated function.
The run-time system sets the thunk state to blackhole during evaluation.
After evaluation, the run-time system changes the thunk into an indirection to the result.
Constructor example¶
Consider this constructor:
Pair has arity two.
AIHC can represent Pair, Pair first, and Pair first second as managed objects.
The backend supplies two info tables:
| Table | Object kind | field_count |
next |
|---|---|---|---|
Pair partial |
Partial constructor | 2 | The saturated table. |
Pair saturated |
Node | 2 | Null. |
Pair and Pair first both use the partial table and differ only in the count
they store:
| Object | Stored count | Stored fields |
|---|---|---|
Pair |
0 | 0 |
Pair first |
1 | 1 |
Pair first second |
None | 2 |
Both tables use the saturated constructor table as their identity value.
This stable identity lets case code compare a value with the Pair constructor.
Both tables also give the complete pointer map for both constructor fields.
Constructor application uses the common slow application path. This path allocates the object for the next stage and copies the stored fields. An application that fills the last field allocates a saturated node instead, which drops the stored count. The final object is a data node, so its entry addresses are null.
Nullary constructors have only the saturated table. The backend also makes an implicit static object for each nullary constructor.
Backend differences¶
The backends do not change the field meanings or the application sequence. They change the table encoding and the backend entry implementation.
AMD64¶
The AMD64 backend emits each table in read-only data.
The backend_entry field points to an entry adapter.
The run-time system provides shared adapters for objects whose fields and supplied values are all pointers, with up to eight fields and one supplied value.
The compiler emits a small adapter for each other directly enterable object.
Generated control code reads this field and jumps to the adapter.
ARM64¶
The ARM64 backend uses the same table contents as the AMD64 backend. It emits the tables and entry adapters with ARM64 sections, symbols, and calling conventions.
LLVM¶
The LLVM backend emits constant values of the %AihcInfo type.
It puts a tailcc adapter in backend_entry for an enterable object.
It emits only the constructor tables that the current compilation unit requires.
Linked constructor table names keep constructor identities equal across compilation units.
WebAssembly¶
The WebAssembly backend emits the same fields in WebAssembly assembly data. Its pointers are 32 bits and its counts, kind values, and flags are one byte each. Thus, its table is 28 bytes instead of the 48-byte table on 64-bit native targets.
WebAssembly calls require a compatible function type.
Its run-time adapter converts backend_entry to the required function type.
The adapter then transfers control through the WebAssembly trampoline.
The backend entry gives the fast path for generated code.