31 KiB
Code Generator Backend Design (IR-based)
This document describes the design of a code generator backend for the Prog8 compiler, targeting 6502 (and eventually 68000). It focuses on the abstract concepts and conventions that a new IR-based code generator should implement.
Warning:
The old 6502 code generator walks the SimpleAST (
PtProgram,PtAssignment,PtFunctionCall, etc.) to emit assembly. A new code generator MUST NOT do this.Instead, it must work exclusively from the .p8ir IR file. The IR is fundamentally different from the AST -- it is a linear sequence of instructions (load, store, add, branch, call, etc.) grouped into chunks, operating on virtual registers and memory locations. The code generator's job is to translate each IR instruction into the target's equivalent instructions, not to walk AST nodes.
The .p8ir file contains every detail needed to generate equivalent assembly: memory layout, symbol definitions, subroutine chunks with their instruction bodies, register allocations, and data sections. The IR code generator and virtual machine demonstrate this approach -- study those modules rather than the old SimpleAST-based code generator.
1. Entry Point
The code generator implements the ICodeGeneratorBackend interface:
interface ICodeGeneratorBackend {
fun generate(program: PtProgram,
symbolTable: SymbolTable,
options: CompilationOptions,
errors: IErrorReporter
): IAssemblyProgram?
}
It receives the full program AST (already analyzed and optimized), a symbol table,
compilation options, and an error reporter. It produces an IAssemblyProgram
which can be assembled into a binary.
Note: The above interface is used by the old SimpleAST-based code generators. A new IR-based code generator will likely need a different entry point that takes the parsed IR file (
IntermediateProgram) instead of aPtProgramAST.
2. Memory Sections
The generated assembly organizes data into these sections:
Constants Read-only values embedded in the code section. Includes numeric constants (simple value equates), float constants (emitted as raw bytes), and memory slab data blocks.
Zero-Page Variables Fast-access zero-page RAM (addresses $02-$FF). Used for frequently accessed scalar variables. Allocation decisions are based on:
- Explicit
%zeropagedirective (require/prefer/dontcare/not) - Usage frequency scoring (weighted by type size and loop nesting depth)
- Split-word arrays of 2-byte elements can be stored as two separate byte arrays (LSB and MSB halves) for efficient indexed access with no scaling.
BSS (Uninitialized Data) Zero-initialized variables in regular RAM. This section is cleared at program startup. Includes:
- Module-level and local variables without initializers
- The virtual register file (
p8_regfile, 400 bytes for 200 word-sized registers) dirtyvariables (not required to be consistent across subroutine calls)- Memory slabs (runtime-allocated blocks, in sub-section
BSS_SLABS)
BSS_NOCLEAR A sub-section of BSS that is NOT zeroed at startup or on subroutine entry. Used for:
dirtyvariables (explicitly marked as undefined on subroutine entry)- Float evaluation temporary variables
- Any variable where the runtime cost of zeroing is not justified
The memory is still allocated in RAM, but its initial content is undefined.
BSS Relocation (Golden RAM / High Bank) BSS variables can be relocated to alternative memory regions:
- Golden RAM: A special RAM area on some targets (e.g., C128) that persists across system resets
- High Bank: Extended RAM bank on targets that support banked memory (e.g., CX16)
When relocation is active via options.varsGolden / options.varsHighBank
and options.slabsGolden / options.slabsHighBank:
BSS_NOCLEARandBSS_SLABSare emitted at the non-relocated locationprog8_program_endis emitted before the relocated sections- An
* = relocatedStartdirective switches to the relocated area BSSis emitted in the relocated area with an overflow check- A
PROG8_VARSHIGH_RAMBANKequate defines the active RAM bank
Initialized Data Data with compile-time initial values. Only arrays and strings can have non-zero initializers; scalar numeric variables with non-zero initial values are initialized via runtime assignment statements.
Struct Instances Statically allocated struct instances, with optional initial values.
- Instances without initial values go into the
BSSsection (zeroed at startup) - Instances with initial values go into the
STRUCTINSTANCESsection (initialized data, inline with the program) - Struct type definitions (
IRStStructDeffrom the IR symbol table) are emitted as 64tass.structblocks with named fields and parameterized constructors, enabling.dstructdirectives in data emission
Variable Alignment
Variables may have an alignment constraint (align: UInt in
IRStStaticVariable and IRStMemorySlab). When align > 1,
a .align N directive is emitted before the variable allocation.
This ensures the variable starts at an address that is a multiple of N.
Alignment only applies to non-zero-page variables (strings and arrays).
Split-Word Arrays
Arrays of 2-byte elements (words, pointers) may be stored in "split" layout
(DataType.isSplitWordArray) as two separate byte arrays:
variable_lsb: the low bytes of each elementvariable_msb: the high bytes of each element
This enables indexed access with a simple 8-bit index (no scaling by 2).
In the generated assembly, split-word arrays with initial values emit a
helper array directive (_array_$varname := ...) followed by _lsb
and _msb labels with < / > byte-selectors.
Uninitialized split-word arrays are emitted as two .fill allocations.
Memory Slabs
Runtime-allocated memory blocks (IRStMemorySlab). Emitted in the
BSS_SLABS section (which is part of BSS but separated for optional
relocation). Each slab gets a .fill directive with its size.
Scratch Space A small set of fixed zero-page locations reserved for temporary values during expression evaluation (e.g., scratch bytes, scratch words, a scratch pointer).
3. Symbol Naming
Symbols are prefixed to avoid collisions with assembler built-in symbols and to encode their kind:
| Kind | Prefix | Example |
|---|---|---|
| Block | p8b_ | p8b_main |
| Sub | p8s_ | p8b_main.p8s_start |
| Variable | p8v_ | p8b_main.p8v_x |
| Constant | p8c_ | p8c_MYCONST |
| Label | p8l_ | p8l_mylabel |
| Struct | p8t_ | p8t_point |
| Instance | p8i_ | p8i_screen |
Scoped names use the target assembler's dot notation (e.g., p8b_main.p8s_start)
to create nested symbol scopes. When generating code inside a subroutine, local
references should use relative/abbreviated names to make the assembly listing
more readable.
Symbol prefixing is performed before code generation by walking the AST and
modifying node names, then constructing a new SymbolTable from the prefixed AST.
4. Label Naming
Control flow labels use a monotonically increasing sequence number appended to a descriptive prefix. The sequence counter is preserved across the entire compilation unit to guarantee uniqueness.
Common label prefixes:
for_loop_N,for_end_N-- for-loop body and exitrepeat_N,repeatend_N-- repeat-loop body and exitfor_modified_N-- self-modifying comparison valuesif_else_N,if_end_N-- conditional branchesshort_circuit_skip_N,short_circuit_end_N-- short-circuit evaluation
A label stack tracks the current innermost loop's exit label, used by
break / exit statements.
5. Startup and Initialization
The generated program's entry point performs these steps in order:
Phase 1: Program Header (emitHeader + emitLauncher)
-
Set CPU type directive and encoding
-
Emit zero-page scratch register definitions as equates
-
Emit user-supplied symbol definitions from command line (
-esa) -
Generate launcher stub based on
options.outputandoptions.launcher:PRGwithBASIClauncher: generates a BASIC SYS line withprog8_entrypointlabel,.word/.nulldirectives for the BASIC stubPRGwithNONElauncher: raw header with load addressRAW: raw header with load addressXEX: Atari XEX header with load addressLIBRARY: minimal header withjmp main.start
-
Emit
prog8_program_startlabel at the beginning of the program -
Emit startup sequence (
emitStartupSequence):cldinstruction- Save stack pointer for
sys.exit()(tsx/stx prog8_lib.orig_stackpointer) - Call
init_systemandinit_system_phase2(unlessnoSysInit) - Call
prog8_lib.program_startup_clear_bssto zero the BSS section - Call
run_global_initsfor block-level variable initializers - Call the user's
main.startsubroutine - On return, jump to
cleanup_at_exitwhich callssys.poweroff_system
Phase 2: Main Entry Point (generated inside the main.start subroutine)
- Clear the BSS section (zero all uninitialized variables)
- For each block, call its block-level variable initializer subroutine
- Block initializers contain the runtime assignment statements for variables with non-zero initial values
- For zero-page arrays/strings with initial values, copy initial data from ROM-stored blocks into the zero-page locations
- Execute the user's program statements
Phase 3: Program Cleanup
- After main returns, jump to cleanup routine (resets system, etc.)
- A
prog8_program_endlabel is emitted at the very end of the generated output (after code, data, and BSS sections). This label marks the end of the program in memory and is used by theprogend()library routine and memory layout calculations (e.g.,MEMORY TOP). - A memtop overflow check is emitted after
prog8_program_endusing 64tass's.cerrordirective:.cerror * >= memtopAddress, "Program too long..."This catches program overflow at assembly time.
5.1 Subroutine Entry Initialization
There is no dynamic stack frame in the traditional sense. Local variables are
statically allocated in BSS (not on the stack). Their initial values (if any)
are established by assignment statements within the subroutine body.
Dirty local variables are explicitly documented as being undefined on
subroutine entry (they get whatever value was left in memory).
6. Subroutine Calling Convention
6.1 Argument Passing
Arguments are passed via CPU registers when possible, falling back to passing through static parameter variable slots.
Optimized register passing (simple integer types):
- 1 byte argument: passed in accumulator (A)
- 2 byte arguments: first in A, second in Y
- 1 word argument: passed in AY (A=lo, Y=hi)
- 1 long argument: passed in combined register pair (e.g., virtual registers)
General case (more arguments or complex types):
- Arguments are assigned to the subroutine's statically allocated parameter
variables before the
jsrcall - Complex expressions that might clobber registers are evaluated first, simpler register-based arguments last
External assembly subroutines (PtAsmSub) with explicit register specs:
- Arguments follow the declared register specification
- Evaluation order is optimized: combined long regs -> virtual word regs -> paired CPU regs -> single CPU regs -> float regs -> carry flag
6.2 Return Values
| Type | Return Convention |
|---|---|
| Byte | Accumulator (A) |
| Word | Accumulator + Y register (A=lo, Y=hi) |
| Long | Combined register pair (e.g., virtual registers) |
| Float | Floating-point accumulator (FAC1) |
| Pointer (address of array/string) | Accumulator + Y register (A=lo, Y=hi) |
Multi-value returns:
- First return value goes in A (byte) or AY (word)
- Subsequent return values use virtual registers (assigned downwards: R15, R14, ...)
- Float returns via FAC1 are assigned last to avoid clobbering by other return value computations
- Status flag returns (Carry, Zero, Negative, Overflow) are extracted from CPU flags and converted to byte values
6.3 Caller/Callee Responsibilities
- Caller is responsible for saving/restoring registers that must be preserved across the call (notably the X register, which is often used as an index base pointer)
- Callee may freely use A, X, Y registers, scratch zero-page locations, and floating-point accumulators; it is NOT responsible for saving them
- Static parameters are assigned by the caller before
jsr; on entry the callee can assume they hold the correct values
6.4 Subroutine Types
- Regular subroutines (
PtSub): Generated as standardjsrtargets with artsreturn - External assembly subroutines (
PtAsmSub): May have explicit register parameter specifications; optionalinlineflag causes the body to be emitted directly at the call site instead of viajsr - Banked calls (
JSRFAR): Subroutine address and bank are embedded as data words/bytes after thejsrinstruction
7. Expression Evaluation
Expression evaluation uses a tiered strategy:
Tier 1: Direct (register-based) Simple expressions involving constants, variables, and basic operators are evaluated directly using CPU registers and scratch zero-page locations. Results remain in registers for immediate use.
Tier 2: Stack-based fallback Complex expressions (e.g., nested arithmetic, mixed types, float operations) are evaluated using a stack discipline on the 6502 hardware stack or via pre-allocated temporary variables. The exact strategy depends on the target's register set and addressing modes.
Tier 3: Augmented (in-place) assignments
Compound assignment operators (+=, -=, *=, etc.) on variables,
array elements, memory-mapped locations, and pointer dereferences are
handled as read-modify-write operations targeting the storage location
directly, avoiding unnecessary temporary copies.
8. Control Flow
If/Else:
Conditional branches test a comparison result (byte, word, long, float)
against zero. The comparison operator (==, !=, <, <=, >,
>=) generates the appropriate conditional branch instructions.
Branch inversion optimization is used: a beq over a jmp can be
replaced with bne to the target.
Loops (For, Repeat):
- For-loops over numeric ranges generate an index variable, a loop body label, an increment/decrement, and a conditional back-branch to the loop top
- For-loops iterating over arrays/strings generate index and pointer variables with appropriate bounds checks
- Repeat-loops are simpler: a loop body label with a back-branch
(unconditional for
repeat, conditional forrepeat while/until) - Self-modifying code may be used for comparison immediates in some targets (when not in ROMable mode)
Ternary expressions (condition ? trueval : falseval):
Generated as a conditional branch around the false branch, with the
result stored in a common destination register or variable.
9. Floating Point
Floating-point operations use the target machine's ROM-based floating-point routines. Each target platform provides (or must provide) entry points for:
- Float addition, subtraction, multiplication, division
- Float-to-integer and integer-to-float conversion
- Float comparison and sign manipulation
- The floating-point accumulator (FAC1) holds the primary operand and result
The compiler emits jsr calls to these platform-provided routines.
Float constants are emitted as raw bytes matching the target's
floating-point byte format.
10. Memory-Mapped I/O
Memory-mapped variables (for hardware registers, etc.) are generated as simple address equates. Reading from them translates to a load from that address; writing to them translates to a store to that address.
11. Struct Handling
Struct types are emitted as assembler struct definitions (or equivalent),
allowing field access by name with automatic offset calculation.
Struct instances are allocated linearly in memory; fields are laid out
sequentially. Pointer dereferencing through a struct chain (e.g.,
ptr.field.subfield) walks the chain of offsets using a scratch pointer
register.
12. Multi-Target Considerations (6502 vs. 68000)
The design should accommodate multiple CPU targets:
Common abstractions:
ZeropageorFast RAMconcept -- a small, fast address range for frequently accessed variables- Scratch registers -- fixed temporary storage for expression evaluation
- CPU register set -- the primary working registers exposed by the ISA
- Addressing modes -- how memory operands are specified
Target-specific differences to abstract:
- Size and availability of CPU registers (6502: A, X, Y, flags; 68000: D0-D7, A0-A7)
- Stack discipline (6502: hardware stack is limited; 68000: full general-purpose stack)
- Instruction set capabilities (e.g.,
STZon 65C02,MULon 68000) - Floating-point support (ROM routines vs. FPU)
- Addressing modes (6502: limited; 68000: rich orthogonal set)
12.1 6502 vs. 65C02 Instruction Set Differences
The 65C02 is a superset of the 6502. A new code generator should prefer 65C02 instructions when targeting that CPU (the CX16 target uses 65C02). Below are the instruction choices that differ:
Zero stores:
- 6502:
lda #0/sta addr(2 instructions) - 65C02:
stz addr(1 instruction, does not affect flags)
Register save/restore (X and Y):
- 6502:
txa/pha/pla/tax(4 instructions) - 65C02:
phx/plx(2 instructions, likewise for Y withphy/ply)
Accumulator increment/decrement:
- 6502:
clc/adc #1(2 instructions, clobbers carry) - 65C02:
ina/dea(1 instruction, does not clobber carry)
Unconditional branch (short range):
- 6502:
jmp addr(3 bytes, 3 cycles) - 65C02:
bra addr(2 bytes, 3 cycles, limited range)
Zero-page indirect addressing (without Y offset):
- 6502:
lda ($zp),ywithldy #0(2 instructions, clobbers Y) - 65C02:
lda ($zp)(1 instruction, direct zero-page indirect)
Bit manipulation (test and reset/set):
- 6502:
lda mask/and mask/sta addrorora - 65C02:
trb addr(test and reset bits),tsb addr(test and set bits)
Stack-relative addressing (local variable access):
- 6502: requires manual stack pointer with
tsx/lda $0100,x - 65C02:
lda $NN,s/sta $NN,s(stack-relative addressing built-in)
65816 compatibility note: The Commander X16 can use a 65816 CPU which is
mostly 65C02 compatible but lacks the rmb, smb, bbs, bbr
instructions. The code generator should avoid these instructions when targeting
CX16 even if a 65C02 CPU is selected, or add a post-generation scan to reject
them.
A code generator should check options.compTarget.cpu at the point of
emitting these instructions and select the appropriate variant, rather than
relying on a post-processing pass.
13. Lessons from the Existing 6502 Code Generator
This section documents design decisions, pitfalls, and notable patterns from the existing 6502 code generator that a new IR-based backend should consider.
13.1 Assignment Engine as Central Abstraction
The existing code generator models all data movement as AsmAssignment
objects connecting an AsmAssignSource to one or more AsmAssignTarget
instances. This decouples "what value to move" from "how to move it" and
provides a clean dispatch table for all (source, target) type combinations.
A new backend should adopt a similar model.
Source kinds: literal number, literal boolean, variable, array element, memory address, CPU register, expression (complex).
Target kinds: variable, array element, memory address, CPU register, pointer dereference, void (discard result).
13.2 Static Variable Allocation, No Stack Frames
All variables are statically allocated. There is no runtime stack frame for local variables -- they live in BSS or zero-page and are initialized by explicit assignments in the subroutine body. Parameters are also statically allocated. This avoids stack depth issues on the 6502 (which has a 256-byte hardware stack) but means recursion is not supported.
Dirty variables (%option dirty) are documented as undefined on subroutine
entry; the optimizer omits their re-initialization at subroutine entry. This is
worth preserving as an optimization hint.
13.3 Zero-Page Scratch Variables
A small set of fixed zero-page locations is reserved for temporary values during expression evaluation:
| Name | Size | Purpose |
|---|---|---|
| SCRATCH_B1 | 1 byte | General-purpose byte temp |
| SCRATCH_REG | 1 byte | Register save/restore temp |
| SCRATCH_W1 | 2 words | General-purpose word temp (2) |
| SCRATCH_PTR | 2 bytes | Pointer for indirect access |
These are defined in the header and used throughout the generated code.
A new backend should reserve equivalent scratch space and use it consistently.
Note that the byte-sized SCRATCH_REG aliases the low byte of a word scratch
location -- this is a known fragility to be handled carefully.
13.4 Zero-Page vs. Non-Zero-Page Code Paths
Many operations have two code paths depending on whether a variable is in
zero-page or not. For example, pointer indirection through a zero-page
variable uses (zp),y addressing directly, while a non-ZP pointer must
first be copied to a scratch pointer in zero-page. A new backend should track
this distinction and generate different code accordingly.
The isZpVar() check is used pervasively. In an IR-based backend, this
information should be available as a property of the variable's storage
allocation.
13.5 Floating-Point is Entirely Library-Driven
All floating-point operations call into target-specific ROM routines via
jsr. The compiler does not generate inline FP code. The floating-point
accumulator (FAC1) is the primary operand register, FAC2 is secondary.
Float-to-integer conversion, comparison, and arithmetic all go through these
library entry points.
Float constants are deduplicated globally across the entire program and emitted as raw bytes in the target's float byte format (typically 5 bytes).
13.6 Self-Modifying Code for Non-ROMable Targets
When romable=false (the default), the code generator may use self-modifying
code patterns (e.g., writing comparison immediates into cmp #xx operands)
to avoid temporary variable overhead. This is incompatible with ROM-based
programs and must be disabled when romable=true. A new backend should
check the romable flag before emitting self-modifying patterns.
13.7 Assembly-Level Peephole Optimization is Necessary
The existing code generator runs a post-generation peephole optimizer on the assembly text. Key optimizations to preserve in a new backend:
- Store-load elimination:
sta X / lda X-> remove the redundant load - Load-load elimination:
lda V / sta X / lda V(same value) -> remove - Push-pop elimination:
pha / pla-> remove both - JSR+RTS to JMP:
jsr Sub / rts->jmp Sub - Comparison with zero:
lda X / cmp #0-> removecmp(flags already set) - Branch inversion:
beq L / jmp A / L:->bne A(shorter) - Canceled increments:
iny / dey-> remove both
Critical safety concern: The optimizer must NOT eliminate loads from I/O-mapped addresses (reading hardware registers has side effects). The code generator should track which addresses are I/O and communicate this to the optimizer, or accept that certain loads cannot be eliminated.
13.8 Symbol Prefixing is Done Before Code Generation
All user symbols are renamed with type prefixes (p8b_, p8s_, etc.)
by walking the AST and modifying node names before code generation. A new
SymbolTable is then constructed from the prefixed AST. This means the
symbol table used during code generation is NOT the same instance produced
by earlier compilation phases.
A new backend should either adopt the same approach or work with unprefixed symbols and let the assembler output handle name mangling differently. Key implications:
- Struct field names are not prefixed during the prefix pass (they are handled lazily at code generation time).
- After renaming, all
subTypereferences in the AST must be updated to point to entries in the new symbol table. - The
SymbolTable's cachedflatproperty becomes stale after renaming and must be rebuilt.
13.9 CPU Register Model Abstraction
The existing code uses a rich RegisterOrPair enum to model registers:
| Register | Composition | Notes |
|---|---|---|
| A | Single byte | Accumulator |
| X | Single byte | Index register |
| Y | Single byte | Index register |
| AY | Word | A (lo), Y (hi) |
| AX | Word | A (lo), X (hi) |
| FAC1 | Float (5 byte) | FP accumulator |
| FAC2 | Float (5 byte) | FP secondary |
| R0-R15 | Byte/Word | CX16 virtual registers (memory-mapped I/O) |
| R0R1-R14R15 | Long (4 byte) | Paired virtual registers |
| Status flags | Bit | Carry, Zero, Negative, Overflow |
A new backend should define a similar abstract register set that maps onto the target CPU's actual registers, with size and pairing information.
13.10 Fixed-Point Scratch Registers on CX16
On the Commander X16 target, there are 16 virtual registers (R0-R15) at fixed memory-mapped I/O addresses. These are used as fast temporary storage for intermediate values, multi-word operations, and multi-value returns. R14/R15 are reserved for long return values; R15 alone is used for secondary word return values. A new backend should define which (if any) target-specific fast registers are available and how they are allocated.
13.11 Struct Instance Initialization Quirk
Zero-page string and array variables with compile-time initial values require
special handling: their init data is stored in a ROM block (labeled with a
_init_value suffix), and a runtime memcopy copies the data from ROM
into the zero-page locations at startup. This is necessary because zero-page
locations cannot have static initializers in most binary formats.
A new backend should replicate this pattern for any variables in a "fast" memory region that lacks static initialization support.
13.12 Float Eval Result Variables
Per-subroutine temporary float variables (subroutineFloatEvalResultVar1
and subroutineFloatEvalResultVar2, 5 bytes each) are allocated in
BSS_NOCLEAR for intermediate float expression results. A new backend should
provide equivalent scratch space for float temporaries, sized according to
the target's float byte format.
The dirty attribute is set on these temporaries because they don't need
initialization.
13.13 Pitfall: TODO and AssemblyError for Unimplemented Paths
The existing code generator uses throw AssemblyError(...) and TODO()
for type combinations or edge cases that are not yet implemented. This is a
placeholder for incomplete features. A new backend should either implement
these paths cleanly or define a clear error-reporting strategy for unsupported
operations.
Several of these TODOs have been fixed in the IR codegen's builtin function
handling (BuiltinFuncGen.kt): rol, ror, rol2, ror2, setlsb, and
setmsb on pointer-deref arrays now fall through to address-computation
paths instead of throwing. The corresponding TODOs remain in the 6502 codegen
(BuiltinFunctionsAsmGen.kt).
13.14 Source Line References in Assembly Output
The generated assembly should include comments referencing the original
.p8 source file and line number for each generated block of instructions.
This is controlled by the includeSourcelines compilation option.
In the old code generator (AST-based), each AST node carries a Position
object with file, line, and column information. The code generator tracks
the last emitted source line and inserts ; source: filename.p8:NN
comments whenever the line changes.
For an IR-based code generator, source positions are stored at the
IRCodeChunk level via sourceLinesPositions: List<Position>
(rather than per-instruction). The code generator should emit a source
comment at the beginning of each chunk (or at subroutine entry) using
the first non-dummy position from this list. A lastSourceLine tracker
avoids duplicate consecutive comments.
The comment format should be:
; source: filename.p8:42
13.15 Output Type and Launcher Handling
The code generator must handle multiple output formats:
PRGwithBASIClauncher: emits a BASIC SYS stub with aprog8_entrypointlabel,.word/.nulldirectivesPRGwithNONElauncher: raw header at load addressRAW: raw binary at load addressXEX: Atari XEX format headerLIBRARY: minimaljmp main.startheader
All formats emit prog8_program_start at the entry point and share a
common startup sequence: cld, stack save via tsx/stx prog8_lib.orig_stackpointer,
system initialization calls, BSS clearing, and jump to main.start.
After main.start returns, execution jumps to cleanup_at_exit
which calls sys.poweroff_system.
The includeSourcelines option and user-supplied symbol definitions
(options.symbolDefs / -esa) are also emitted in the header.
13.16 The prog8_program_end Label
The code generator must emit a prog8_program_end label at the very end
of the generated output (after all code, data, and BSS sections). This label
marks where the program ends in memory and is used by:
- The
progend()library routine - Memory layout calculations (
MEMORY TOP) - Determining available space between program end and hardware boundaries
The old codegen emits this label at the end of its footer() method,
positioned after all sections. A new code generator should place it as the
last thing before assembly symbols and final output.
13.17 BSS_NOCLEAR and BSS Section Organization
Variables in BSS are split into two categories:
- Clean (
dirty == false): cleared to zero at program startup. These go into theBSSsection. - Dirty (
dirty == true): cleared at program startup but NOT at each subroutine entry. These go intoBSS_NOCLEAR. The runtime cost of zeroing on every call is avoided while still having deterministic initial state at program start.
The virtual register file (p8_regfile) always lives in BSS (cleared).
Memory slabs go into a separate BSS_SLABS sub-section to allow selective
relocation (e.g., to golden RAM or a high memory bank).
A PROG8_VARSHIGH_RAMBANK equate documents which RAM bank is active for
relocated BSS sections. When relocation is active, non-relocatable sections
(BSS_NOCLEAR, BSS_SLABS) are emitted before the * = directive
that switches to the relocated area.