amiga: custom now includes interrupt vector offset symbols, cpuVBR and isPAL (set in grab_system)

This commit is contained in:
Irmen de Jong committed 2026-08-18 22:07:13 +02:00
1 parent df659d285b
commit ffb0a4d603
10 files changed
+274 -1060

No files matched your search

+14 -14
View File
@@ -14,6 +14,7 @@ exec_AttnFlags = 296
gfx_ActiView = $22
gfx_copinit = $26
gfx_LOFlist = $32
gfx_DisplayFlags = $dc
GfxLoadView = -222
GfxWaitTOF = -270
IRQ1 = $64
@@ -30,9 +31,13 @@ IRQ7 = $7C
beq.s 1$ ; On 68000 no VBR (always zero)
lea.l _S_GetVBR(PC),a5 ; Function to call as supervisor
jsr exec.Supervisor(a6) ; Call supervisor function in A5
move.l d0,S_VBR ; Store the returned VBR contents
move.l d0,custom.cpuVBR ; Store the returned VBR contents
1$
move.l sys.GfxBase,a6 ; A6 = Graphics base
btst.b #0,gfx_DisplayFlags(a6) ; Check NTSC bit (bit 0)
bne.s .isNTSC
move.b #-1,custom.isPAL
.isNTSC:
move.l gfx_ActiView(a6),-(sp) ; Store current View pointer
sub.l a1,a1 ; NULL view = default settings
jsr GfxLoadView(a6) ; Load the view
@@ -46,36 +51,31 @@ IRQ7 = $7C
or.w d0,(sp) ; SET/CLR set to SET
move.w custom.ADKCONR,-(sp) ; Audio, disk and UART
or.w d0,(sp) ; SET/CLR set to SET
move.w custom.VPOSR,d0 ; Vertical pos and Agnus ID
btst #13,d0 ; When set: NTSC, when clear: PAL
bne.s 2$ ; Leave value 0 for NTSC
move.w #$FFFF,S_PAL ; Set all bits for PAL
2$ btst.b #14-8,custom.DMACONR ; Dummy read
btst.b #14-8,custom.DMACONR ; Dummy read
3$ btst.b #14-8,custom.DMACONR ; Blitter still busy?
bne.s 3$ ; If yes, wait a bit
move.w #$01FF,custom.DMACON ; Disable all DMA
move.w #$3FFF,custom.INTENA ; Disable all interrupts
move.l S_VBR,a0 ; A0 = Pointer to vector base
move.l custom.cpuVBR,a0 ; A0 = Pointer to vector base
move.l IRQ1(a0),-(sp) ; Store IRQ1 vector
move.l IRQ3(a0),-(sp) ; Store IRQ3 vector
move.l IRQ4(a0),-(sp) ; Store IRQ4 vector
bra.s _skip
_S_GetVBR: dc.l $4E7A0801 ; MOVEC VBR,d0 - privileged instruction
rte ; Return from supervisor mode
rte
_skip:
SECTION .bss,bss
S_VBR: ds.l 1
S_PAL: ds.w 1
SECTION .text,code
}}
}
pointer @shared cpuVBR ; after calling grab_system: contains CPU VBR register
bool @shared isPAL ; after calling grab_system: is true when system is PAL, or false if NTSC TODO doesn't work yet
asmsub waitvsync() {
%asm {{
; synchronize to the screen's vsync, this routine doesn't require the OS to be active (sys.waitvsync is the OS-compatible version of this routine)
@@ -109,7 +109,7 @@ S_PAL: ds.w 1
move.w #$01FF,custom.DMACON ; Disable all DMA
move.w #$3FFF,custom.INTENA ; Disable all interrupts
move.l S_VBR,a0 ; A0 = Pointer to vector base
move.l custom.cpuVBR,a0 ; A0 = Pointer to vector base
move.l (sp)+,IRQ4(a0) ; Restore IRQ4 vector
move.l (sp)+,IRQ3(a0) ; Restore IRQ3 vector
move.l (sp)+,IRQ1(a0) ; Restore IRQ1 vector
@@ -1,11 +1,10 @@
Prog8 compiler v14.0-SNAPSHOT by Irmen de Jong (irmen@razorvine.net)
Prerelease version from git commit d870c39a in branch new-codegens
Prerelease version from git commit df659d28 in branch new-codegens
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
Compiling program import-all-amiga500.p8
Compiler target: amiga500
WARN file:///home/irmen/Projects/prog8/docs/import-all-amiga500.p8:1:54: floating-point code requires a 68020 or better CPU with an 68881/68882 FPU; assembler options set to -m68020 -m68881. This program will NOT run on an Amiga 500/600 or 1200 without fp coprocessor!
BUILTIN FUNCTIONS
@@ -265,6 +264,7 @@ blitter {
line_draw (uword x1 @D0, uword y1 @D1, uword x2 @D2, uword y2 @D3, uword screen_mod @D4, long screen_ptr @A1) clobbers (D0,D1,D2,D3,D5,D6,A0,A1)
line_init (uword mask @D0, uword pixel @D1, uword pattern @D2, uword screen_mod @D4) clobbers (D0,D1,D2,D4,A0,A1)
masked_blit (long src @A0, long mask @A1, long dst @A2, uword width_words @D0, uword height @D1, uword src_mod @D2, uword mask_mod @D3, uword dst_mod @D4) clobbers (D0,D1,D2,D3,D4,A0,A1,A2)
set_blitpri (bool hasprio @D0) clobbers (D0)
wait ()
}
@@ -775,6 +775,10 @@ custom {
&uword VSSTOP
&uword VSSTRT
&uword VTOTAL
long @shared cpuVBR
bool @shared isPAL
filter_off ()
filter_on ()
grab_system ()
left_button () -> bool
middle_button () -> bool
@@ -4250,7 +4254,6 @@ LIBRARY MODULE NAME: syslib
---------------------------
sys {
^^ubyte @shared arguments
long @shared DOSBase
long @shared GfxBase
long @shared IFFParseBase
@@ -4277,6 +4280,7 @@ sys {
const ubyte SIZEOF_WORD
long @shared TimerBase
long @shared UtilityBase
^^ubyte @shared arguments
const ubyte target
clear_carry ()
cpuAtLeast68020 () clobbers (D0,A6) -> bool @Pz
-687
View File
@@ -1,687 +0,0 @@
# 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:
```kotlin
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 a `PtProgram` AST.
## 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 `%zeropage` directive (`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)
- `dirty` variables (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:
- `dirty` variables (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_NOCLEAR` and `BSS_SLABS` are emitted at the non-relocated location
- `prog8_program_end` is emitted before the relocated sections
- An `* = relocatedStart` directive switches to the relocated area
- `BSS` is emitted in the relocated area with an overflow check
- A `PROG8_VARSHIGH_RAMBANK` equate 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 `BSS` section (zeroed at startup)
- Instances **with** initial values go into the `STRUCTINSTANCES` section
(initialized data, inline with the program)
- Struct type definitions (`IRStStructDef` from the IR symbol table) are emitted
as 64tass `.struct` blocks with named fields and parameterized constructors,
enabling `.dstruct` directives 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 element
- `variable_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 exit
- `repeat_N`, `repeatend_N` -- repeat-loop body and exit
- `for_modified_N` -- self-modifying comparison values
- `if_else_N`, `if_end_N` -- conditional branches
- `short_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.output` and `options.launcher`:
- `PRG` with `BASIC` launcher: generates a BASIC SYS line with
`prog8_entrypoint` label, `.word` / `.null` directives for the
BASIC stub
- `PRG` with `NONE` launcher: raw header with load address
- `RAW`: raw header with load address
- `XEX`: Atari XEX header with load address
- `LIBRARY`: minimal header with `jmp main.start`
- Emit `prog8_program_start` label at the beginning of the program
- Emit startup sequence (`emitStartupSequence`):
- `cld` instruction
- Save stack pointer for `sys.exit()` (`tsx` / `stx prog8_lib.orig_stackpointer`)
- Call `init_system` and `init_system_phase2` (unless `noSysInit`)
- Call `prog8_lib.program_startup_clear_bss` to zero the BSS section
- Call `run_global_inits` for block-level variable initializers
- Call the user's `main.start` subroutine
- On return, jump to `cleanup_at_exit` which calls `sys.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_end` label 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 the `progend()` library routine and
memory layout calculations (e.g., `MEMORY TOP`).
- A **memtop overflow check** is emitted after `prog8_program_end` using
64tass's `.cerror` directive:
`.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 `jsr` call
- 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 standard `jsr` targets
with a `rts` return
- **External assembly subroutines (`PtAsmSub`)**: May have explicit register
parameter specifications; optional `inline` flag causes the body to be
emitted directly at the call site instead of via `jsr`
- **Banked calls (`JSRFAR`)**: Subroutine address and bank are embedded
as data words/bytes after the `jsr` instruction
## 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 for `repeat 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:**
- `Zeropage` or `Fast RAM` concept -- 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., `STZ` on 65C02, `MUL` on 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 with `phy`/`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),y` with `ldy #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 addr` or `ora`
- 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` -> remove `cmp` (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 `subType` references in the AST must be updated to
point to entries in the new symbol table.
- The `SymbolTable`'s cached `flat` property 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:
- `PRG` with `BASIC` launcher: emits a BASIC SYS stub with a
`prog8_entrypoint` label, `.word` / `.null` directives
- `PRG` with `NONE` launcher: raw header at load address
- `RAW`: raw binary at load address
- `XEX`: Atari XEX format header
- `LIBRARY`: minimal `jmp main.start` header
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 the `BSS` section.
- **Dirty** (`dirty == true`): cleared at program startup but NOT at each
subroutine entry. These go into `BSS_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.
@@ -1,91 +0,0 @@
# INITGLOBALS Chunk and ZP String/Array Initialization in the IR
## The Problem
The IR codegen pipeline never emitted runtime init instructions for non-BSS variables
(such as zero-page variables) that have string or array initialization values.
The existing mechanisms only handled numeric init values:
- **Non-romable mode**: Numeric init values are pulled into the symbol table
(`changeGlobalVarInits`) and the corresponding AST assignment is removed.
String/array init values remain as AST assignments at the block level, BUT
these are NOT separate `PtAssignment` nodes -- they are embedded in the
variable declaration itself. So the loop at `IRCodeGen.kt:35-42`, which
collects block-level `PtAssignment` nodes and translates them into
`globalInits` IR instructions, finds nothing for string/array init values.
- **Romable mode**: BSS variables with numeric init values get `LOAD`+`STOREM`
instructions in `globalInits`. BSS variables with string/array init values
are left as readonly inline data (no init instructions). Non-BSS variables
(including ZP) are not processed at all.
The Virtual Machine works around this by reading init values directly from the
`<INIT>` section variable declarations during `varsToMemory()`. For a future
6502 backend built on IR, no such mechanism would exist -- zero-page variables
with string/array init values would silently have zeroed/bogus data at runtime.
## Affected Variables
Variables that are:
- NOT in BSS (`inBss == false`)
- Have a ZP wish (`zpwish == PREFER_ZEROPAGE` or `REQUIRE_ZEROPAGE`)
- Have a non-numeric init value (`IRVariableInitializer.Str` or `.Array`)
- Are NOT split word arrays (those are handled as separate `_lsb`/`_msb` vars)
These are typically block-level variables declared with the `@zp` tag and an
initialization value such as:
```prog8
str @zp msg = "hello from zp"
ubyte[5] @zp counts = [10, 20, 30, 40, 50]
uword[3] @zp values = [1000, 2000, 3000]
```
## Current Fix
The `generateStringArrayInits()` method in `IRCodeGen.kt` iterates over all
IR symbol table variables matching the criteria above. For each one it:
1. Creates a non-ZP "shadow" variable named `<original>_init_value` containing
the raw init bytes as a `ubyte[N]` array in the `<INIT>` section.
2. Clears the original ZP variable's init value so it moves to the
`<NOINIT>` section and is zeroed at startup.
3. Emits a `SYSCALL MEMCOPY` into the `INITGLOBALS` chunk to copy the shadow
data into the ZP variable at runtime.
For example, a 14-byte ZP string `"hello from zp"` generates:
```asm
load.w r1,main.msg_init_value
load.w r2,main.msg
load.w r3,#14
syscall $1019(r1.w,r2.w,r3.w)
```
This mirrors the approach used in the 6502 code generator
(`ProgramAndVarsGen.kt:628-682`), where shadow `_init_value` data arrays are
copied into zero-page variables at program startup.
### Instruction Count
For a ZP array of N bytes, this generates 4 instructions (3 `LOAD.w` + 1
`SYSCALL`). Since all ZP variables combined can occupy at most ~200 bytes, the
worst-case overhead is ~8 instructions. Typical programs add 4-20 instructions,
a large reduction from the previous per-byte approach.
## Implementation Notes
- The shadow variable is added to the IR symbol table during code generation,
before the IR program is returned. This ensures it is present when the VM's
`VmVariableAllocator` builds its address map.
- The original variable's init value is cleared so a future 6502 backend can
place it in zero-page RAM rather than treating it as ROM-initialized data.
- `IRUnusedCodeRemover.removeBlockInits()` had to be fixed so that its
"stray load" cleanup computes register uses from `fcallArgs` as well as
from `reg1`/`reg2`. Without this fix, the `LOAD.w` instructions feeding the
`SYSCALL MEMCOPY` were incorrectly identified as dead and removed, taking
the referenced variables with them.
## Future Work
- Consider adding a `zpAddress` field to `IRStStaticVariable` (see
`zp-ir-issue.md`) so that ZP allocation is explicit in the IR and the
backend doesn't need to re-derive it from the variable name.
-131
View File
@@ -1,131 +0,0 @@
# Zero-Page Variable Initialization in IR-based Code Generation
## The Problem
Zero-page variables (allocated to addresses $02-$FF for fast access) that have
compile-time initial values (strings, arrays) need special handling at program
startup. The old 6502 code generator handles this, but the gap between what
the IR provides and what the code generator needs makes this hard to implement
in an IR-based backend.
## How the Old 6502 Codegen Handles It
The old codegen (`ProgramAndVarsGen.kt`) has an `entrypointInitialization()`
method inside the `main.start` subroutine that:
1. **Determines which ZP variables have init values** by querying the
`VariableAllocator.zeropageVars` map -- the actual allocation result from
the ZP allocator that runs during code generation.
2. **Emits runtime copy code** for each such variable:
- Stores the initial value data at a ``_init_value`` label (in ROM/data section)
- Generates code to copy from that ROM label into the ZP address at startup
- Uses `strcpy` for strings, `sys.memcopy` for arrays
3. **Example emitted code:**
```asm
; zp str and array initializations
lda #<varname
ldy #>varname
sta P8ZP_SCRATCH_W1
sty P8ZP_SCRATCH_W1+1
lda #<varname_init_value
ldy #>varname_init_value
jsr prog8_lib.strcpy
```
## What the IR Provides
The IR symbol table has `IRStStaticVariable` with these relevant fields:
- `zpwish: ZeropageWish` -- the programmer's intent (REQUIRE, PREFER, DONTCARE,
NOT_IN_ZEROPAGE), written at compile time
- `initializationValue: IRVariableInitializer?` -- the init value (Str, Array,
or Numeric)
- `inBss: Boolean` -- whether the variable is in BSS (zero-page vars are NOT
in BSS)
- `dirty: Boolean` -- whether the variable needs re-initialization
Notably **missing** from the IR:
- **No `isZp` or `zpAddress` field** -- the actual ZP allocation decision is not
recorded. The IR only knows the programmer's *wish*, not the final allocation.
- **No relationship between ZP status and initialization value** -- the IR
doesn't express "this ZP variable needs a runtime init copy from ROM".
## Why This Is Hard
### 1. ZP Allocation is a Backend Responsibility
In the old codegen, `VariableAllocator` makes the final ZP allocation decision
at assembly-generation time. It scores variables by usage frequency, type size,
and loop depth, then assigns actual ZP addresses. This allocator runs inside
the 6502 backend, not in the shared IR generation pipeline.
An IR-based code generator would need to either:
- **Reimplement ZP allocation** (duplicating the allocator logic per backend)
- **Have the IR pipeline perform ZP allocation** and store results in the IR
symbol table (adding `zpAddress` etc.)
### 2. The Init Copy Code is Generated by the Backend
The `jsr prog8_lib.strcpy` / `jsr sys.memcopy` approach is target-specific
and uses ZP scratch registers (P8ZP_SCRATCH_W1). This code is emitted by the
backend inside `main.start`, not by the IR pipeline. The IR `globalInits`
chunk only handles numeric init values (converted to LOAD+STOREM instructions
in `changeGlobalVarInits()`).
### 3. No Precedent in the IR Pipeline
Looking at `IRCodeGen.kt` lines 111-139 (`generateRomableInits`):
```kotlin
is IRVariableInitializer.Str, is IRVariableInitializer.Array -> {
// Strings and arrays: keep inline data but now marked readonly.
// They can serve as ROM-based read-only data.
}
```
For romable mode, strings/arrays with init values are left as inline data
(readonly). Non-romable mode doesn't handle them at all -- they stay as
assignment statements in the AST which become regular instructions.
Neither path encodes the ZP-copy-init pattern into the IR.
## Possible Solutions
### Option A: Add ZP allocation info to the IR
Add fields to `IRStStaticVariable`:
```kotlin
val zpAddress: UInt? = null // set if allocated in zero-page
```
The IR pipeline would need to run ZP allocation (or the IR file would need
to record the allocation from the earlier AST-based allocator). The backend
can then check `zpAddress != null && initializationValue != null` to emit
the copy code.
### Option B: Emit the copy pattern as IR instructions
Have the IR codegen emit LOAD+STOREM or a new `COPY` instruction for each
ZP string/array init, using temporary registers and scratch ZP addresses.
This would make the copy visible in the IR and any backend can translate it.
### Option C: Leave it to each backend
Each backend reimplements ZP allocation and emits the copy pattern.
Simple but duplicates logic across backends.
### Option D: Record the init as a data section entry
Instead of runtime copy, store the initialized ZP variables' values in a
special data section with metadata (target ZP address, size). The backend
emits a startup copy loop for all entries in this section. This is more
generic but less efficient.
## Current Status
The new6502gen code generator does NOT handle ZP string/array init. All
zero-page scratch registers are hardcoded to fixed addresses, and no ZP
allocation scoring is implemented. Any program using `%zeropage` with
initialized strings/arrays in ZP will get incorrect behavior.
+1 -2
View File
@@ -7,14 +7,13 @@ TODO
Future Things and Ideas
^^^^^^^^^^^^^^^^^^^^^^^
- DataType.ARRAY_POINTER depends on the compilation target to be either a split word array or not. This is horrible because now we have to check with the compilation target everywhere to see if a DataType enumeration value is split word array, and PtVariable and PtArrayIndexer need an explicit boolean to tell us if this is the case
- DataType.ARRAY_POINTER depends on the compilation target to be either a split word array or not. This is horrible because now we have to check with the compilation target everywhere to see if a DataType enumeration value is split word array, and PtVariable and PtArrayIndexer need an explicit boolean to tell us if this is the case. See ideas/remove_array_pointer_plan.md for the plan.
- m68k codegen: make use of scaling factors in the indexed instructions on 68020+ ? see ideas/scaled-indexing-IR.md
- support arrays-of-structs, see ideas/arrays-of-structs.md
- split up AssignmentAsmGen.kt in codeGenCpu6502 it is by far the largest file 6000+ lines
- make enums strongly typed instead of just syntactic sugar for ints (see ideas/enum-strong-type.md for the plan)
- symboldump: some sort of javadocs generated from the p8 source files (instead of just the function signatures). Use markdown for formatting, not html.
- if implementing unsigned longs: remove the (multiple?) "TODO "hack" to allow unsigned long constants to be used as values for signed longs, without needing a cast
- struct/ptr: implement the remaining TODOs in PointerAssignmentsGen (see ``ideas/ptr-assignment-todo.md`` for the severity breakdown).
- struct/ptr: really fixing the pointer dereferencing issues (cursed hybrid between IdentifierReference, PtrDereferece and PtrIndexedDereference) may require getting rid of scoped identifiers altogether and treat '.' as a "scope or pointer following operator"
- struct/ptr: (later, nasty parser problem:) support chaining pointer dereference on function calls that return a pointer. (type checking now fails on stuff like func().field and func().next.field)
- add documentation for more library modules instead of just linking to the source code
-1
View File
@@ -21,7 +21,6 @@ main {
pointer back_bitplane = bitplane2
setup_copper(current_bitplane, copper_list)
blitter.set_blitpri(true) ; make blitter take all dma cycles
const ubyte TEXT_Y = 240
; Copy the generated monochrome message into the reserved bottom strip of BOTH bitplanes.
File renamed without changes.
-131
View File
@@ -1,131 +0,0 @@
# Pointer Assignment TODOs
`codeGenCpu6502/src/prog8/codegen/cpu6502/assignment/PointerAssignmentsGen.kt`
contains many active `TODO()` calls. These are generally compiler crash paths,
not runtime fallbacks: valid source code reaching one of them fails during 6502
code generation with `NotImplementedError`.
## Highest Impact
### Pointer dereference assignment helpers
- Line 47: assign a float from AY to a pointer dereference.
- Lines 813-850: assign float, byte, word, and long values to indexed pointer dereferences.
These helpers are still explicit `TODO()` paths, but representative source
cases such as `pointers[idx]^^ = value` currently compile successfully for the
C64 and CX16 targets. Existing pointer tests also cover byte, word, long, and
float indexed assignments. The actual reachable source construct for these
helpers has not been identified, so their severity is currently unverified
rather than a confirmed compiler crash.
### Pointer-array index scaling
- Lines 503, 520, 592, 610, 709, and 734: multiply an index by the element
size for pointer-indexed byte, word, long, or struct arrays.
Representative variable-index assignments for byte, word, and long pointees
compile successfully for C64 and CX16 with and without optimization. The
reachable source construct for these helpers has not been identified, so their
severity is currently unverified rather than a confirmed compiler crash.
### Pointer augmented multiplication and division
- Lines 390 and 399: byte `*=` and `/=` through pointers.
- Lines 393 and 402: long `*=` and `/=` through pointers.
- Line 410: byte `%=` through a pointer.
Representative direct pointer dereference operations for byte and long values,
including `*=`, `/=`, and byte `%=`, compile successfully for C64 and CX16 with
and without optimization. The reachable source construct for these helpers has
not been identified, so their severity is currently unverified rather than a
confirmed compiler crash.
### Pointer augmented float operations
- Lines 2005-2007: variable, expression, and register operands for float `+=`
and `*=`.
- Lines 2258-2260: variable, expression, and register operands for float `-=`
and `/=`.
Representative direct pointer dereferences using shared variable operands for
`+=`, `*=`, `-=`, and `/=` compile successfully for C64 and CX16 with and
without optimization. The reachable source construct for these helpers has
not been identified, so their severity is currently unverified rather than a
confirmed compiler crash.
## Medium-High Impact
### Pointer augmented operations with register operands
- Line 2056: word subtraction from a register.
- Lines 2348 and 2397: byte addition and subtraction from a register.
- Lines 2440, 2484, 2527, 2571, 2614, and 2658: byte and word XOR, OR, and
AND from a register.
The corresponding literal, variable, or expression paths are implemented in
many cases. Severity: medium-high, compiler crash for register-source paths.
### Long pointer augmented operations
- Lines 1849 and 1919: unsupported source kinds for long add and subtract.
- Lines 2729, 2800, and 2871: long AND, OR, and XOR from a register.
Severity: medium-high, compiler crash for the affected source forms.
### Struct and large-offset dereferences
- Line 1079: read through an address-of dereference when the final offset is
greater than 255.
- Line 1100: obtain a struct pointer from a pointer dereference before field
access.
Small offsets and simpler pointer chains work. Severity: high for large
structures or nested pointer/struct access.
### Pointer comparisons
- Lines 942 and 1065: byte pointer comparisons using `<`, `<=`, `>`, and `>=`.
Equality and inequality are implemented. Severity: medium, compiler crash for
relational comparisons.
### Signed byte pointer shifts
- Line 1543: signed byte right shift through a pointer.
Unsigned shifts and other shift widths have implementations. Severity: medium,
compiler crash for signed `>>=` cases.
## Lower Impact or Non-Functional TODOs
- Lines 2943 and 2978: save and restore combined long register pairs on the
stack. This is likely an internal register-allocation path and is less likely
to be reached by ordinary source code, but still crashes if selected.
- Line 472: comment that the zeropage scratch-variable detection is not robust.
This is a code-quality concern, not an unimplemented operation.
- Line 1122: comment about avoiding an unnecessary zeropage scratch register.
This is an optimization opportunity, not a correctness issue.
- Lines 412-413: commented-out float and long modulo TODOs. They are inactive
and have no current effect.
## Overall Assessment
The file contains many explicit `TODO()` calls, so any genuinely reachable
path would fail during compilation rather than silently generating incorrect
machine code. However, representative tests for indexed pointer assignments,
pointer-array index scaling, pointer byte/long multiplication and division, and
pointer float augmented operations all compile successfully for C64 and CX16,
with and without optimization. Those entries are therefore currently latent or
unreachable from the tested source forms, not confirmed user-facing crashes.
The remaining struct, large-offset, comparison, register-source, and stack
paths still need targeted reachability tests before their severity can be
assessed reliably.
Recommended implementation order:
1. Identify reachable source constructs for the remaining TODOs.
2. Add regression tests for confirmed failures.
3. Implement confirmed pointer and struct gaps.
4. Reassess or remove TODOs that remain unreachable.
+252
View File
@@ -0,0 +1,252 @@
# Implementation Plan: Remove `ARRAY_POINTER` from Type System
## Overview
**Goal**: Remove the target-dependent `ARRAY_POINTER` type and replace it with `ARRAY` using a pointer element type, making the type system target-independent.
**Current Problem**: `ARRAY_POINTER` has different memory layouts on different targets (split word on 6502, regular long on m68k/virtual), requiring target checks throughout the codebase. This is described as "horrible" in `todo.rst`.
**Solution**: Use `ARRAY(sub=POINTER, ...)` with a new field to store the pointee base type, making the layout decision explicit at allocation/codegen time.
**Decisions**:
1. Remove `ARRAY_POINTER` entirely (no deprecated alias)
2. No backward compatibility for old `.p8ir` files
3. Implement incrementally, phase by phase with verification
---
## New Type Representation
### Current
```kotlin
DataType(ARRAY_POINTER, sub=UBYTE) // array of pointers to ubyte
DataType(ARRAY_POINTER, sub=STRUCT, subType=X) // array of pointers to struct X
```
### New
```kotlin
DataType(ARRAY, sub=POINTER, pointeeSub=UBYTE) // array of pointers to ubyte
DataType(ARRAY, sub=POINTER, pointeeSub=STRUCT, subType=X) // array of pointers to struct X
```
**Key Change**: Add `pointeeSub: BaseDataType?` field to `DataType` to store what the pointers point to.
---
## Phase 1: Core Type System Changes
**File**: `codeCore/src/prog8/code/core/DataTypes.kt`
### Changes:
1. **Remove `ARRAY_POINTER` from `BaseDataType` enum** (line 23)
2. **Add `pointeeSub: BaseDataType?` field to `DataType`** (after line 196)
3. **Update `DataType` constructor and `copy()` method** to include new field
4. **Remove `arrayOfPointersTo()` factory methods** (lines 218-222) - replace with new versions that create `ARRAY` with `POINTER` subtype
5. **Update `elementType()`** (line 252-259):
- For `ARRAY` with `sub=POINTER`: return `DataType(POINTER, sub=pointeeSub, subType=subType)`
6. **Update `isSplitWordArray()`** (line 476-482):
- Check `sub==POINTER && memsizer.POINTER_MEM_SIZE<=2u` instead of `base==ARRAY_POINTER`
7. **Remove `isPointerArray` alias from `BaseDataType`** (line 93)
8. **Add `DataType.isPointerArray` extension property**: `isArray && sub==POINTER`
9. **Update `isAssignableTo()`** (lines 396-409):
- Replace `ARRAY_POINTER` cases with `ARRAY` + `sub=POINTER` checks
10. **Update `toString()` and `sourceString()`** (lines 323-364):
- Handle `ARRAY` with `sub=POINTER` to print as `^^subtype[]`
### Verification:
- Compile `codeCore` module: `gradle :codeCore:compileKotlin --console=plain`
- Run `codeCore` tests: `gradle :codeCore:test --console=plain`
---
## Phase 2: Type Creation
### File: `compilerAst/src/prog8/ast/antlr/Antlr2KotlinVisitor.kt`
**Line 232-233**: Update parser to create `ARRAY` with `POINTER` subtype instead of `ARRAY_POINTER`
### File: `compilerAst/src/prog8/ast/expressions/AstExpressions.kt`
**Lines 1284-1292**: Update array literal type inference to create `ARRAY` with `POINTER` subtype
### Verification:
- Compile `compilerAst` module: `gradle :compilerAst:compileKotlin --console=plain`
---
## Phase 3: Type Checking and Validation
### File: `compiler/src/prog8/compiler/astprocessing/AstChecker.kt`
**8 references** (lines 306, 992, 1050, 1146, 1258, 1525, 1688, 2540):
- Replace `isPointerArray` checks with `datatype.isPointerArray` (using new extension property)
- Or inline as `datatype.isArray && datatype.sub==POINTER`
### File: `compiler/src/prog8/compiler/astprocessing/ImplicitForIteratorDecls.kt`
**Lines 61-62**: Update for-loop iterator type checking
### Verification:
- Compile `compiler` module: `gradle :compiler:compileKotlin --console=plain`
---
## Phase 4: AST Transformations
### File: `compiler/src/prog8/compiler/astprocessing/CodeDesugarer.kt`
**Lines 132, 1015**: Update prefix operator and struct dereference checks
### File: `compiler/src/prog8/compiler/astprocessing/VariousCleanups.kt`
**Lines 34, 96, 145-152**: Update type compatibility and `@nosplit` handling
### File: `compiler/src/prog8/compiler/astprocessing/LiteralsToAutoVarsAndRecombineIdentifiers.kt`
**Line 88**: Update struct field resolution for pointer arrays
### Verification:
- Compile `compiler` module: `gradle :compiler:compileKotlin --console=plain`
---
## Phase 5: Memory Sizing
### File: `codeCore/src/prog8/code/target/NormalMemSizer.kt`
**Lines 13-14**: Update memory size calculation for `ARRAY` with `POINTER` subtype
### File: `codeCore/testFixtures/kotlin/prog8tests/helpers/Dummies.kt`
**Lines 15-16**: Update test memory sizer
### File: `compiler/test/helpers/Dummies.kt`
**Lines 38-39**: Update compiler test memory sizer
### Verification:
- Compile and test `codeCore`: `gradle :codeCore:test --console=plain`
---
## Phase 6: IR Handling
### File: `intermediate/src/prog8/intermediate/Utils.kt`
**Lines 18-22**: Update IR type string serialization
- Change `^^` prefix format to regular array format with pointer subtype
### File: `intermediate/src/prog8/intermediate/IRFileReader.kt`
**Lines 601-611**: Update IR file deserialization
- Parse new format and create `ARRAY` with `POINTER` subtype
### Verification:
- Compile `intermediate` module: `gradle :intermediate:compileKotlin --console=plain`
- Run `intermediate` tests if any
---
## Phase 7: Code Generation
### File: `codeGenCpu6502/src/prog8/codegen/cpu6502/ProgramAndVarsGen.kt`
**Lines 831-832**: Update uninitialized pointer array emission for 6502
### File: `codeGenIntermediate/src/prog8/codegen/intermediate/ExpressionGen.kt`
**Lines 908-910**: Update typecast to pointer array (currently TODO)
### File: `codeGenIntermediate/src/prog8/codegen/intermediate/IRCodeGen.kt`
**Line 752**: Update for-loop iteration to use new `isSplitWordArray` logic
### Verification:
- Compile codegen modules: `gradle :codeGenCpu6502:compileKotlin :codeGenIntermediate:compileKotlin --console=plain`
---
## Phase 8: VM Updates
### File: `virtualmachine/src/prog8/vm/VmProgramLoader.kt`
**Lines 245-247, 447-456**: Update variable zeroing and array initialization
### Verification:
- Compile `virtualmachine` module: `gradle :virtualmachine:compileKotlin --console=plain`
---
## Phase 9: Builtin Functions
### File: `codeCore/src/prog8/code/core/BuiltinFunctions.kt`
**Line 27**: Update `IterableDatatypes` array (remove `ARRAY_POINTER`)
---
## Phase 10: Tests and Documentation
### Test Files:
- `codeCore/test/prog8tests/codecore/TestDataType.kt` (lines 96, 110-112)
- `compiler/test/ast/TestVariousCompilerAst.kt` (line 1557)
### Documentation:
- `docs/source/todo.rst` (line 10): Remove TODO item
- `vm32.md` (lines 133-142): Update dev notes
### Verification:
- Run all tests: `gradle build --console=plain`
---
## Verification Strategy
After each phase:
1. Compile the affected module(s)
2. Run module-specific tests
3. After all phases: run full build with `gradle build --console=plain`
### Manual Testing:
Create test programs with pointer arrays:
```prog8
%zeropage basicsafe
%option no_sysinit
main {
^ubyte[3] ptrs
^structDef[2] structPtrs
sub start() {
; test pointer array operations
}
}
```
Compile for multiple targets:
- `prog8c -target cx16 test.p8`
- `prog8c -target virtual test.p8`
- `prog8c -target c64 test.p8`
---
## Risk Mitigation
1. **Incremental implementation**: Complete and verify each phase before moving to next
2. **Type safety**: Ensure `pointeeSub` field is properly propagated through all type operations
3. **Memory layout**: Verify split-word logic works correctly on all targets
---
## Estimated Effort
- **Phase 1-2**: 2-3 hours (core type system)
- **Phase 3-4**: 2-3 hours (validation and transforms)
- **Phase 5-6**: 1-2 hours (memory and IR)
- **Phase 7-8**: 2-3 hours (codegen and VM)
- **Phase 9-10**: 1-2 hours (tests and docs)
- **Total**: 8-13 hours
---
## Files Affected Summary
**Total**: ~18 Kotlin source files + 2 documentation files, ~60 reference points
### By Category:
| Category | Files | References |
|----------|-------|------------|
| Type system | DataTypes.kt | 15+ |
| AST validation | AstChecker.kt | 8 |
| AST transforms | CodeDesugarer.kt, VariousCleanups.kt, LiteralsToAutoVars.kt | 7 |
| IR handling | Utils.kt, IRFileReader.kt | 10 |
| Code generation | ProgramAndVarsGen.kt, ExpressionGen.kt, IRCodeGen.kt | 3 |
| VM | VmProgramLoader.kt | 2 |
| Memory sizing | NormalMemSizer.kt, Dummies.kt (2 files) | 3 |
| Type creation | Antlr2KotlinVisitor.kt, AstExpressions.kt | 2 |
| Builtin functions | BuiltinFunctions.kt | 1 |
| Tests | TestDataType.kt, TestVariousCompilerAst.kt | 3 |
| Documentation | todo.rst, vm32.md | 2 |