How Maru’s Bootstrap Process Handles Type ID Changes
Maru’s bootstrap process maintains stability across compilation stages by building a dynamic host-to-slave type ID mapping that translates numeric identifiers between the host VM and the freshly compiled slave VM.
The Maru programming language uses a unique self-hosting bootstrap mechanism where a host VM loads a slave VM inside a fresh module. Because each VM allocates its own integer type-id for every type object, the same logical type can receive different numeric identifiers in the host and slave. This article examines how Maru’s bootstrap process handles type ID changes without breaking object compatibility, referencing the actual implementation in source/types.l and source/bootstrapping/host-ready.l.
The Challenge of Type ID Mismatches During Bootstrap
When Maru bootstraps, the host VM has already compiled its own set of built-in types (<type>, <record>, <long>, etc.) and assigned them sequential type IDs. The slave VM, however, starts with a fresh counter and allocates its own IDs as it imports these types. Without intervention, <type> might have ID 1 in the host but ID 5 in the slave, causing catastrophic failures when the host passes type-tagged objects to the slave.
How Type IDs Are Allocated in Host and Slave VMs
The allocation strategy diverges based on the bootstrapping? flag defined in source/bootstrapping/host-ready.l.
Host-Side Allocation
When bootstrapping? is true, the host retains control of canonical type ID assignment. The allocate-type-id function delegates to the host’s existing allocator:
(if-at-expand bootstrapping?
(let ()
(define-function allocate-type-id ()
(eval-in-host '(allocate-type-id)))
...
Source: source/types.l lines 33-36
Slave-Side Allocation
The slave maintains its own independent counter via allocate-type-id/slave, which simply increments %last-type-id:
(define-function allocate-type-id/slave ()
(incr %last-type-id))
Source: source/types.l line 38
Building the Host-to-Slave Type ID Map
Maru stores the translation table in %type-id-map/host->slave, an array where the index represents the host type ID and the value stores the corresponding slave type ID.
Mapping Core Types
During initialization, the bootstrap code manually maps the fundamental <type> and <record> objects:
(set <type> (eval-in-host '<type>))
(set <record> (eval-in-host '<record>))
(let ((slave-id (allocate-type-id/slave)))
(set (array-at %type-id-map/host->slave (<type>-id <type>)) slave-id)
(set (array-at %types slave-id) <type>))
Source: source/types.l lines 89-96
The register-type Function
For dynamically created types, the register-type function automates the mapping process:
(define-function register-type (type)
(let ((slave-id (allocate-type-id/slave))
(host-id (<type>-id type)))
(set (array-at %type-id-map/host->slave host-id) slave-id)
(set (array-at %types slave-id) type)
(eval-in-host `(register-type ',type))))
Source: source/types.l lines 107-115
Resolving Type IDs at Runtime
All higher-level operations delegate to type-object-for-id to translate host IDs into slave type objects.
The type-object-for-id Function
This function performs the lookup and validates that a mapping exists:
(define-function type-object-for-id (host-id)
(or (array-at %types
(let ((slave-id (array-at %type-id-map/host->slave host-id)))
(assert slave-id "type-object-for-id: no host→slave mapping for host id " host-id)
slave-id))
(error "type-object-for-id has failed for host id " host-id)))
Source: source/types.l lines 117-123
Because type-of, accessor generation, and other reflective operations use this function, the numeric value of a type ID can change between bootstrap stages without breaking object compatibility.
Practical Examples
Mapping Primitive Literals
The bootstrap extends the type ID map to built-in objects created by the host reader:
(list-do entry
`(("" ,<string>)
(42 ,<long>)
(t ,<symbol>)
((1) ,<pair>)
(() ,<undefined>))
(apply (lambda (instance type)
(set (array-at %type-id-map/host->slave (type-id-of instance))
(<type>-slave-id type))
(assert (= (type-of instance) type)))
entry))
Source: source/types.l lines 125-135
Verifying the Mapping at Runtime
You can inspect the translation table during development:
(types-do t
(warn "host id " (<type>-id t) " → slave id "
(array-at %type-id-map/host->slave (<type>-id t))))
This outputs the complete host-to-slave correspondence, useful for debugging type ID mismatches during bootstrap.
Summary
- Dual allocation strategy: The host allocates canonical type IDs via
eval-in-host, while the slave usesallocate-type-id/slavefor independent counters. - Translation table:
%type-id-map/host->slavestores the mapping from host IDs to slave IDs, indexed by host type ID. - Automatic registration:
register-typepopulates the map for new types, while manual initialization handles core types like<type>and<record>. - Runtime resolution:
type-object-for-idtranslates host IDs to slave type objects, insulating the rest of the system from numeric changes. - Primitive coverage: The map extends to built-in literals (strings, longs, symbols) created by the host reader.
Frequently Asked Questions
What happens if a type ID changes between bootstrap stages?
Maru’s bootstrap process remains stable because it never relies on hardcoded type ID values. Instead, the %type-id-map/host->slave array translates host type IDs to their slave counterparts at runtime. When register-type creates a new mapping or when type-object-for-id performs a lookup, the system automatically adapts to any numeric shifts caused by adding, removing, or reordering type definitions.
How does the bootstrap flag control type ID allocation?
The bootstrapping? flag, defined in source/bootstrapping/host-ready.l, switches between two allocation modes. When true, allocate-type-id delegates to the host via eval-in-host, ensuring the host maintains canonical IDs. Simultaneously, allocate-type-id/slave allows the slave to generate its own independent sequence. This dual-track system enables the construction of the host-to-slave mapping while both VMs remain operational.
Can I debug type ID mappings during a bootstrap run?
Yes, you can inspect the translation table by iterating over registered types and querying %type-id-map/host->slave. The snippet (types-do t (warn "host id " (<type>-id t) " → slave id " (array-at %type-id-map/host->slave (<type>-id t)))) prints the complete correspondence. Additionally, type-object-for-id contains assertions that trigger if a host ID lacks a slave mapping, helping catch synchronization errors early in the bootstrap process.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →