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 functionWith 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 functionThe 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 pooltype.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 ifThe 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 Ais 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 bydelete()— callclear_mem(ref)explicitly if you need sentinel-based stale detection. Any code that stores the reference must check it withcheck_refbefore 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.
Related¶
- 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_fielddeclarations