Skip to content

Latest commit

 

History

History
512 lines (419 loc) · 31.1 KB

File metadata and controls

512 lines (419 loc) · 31.1 KB

cc45 — Mega65 C Compiler

cc45 is a C compiler designed for the MEGA65 (45GS02). It translates a subset of the C language into optimized assembly code.

Features

  • Function Declarations: Full support for function definitions with parameters.
  • Local Variables: Support for local variables allocated on the stack.
  • Structures & Unions: Full support for struct and union definitions, members, and dot/arrow operators.
  • Anonymous Aggregates: Support for nested structures and unions without names, allowing direct member access from the parent scope.
  • Control Flow: if/else statements, while loops, and for loops.
  • Increment/Decrement: Optimized ++ and -- (prefix and postfix) using simulated opcodes.
  • Nested Expressions: Robust handling of complex arithmetic expressions and nested function calls.
  • String Pooling: Automatic identification and pooling of string literals into a data section using .text (PETSCII).
  • Procedure System: Translates C functions into PROC blocks for ca45, leveraging high-level procedure syntax.
  • Volatile Support: Full support for the volatile keyword to prevent optimization of hardware-mapped or interrupt-modified variables.
  • Static Assertions: Support for _Static_assert to perform compile-time validation of constant expressions.
  • Function Specifiers: Support for _Noreturn to indicate that a function does not return to its caller, enabling optimizations to skip return instructions.
  • Alignment: Support for C11 _Alignas and _Alignof to manage data alignment for global variables and struct members.
  • Generic Selections: Support for C11 _Generic expressions for compile-time type-based dispatch.
  • Arrays: Native support for one-dimensional arrays (type name[size]) with subscript indexing and automatic pointer decay.
  • Ternary Operator: Support for conditional expressions (cond ? then : else).
  • Bitwise Operators: Verified support for &, |, ^, <<, and >> with constant folding.
  • Modulo Operator: Support for % operator for integer remainder.
  • Compound Assignment: Support for +=, -=, *=, /=, %=, &=, |=, ^=, <<=, and >>=.
  • for Loop Declarations: Support for C99-style variable declarations in the for loop initializer.
  • Type Definitions: Support for typedef to create type aliases.
  • Enumerations: Support for enum types and constants.
  • Explicit Casts: Support for C-style cast expressions (type)expr for explicit type conversions, including narrowing, widening, and pointer casts.
  • Implicit Narrowing Warnings: Compile-time warnings when implicit conversions lose data (e.g., int to char). Explicit casts suppress the warning.

Language Syntax (BNF)

Statements

The compiler supports a variety of C statements for program flow:

<statement> ::= <compound-statement>
              | <expression-statement>
              | <selection-statement>
              | <iteration-statement>
              | <jump-statement>
              | <declaration-statement>
              | <static-assert-statement>
              | <typedef-definition>
              | <enum-definition>

<compound-statement> ::= "{" <block-item-list>? "}"
<block-item-list>    ::= <block-item> <block-item-list>?
<block-item>         ::= <statement> | <declaration-statement> | <typedef-definition> | <enum-definition>

<expression-statement> ::= <expression>? ";"

<selection-statement> ::= "if" "(" <expression> ")" <statement> ( "else" <statement> )?
                        | "switch" "(" <expression> ")" <statement>
                        | "case" <expression> ":" <statement>?
                        | "default" ":" <statement>?

<iteration-statement> ::= "while" "(" <expression> ")" <statement>
                        | "do" <statement> "while" "(" <expression> ")" ";"
                        | "for" "(" ( <expression-statement> | <declaration-statement> ) <expression>? ";" <expression>? ")" <statement>

<jump-statement> ::= "return" <expression>? ";"
                   | "break" ";"
                   | "continue" ";"
                   | "continue" <expression> ";"
                   | "continue" "default" ";"
                   | "asm" "(" <string-literal> ")" ";"
                   | "__asm__" "(" <string-literal> ")" ";"

<declaration-statement> ::= ( "_Alignas" "(" ( <expression> | <type-specifier> "*"* ) ")" )? ( "volatile" )? ( "auto" )? <type-specifier> "*"* <identifier> ( "[" <integer-literal> "]" )? ( "=" <expression> )? ";"
<static-assert-statement> ::= "_Static_assert" "(" <expression> "," <string-literal> ")" ";"
<typedef-definition>    ::= "typedef" <type-specifier> "*"* <identifier> ";"
<enum-definition>       ::= "enum" <identifier>? "{" <enumerator-list> "}" ";"
<enumerator-list>       ::= <enumerator> ( "," <enumerator> )*
<enumerator>            ::= <identifier> ( "=" <integer-literal> )?
<struct-definition>     ::= ( "struct" | "union" ) <identifier>? "{" <member-list> "}" ";"
<member-list>           ::= <member-declaration> <member-list>?
<member-declaration>    ::= ( "_Alignas" "(" ( <expression> | <type-specifier> "*"* ) ")" )? <type-specifier> "*"* <identifier>? ( "[" <integer-literal> "]" )? ";"
                        | <struct-definition>
<type-specifier>        ::= "int" | "char" | "void" | ( "struct" | "union" | "enum" ) <identifier> | <identifier>

Expression Evaluation

Expressions are evaluated using standard C precedence rules:

<expression>     ::= <assignment>
<assignment>     ::= <unary> <assignment-op> <assignment> | <conditional>
<assignment-op>  ::= "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|=" | "^=" | "<<=" | ">>="
<conditional>    ::= <logical-or> ( "?" <expression> ":" <conditional> )?
<logical-or>     ::= <logical-and> ( "||" <logical-and> )*
<logical-and>    ::= <bitwise-or> ( "&&" <bitwise-or> )*
<bitwise-or>     ::= <bitwise-xor> ( "|" <bitwise-xor> )*
<bitwise-xor>    ::= <bitwise-and> ( "^" <bitwise-and> )*
<bitwise-and>    ::= <equality> ( "&" <equality> )*
<equality>       ::= <relational> ( ("==" | "!=") <relational> )*
<relational>     ::= <shift> ( ("<" | ">" | "<=" | ">=") <shift> )*
<shift>          ::= <additive> ( ("<<" | ">>") <additive> )*
<additive>       ::= <multiplicative> ( ("+" | "-") <multiplicative> )*
<multiplicative> ::= <unary> ( ("*" | "/") <unary> )*
<unary>          ::= ( "!" | "~" | "-" | "*" | "&" | "++" | "--" ) <unary>
                   | "(" <type-specifier> "*"* ")" <unary>
                   | <primary> ( "++" | "--" )
                   | <primary> "[" <expression> "]"
                   | <primary>
