How the Maru Bootstrap Isolates Host and Target Environments: A Three-Stage Architecture

The Maru bootstrap isolates host and target environments by creating three distinct environment objects—*host-env*, *slave-env*, and *target-env*—and switching between them by rebinding the global *globals* pointer, ensuring that host-side primitives never leak into the compiled target binary.

The self-hosting Maru compiler uses a sophisticated bootstrapping process to ensure complete separation between the environment running the bootstrap code and the environment that will become the final compiled binary. This article examines how the attila-lendvai/maru repository implements a three-stage architecture to bootstrap isolate host and target environments, preventing contamination between the host VM and the generated target code.

The Three-Environment Architecture

The bootstrap creates three distinct environments with specific roles and parent-child relationships:

Environment Purpose Creation Location
Host The original VM that runs the bootstrap code, owns the initial global environment (*globals*), and provides primitive I/O and error handling. source/boot.lsource/bootstrapping/early.l
Slave A fresh module with no parent that becomes the syntax-module for the target. Isolated from the host to prevent accidental use of host-side definitions while generating target code. source/bootstrapping/host-ready.l
Target The final environment holding compiled definitions that will be level-shifted into the produced binary. Its parent is the slave environment, making all slave macros visible to the target. source/compiler/target.l

Stage 1: Initializing the Host Environment

The bootstrap begins in source/boot.l, which sets up a minimal standard library and defines the bootstrap flag bootstrapping?:

;; source/boot.l
(if (not (defined? 'bootstrapping?))
    (eval '(define bootstrapping? ())))

Immediately after, source/bootstrapping/early.l initiates the transition to the slave environment:

;; source/bootstrapping/early.l
(switch-to-slave-env)

This early switch ensures that subsequent bootstrap files are loaded into an isolated context rather than polluting the host's global environment.

Stage 2: Creating the Slave Environment

The critical isolation mechanism is implemented in source/bootstrapping/host-ready.l. Here, the bootstrap creates *slave-env* as a fresh environment with no parent (level 0):

;; source/bootstrapping/host-ready.l
(let ((slave-env (environment (if-at-expand (defined? '*maru-module*)
                                   (<module>-globals *maru-module*)
                                   *maru*))))
  (assert (= 0 (<env>-level slave-env)))  ; Verifies no parent
  ;; ...
  )

This level-0 environment ensures complete separation from the host. The file then defines the environment switching primitives:

;; source/bootstrapping/host-ready.l
(environment-define *slave-env* 'switch-to-host-env
    (lambda ()
      (verbosity 2 (warn "---> host env\n"))
      (set *globals* *host-env*)))

(define-function switch-to-slave-env ()
  (verbosity 2 (warn "---> slave env\n"))
  (set *globals* *slave-env*))

The bootstrap also provides eval-in-host for cross-environment manipulation without switching:

;; source/bootstrapping/host-ready.l
(define eval-in-host (lambda (form) (eval form *host-env*)))

Stage 3: Establishing the Target Environment

Once the compiler infrastructure is loaded into the slave environment, source/compiler/target.l creates the target environment as a child of the slave:

;; source/compiler/target.l
(define *target-env* (environment *slave-env*))

This parent-child relationship ensures that all macros and syntax definitions stored in the slave are automatically visible to the target during compilation, while keeping the target's runtime definitions separate.

The target environment is activated by rebinding *globals*:

;; source/compiler/target.l
(define-function switch-to-target-env ()
  (verbosity 2 (warn "---> target env\n"))
  (set *globals* *target-env*))

(switch-to-target-env)

Cross-Environment Evaluation Primitives

To manipulate one environment while executing in another, the bootstrap provides thin wrappers:

Wrapper Function Source Location
host-value Evaluates a form in the host environment source/bootstrapping/late.l
eval-in-slave Evaluates a form in the slave environment source/bootstrapping/host-ready.l
target-value Evaluates a form in the target environment source/compiler/target.l

These functions allow the bootstrap to query host-side I/O or debugging facilities while the *globals* pointer remains bound to the target environment.

Isolation Guarantees and Safety Checks

The bootstrap enforces strict isolation through several mechanisms:

  • No shared mutable state: Each environment maintains its own *globals* binding and definition tables.
  • Level-0 slave: The assertion (= 0 (<env>-level slave-env)) in source/bootstrapping/host-ready.l guarantees the slave has no parent environment.
  • Safety checks: source/compiler/target.l includes verification that *globals* is not accidentally the same object during critical phases (lines 123-127).
  • Macro isolation: Macro expansion occurs in the slave environment, ensuring that target-specific redefinitions—such as redefining CONS for the generated binary—do not affect the host's macro system.

Summary

  • The Maru bootstrap creates three distinct environments—host, slave, and target—to maintain strict separation between the bootstrap machinery and the compiled output.
  • Environment switching is achieved by rebinding the global pointer *globals* using switch-to-host-env, switch-to-slave-env, and switch-to-target-env.
  • The slave environment is created as a level-0 environment with no parent in source/bootstrapping/host-ready.l, ensuring complete isolation from host definitions.
  • The target environment is created as a child of the slave in source/compiler/target.l, allowing it to access slave macros while keeping runtime definitions separate.
  • Cross-environment evaluation is supported through wrappers like host-value and target-value, enabling manipulation of one environment while executing in another.

Frequently Asked Questions

What is the difference between the slave and target environments in Maru?

The slave environment (*slave-env*) serves as the syntax module for the target, holding macro definitions and compiler forms that must be visible during compilation but should not be part of the final runtime. The target environment (*target-env*) is a child of the slave environment and contains the actual compiled definitions that will be level-shifted into the produced binary, ensuring macros live in the parent while runtime code lives in the child.

How does Maru prevent host environment definitions from leaking into the target?

Maru prevents leakage by creating the slave environment as a fresh module with no parent (level 0) in source/bootstrapping/host-ready.l, ensuring it shares no bindings with the host. The bootstrap then explicitly switches between environments by rebinding *globals* using functions like switch-to-slave-env and switch-to-target-env, guaranteeing that code loaded in one environment cannot accidentally reference bindings from another.

Why does the Maru bootstrap use three environments instead of two?

The three-environment architecture separates concerns between the host (running the bootstrap machinery), the slave (holding compiler macros and syntax), and the target (holding the final runtime definitions). Using only two environments would force either the host to be polluted with target macros or the target to inherit host-specific primitives, breaking the isolation required for self-hosting and cross-compilation.

Can the bootstrap process modify the host environment after switching to the target?

Yes, the bootstrap retains the ability to modify the host environment even after switching to the target through the eval-in-host function defined in source/bootstrapping/host-ready.l and the host-value wrapper from source/bootstrapping/late.l. These cross-environment evaluation primitives allow the bootstrap to query or manipulate host-side state—such as file I/O or debugging facilities—without permanently switching *globals* back to the host environment.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →