# How Maru’s Bootstrap Process Handles Type ID Changes

> Discover how Maru's bootstrap process ensures stability by dynamically mapping type IDs between host and slave VMs. Learn how Maru handles type ID changes.

- Repository: [Attila Lendvai/maru](https://github.com/attila-lendvai/maru)
- Tags: internals
- Published: 2026-02-25

---

**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:

```lisp
(if-at-expand bootstrapping?
    (let ()
      (define-function allocate-type-id ()
        (eval-in-host '(allocate-type-id)))
      ...

```

*Source*: [source/types.l lines 33-36](https://github.com/attila-lendvai/maru/blob/maru.10/source/types.l#L33)

### Slave-Side Allocation

The slave maintains its own independent counter via `allocate-type-id/slave`, which simply increments `%last-type-id`:

```lisp
(define-function allocate-type-id/slave ()
  (incr %last-type-id))

```

*Source*: [source/types.l line 38](https://github.com/attila-lendvai/maru/blob/maru.10/source/types.l#L38)

## 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:

```lisp
(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](https://github.com/attila-lendvai/maru/blob/maru.10/source/types.l#L89)

### The register-type Function

For dynamically created types, the `register-type` function automates the mapping process:

```lisp
(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](https://github.com/attila-lendvai/maru/blob/maru.10/source/types.l#L107)

## 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:

```lisp
(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](https://github.com/attila-lendvai/maru/blob/maru.10/source/types.l#L117)

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:

```lisp
(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](https://github.com/attila-lendvai/maru/blob/maru.10/source/types.l#L125)

### Verifying the Mapping at Runtime

You can inspect the translation table during development:

```lisp
(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 uses `allocate-type-id/slave` for independent counters.
- **Translation table**: `%type-id-map/host->slave` stores the mapping from host IDs to slave IDs, indexed by host type ID.
- **Automatic registration**: `register-type` populates the map for new types, while manual initialization handles core types like `<type>` and `<record>`.
- **Runtime resolution**: `type-object-for-id` translates 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.