<primary>        ::= <identifier>
                   | <integer-literal>
                   | <string-literal>
                   | <function-call>
                   | <generic-selection>
                   | "_Alignof" "(" <type-specifier> "*"* ")"
                   | <primary> "." <identifier>
                   | <primary> "->" <identifier>
                   | "(" <expression> ")"

                   <generic-selection> ::= "_Generic" "(" <expression> "," <generic-assoc-list> ")"
                   <generic-assoc-list> ::= <generic-association> ( "," <generic-association> )*
                   <generic-association> ::= ( <type-specifier> "*"* | "default" ) ":" <expression>
                   ```
<function-call> ::= <identifier> "(" <argument-list>? ")"
<argument-list> ::= <expression> ( "," <expression> )*

Statement Descriptions

Selection Statements (if, else, switch)

Used for conditional branching.

  • if / else: If the condition evaluates to a non-zero value, the first statement is executed. If an else clause is present and the condition is zero, the second statement is executed.
  • switch: Evaluates an expression and transfers control to the case label with a matching constant value.
    • case <const>: Defines a jump target for a specific value.
    • default: (Optional) Defines the jump target if no cases match.
    • Fall-through: Control flows from one case into the next unless interrupted by a break statement.
    • continue <value> / default: (Extension) Jumps to the specified case or default label within the current switch block. This can be used to re-execute logic or share code between cases without using goto.

Iteration Statements (while, do, for)

  • while: Repeatedly executes the body as long as the condition is non-zero. The condition is checked before each iteration.
  • do: Repeatedly executes the body as long as the condition is non-zero. The condition is checked after each iteration, ensuring the body executes at least once.
  • for: A flexible loop. The initializer is executed once. The condition is checked before each iteration. The increment expression is executed after each iteration of the body.

Loop Control: break and continue

cc45 supports standard C loop control statements break and continue within while, do-while, and for loops.

break Statement The break statement terminates the execution of the innermost enclosing loop. Control passes to the statement immediately following the loop body.

Example: Searching

int find_first_even(int* arr, int size) {
    int i;
    int result = -1;
    for (i = 0; i < size; i = i + 1) {
        if (arr[i] % 2 == 0) {
            result = arr[i];
            break; // Exit the for loop immediately
        }
    }
    return result;
}

continue Statement The continue statement skips the remainder of the current iteration of the innermost enclosing loop and proceeds to the next iteration.

  • In a while or do-while loop, it jumps to the condition evaluation.
  • In a for loop, it jumps to the increment expression.

Example: Filtering

int sum_odds(int limit) {
    int i;
    int sum = 0;
    for (i = 0; i < limit; i = i + 1) {
        if (i % 2 == 0) {
            continue; // Skip even numbers
        }
        sum = sum + i;
    }
    return sum;
}

Nested Loops break and continue only affect the innermost loop in which they appear.

for (i = 0; i < 10; i = i + 1) {
    while (j < 10) {
        if (condition) break; // Exits the while loop, but stays in the for loop
        j = j + 1;
    }
}

Jump Statements (return, break, continue, asm, goto)

  • return: Exits the current function and returns the value of the optional expression to the caller. On Mega65, the return value is passed via the A and X registers.
  • break / continue: Controls the flow of loops (see Loop Control for details).
  • goto <label>: Transfers control unconditionally to the specified label within the current function.
  • <label>:: Defines a jump target for goto statements. Labels are function-scoped.
  • asm / __asm__: Inserts a string of raw assembly directly into the compiler's output. Each __asm__("...") call emits one line of assembly. See Inline Assembly and Variable Access for detailed usage.

Inline Assembly and Variable Access

The __asm__("...") statement inserts a single line of assembly into the compiler's output. Each call emits exactly one assembler instruction or directive. Inline assembly can reference C variables using the compiler's naming conventions, giving direct access to parameters, locals, and globals from hand-written assembly.

Variable Naming Prefixes

The compiler mangles C variable names to avoid collisions with CPU registers and assembler keywords:

Scope Prefix Storage Addressing Example
Parameter _p_ Hardware stack Stack-relative (, s) _p_val
Local _l_ Hardware stack Stack-relative (, s) _l_count
Global _ Data segment Absolute _total

Accessing Global Variables

Global variables are stored at fixed addresses in the .data segment. Access them with absolute addressing — no , s suffix needed:

volatile int g_value = 0;

void store_to_global(int val) {
    // Load 16-bit parameter from stack, store to global
    __asm__("ldax _p_val, s");
    __asm__("stax _g_value");
}

The compiler prepends _ to the C variable name: g_value becomes _g_value in assembly. For a variable named total, the assembly name would be _total. This follows the traditional C linkage convention (single leading underscore).

8-bit globals use lda/sta instead of ldax/stax:

volatile char g_flag = 0;

void set_flag(char f) {
    __asm__("lda.sp _p_f");     // 8-bit stack-relative load
    __asm__("sta _g_flag");     // 8-bit absolute store
}

Accessing Parameters (Frame-Pointer-Relative)

Function parameters are accessed via a saved frame pointer using the 45GS02's native ($nn,SP),Y indirect addressing mode. At function entry, the proc directive saves the stack pointer as a 16-bit pointer on the stack. Parameters are then accessed through this frame pointer with fixed Y offsets — their offsets never change regardless of how many local variables are declared.

The assembler handles this transparently. For 16-bit (int, pointers) parameters, use ldax/stax:

void process(int value) {
    __asm__("ldax _p_value, s");   // Load 16-bit param into A (low) / X (high)
    // ... use AX ...
}

For 8-bit (char) parameters, use lda.sp/sta.sp:

void process_byte(char b) {
    __asm__("lda.sp _p_b");   // Load 8-bit param into A
    // ... use A ...
}

The assembler detects that _p_ symbols are frame-relative and generates LDY #<offset>; LDA ($fp,SP),Y instead of the TSX; LDA $0101+offset,X sequence used for locals.

Accessing Local Variables (Stack-Relative)

Local variables live on the stack and are accessed via SP-relative addressing (synthesized as TSX; LDA/STA $0101+offset,X). The compiler tracks their offsets via .var directives. Volatile locals are always read from and written to the stack (non-volatile locals may be optimized away):

int copy_value(int val) {
    volatile int result = 0;
    __asm__("ldax _p_val, s");       // Load parameter (frame-relative)
    __asm__("stax _l_result, s");    // Store to local (SP-relative)
    return result;                    // Compiler reads _l_result back
}

Stack Layout

When a function with parameters and locals is executing, the stack looks like this (growing downward):

        ┌──────────────────┐  ← Higher addresses
        │   Caller's frame │
        ├──────────────────┤
        │   Parameter N    │  ← _p_paramN (fixed Y offset from FP)
        │       ...        │
        │   Parameter 1    │  ← _p_param1
        ├──────────────────┤
        │  Return address  │  ← 2 bytes (pushed by JSR)
        ├──────────────────┤
        │  Saved FP (SP)   │  ← 2 bytes (pushed by proc prologue)
        ├──────────────────┤
        │   Local var 1    │  ← _l_var1 (SP-relative, offset 0)
        │       ...        │
        │   Local var M    │  ← _l_varM
        └──────────────────┘  ← SP points here

The _fp assembler variable tracks the frame pointer's position relative to SP. It starts at 1 and is automatically adjusted by .var directives as locals are pushed. Parameter offsets (Y values) are fixed at proc declaration time and never change. Local offsets remain SP-relative and shift as new locals are declared. Inline assembly uses the symbolic names with , s and the assembler resolves the correct addressing mode automatically.

Important Caveats

  1. Do not modify SP between reads. Inline push/pop instructions change SP, which invalidates all symbolic stack offsets. Load all needed parameters into registers before pushing anything:

    // WRONG: push changes SP, making _p_b offset incorrect
    __asm__("ldax _p_a, s");
    __asm__("push .ax");          // SP shifts!
    __asm__("ldax _p_b, s");     // reads wrong location
    
    // CORRECT: load both params first, then operate
    __asm__("ldax _p_a, s");
    __asm__("stax $02");          // save to ZP temp (no SP change)
    __asm__("ldax _p_b, s");     // SP unchanged, correct offset
  2. Register clobbering. After __asm__, the compiler assumes all registers are unknown (invalidateRegs()). The compiler will reload any values it needs from the stack.

  3. ZP temporaries. The compiler uses ZP addresses starting at $02 (configurable via -Dcc45.zeroPageStart) for intermediate results. Simulated opcodes also use ZP $00 as a scratch byte. Avoid relying on ZP values persisting across inline assembly boundaries.

  4. Struct members. Struct members are not emitted as individual labels. Access a member by adding its byte offset to the base variable name:

    // struct { char id; int value; } s; — id at offset 0, value at offset 1
    __asm__("lda.sp _l_s+0");    // s.id (8-bit)
    __asm__("ldax _l_s+1, s");   // s.value (16-bit)
  5. @ labels. Labels within inline assembly can use the @ prefix (e.g., @loop:) to signal internal branch targets. Unlike standard labels, @ labels do not reset the assembler's register tracking, allowing better optimization.

Compound Statements ({ ... })

Allows grouping multiple statements into a single block, which is essential for loop bodies and conditional branches.

Alignment

cc45 supports the C11 alignment keywords to manage how data is positioned in memory.

_Alignas

The _Alignas specifier can be used to request a specific alignment (in bytes) for global variables and struct members. The alignment must be a power of two.

  • Global Variables: _Alignas(16) int buffer[256]; ensures the buffer starts at an address divisible by 16. The compiler emits a .align directive to the assembler.
  • Struct Members: _Alignas on a member forces padding to be inserted before that member to satisfy the alignment requirement. The overall size of the struct is also padded to the maximum alignment of any of its members.

_Alignof

The _Alignof operator returns the alignment requirement of its operand type as a constant of type int.

int a = _Alignof(int);   // a = 2
int b = _Alignof(char);  // b = 1

Type System

  • int: 16-bit unsigned word. Arithmetic and comparisons are performed using 16-bit logic.
  • char: 8-bit unsigned byte. Promoted to 16-bit during most expression evaluations to maintain consistency.
  • struct & union: Custom aggregate types.
    • struct: Members are allocated sequentially in memory with appropriate padding for alignment.
    • union: All members share the same base address, allowing different interpretations of the same memory. The total size is the size of the largest member, padded for max alignment.
  • Anonymous Aggregates: Nested struct or union definitions without a member name. Their members are "flattened" into the parent scope, allowing direct access (e.g., s.inner_member).
  • Pointers: All pointers are 16-bit addresses. The compiler supports multiple levels of indirection (e.g., char **pp).
    • Dereferencing: Standard * operator for accessing memory.
    • Address-of: Standard & operator for obtaining the stack-relative address of a variable.
    • Arithmetic: Supports pointer addition and subtraction, scaled by the size of the pointed-to type.
  • volatile: Prevents the compiler from optimizing away reads or writes to the variable. Essential for I/O registers or variables shared with interrupt handlers.
  • auto: (Classic C) Declares a variable with automatic storage duration. In ccomp, all local variables are automatic by default, so this keyword is functionally a no-op provided for compatibility.
  • unsigned: Explicitly declares an unsigned integer type. Supported as unsigned int (16-bit), unsigned char (8-bit), or a bare unsigned (equivalent to unsigned int).
  • signed: Explicitly declares a signed integer type. Supported as signed int (16-bit) and signed char (8-bit). Signed types use two's complement representation. Relational operators (<, >, <=, >=) are signedness-aware when operating on these types.

sizeof Operator

The sizeof operator returns the size, in bytes, of its operand. The operand can be a type name (e.g., sizeof(int)) or an expression (e.g., sizeof x). The result is a constant of type int.

int a = sizeof(int);      // a = 2
int b = sizeof(char);     // b = 1
struct Point { int x, y; };
int c = sizeof(struct Point); // c = 4

Note on Padding: cc45 may insert padding in structs to satisfy alignment requirements, which will be reflected in the sizeof result.

Cast Expressions

The cast operator (type)expr performs an explicit type conversion. The compiler supports casts between all scalar types:

int big = 0x1234;
char low = (char)big;      // Narrowing: keeps low byte (0x34)
char c = 200;
int wide = (int)c;         // Widening: zero-extends to 16-bit (200)
char *p = &c;
int addr = (int)p;         // Pointer to int

Narrowing casts (int/pointer to char) discard the high byte, keeping only the low 8 bits. Widening casts (char to int/pointer) zero-extend the value to 16 bits.

Cast expressions are folded at compile time when the operand is a constant (e.g., (char)0x1FF becomes 0xFF).

Implicit Narrowing Warnings

When an implicit conversion loses data, the compiler emits a warning to stderr:

file.c:8:5: warning: implicit conversion loses data from 'int' to 'char'

This occurs when assigning an int or pointer value to a char variable without an explicit cast. Using a cast expression suppresses the warning, signalling that the truncation is intentional.

Note on Declaration Order: Currently, type qualifiers (volatile, auto) must appear before the signedness specifiers (signed, unsigned). For example, volatile signed int x; is valid, but signed volatile int x; is not yet supported.

Optimizations

  • Register Tracking: The compiler tracks the state of A, X, Y, and Z registers to eliminate redundant loads.
  • Zero Page Cache: Intermediate expression results are cached in a pool of Zero Page registers, reducing stack traffic.
  • Direct Stack Access: Access to local variables and struct members on the stack is synthesized using TSX + absolute,X indexed addressing (e.g., TSX; LDA $0103,X), since the 45GS02 has no native direct stack-relative load/store instructions.
  • Push/Pop Abstractions: Utilizes the assembler's high-level push and pop instructions for 8-bit and 16-bit register saves/restores, improving code readability.
  • Strength Reduction: Converts expensive operations into faster equivalents (e.g., x * 2 becomes x << 1, x % 8 becomes x & 7).
  • Constant Propagation: Substitutes variables with known constant values into expressions at compile time.
  • Constant Folding: Simple arithmetic expressions like 1 + 2 are evaluated at compile time.
  • Dead Variable Elimination: Removes stack allocation and initialization for local variables that are initialized with constants and not subsequently used (skips volatile variables).
  • Dead Code Removal: Detects and removes code following a return statement.
  • Segment-based Separation: Automatically organizes output into code, data, and bss segments. Initialized globals are placed in data, uninitialized globals in bss, and function bodies in code, allowing for optimized memory layout by the assembler.
  • Logical Short-circuiting: Optimizes logical && and || operators to jump directly to target labels, bypassing intermediate boolean conversions.
  • Increment Optimization: Uses INC A, INX, INW, etc., for + 1 operations.
  • Tiered Branching: Automatically selects between short (8-bit) and long (16-bit) relative branches.

Standard C Compatibility

cc45 supports a subset of the ISO C standard (C11). It is designed to be lean and efficient for the MEGA65 architecture.

Key Differences

  • Integer Sizes: int is 16-bit. char is 8-bit.
  • Signedness: All types default to unsigned. Signedness is supported via the signed keyword, which enables signed comparison logic (cmp.s16).
  • Promotion: char types are promoted to 16-bit int during expression evaluation.
  • Preprocessor: cc45 includes a fully-featured integrated C preprocessor (cp45) supporting macros, file inclusion, and conditional compilation.

Unimplemented Keywords

The following standard C keywords are not supported by cc45:

  • Storage Classes: extern, register, static.
  • Type Qualifiers: const, restrict, inline.
  • Data Types: float, double, long, short.

Current Limitations

  • Function Pointers: Not supported.
  • Stack Alignment: _Alignas is currently not supported for local (stack) variables.
  • Standard Library: Minimal. No standard C library is provided by default.

Command-Line Usage

cc45 [options] <input_file.c>

Options

  • -E: Run only the preprocessor. Output is sent to stdout by default, or to a file if -o is specified.
  • -o <file>: Specify the output assembly file name (default is out.s).
  • -c: Compile and then invoke ca45 to generate a binary object file (.bin).
  • -v: Verbose mode. Prints phase status messages (preprocessing, lexing, parsing, code generation).
  • -vv: Extra verbose mode. Includes everything from -v plus lexical token dumps and AST printing.
  • -I<path>: Add an include search path for the preprocessor.
  • -Dname=val: Define a symbol for the preprocessor/assembler or configure compiler parameters:
    • -Dcc45.zeroPageStart=$addr: Set the start of the ZP register pool (default: $02).
    • -Dcc45.zeroPageAvail=n: Set the number of ZP registers available for caching (default: 9).
  • -O0: Disable all optimizations (constant folding, constant propagation, dead variable elimination). Useful for debugging code generation issues.
  • -?: Display help and exit.

Compilation Process

  1. Preprocessing: Invokes cp45 to handle file inclusion, macro definition, and conditional compilation.
  2. Lexical Analysis: Strips comments and tokenizes the preprocessed source.
  3. Parsing: Builds an AST using recursive descent.
  4. Constant Folding: Simplifies the AST by evaluating constant expressions.
  5. Code Generation: Traverses the AST and emits high-level ca45 assembly.
    • Function arguments and local variables are managed via the stack using stack-relative addressing (offset, s).
    • The compiler uses the .var and .cleanup assembler directives to maintain correct stack offsets.

Implementation Details

Variable Naming Conventions

To prevent naming collisions between C variables and 45GS02 processor registers (A, X, Y, Z), status flags (P.C, P.Z, etc.), or internal assembler labels, cc45 uses a prefixing scheme in the generated assembly:

  • Parameters: Prefixed with _p_ (e.g., val becomes _p_val). Declared on the proc instruction line with size qualifiers (W# for 16-bit, B# for 8-bit).
  • Local Variables: Prefixed with _l_ (e.g., i becomes _l_i). Offsets are tracked via .var directives as variables are pushed onto the stack.
  • Global Variables: Prefixed with _ (e.g., total becomes _total). Stored in the .data segment at absolute addresses. This follows the traditional C linkage convention (single leading underscore), ensuring compatibility with other 6502 toolchains (cc65, etc.).
  • Functions: Prefixed with _ (e.g., main becomes _main). Emitted as proc _main and called via jsr _main.

These prefixes are applied to both variable declarations and all subsequent instructions referencing those variables. The same names are accessible from __asm__() inline assembly (see Inline Assembly and Variable Access).

Label Generation and Scoping

To ensure labels are unique across different functions and scopes, cc45 utilizes a hierarchical scoping system in the generated assembly:

  • Temporary Labels: Internal loop and branch targets are generated as L<n>.
  • Fully Qualified Names: When processed by the internal assembler or viewed in an expanded listing (Level 2), these labels are prefixed with their containing procedure or scope name, separated by colons (e.g., main:L0:).
  • Scoping: This prevents collisions between common label names used in different C functions by flattening the hierarchy into globally unique identifiers.

Major Structures

  • ASTNode / ASTVisitor: The core of the compiler's architecture. Every language construct is an ASTNode, and CodeGenerator is an ASTVisitor that traverses the tree to emit code.
  • RegState: A structure used to track the contents of the CPU registers (A, X, Y, Z). It stores whether a register's value is currently known, if it holds a specific variable and offset, or a literal value.
  • ZPReg: Manages the allocation of virtual zero-page registers used for intermediate expression results.

Register Tracking

The compiler tracks the state of all four primary 45GS02 registers to eliminate redundant LDA and LDX instructions.

  • Scope: Tracking is local to basic blocks. It is automatically invalidated (invalidateRegs()) at control flow boundaries such as labels, branches, and function calls.
  • Optimization: If a variable or constant is already present in a register, the compiler will skip the corresponding load instruction.

Processor Flag Tracking

The compiler tracks the state of the processor status register (Carry, Zero, Negative, and Overflow flags) to eliminate redundant instructions.

  • TriState Logic: Flags are tracked using a TriState (SET, CLEAR, UNKNOWN).
  • ZN Source Tracking: The compiler tracks which register (A, X, Y, or Z) was the last to affect the Zero (Z) and Negative (N) flags.
  • Optimizations:
    • Carry Flag: Redundant CLC and SEC instructions are omitted if the carry flag is already known to be in the required state (e.g., before an ADC or SBC).
    • CMP Elimination: CMP #$00 instructions are skipped in control flow statements (if, while, etc.) if the flags already reflect the state of the register holding the condition.
    • 8-bit Control Flow: Conditions using char (8-bit) types are optimized to use a single BEQ/BNE instruction, bypassing the 16-bit truthiness check used for int and pointers.
  • Invalidation: Flags are invalidated at control flow boundaries (labels, branches) and after instructions that affect flags in complex ways (like ADC, SBC, or function calls).

Adding Custom Opcodes

To add support for a new opcode or addressing mode in the compiler's output:

  1. M65Emitter: Add a new method to include/M65Emitter.hpp and implement it in src/main/M65Emitter.cpp.
  2. CodeGenerator: Call the new emitter method from the appropriate visit() function in src/main/CodeGenerator.cpp.

See Also

  • ca45 — Assembler
  • nm45 — Symbol lister for .o45 object files
  • lib.md.o45 format specification
  • ln45 — Linker design