How the Maru Compiler Emits Literal Objects into the Read-Only Segment
The Maru compiler emits string literals and immutable objects into the .rodata section using the emit-object/C-string function, which generates assembler directives like .section .rodata and .asciz, while omitting heap header fields to prevent garbage collection traversal.
The Maru self-hosting Lisp compiler targets native x86 architectures and places constant data in read-only memory to protect immutability and reduce binary size. Understanding how literal objects are emitted into the read-only segment requires examining the interaction between assembler pseudo-instructions, object header generation, and the compiler’s code generation pipeline.
The Three-Stage Emission Process
Maru’s native back-ends coordinate three distinct steps to place literal objects in the read-only data segment. Each step is implemented in the compiler’s emission layer.
Defining the RODATA Pseudo-Instruction
The assembler abstraction defines a RODATA instruction that prints the platform-specific assembler directive for read-only data sections. In source/compiler/emit-x86-common.l at lines 97–99, this directive maps to .section .rodata on Linux and .section __TEXT,__const on macOS.
;; From source/compiler/emit-x86-common.l
(define-instruction RODATA ()
(emit " .section .rodata"))
Emitting the Literal Object
When the compiler encounters a string literal, it invokes emit-object/C-string (lines 90–95 in source/compiler/emit-x86-common.l). This function switches to the read-only segment, generates a unique label (e.g., cstr0, cstr1), and emits the zero-terminated string using the ASCIZ pseudo-instruction.
;; Conceptual flow from emit-object/C-string
(RODATA) ; switch to .rodata section
(label "cstr0") ; define label
(ASCIZ "Maru") ; emit .asciz "Maru"
Handling Object Headers for Read-Only Data
Heap-allocated objects in Maru contain a header with next and chunk-size fields for garbage collection. For read-only literals, the compiler omits these first two header words to prevent the GC from traversing them. This special case is documented in source/compiler/emit-x86-objects.l at lines 38–41 within emit-object/header.
;; From source/compiler/emit-x86-objects.l
;; For read-only objects, we omit the 'next' and 'chunk-size' slots
;; because the GC must never see them as heap blocks.
Code Generation Flow for String Literals
The complete pipeline from source code to read-only segment involves multiple compiler phases. When the compiler processes a form like (C-string "hello"), the following occurs:
- Compilation –
compile/C-stringrecognizes the literal and prepares it for emission. - Label Generation –
emit-object/C-stringcreates a unique identifier (e.g.,cstr0). - Section Switching – The
RODATAdirective switches the assembler output to.rodata. - Data Emission – The
ASCIZdirective outputs the null-terminated bytes. - Reference Generation – The compiler emits a
LOADinstruction to place the label address into a register.
.section .rodata
cstr0:
.asciz "hello"
.text
lea cstr0(%rip), %rax ; Load address of read-only literal
Distinguishing Read-Only Literals from Immediate Values
Not all literals in Maru occupy the read-only segment. Immediate numeric literals (e.g., (LITERAL 42)) are handled by the LITERAL operand class defined in source/compiler/emit-x86-common.l at line 66. These values are encoded directly into the instruction stream with the $ prefix and do not require storage in .rodata.
mov $42, %rax ; Immediate literal, no .rodata entry
This distinction ensures that small constants incur no memory overhead, while larger immutable objects like strings reside safely in the read-only segment where the garbage collector cannot corrupt or relocate them.
Summary
- The Maru compiler emits string literals into the
.rodatasection using theRODATApseudo-instruction defined insource/compiler/emit-x86-common.l. - The
emit-object/C-stringfunction generates unique labels and emits zero-terminated strings via theASCIZdirective. - Read-only objects omit the
nextandchunk-sizeheader fields to prevent garbage collection traversal, as implemented insource/compiler/emit-x86-objects.l. - Immediate numeric literals are encoded directly into instructions using the
LITERALoperand class and do not occupy the read-only segment.
Frequently Asked Questions
How does Maru prevent the garbage collector from scanning read-only literals?
Maru omits the first two header words (next and chunk-size) when emitting read-only objects in source/compiler/emit-x86-objects.l. Because the GC uses these fields to traverse the heap, their absence ensures the collector never interprets a .rodata address as a heap-allocated block to be scanned or relocated.
What is the difference between RODATA and ASCIZ in Maru’s compiler?
RODATA is a pseudo-instruction that switches the assembler output to the read-only data section (.rodata on Linux, __TEXT,__const on macOS). ASCIZ is a separate pseudo-instruction that emits a null-terminated ASCII string at the current position. emit-object/C-string uses RODATA to select the section, then ASCIZ to write the actual bytes.
Why are immediate numbers not stored in the read-only segment?
Immediate numeric literals use the LITERAL operand class defined in source/compiler/emit-x86-common.l, which encodes values directly into the instruction stream using the $ prefix (e.g., $42). This avoids the memory overhead and cache pressure of storing small constants in .rodata, while strings and larger immutable objects benefit from the space savings and protection offered by the read-only segment.
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 →