Skip to content

Type System

IVLS provides a compile-time object system built entirely on macro expansion and integer memory pools. There are no heap allocations at runtime — all objects live in pre-declared integer arrays, and all CRUD operations are generated at compile time from a declarative type definition.

Why This Exists

KSP has no dynamic memory allocation. Everything must be pre-declared. The type system solves the problem of managing variable numbers of structured objects (routing entries, lookahead actions, browser entries, etc.) by providing pool allocation, O(1) new/delete, named field access, and constructor/destructor hooks — all as a compile-time macro expansion over flat integer arrays.

Key Concepts

Defining a Type

Three declarations are required before calling type.Create:

// 1. Field list
define MyType.MEMBERS := field_a, field_b, field_c

// 2. Default values — parallel to MEMBERS
declare MyType.INIT[] := (0, -1, 100)

// 3. Custom init hook — can be empty
function MyType.def()
end function

With these in place, type.Create(MyType) generates the full infrastructure. type.CreateCustomPool(MyType, N) uses a custom pool of N integers.

What Gets Generated

Field index constants: A const MyType.field block enumerating each member by name, plus MyType.field.SIZE for the total count.

Pool and allocation state: A flat integer array MyType.pool[], a parallel MyType.is_alloc[] tracking which slots are in use, and a MyType.count for live objects.

Free block queue: An ADT queue pre-filled with all valid slot indices. Allocation pops from this queue; deallocation pushes back. This makes new() and delete() O(1).

Named property accessors: A KSP property with getter and setter for each field. Usage: MyType[ref].field_a := 5 and declare x := MyType[ref].field_a.

CRUD functions (from type.Functions(MyType)): - MyType.new() -> ref — allocates, writes INIT defaults, calls Constructor - MyType.new_copy(src) -> ref — allocates and copies all fields from src - MyType.copy_a_to_b(src, dst) — copies all fields from src to dst (both must be allocated) - MyType.delete(ref) — calls Destructor, decrements count, marks the slot as free, pushes it to the free queue, and sets the caller's reference to -1. Does NOT call clear_mem() or overwrite field values - MyType.init(ref) — writes INIT defaults to all fields of an existing allocated object - MyType.clear_mem(ref) — overwrites all fields with TYPE.MEM_CLEAR sentinel. This is a separate function that can be called explicitly; it is NOT called by delete() - MyType.clear_pool() — resets the entire pool to initial state - MyType.print(ref) — debug dump of all field values

Constructor and Destructor

Override these to add lifecycle hooks:

function MyType.Constructor(ref) override
    // called at end of MyType.new(), after INIT defaults are written
    MyType[ref].timestamp := ENGINE_UPTIME
end function

function MyType.Destructor(ref) override
    // called at start of MyType.delete(), before slot is freed
    call cleanup_resources(MyType[ref].handle)
end function

The lookahead engine uses this pattern: Action.Constructor automatically enqueues the new action into lkh.ActionBuffer, and Action.Destructor removes it. This means Action.new() and Action.delete() carry their own bookkeeping — no manual registration at call sites.

Local Variable Helpers

When a function needs to work with many fields of the same object, batch-load helpers reduce repeated property accesses:

type.Locals(MyType, ref)      // declares locals prefixed with type name for each field
type.FillLocals(MyType, ref)  // reads all fields from pool into locals
// ... modify locals ...
type.FillObj(MyType, ref)     // writes locals back to pool

type.RawLocals(MyType, ref) is the unprefixed variant — declares locals with just the field name. Useful when the type name is long or when working with two different types simultaneously.

Reference Validation

check_ref(MyType, ref) returns TRUE if ref is a valid, currently allocated object:

if check_ref(MyType, ref)
    // safe to access MyType[ref] fields
end if

The sentinel value TYPE.MEM_CLEAR (-123547689) can be written to an object's fields by calling clear_mem() explicitly, making use-after-free detectable. Note that delete() itself does NOT write this sentinel — it only marks the slot as free. Code that needs sentinel-based stale detection should call clear_mem() before or after delete(). The lookahead system uses this pattern, holding Action references across async operations where the object might be deleted on another path.

Sentinel values: TYPE.MEM_CLEAR is -123547689. adt.NULL is -39867414. adt.TASK_FAIL is -123456789. Do not confuse them.

How Voice Is Built on This System

Voice is defined exactly like any user type. The vo_field macro generates the same pool infrastructure but with per-node field registration syntax:

// In any node's cb ICB:
vo_field lkh.use_legato = FALSE if (in_range(lkh.use_legato, 0, 1))
vo_field lkh.on_action  = -1   if (lkh.on_action >= -1)

The = value clause sets the default. The if (...) clause is the validity predicate used by check_ref for this field.

Pool Sizing

The default pool of 524,288 integers handles moderately sized types at moderate concurrency. For a type with 8 fields and 64 concurrent objects, that is 512 integers — well under the default. Use type.CreateCustomPool(MyType, N) when you want explicit RAM control. Pool size is adjusted down to the nearest multiple of BLOCK automatically.

Connections to Other Parts of IVLS

The type system is the foundation for lookahead Actions, modrix routings and modulators, data-structures FIFO linked lists (the Entry and FIFO types), and the voices Voice struct itself. Anywhere IVLS manages a pool of structured objects at runtime, the type system is underneath it.

Patterns and Caveats

  • One-level-deep inheritance only (node C from B from A is not supported for types either).
  • After MyType.delete(ref), the slot is freed and the caller's reference is set to -1. Field values are NOT overwritten by delete() — call clear_mem(ref) explicitly if you need sentinel-based stale detection. Any code that stores the reference must check it with check_ref before use.
  • type.Functions(MyType) must be called to generate the CRUD functions. Defining the type without calling this macro leaves the pool declared but unusable.
  • data-structures — the ADT queue used for the free block list inside each type
  • lookahead — the Action type uses Constructor/Destructor for automatic buffer management
  • voices — Voice is built on this system via vo_field declarations