mirror of
https://github.com/irmen/prog8.git
synced 2026-10-24 03:02:20 +00:00
Compare commits
375
Commits
v12.3.3
...
v14.0-BETA1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ba2ee9857 | ||
|
|
a025d68291 | ||
|
|
81794aded3 | ||
|
|
9621552056 | ||
|
|
5685a1f229 | ||
|
|
b014c78c94 | ||
|
|
ca729d279e | ||
|
|
3b0faa749a | ||
|
|
22a2f8cbc5 | ||
|
|
468850bc47 | ||
|
|
20374f4059 | ||
|
|
ceffa4d83d | ||
|
|
af14562e2b | ||
|
|
dbba6a94fa | ||
|
|
302e4bdf8e | ||
|
|
ccc26a0eb2 | ||
|
|
e017c541bb | ||
|
|
3d5a7795d0 | ||
|
|
af06fdfffc | ||
|
|
e811ecdbde | ||
|
|
4c65049817 | ||
|
|
a7cbc2c5bf | ||
|
|
d51ca62431 | ||
|
|
9ac60eec40 | ||
|
|
c55b16f4cb | ||
|
|
bd0457f262 | ||
|
|
ce2b298d6f | ||
|
|
cf7afaf77a | ||
|
|
54507ad9b3 | ||
|
|
619a215a87 | ||
|
|
c12e27dacc | ||
|
|
db32b81d7a | ||
|
|
687d86d36e | ||
|
|
e074fe614f | ||
|
|
bbfb09e467 | ||
|
|
bc62b2de81 | ||
|
|
58f544fe14 | ||
|
|
7dc05688bf | ||
|
|
aac7d409dc | ||
|
|
0771105694 | ||
|
|
9a461628f2 | ||
|
|
b0f1f655c3 | ||
|
|
59c41ad129 | ||
|
|
60eeaba791 | ||
|
|
30266c1b92 | ||
|
|
9d537ab3c0 | ||
|
|
1ba08ec6ec | ||
|
|
199c3e4efb | ||
|
|
a7c035f4d1 | ||
|
|
05cbe74a60 | ||
|
|
d652d7a924 | ||
|
|
9c53791ef4 | ||
|
|
420a9b7249 | ||
|
|
9bf8318b1c | ||
|
|
94822b8662 | ||
|
|
b0d50c2a68 | ||
|
|
deae2e6422 | ||
|
|
c8d29e6dc9 | ||
|
|
a7b14c2931 | ||
|
|
a14a8f4e62 | ||
|
|
cf50372515 | ||
|
|
da0df8f133 | ||
|
|
27070f31e8 | ||
|
|
277e9c09bb | ||
|
|
70521ce301 | ||
|
|
93b5faa038 | ||
|
|
f33814a594 | ||
|
|
986ae35821 | ||
|
|
937b520838 | ||
|
|
250ddda11b | ||
|
|
bde52cc134 | ||
|
|
8e739febf0 | ||
|
|
3fcb4b4262 | ||
|
|
56643f43df | ||
|
|
a9cad9b51d | ||
|
|
8647b136bd | ||
|
|
4179f2c571 | ||
|
|
02a118ec0e | ||
|
|
561f31329d | ||
|
|
c3920de251 | ||
|
|
14bf07907b | ||
|
|
c657935f10 | ||
|
|
c08def79ae | ||
|
|
4199ab4200 | ||
|
|
fe6850816c | ||
|
|
eff1a9fc53 | ||
|
|
5857382242 | ||
|
|
dfda43e747 | ||
|
|
1029929111 | ||
|
|
ae55109a18 | ||
|
|
2ce7a63c1e | ||
|
|
da6934ea33 | ||
|
|
2c8ccb706e | ||
|
|
0d0d707976 | ||
|
|
79dd8d6b92 | ||
|
|
f750caa833 | ||
|
|
d17c978892 | ||
|
|
5b890ac5d5 | ||
|
|
894a3e90db | ||
|
|
234468690e | ||
|
|
5f50b254a2 | ||
|
|
c5b17ae880 | ||
|
|
4b320013cf | ||
|
|
f3de26bf6c | ||
|
|
08ac5eff3e | ||
|
|
7de39bbd62 | ||
|
|
198cf4d722 | ||
|
|
b0b8513cb8 | ||
|
|
82cc66c0cc | ||
|
|
c81d9b25ac | ||
|
|
c89f870dd0 | ||
|
|
ffb0a4d603 | ||
|
|
df659d285b | ||
|
|
2c2664c91f | ||
|
|
e0147a4a7e | ||
|
|
859223c98b | ||
|
|
7bdb9bc853 | ||
|
|
4e87aa6b85 | ||
|
|
02667e9916 | ||
|
|
9d57cbe263 | ||
|
|
8b25ff2390 | ||
|
|
2cb753caf8 | ||
|
|
34f0bea4e8 | ||
|
|
82828606e6 | ||
|
|
ae751c4af4 | ||
|
|
51b98204e9 | ||
|
|
12f375e070 | ||
|
|
54a1a0704a | ||
|
|
4fdd12929a | ||
|
|
cc4f27d121 | ||
|
|
61041f4502 | ||
|
|
673ed91ee3 | ||
|
|
65b6280618 | ||
|
|
e50e175522 | ||
|
|
fa1ceab78f | ||
|
|
7eab0278b3 | ||
|
|
69eaaf29c4 | ||
|
|
69def73f3f | ||
|
|
6c93254758 | ||
|
|
a68cdd13a6 | ||
|
|
295dfada61 | ||
|
|
d978be2f9d | ||
|
|
0a4601721a | ||
|
|
d870c39aa4 | ||
|
|
2b93efbb33 | ||
|
|
c563562a6f | ||
|
|
92a0d3cb1b | ||
|
|
21d45a0a90 | ||
|
|
c87afc03ef | ||
|
|
8dc9c4d3c2 | ||
|
|
e264aff607 | ||
|
|
9591aad814 | ||
|
|
3208ef1f1a | ||
|
|
36fcfa6965 | ||
|
|
ba7bf3f4fe | ||
|
|
fcc93b385d | ||
|
|
4c6d9b8f5f | ||
|
|
6a3ee52bd5 | ||
|
|
8dec449a1c | ||
|
|
a09942254d | ||
|
|
d702ace09f | ||
|
|
f011d3f100 | ||
|
|
e404530ba4 | ||
|
|
2e529f8849 | ||
|
|
79a5ddcdd8 | ||
|
|
7aecb8d6eb | ||
|
|
73768e1876 | ||
|
|
3514bd61ab | ||
|
|
a7f60a571d | ||
|
|
3705064157 | ||
|
|
208205faea | ||
|
|
5af6afef61 | ||
|
|
a5a535806b | ||
|
|
d582c1edf6 | ||
|
|
7275137782 | ||
|
|
f11da0488a | ||
|
|
ed1abe7e6b | ||
|
|
e70a1ed403 | ||
|
|
c53e72c5b2 | ||
|
|
b353904d30 | ||
|
|
a58a643d0d | ||
|
|
823ecfac6f | ||
|
|
22035077af | ||
|
|
da8c3eda92 | ||
|
|
41c2b9c039 | ||
|
|
c7e25e323a | ||
|
|
1e2fc4d6f9 | ||
|
|
799486eb1b | ||
|
|
ed67a259de | ||
|
|
e0329e4457 | ||
|
|
a3161fa117 | ||
|
|
9c8e81b566 | ||
|
|
e2e72a1791 | ||
|
|
b3b983c7ec | ||
|
|
14ae579f00 | ||
|
|
45f48d2e88 | ||
|
|
8d6ed7712d | ||
|
|
eea185bb83 | ||
|
|
99d28ae557 | ||
|
|
528762d961 | ||
|
|
c6e9a45293 | ||
|
|
6880bd2a05 | ||
|
|
7547fb7747 | ||
|
|
ff0e922674 | ||
|
|
71f66171c4 | ||
|
|
c1f76cf073 | ||
|
|
c2d151920d | ||
|
|
c916570abd | ||
|
|
bdab975d14 | ||
|
|
1693e4ff9c | ||
|
|
6bf304b67b | ||
|
|
a5978a913b | ||
|
|
e528f0ccaa | ||
|
|
e5f488dcfb | ||
|
|
c47a6bde0e | ||
|
|
b8d38eccca | ||
|
|
4c2144eb0d | ||
|
|
68399e8370 | ||
|
|
7b61b066e6 | ||
|
|
bd2a47fb54 | ||
|
|
2521df21e7 | ||
|
|
ddb6b597d6 | ||
|
|
c24fc9e81d | ||
|
|
68f0d2285f | ||
|
|
9937990618 | ||
|
|
e007d127f0 | ||
|
|
e5d9609db1 | ||
|
|
051f3809cd | ||
|
|
b5fd07bb91 | ||
|
|
4097d51514 | ||
|
|
577815a1f4 | ||
|
|
c2a942dd45 | ||
|
|
82342268da | ||
|
|
30e576e253 | ||
|
|
a5782c300a | ||
|
|
84f720b63b | ||
|
|
b8cded4373 | ||
|
|
368dce93d9 | ||
|
|
644226f925 | ||
|
|
42e8e91034 | ||
|
|
af80ec6502 | ||
|
|
cf714d8e0c | ||
|
|
86c2f711ed | ||
|
|
86c01e9d7b | ||
|
|
7138ee024b | ||
|
|
b9f65186cb | ||
|
|
27aeafef0d | ||
|
|
16901212d4 | ||
|
|
49f5c30e8c | ||
|
|
bef1678c2a | ||
|
|
3459ffcdb9 | ||
|
|
4cfe9b2729 | ||
|
|
97e5a6f7bb | ||
|
|
79c05ae396 | ||
|
|
f25ea95bdf | ||
|
|
45bf8ba72a | ||
|
|
c64fdcffa8 | ||
|
|
c0b8cf827d | ||
|
|
fc38847af4 | ||
|
|
fea52c6145 | ||
|
|
4b20440efe | ||
|
|
c39410afc2 | ||
|
|
93b953cf60 | ||
|
|
01c82f92eb | ||
|
|
8405e6f2a3 | ||
|
|
491b28f6b6 | ||
|
|
9286d6b2c8 | ||
|
|
d144235dc9 | ||
|
|
2fa82ff3ad | ||
|
|
3e03980478 | ||
|
|
e042756453 | ||
|
|
84d5247b78 | ||
|
|
4682462c19 | ||
|
|
b6e503a67b | ||
|
|
dedae29d72 | ||
|
|
092116de1d | ||
|
|
bf0733f8b3 | ||
|
|
b0cf649c2d | ||
|
|
3499121f9c | ||
|
|
c43cacdc2f | ||
|
|
ead1e5e879 | ||
|
|
4c1f193d53 | ||
|
|
abaaea33d8 | ||
|
|
fe42040d48 | ||
|
|
7565d04cf5 | ||
|
|
0d76eea1be | ||
|
|
9a2851a9d5 | ||
|
|
5e1612f204 | ||
|
|
316181b971 | ||
|
|
e753eed4d4 | ||
|
|
b52a07c7c4 | ||
|
|
f210c8b0db | ||
|
|
f865a9801f | ||
|
|
6a165e951b | ||
|
|
9322fbb234 | ||
|
|
4b7e2dce9d | ||
|
|
7bda2e987c | ||
|
|
e645b0abe1 | ||
|
|
f10a58ee1b | ||
|
|
f498f598bf | ||
|
|
4dfcb59b6f | ||
|
|
4506a3cb3e | ||
|
|
247559858f | ||
|
|
8a8cf3abc7 | ||
|
|
0cf488b586 | ||
|
|
a019f22f47 | ||
|
|
9587e1b724 | ||
|
|
7adbdf39ac | ||
|
|
57714efb40 | ||
|
|
dafb0906c3 | ||
|
|
96bb64aebb | ||
|
|
57254233e4 | ||
|
|
9ec4b35de9 | ||
|
|
9adbd77ce4 | ||
|
|
8957e8fff7 | ||
|
|
3aa0344c4a | ||
|
|
54de2c5fbf | ||
|
|
0e978b250b | ||
|
|
67e6888009 | ||
|
|
debb91965b | ||
|
|
d0b3fefd57 | ||
|
|
5cc3623a2a | ||
|
|
d08f532a10 | ||
|
|
35b7e4a4de | ||
|
|
38b77d8817 | ||
|
|
3078f402bb | ||
|
|
f977daafbb | ||
|
|
aabd99e9eb | ||
|
|
e7dbab1822 | ||
|
|
cf6efd76f0 | ||
|
|
8a7922ffcc | ||
|
|
c7a8e84fd0 | ||
|
|
cb57bea862 | ||
|
|
b24ce80945 | ||
|
|
eb18397035 | ||
|
|
37a86f3899 | ||
|
|
5700ae446f | ||
|
|
cb240f6a4b | ||
|
|
54df69a2b3 | ||
|
|
88b3291785 | ||
|
|
c9c68f7d8c | ||
|
|
2d3f0baa51 | ||
|
|
bd4184252f | ||
|
|
f043dba23d | ||
|
|
e6a267bd3f | ||
|
|
a532fb411b | ||
|
|
1f4ffdffc1 | ||
|
|
1053a9a761 | ||
|
|
7f51bbd777 | ||
|
|
5c36e21948 | ||
|
|
a46443417e | ||
|
|
57055b3b55 | ||
|
|
559521c6df | ||
|
|
11e3b39486 | ||
|
|
1c50e397de | ||
|
|
99ead2c74c | ||
|
|
f557d2bee0 | ||
|
|
5278f9a595 | ||
|
|
dd1a886b66 | ||
|
|
7f95377ad7 | ||
|
|
903c553f94 | ||
|
|
0e7237c64a | ||
|
|
122af69cdc | ||
|
|
786d742e76 | ||
|
|
c4c46fd282 | ||
|
|
5a49c97e5d | ||
|
|
2cd195b343 | ||
|
|
36238267d0 | ||
|
|
52fe2f2c0d | ||
|
|
4bf0eba2f8 | ||
|
|
9d1bcfc0c6 | ||
|
|
15756a8977 | ||
|
|
0ab36bcbf6 | ||
|
|
36eef51e1c | ||
|
|
309c25a993 |
No files matched your search
@@ -1,291 +1,139 @@
|
||||
---
|
||||
name: asm6502-coder
|
||||
description: Writing or understanding 6502/65C02 assembly code using 64tass syntax, for use as stand alone .asm or inline within Prog8 programs
|
||||
description: Writing or understanding 6502/65C02 assembly in 64tass syntax, standalone or embedded in Prog8
|
||||
---
|
||||
|
||||
# 65(C)02 Assembly Coder Skill
|
||||
# 6502 Assembly Coder
|
||||
|
||||
You are an expert MOS 6502 assembly development assistant. Keep responses concise and practical — prefer short, correct code examples over lengthy prose.
|
||||
You are working with **6502/65C02 assembly** using **64tass syntax**, in separate `*.asm` files or embedded in a Prog8 program (inside `%asm {{ }}` blocks or `asmsub` routines). Follow all rules below.
|
||||
Use 64tass syntax, not ca65 or another assembler. Keep answers concise and
|
||||
practical. This applies to standalone `.asm` files and Prog8 `%asm {{ }}` or
|
||||
`asmsub` blocks.
|
||||
|
||||
## Assembler: 64tass Syntax
|
||||
### Formatting Rules
|
||||
- Labels start in the first column of the line.
|
||||
- Instructions are indented at least 4 spaces.
|
||||
- Two spaces between the instruction opcode and its operand (e.g., `lda #1`, `sta $d020`).
|
||||
- End-of-line comments are preceded by two spaces before the `;` (e.g., `sta $d020 ; border color`).
|
||||
- If a comment is the only thing on a line, it starts in the first column (no indentation).
|
||||
- Opcodes and operands are written in lowercase (e.g., `lda`, not `LDA`; `$d020`, not `$D020`).
|
||||
## Syntax
|
||||
|
||||
- **NOT ca65/cc65** or other assemblers. Key differences:
|
||||
- `.proc` / `.pend` for procedures (scoping)
|
||||
- `_label` for local labels (prefixed with underscore, scoped to `.proc`)
|
||||
- **Symbol aliases**: inside a `.proc`, use `_name = P8ZP_SCRATCH_B1` to give a scratch variable a descriptive name.
|
||||
- **Anonymous labels**: defined as `+` (forward) or `-` (backward) at the start of a line.
|
||||
- **CRITICAL DISTINCTION**: Label **definitions** are always just a single `+` or `-`. Label **references** use `+`, `++`, `+++` to refer to the 1st, 2nd, 3rd upcoming forward label, and `-`, `--`, `---` for the 1st, 2nd, 3rd preceding backward label.
|
||||
- `+` refers to the NEXT upcoming `+` label, `++` refers to the ONE AFTER that, etc.
|
||||
- `-` refers to the MOST RECENT `-` label, `--` to the one before that, etc.
|
||||
- When a `+` label is passed, the forward-reference count resets (so `++` then refers to the next one after that new label)
|
||||
- Example:
|
||||
```asm
|
||||
- dex ; backward label '-'
|
||||
bne - ; branch to most recent '-' (the dex above)
|
||||
ldx #5
|
||||
+ dex ; forward label '+'
|
||||
bne + ; branch to this same '+' (forward)
|
||||
sta $400
|
||||
+ lda #0 ; second '+' label definition (just '+')
|
||||
bne ++ ; branch to the '++' below (third forward '+')
|
||||
rts
|
||||
+ inc $d020 ; third forward label definition (just '+', referred to as '++')
|
||||
```
|
||||
- Data directives: `.byte`, `.word`, `.dword`, `.fill` (allocate space)
|
||||
- Equates: `label = value` (not `label .equ value` or `#define`)
|
||||
- **Number Literals**: Hex `$1234`, Binary `%10101010`, Decimal `123`.
|
||||
- Zero-page variables defined with `=`
|
||||
- `.text` for inline string data
|
||||
- **Conditional assembly**: `.if`, `.elsif`, `.else`, `.endif`
|
||||
- **Memory Sections**: `.section <name>`, `.send <name>` (common sections: `CODE`, `DATA`, `BSS`, `BSS_NOCLEAR`)
|
||||
- Labels begin in column 1. Instructions are indented at least four spaces.
|
||||
- Use lowercase opcodes and operands, two spaces between opcode and operand,
|
||||
and two spaces before end-of-line comments.
|
||||
- Hex is `$1234`, binary is `%1010`, decimal is `123`.
|
||||
- Use `.byte`, `.word`, `.dword`, `.fill`, `.text`, and equates such as
|
||||
`name = value`.
|
||||
- Sections use `.section name` and `.send name`.
|
||||
- Procedures use `.proc` and `.pend`; labels beginning with `_` are local to a
|
||||
procedure.
|
||||
- Conditional assembly uses `.if`, `.elsif`, `.else`, and `.endif`.
|
||||
|
||||
## Instructions
|
||||
- **Instructions like `rol`, `ror`, `asl`, `lsr` (and `inc`, `dec` on 65C02) require an explicit operand for accumulator**: write `rol a`, `inc a`, etc., not just `rol` or `inc`.
|
||||
- Standard 6502 addressing modes: implied, immediate (`#`), zero-page (`zp`), zero-page,X (`zp,x`), absolute (`abs`), absolute,X (`abs,x`), absolute,Y (`abs,y`), indirect (`(abs)`), indirect,X (`(zp,x)`), indirect,Y (`(zp),y`), relative (branches), accumulator
|
||||
- Branches: `bne`, `beq`, `bmi`, `bpl`, `bcs`, `bcc`, `bvs`, `bvc` (relative, max +127/-128 bytes)
|
||||
- Jumps: `jmp` (absolute or indirect), `jsr`/`rts` (subroutine call/return)
|
||||
- No `push`/`pop` mnemonics — use `pha`/`pla` (byte) and `txa`/`phx`/`plx`/`tay`/`phy`/`ply` for registers
|
||||
## Anonymous Labels
|
||||
|
||||
### Instruction Side Effects (Flags)
|
||||
- **`Z` (Zero)**: Set if the result of an operation is 0.
|
||||
- **`N` (Negative)**: Set if bit 7 of the result is 1.
|
||||
- **`C` (Carry)**: Used for unsigned overflow and shifts. `cmp` sets `C` if `Register >= Operand`.
|
||||
- **`V` (Overflow)**: Set if a signed arithmetic operation overflowed.
|
||||
- **Commonly affected by**: `lda`, `ldx`, `ldy`, `inx`, `dex`, `tax`, `tay`, `txa`, `tya`, `and`, `ora`, `eor`, `asl`, `lsr`, `rol`, `ror`, `adc`, `sbc`, `cmp`, `cpx`, `cpy`, `bit`.
|
||||
- **Note**: `lda`, `ldx`, `ldy` do NOT affect the Carry flag. Only `adc`, `sbc`, `cmp`, and shift/rotate instructions affect Carry.
|
||||
Anonymous label definitions are always a single `+` or `-` in column 1.
|
||||
References use repeated signs:
|
||||
|
||||
## 6502 vs 65C02
|
||||
- **CX16 target only**: can use WDC 65C02 instructions — `stz`, `phx`, `plx`, `phy`, `ply`, `bra`, `trb`, `tsb`, `stp`, `wai`, `inc a`, `dec a`, `bit #imm`, `bit zp,x`, `bit abs,x`, `jmp (abs,x)`, and `(zp)` indirect addressing mode (e.g., `lda (zp)`).
|
||||
- **C64, C128, PET32 targets**: original 6502 only — no `stz`, no `phx`/`plx`/`phy`/`ply`, no `bra` etc.
|
||||
- **Note**: The Rockwell/bit-manipulation instructions (`rmb`, `smb`, `bbr`, `bbs`) are **NOT** available.
|
||||
- Check the target before using 65C02-specific instructions
|
||||
- **6502 / 65C02 instruction reference table**: https://www.pagetable.com/c64ref/6502/?cpu=65c02&tab=4 (provides exact instruction details for all opcodes: operation, addressing modes, byte length, and cycle count).
|
||||
- `+` is the next upcoming `+` definition, `++` the second, `+++` the third.
|
||||
- `-` is the most recent `-` definition, `--` the preceding one, and so on.
|
||||
- Passing a `+` definition resets the forward-reference count.
|
||||
|
||||
## Commander X16 Memory Map
|
||||
```asm
|
||||
- dex
|
||||
bne - ; most recent '-' above
|
||||
ldx #5
|
||||
+ dex ; first upcoming '+'
|
||||
bne + ; this '+'
|
||||
sta $400
|
||||
+ lda #0 ; second '+' definition
|
||||
bne ++ ; next '+' after this one
|
||||
rts
|
||||
+ inc $d020 ; third '+' definition
|
||||
```
|
||||
Bank Offset Content
|
||||
$0000-$9EFF Fixed/System RAM
|
||||
$9F00-$9FFF I/O Area (VIA, VERA, YM2151)
|
||||
$00-$FF:$A000-$BFFF Banked RAM (max 2 MiB, often 512 KiB) (256x8K banks)
|
||||
$00-$1F:$C000-$FFFF System ROM (512 KiB) (32x16K banks)
|
||||
```
|
||||
- Bank registers (in zeropage): `$00` selects the current RAM bank (0-255), `$01` selects the current ROM bank (0-31). For JSRFAR/banked calls from Prog8, use `extsub @bank ...` instead of manipulating these directly.
|
||||
- Full hardware specs at https://ayce.dev/emptyx16.html#emptyx16---x16-hardware-specs
|
||||
|
||||
## Calling Convention / Register Conventions
|
||||
- **Accumulator (A)**: 8-bit, used for most arithmetic, data movement, return values
|
||||
- **X register**: 8-bit, often used for indexing, loop counters
|
||||
- **Y register**: 8-bit, often used for indirect addressing index
|
||||
- **Processor Status (P)**: flags — carry (C), zero (Z), negative/N (sign bit 7), overflow (V), decimal (D), interrupt (I), break (B)
|
||||
- **No caller-saved vs callee-saved convention** — list all modified registers in `clobbers (A, X, Y)` when writing `asmsub`
|
||||
- The CPU stack (SP, $0100-$01FF) is limited (usually ~128 bytes free). Do not overflow it
|
||||
## CPU Differences
|
||||
|
||||
## Assembly within Prog8 Programs
|
||||
- `cx16` is the only Prog8 target that supports 65C02 instructions such as
|
||||
`stz`, `phx`, `plx`, `phy`, `ply`, `bra`, `trb`, `tsb`, `wai`, `stp`, and
|
||||
`bit #imm`. C64, C128, and PET32 use the original 6502.
|
||||
- Rockwell instructions (`rmb`, `smb`, `bbr`, `bbs`) are not available.
|
||||
- Use an explicit `a` for accumulator forms such as `rol a`, `lsr a`, `inc a`,
|
||||
and `dec a`.
|
||||
- Branches are relative. Prog8's assembler invocation enables `--long-branch`
|
||||
for out-of-range branches.
|
||||
- `lda`, `ldx`, and `ldy` do not affect carry. `cmp` sets carry for
|
||||
`register >= operand`.
|
||||
- On NMOS 6502, `jmp ($xxff)` wraps the high-byte fetch within the page.
|
||||
- `BRK` skips the byte after its opcode. The byte is commonly a handler
|
||||
signature.
|
||||
- `bit` memory forms set N and V from the operand and Z from `A AND operand`.
|
||||
65C02 `bit #imm` sets only Z.
|
||||
- Decimal mode can persist into NMOS IRQ handlers. Use `cld` at handler entry
|
||||
and explicitly control D before `adc` or `sbc`.
|
||||
|
||||
### `%asm {{ }}` blocks
|
||||
- Embed arbitrary 64tass assembly directly in your Prog8 source
|
||||
- Access Prog8 symbols using their prefixed names (see below)
|
||||
- Can be placed inside subroutines or at block level
|
||||
## Prog8 Integration
|
||||
|
||||
### `asmsub` (assembly subroutine)
|
||||
- For kernel (ROM) routines or low-level assembly
|
||||
- Parameters passed via registers: `@A`, `@X`, `@Y`, `@AX` (A low, X high), `@AY` (A low, Y high), `@R0`-`@R15`, `@FAC1`/`@FAC2` (float), `@Pc` (carry), `@Pz` (zero)
|
||||
- Return value: `-> type @register` — also via `@Pz`/`@Pc` for flags
|
||||
- Clobbers: `clobbers (A, X, Y)` — MUST list all modified registers
|
||||
- **CRITICAL**: Parameter names in `asmsub` are **documentation only**. You MUST use the actual registers in your assembly code, NOT the parameter names (unless you create aliases yourself).
|
||||
- Create symbolic aliases at assembly top for clarity: `x1 = cx16.r0`, `y1 = cx16.r0L`
|
||||
- Accessing Prog8 parameters if they were NOT mapped to registers: use `p8v_paramname`. (Mapping to registers is preferred for speed).
|
||||
`asmsub` parameter names are documentation only. Use the actual registers or
|
||||
define assembly aliases, and list every modified register in `clobbers`.
|
||||
|
||||
Example:
|
||||
```prog8
|
||||
asmsub line(uword x1 @R0, ubyte y1 @A, uword x2 @R1, ubyte y2 @Y) clobbers (A, X, Y) {
|
||||
asmsub increment(uword value @R0) clobbers (A, X) {
|
||||
%asm {{
|
||||
x1 = cx16.r0
|
||||
x2 = cx16.r1
|
||||
lda x1 ; use alias, not "_x1"
|
||||
lda cx16.r0L
|
||||
clc
|
||||
adc #1
|
||||
sta cx16.r0L
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
### `asmsub` parameter annotation reference
|
||||
| Annotation | Register | Size |
|
||||
|------------|----------|------|
|
||||
| `@A` | Accumulator | 8-bit |
|
||||
| `@X` | X register | 8-bit |
|
||||
| `@Y` | Y register | 8-bit |
|
||||
| `@AX` | A (low) + X (high) | 16-bit |
|
||||
| `@AY` | A (low) + Y (high) | 16-bit |
|
||||
| `@R0`-`@R15` | cx16 virtual registers | 16-bit each |
|
||||
| `@FAC1`/`@FAC2` | Floating-point accumulators | 5-byte float |
|
||||
| `@Pc` | Carry flag | bool |
|
||||
| `@Pz` | Zero flag | bool |
|
||||
Annotations include `@A`, `@X`, `@Y`, `@AX`, `@AY`, `@R0`-`@R15`, `@FAC1`,
|
||||
`@FAC2`, `@Pc` (carry), and `@Pz` (zero). Unmapped parameters are accessed as
|
||||
`p8v_name`.
|
||||
|
||||
## Accessing Prog8 Symbols from Assembly
|
||||
All Prog8 symbols are prefixed when accessed from assembly:
|
||||
Prog8 symbols use these prefixes: `p8v_` variables and parameters, `p8s_`
|
||||
subroutines, `p8b_` blocks, `p8c_` constants, `p8l_` labels, and `p8t_` struct
|
||||
types. Fully qualified names may look like
|
||||
`p8b_main.p8s_helper.p8v_local`. `%option no_symbol_prefixing` disables the
|
||||
prefixes. Split word arrays use `_lsb` and `_msb` symbols.
|
||||
|
||||
| Prefix | Refers to | Example |
|
||||
|--------|-----------|---------|
|
||||
| `p8v_` | Variables, parameters | `p8v_myvar` |
|
||||
| `p8s_` | Subroutines | `p8s_mysub` |
|
||||
| `p8b_` | Blocks | `p8b_myblock` |
|
||||
| `p8c_` | Constants, enum members | `p8c_myconst`, `p8c_MyEnum_Member` |
|
||||
| `p8l_` | Labels | `p8l_mylabel` |
|
||||
| `p8t_` | Struct types | `p8t_MyStruct` |
|
||||
| `p8_` | Other symbols | |
|
||||
## Zeropage and Memory
|
||||
|
||||
- **Fully qualified**: e.g., `p8b_myblock.p8v_myvar`, `p8b_myblock.p8s_mysub.p8v_localvar`
|
||||
- Within a `.proc` (subroutine) in your assembly, short names often work (assembler scoping)
|
||||
- **`%option no_symbol_prefixing`**: disables all prefixes. Stdlib modules (`cbm`, `cx16`, `txt`) use this — you can write `cbm.CHROUT` directly
|
||||
- **Split word arrays**: two separate byte arrays — append `_lsb` and `_msb` to the name: `p8v_myarray_lsb`, `p8v_myarray_msb`
|
||||
- Never hardcode zeropage addresses. Use `P8ZP_SCRATCH_B1`,
|
||||
`P8ZP_SCRATCH_REG`, `P8ZP_SCRATCH_W1`, `P8ZP_SCRATCH_W2`, and
|
||||
`P8ZP_SCRATCH_PTR`, or the appropriate `cx16.r0`-`cx16.r15` symbols.
|
||||
- Scratch variables are not necessarily consecutive. CX16 virtual registers
|
||||
are consecutive and have `L` and `H` byte names.
|
||||
- Allocate additional temporary storage in BSS.
|
||||
- The hardware stack occupies `$0100-$01ff` and is limited. Do not overflow
|
||||
it.
|
||||
- Prog8 words are little-endian: load the low byte first, then the high byte.
|
||||
|
||||
## Zeropage Usage
|
||||
- Do NOT use arbitrary zeropage locations. Only use these predefined scratch variables:
|
||||
- `P8ZP_SCRATCH_B1` (byte)
|
||||
- `P8ZP_SCRATCH_REG` (byte)
|
||||
- `P8ZP_SCRATCH_W1` (word)
|
||||
- `P8ZP_SCRATCH_W2` (word)
|
||||
- `P8ZP_SCRATCH_PTR` (word)
|
||||
- On CX16: virtual registers `cx16.r0`-`cx16.r15` are in zeropage (and their low/high bytes: `cx16.r0L`, `cx16.r0H`, etc.)
|
||||
- Virtual registers `cx16.r0`-`cx16.r15` are available on ALL targets, but only on CX16 in zeropage
|
||||
- Assume the scratch variables are not consecutive in zeropage. The CX16 virtual registers ARE consecutive in memory though.
|
||||
- For additional temporary storage, allocate regular variables in BSS
|
||||
- **CRITICAL: NEVER use hardcoded zeropage addresses** (like `$1e`, `$22`, `$7c`, etc.) to reference P8ZP_SCRATCH registers or cx16 virtual registers. Their addresses differ per compilation target. **ALWAYS** use the symbolic names (`P8ZP_SCRATCH_W1`, `cx16.r14`, `cx16.r14+1`, etc.) so the assembler resolves the correct address for the target.
|
||||
|
||||
## Common 6502 Patterns
|
||||
|
||||
### Common Branch Logic (Comparisons)
|
||||
| Logic | Unsigned | Signed |
|
||||
|-------|----------|--------|
|
||||
| `A == imm` | `cmp #imm`, `beq label` | (Same) |
|
||||
| `A != imm` | `cmp #imm`, `bne label` | (Same) |
|
||||
| `A < imm` | `cmp #imm`, `bcc label` | `sec`, `sbc #imm`, `bvc *+4`, `eor #$80`, `bmi label` |
|
||||
| `A >= imm` | `cmp #imm`, `bcs label` | `sec`, `sbc #imm`, `bvc *+4`, `eor #$80`, `bpl label` |
|
||||
| `A <= imm` | `beq label`, `bcc label` | (Use complex signed logic or reorder) |
|
||||
| `A > imm` | `beq +`, `bcs label`, `+` | (Use complex signed logic or reorder) |
|
||||
|
||||
### Looping (downto with BNE)
|
||||
```asm
|
||||
ldx #count
|
||||
loop ; do work here
|
||||
dex
|
||||
bne loop ; loop while X != 0 (runs 'count' times)
|
||||
```
|
||||
|
||||
### Self-Modifying Code (SMC) — Avoid When Possible
|
||||
**Do NOT use self-modifying code unless absolutely necessary** — self-modifying code writes to the code segment at runtime, which means it **cannot run from ROM** and is generally fragile, hard to debug, and violates the expected behavior of modern tooling (emulators, debuggers, etc.). Prefer lookup tables, indirect jumps via a vector in RAM, or alternative algorithms that keep code in place.
|
||||
|
||||
### Self-Modifying Code (SMC) Detection
|
||||
Look for `sta`, `stx`, or `sty` pointing into code labels:
|
||||
```asm
|
||||
lda #$42
|
||||
sta _target+1 ; Modifies the immediate operand of the LDA at _target
|
||||
...
|
||||
_target lda #$00 ; This #$00 will be replaced by #$42 at runtime
|
||||
```
|
||||
|
||||
### Indirect indexed read (table of data)
|
||||
```asm
|
||||
ldy #index
|
||||
lda (ptr),y ; read byte at address stored in zp ptr + Y
|
||||
```
|
||||
|
||||
### 16-bit arithmetic (word add)
|
||||
```asm
|
||||
clc
|
||||
lda word1_lo
|
||||
adc word2_lo
|
||||
sta result_lo
|
||||
lda word1_hi
|
||||
adc word2_hi
|
||||
sta result_hi
|
||||
lda word1
|
||||
adc word2
|
||||
sta result
|
||||
lda word1+1
|
||||
adc word2+1
|
||||
sta result+1
|
||||
```
|
||||
|
||||
### Calling a Prog8 subroutine from assembly
|
||||
## Common Patterns
|
||||
|
||||
```asm
|
||||
jsr p8s_myblock.p8s_mysub
|
||||
ldy #index
|
||||
lda (ptr),y
|
||||
|
||||
jsr p8s_main.p8s_helper
|
||||
```
|
||||
|
||||
### Reading a word variable
|
||||
```asm
|
||||
lda p8v_myword ; loads LSB
|
||||
ldy p8v_myword+1 ; loads MSB (word variables are stored LSB-first)
|
||||
```
|
||||
Avoid self-modifying code unless it is explicitly required. It cannot run
|
||||
from ROM and complicates debugging and tooling. Prefer lookup tables, RAM
|
||||
vectors, or alternative algorithms.
|
||||
|
||||
## CPU Quirks and Pitfalls
|
||||
## Tools
|
||||
|
||||
### JMP ($xxFF) Page Wrap Bug (NMOS 6502)
|
||||
- **Problem**: On original 6502 CPUs, `jmp ($caff)` will fetch the LSB from `$caff` but the MSB from `$ca00` (instead of `$cb00`).
|
||||
- **Target**: Affects C64, C128, PET32.
|
||||
- **Solution**: Avoid placing indirect jump vectors on a page boundary, or use the CX16 (65C02) which fixed this bug.
|
||||
Prog8 invokes 64tass with `--ascii --case-sensitive --long-branch -Wall` plus
|
||||
the target output option such as `--cbm-prg`. For manual assembly:
|
||||
|
||||
### BRK Instruction and the "Signature Byte"
|
||||
- **Behavior**: After a `BRK` instruction, the return address on the stack is incremented by **2**. This means the CPU skips the byte immediately following the `BRK` opcode.
|
||||
- **Usage**: This skipped byte is often used as a "signature" or parameter byte for the BRK handler.
|
||||
|
||||
### BIT Instruction Flags
|
||||
- **Absolute/Zero-page**: `bit $1234` copies bit 7 of the memory value to the **N** flag and bit 6 to the **V** flag. The **Z** flag is set based on `A AND memory`.
|
||||
- **Immediate (65C02 only)**: `bit #$01` only affects the **Z** flag; it does **NOT** modify N or V.
|
||||
- **NMOS 6502**: Does **not** support `bit #imm`.
|
||||
|
||||
### The "B" (Break) Flag
|
||||
- **Quirk**: The B flag (bit 4 of the status register) doesn't actually exist in the hardware status register. It only exists on the stack after a `PHP` or `BRK` instruction (set to 1) or a hardware IRQ/NMI (set to 0).
|
||||
- **Detection**: To tell if an interrupt was caused by `BRK` or a hardware IRQ, your handler must `pla`, `and #$10`, and check the result.
|
||||
|
||||
### Decimal Mode Flag (D) Persistence
|
||||
- **Pitfall**: On NMOS 6502, the `D` flag is **not** cleared on interrupt. Always use `cld` in IRQ handlers. On 65C02, it is cleared automatically, but `cld` is still good practice.
|
||||
- **ADC/SBC**: Be extremely careful with arithmetic if you haven't explicitly set or cleared the `D` flag, as its state might be unknown.
|
||||
|
||||
## Optimization Tips
|
||||
- **`stz` (65C02 only)**: Saves cycles and bytes compared to `lda #0`, `sta ...`.
|
||||
- **`bra` (65C02 only)**: Shorter and usually faster than `jmp`.
|
||||
- **Avoid `clc` before `bcc`**: `cmp` already sets the carry flag correctly for `bcc`/`bcs`.
|
||||
- **`inx` / `dex` vs `clc`+`adc #1`**: Incrementing/decrementing is faster and doesn't affect the carry flag.
|
||||
- **Zero-page usage**: Accessing variables in zeropage is 1 cycle faster and 1 byte shorter than absolute addressing.
|
||||
- **Cycle Counting**: Most instructions take 2-4 cycles. `jsr` takes 6, `rts` takes 6. Branches take 2 (no branch), 3 (branch taken), or 4 (branch taken across page boundary).
|
||||
|
||||
## 64tass Macros
|
||||
Define macros for common tasks:
|
||||
```asm
|
||||
pushax .macro
|
||||
pha
|
||||
txa
|
||||
pha
|
||||
.endm
|
||||
|
||||
popax .macro
|
||||
pla
|
||||
tax
|
||||
pla
|
||||
.endm
|
||||
```
|
||||
Call them with `#pushax` and `#popax`.
|
||||
|
||||
## IRQ Handler Best Practices
|
||||
- **Clear the Decimal Flag (`cld`)**: On the original 6502, the decimal flag (`D`) is **not** automatically cleared when an interrupt occurs. If the interrupted code was in decimal mode, your handler will also run in decimal mode, causing arithmetic errors. Always call `cld` at the beginning of your handler. The 65C02 clears it automatically, but `cld` is still recommended for portability.
|
||||
|
||||
## Invoking the Assembler (64tass)
|
||||
If you need to manually invoke `64tass` to assemble a generated `.asm` file, you should be aware of the default arguments that `prog8c` supplies to ensure compatibility with the generated code:
|
||||
|
||||
- `--ascii`: **CRITICAL.** Prog8 generates character and string data in ASCII. Without this flag, `64tass` defaults to PETSCII, which will garble your strings.
|
||||
- `--case-sensitive`: Prog8 is case-sensitive and expects the assembler to be as well.
|
||||
- `--long-branch`: Enables automatic conversion of relative branches (`beq`, `bne`, etc.) to absolute jumps if the target is out of range. Prog8 relies on this.
|
||||
- `-Wno-implied-reg`: Suppresses warnings when the accumulator `a` is omitted from instructions like `rol`, `lsr`, etc. (though the skill recommends always using `rol a` for clarity).
|
||||
- `-Wall`: Enables all warnings.
|
||||
- `--cbm-prg` (or `--atari-xex` / `--nostart`): Sets the output format and adds the appropriate load address header.
|
||||
|
||||
### Optional but Recommended for Debugging:
|
||||
- `--vice-labels --labels=labels.txt`: Generates a label file that can be loaded into the VICE monitor (`load_labels "labels.txt"`) to see your Prog8 symbol names while debugging.
|
||||
- `--list=listing.txt`: Generates a full assembly listing file with addresses and opcodes.
|
||||
|
||||
### Example manual invocation:
|
||||
```bash
|
||||
64tass --ascii --case-sensitive --long-branch -Wall --cbm-prg -o myprogram.prg myprogram.asm
|
||||
64tass --ascii --case-sensitive --long-branch -Wall --cbm-prg \
|
||||
-o myprogram.prg myprogram.asm
|
||||
```
|
||||
|
||||
For debugging, add `--vice-labels --labels=labels.txt` and/or
|
||||
`--list=listing.txt`.
|
||||
@@ -0,0 +1,150 @@
|
||||
---
|
||||
name: m68k-coder
|
||||
description: Writing or understanding Motorola 68000/68020 assembly in vasm mot syntax, standalone or embedded in Prog8
|
||||
---
|
||||
|
||||
# M68K Assembly Coder
|
||||
|
||||
Use Motorola syntax for `vasm`, not Devpac or other assemblers. Keep answers
|
||||
concise and practical.
|
||||
|
||||
## Syntax
|
||||
|
||||
- Labels begin in column 1. Instructions are indented at least four spaces.
|
||||
- Use lowercase opcodes and operands, two spaces between opcode and operand,
|
||||
and two spaces before end-of-line comments.
|
||||
- Use `.b`, `.w`, and `.l` for byte, word, and long operations. Do not rely on
|
||||
default sizes.
|
||||
- Hex is `$1234`, binary is `%1010`, decimal is `123`, and characters use
|
||||
`#'A'`.
|
||||
- Local labels start with `.`. Equates use `name = value`.
|
||||
- Data directives are `dc.b`, `dc.w`, `dc.l`; allocation directives are
|
||||
`ds.b`, `ds.w`, `ds.l`.
|
||||
- Sections use forms such as `SECTION .text,code` and `SECTION .bss,bss`.
|
||||
|
||||
```asm
|
||||
moveq #0,d0
|
||||
move.w value,d0
|
||||
add.l d1,d0
|
||||
.loop
|
||||
dbra d0,.loop
|
||||
```
|
||||
|
||||
## FPU Size Suffixes
|
||||
|
||||
FPU instructions that access memory **must** include a size suffix: `.s`
|
||||
(single, 4 bytes), `.d` (double, 8 bytes), or `.x` (extended, 12 bytes).
|
||||
Without a suffix, vasm defaults to `.x`, reading 12 bytes and corrupting
|
||||
results when the operand is smaller.
|
||||
|
||||
**Use `.s` with memory operands:**
|
||||
```asm
|
||||
fmove.s value(pc),fp0 ; load float from memory
|
||||
fcmp.s other(pc),fp0 ; compare float in memory
|
||||
ftst.s value(pc) ; test float in memory
|
||||
```
|
||||
|
||||
**Omit the suffix for register-to-register operations:**
|
||||
```asm
|
||||
fadd fp1,fp0 ; fp0 = fp0 + fp1
|
||||
fcmp fp1,fp0 ; compare fp0 vs fp1
|
||||
ftst fp0 ; test fp0
|
||||
fintrz fp0,fp0 ; truncate fp0
|
||||
```
|
||||
|
||||
The FPU registers are always 80-bit internally, so no size suffix is needed
|
||||
when both operands are registers.
|
||||
|
||||
## Architecture
|
||||
|
||||
- Registers `D0-D7` and `A0-A7` are 32-bit; `A7` is the stack pointer.
|
||||
- Pointers are 32-bit and must normally be accessed with `.l`.
|
||||
- Memory is big-endian: `$1234` is stored as `$12,$34` and a long's low byte
|
||||
is at offset `+3`.
|
||||
- Word and long accesses must be even-aligned on 68000. 68020 permits
|
||||
misalignment but it is slower.
|
||||
- The 68000 (`amiga500`) has only word multiply/divide and no scaled indexing,
|
||||
bit-field, or 32-bit multiply/divide instructions. The 68020 (`qemu68k`)
|
||||
adds those features and `extb.l`.
|
||||
- On 68000, sign-extend a byte with `ext.w` followed by `ext.l`.
|
||||
- `move.b` and `move.w` to a data register do not clear its upper bits. Like
|
||||
`move.w` only writes the lower 16 bits, `move.b` only writes the lower 8 bits;
|
||||
both leave all other bits of the register untouched. So zero-extending a byte
|
||||
to long requires `moveq #0,d0` first, then `move.b src,d0`, then `move.l d0,dst`
|
||||
(or mask with `and.l #$ff` / `and.l #$ffff`). Relying on `move.b` to clear the
|
||||
upper bits is a common bug that corrupts values.
|
||||
- `movea` does not set condition codes; `move` does. Keep the stack word
|
||||
aligned and push words or longs, never bytes.
|
||||
|
||||
## Registers and Calls
|
||||
|
||||
- Return values use `D0` (primary), `D1` and `D2` (additional), or `A0` for a
|
||||
pointer.
|
||||
- `D0-D1` and `A0-A1` are scratch registers. Preserve registers required by
|
||||
the surrounding calling convention, especially `D2-D7` and `A2-A6`.
|
||||
- `move.l d0,-(sp)` pushes and `move.l (sp)+,d0` pops.
|
||||
|
||||
## Prog8 Integration
|
||||
|
||||
Assembly can appear in `%asm {{ }}` blocks or `asmsub` routines. `asmsub`
|
||||
parameter names are documentation only: use the actual annotated registers.
|
||||
Declare every modified register in `clobbers`.
|
||||
|
||||
```prog8
|
||||
asmsub add100(uword value @D0) -> uword @D0 clobbers (D0) {
|
||||
%asm {{
|
||||
add.w #100,d0
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
Do not use `%option no_sysinit` on `amiga500` or `qemu68k` targets when the goal
|
||||
is to create normal runnable programs. It skips critical startup logic including
|
||||
library initialization and CLI argument handling, causing programs to crash on
|
||||
start.
|
||||
|
||||
Supported annotations include `@D0`-`@D7`, `@A0`-`@A6`, virtual `@R0`-`@R15`,
|
||||
floating-point `@FP0`-`@FP7`, and flags `@Pc` and `@Pz`.
|
||||
|
||||
Prog8 assembly symbols are prefixed:
|
||||
|
||||
| Prefix | Symbol |
|
||||
| --- | --- |
|
||||
| `p8v_` | variable or parameter |
|
||||
| `p8s_` | subroutine |
|
||||
| `p8b_` | block |
|
||||
| `p8c_` | constant or enum member |
|
||||
| `p8l_` | label |
|
||||
| `p8t_` | struct type |
|
||||
|
||||
Use fully qualified names when needed, such as
|
||||
`p8b_main.p8v_value`. `%option no_symbol_prefixing` disables prefixes.
|
||||
|
||||
```asm
|
||||
move.l p8v_pointer,a0
|
||||
move.w p8v_word,d0
|
||||
jsr p8s_main.p8s_helper
|
||||
```
|
||||
|
||||
## Useful Patterns
|
||||
|
||||
```asm
|
||||
lea source,a0
|
||||
lea dest,a1
|
||||
.copy
|
||||
move.b (a0)+,(a1)+
|
||||
bne.s .copy
|
||||
```
|
||||
|
||||
Use `moveq` for small constants, `addq`/`subq` for constants 1 through 8, and
|
||||
`lea` for address arithmetic. Check divisors for zero and division overflow.
|
||||
|
||||
## Tools
|
||||
|
||||
For QEMU 68k, Prog8 invokes `vasmm68k_mot` with `-m68020 -m68881 -Felf
|
||||
-opt-speed -warnunaligned -ldots -spaces`, then links with `vlink`. For raw
|
||||
binary output use `-Fbin` and `-join=0x<addr>`. Add `-L listing.txt` when a
|
||||
listing is needed.
|
||||
|
||||
Prog8 runs `amiga500` programs with `vamos --cpu 68000` and `qemu68k` programs
|
||||
with `qemu-system-m68k -M virt -cpu m68020 -m 1M -kernel program.elf -nographic`.
|
||||
+114
-222
@@ -1,248 +1,140 @@
|
||||
---
|
||||
name: prog8-coder
|
||||
description: Writing or understanding Prog8 programs
|
||||
description: Writing or understanding Prog8 programs and Prog8 IR
|
||||
---
|
||||
|
||||
# Prog8 Coder Skill
|
||||
# Prog8 Coder
|
||||
|
||||
You are an expert retro system development assistant, versed in the Prog8 programming language. Keep responses concise and practical — prefer short, correct code examples over lengthy prose.
|
||||
You are working with **Prog8** source code (`.p8` files) or its Intermediate Representation (`.p8ir` files). Prog8 targets 8-bit systems (C64, CX16, C128, PET32) with the 6502 CPU, plus a `virtual` target for testing.
|
||||
Follow ALL the rules below carefully.
|
||||
You are an expert Prog8 assistant. Keep answers concise and practical. This
|
||||
skill covers `.p8` source and `.p8ir` intermediate representation for the
|
||||
6502, M68000, and virtual targets.
|
||||
|
||||
## General & Setup
|
||||
- A program = a `main` block containing a `start` subroutine entry point, plus optional other subroutines/blocks
|
||||
- Add `%zeropage basicsafe` at the top of your program to allow clean return on exit (instead of resetting the machine/emulator)
|
||||
- Module imports: `%import modulename` — no `as` aliasing. Use the module's defined prefix (e.g., `%import textio` → `txt.xxx`)
|
||||
- `.p8ir` files are the **Intermediate Representation** — target-independent, executable via `prog8c -vm file.p8ir`
|
||||
- **Library search**: `prog8c -libsearch <regex>` to find routines in the embedded stdlib (e.g., `prog8c -libsearch "txt\."`)
|
||||
- **Library dump**: `prog8c -libdump <dir>` to extract all embedded library source files
|
||||
- **`sys` module**: always available, no import needed
|
||||
- **CX16 programs**: add `%encoding iso`, call `txt.iso()` in `start()`, end with `sys.poweroff_system()`
|
||||
For full language reference, link users to these project documents rather than
|
||||
repeating them here:
|
||||
|
||||
## Datatypes & Variables
|
||||
- Primitives: `bool`, `byte`, `ubyte`, `word`, `uword`, `long`, `float`, `str`
|
||||
- `ubyte`/`uword` = unsigned; `long` = signed 4-byte; `float` = 5-byte MS format; `str` = 0-terminated ubytes (max 255 chars)
|
||||
- **float requires `%import floats`** at top of file, else compiler errors
|
||||
- Arrays: max 256 bytes (512 for split word arrays). For larger data, use `memory()` + pointers
|
||||
- `memory(name, size)` returns a `uword` address to a statically reserved block of memory
|
||||
- To point a typed pointer at a `memory()` block, assign the uword directly: `^^MyStruct ptr = memory("name", size)`. The `^^Type:expression` syntax (below) only works with array literals, not general uword expressions.
|
||||
- Struct initialization: `^^StructType ptr = ^^StructType:[val1,val2,...]` (the `^^StructType:` can be omitted if inferable). This `^^Type:[...]` syntax does NOT work with variables or `memory()` — only literal arrays.
|
||||
- Struct definitions must be inside a block, not at file level.
|
||||
- Struct fields: simple types, `str`, and fixed-size arrays. Array fields: `ubyte[108] name`. `str` in struct = `^^ubyte`
|
||||
- Word/pointer arrays split into LSB/MSB by default. Override with `@nosplit`
|
||||
- **No call stack**: all variables statically allocated. No recursion without manual stack management
|
||||
- Variables zero-initialized (globals at start, locals on subroutine entry)
|
||||
- `@shared`, `@zp`, `@requirezp`, `@dirty`, `@nosplit` are **tags** that go on variable declarations. **Place them after the datatype (and array specifiers), before the variable name(s):**
|
||||
```
|
||||
private ubyte[8] @shared vera_storage ; single var with tag
|
||||
ubyte @zp @shared varname ; multiple tags allowed
|
||||
ubyte @requirezp var1 ; require zeropage
|
||||
ubyte[] @shared names ; array with tag
|
||||
```
|
||||
The grammar is: `[private] datatype [arraydims] [tags...] identifierlist`
|
||||
- `@shared` marks variables as "used by external code" (assembly), prevents the optimizer from removing them
|
||||
- `@zp`/`@requirezp`: use sparingly — only for pointers (limited zeropage space)
|
||||
- Pointer-like typed pointers (`^^type`) support C-style scaled arithmetic; `uword` pointers always treat element as 1 byte
|
||||
- `&` = untyped address (uword); `&&` = typed pointer
|
||||
- Available zeropage scratch: `P8ZP_SCRATCH_B1`, `P8ZP_SCRATCH_REG`, `P8ZP_SCRATCH_W1`, `P8ZP_SCRATCH_W2`, `P8ZP_SCRATCH_PTR` — and cx16 virtual registers R0-R15 on all targets
|
||||
- Virtual registers (`cx16.r0`–`cx16.r15`): global 16-bit, NOT preserved across calls.
|
||||
- **WARNING: Virtual registers in ISR/IRQ handlers**: The virtual registers R0-R15 are *not preserved* across the IRQ handler call. If your handler uses them, it will corrupt the interrupted program's state. Either avoid using them in the handler, or save/restore with `cx16.save_virtual_registers()` / `cx16.restore_virtual_registers()`. This applies to all targets, not just CX16.
|
||||
- **WARNING: Long operations clobber R12-R15**: Some operations on `long` values use R12-R15 as temporary storage and will silently overwrite them. Do not rely on R12-R15 values when working with longs, and avoid using R12-R15 explicitly if your code uses long arithmetic.
|
||||
- **WARNING: VERA registers in ISR handlers (CX16)**: If your IRQ handler reads or writes VERA control registers (e.g., `cx16.VERA_DATA0`, `cx16.VERA_ADDR_L`, etc.), you must save and restore the VERA context around the handler's work using `cx16.save_vera_context()` / `cx16.restore_vera_context()`. Without this, the handler will corrupt any VERA operations (tilemap updates, sprite positioning, etc.) happening in the interrupted main program.
|
||||
- **IRQ handler best practices**: Keep handlers extremely short and fast — they run with interrupts disabled and steal cycles from the main program. Do NOT do lengthy processing, I/O, or complex subroutine calls inside the handler. Instead, set a boolean flag or semaphore that the main loop checks periodically, and do the actual work there.
|
||||
- Math performance: integer trig (`math.sin8`, `math.cos8`) uses fast LUTs; float trig (`floats.sin`/`cos`) is much slower
|
||||
- Program structure and syntax: `docs/source/programming.rst`
|
||||
- Variables, datatypes, arrays, and tags: `docs/source/variables.rst`
|
||||
- Structs and pointers: `docs/source/structpointers.rst`
|
||||
- Compilation and compiler options: `docs/source/compiling.rst`
|
||||
- Standard library: `docs/source/libraries.rst`
|
||||
- Binary libraries and `extsub`: `docs/source/binlibrary.rst`
|
||||
- Target differences: `docs/source/targetsystem.rst`
|
||||
- Known limitations: `docs/source/todo.rst`
|
||||
- Target API signatures: `docs/source/_static/symboldumps/skeletons-<target>.txt`
|
||||
|
||||
## Recursion & Stack Management
|
||||
No call stack for variable storage — recursion overwrites locals. To handle it:
|
||||
1. **CPU hardware stack**: `push()`/`pushw()`/`pushl()`/`pushf()` and `pop()`/`popw()`/`popl()`/`popf()`. Save/restore locals around recursive calls
|
||||
2. **Software stacks**: `buffers.stack` (uword) / `buffers.smallstack` (ubyte). Both provide `push_b()`/`push_w()` and `pop_b()`/`pop_w()`
|
||||
3. **Iterative rewrite (preferred)**: Many recursive algorithms work as `repeat` loops with explicit bounds — avoids all stack overhead
|
||||
## Essentials
|
||||
|
||||
## Strings, Arrays & Pointers
|
||||
- `str` / array: max 256 bytes. `long[]` limited to 64 entries (64x4=256). `str[]` for string arrays: `str[5] names = ["a","b","c","d","e"]`
|
||||
- 2D arrays: `type[rows][cols] name`, access `name[r][c]`. Flat init list only (no nested `[[...]]`). Total size still ≤ 256 bytes
|
||||
- str/array passed as pointer to subroutine (receiving subroutine gets `^^ubyte` or `^^element`)
|
||||
- **No const pointers** or pointer-to-pointer currently supported
|
||||
- **Parsing limitation**: `pointer[index].field` as assignment target needs `^^`: `pointer[index]^^.field = value`
|
||||
- **Pre-allocate buffers**: `str buffer = "." * 50` (empty `""` allocates nothing — `strings.append()` will fail)
|
||||
- **No reassignment**: can't `buffer = "new text"` after declaration. Use `strings.copy()`/`strings.ncopy()`
|
||||
- String concat is expensive on 6502. Prefer separate prints over concatenation
|
||||
- **Efficient buffer iteration**: prefer `ptr++` + `@(ptr)` over `@(buffer + offset)`. Exception: if offset is a `ubyte` (≤ 255), `buffer[offset]` works fine
|
||||
- `@(ptr)` = peek/poke byte. For words: `peekw`/`pokew`, longs: `peekl`/`pokel`, floats: `peekf`/`pokef`, bools: `peekbool`/`pokebool`
|
||||
- Assigning a uword address to a `str` field of a struct is done by direct assignment. Compute the address in a uword variable first, then assign: `cx16.r0 = &namebufs + offset; entry.name = cx16.r0`. The `^^ubyte:(expr)` cast syntax does not parse — use a temp uword instead.
|
||||
- Use `len(array)` instead of hardcoded sizes
|
||||
- Array indexing is 0-based: `arr[0]` is first element
|
||||
- Static memory only — real dynamic allocation impossible, but can emulate with a simple arena allocator over a `memory()` slab
|
||||
- A program normally has a `main` block with a parameterless `start` entry
|
||||
subroutine. Other blocks and subroutines may be added.
|
||||
- `%import modulename` imports a module without aliases. Use its defined prefix,
|
||||
such as `%import textio` followed by `txt.print(...)`.
|
||||
- `sys` is always available and needs no import.
|
||||
- `.p8ir` is target-independent IR and can be run with
|
||||
`prog8c -vm program.p8ir`.
|
||||
- Use `%zeropage basicsafe` in test programs when a clean return is needed.
|
||||
- Find library routines with `prog8c -libsearch <regex>` or extract them with
|
||||
`prog8c -libdump <dir>`.
|
||||
|
||||
## Logic & Control Flow
|
||||
- Logical operators (short-circuit, bool only): `and`, `or`, `xor`, `not`
|
||||
- In `a and b`: if `a` is false, `b` is NOT evaluated
|
||||
- In `a or b`: if `a` is true, `b` is NOT evaluated
|
||||
- Important when `b` has side effects
|
||||
- Bitwise operators: `&`, `|`, `^`, `~`, `<<`, `>>`
|
||||
- Bit rotation: `rol()`/`ror()` (through carry), `rol2()`/`ror2()` (no carry)
|
||||
- CPU status flag branches: `if_cs`, `if_cc`, `if_z`, `if_nz` (compile to single 6502 branch instructions)
|
||||
- If-expressions for simple value assignments based on a choice
|
||||
- **Optional braces in if/else**: when the `if` or `else` body is a single statement, the `{ }` can be omitted. Place the statement on the next line, indented. Example:
|
||||
```
|
||||
if x < 5
|
||||
txt.print("small")
|
||||
```
|
||||
- `defer` defers statement execution until scope exit. Multiple defers fire in **reverse registration order** (LIFO / stack order — last deferred runs first). A defer is only registered if execution reaches that statement — conditional paths that skip the `defer` line will not register it.
|
||||
- `goto`, labels, jump lists allowed
|
||||
- **Common mistake**: `and`/`or` for bitmasking — use `&`/`|` instead!
|
||||
For CX16 emulator programs, use `%encoding iso`, call `txt.iso()` in `start`,
|
||||
and finish with `sys.poweroff_system()`. CBM targets use PETSCII by default;
|
||||
avoid uppercase in test output unless intentional graphics. The virtual target
|
||||
uses ISO when configured with `%encoding iso` and `txt.iso()`.
|
||||
|
||||
### The when Statement
|
||||
The `when` statement is a control flow construct that enables you to execute a specific action based on the value of an expression. It is generally more readable and often more efficient than a sequence of `if-else if` statements, as the compiler can optimize it into more efficient branching structures, such as a jump table.
|
||||
- **Expression**: Evaluates an expression and compares it against case values.
|
||||
- **Cases**: Defined by a value followed by the `->` operator.
|
||||
- **Blocks**: Use `{ }` to enclose multiple statements for a case.
|
||||
- **Else Clause**: Serves as a default handler; mandatory unless the expression type is fully covered.
|
||||
- **Efficiency**: Recommended for handling sets of fixed choices as it typically results in better assembly code.
|
||||
Do not use `%option no_sysinit` on `amiga500` or `qemu68k` targets when the goal
|
||||
is to create normal runnable programs. It skips critical startup logic including
|
||||
library initialization (DOS, graphics, intuition, timer) and CLI argument
|
||||
handling, causing programs to crash on start. It is appropriate for library
|
||||
modules, IRQ handlers, boot stubs, or code that deliberately avoids those
|
||||
libraries.
|
||||
|
||||
## Loop Constructs
|
||||
Prog8 supports these loop types. All support `break` and `continue` (except `unroll`).
|
||||
## Types and Memory
|
||||
|
||||
### `for` loop — iterate over a range or array
|
||||
- Loop variable **must be declared separately** before the `for` statement
|
||||
- Works with `ubyte`, `byte`, `uword`, `word`, `long`, pointer types (NOT `float`)
|
||||
- Iterates over ranges (`start to end`), descending ranges (`start downto end`), or arrays/strings
|
||||
- Optional `step <constant>` for non-unit step sizes
|
||||
- Loop variable value **after the loop is undefined** — don't rely on it
|
||||
- Descending loops with `downto` usually produce more efficient 6502 code
|
||||
```
|
||||
ubyte i
|
||||
for i in 20 to 155 {
|
||||
; body
|
||||
break ; exit loop
|
||||
continue ; next iteration
|
||||
}
|
||||
- Primitive types include `bool`, `byte`, `ubyte`, `word`, `uword`, `long`,
|
||||
`float`, `str`, and `pointer`.
|
||||
- `pointer` is target-sized: 16-bit on 6502 targets, 32-bit on `amiga500`,
|
||||
`qemu68k`, and `virtual`. Use it for portable addresses, not `uword` or
|
||||
`long` chosen by assumption.
|
||||
- M68000 targets are big-endian and use 32-bit pointers. Load the `m68k-coder`
|
||||
skill for assembly details.
|
||||
- `float` requires `%import floats`.
|
||||
- Variables are statically allocated and zero-initialized. There is no normal
|
||||
call stack for locals, so recursion overwrites locals unless an explicit
|
||||
hardware or software stack is used. Iterative rewrites are preferred.
|
||||
- `memory(name, size)` reserves static memory and returns a `pointer`.
|
||||
- `str` and arrays have compile-time byte budgets. On 6502 targets the usual
|
||||
limit is 256 bytes; M68000 targets allow up to 32768 bytes. Use `memory()`
|
||||
for larger data.
|
||||
- Word arrays are split into LSB/MSB arrays by default. Use `@nosplit` when
|
||||
contiguous storage is required.
|
||||
- Variable tags follow the type and array dimensions, before the name:
|
||||
|
||||
; descending
|
||||
for i in 155 downto 20 {}
|
||||
for i in 155 to 20 step -1 {}
|
||||
|
||||
; iterate over array elements
|
||||
uword[] fib = [0, 1, 1, 2, 3, 5, 8, 13]
|
||||
uword num
|
||||
for num in fib {
|
||||
...
|
||||
}
|
||||
```prog8
|
||||
ubyte[8] @shared vera_storage
|
||||
uword @requirezp address
|
||||
```
|
||||
|
||||
### `while` loop — repeat while condition is true
|
||||
```
|
||||
while condition {
|
||||
; body
|
||||
break
|
||||
continue
|
||||
}
|
||||
```
|
||||
Use `@shared` for values accessed externally by assembly. Use `@zp` and
|
||||
`@requirezp` sparingly because zeropage is limited. Never assume virtual
|
||||
registers survive calls. Long operations may clobber `R12-R15`.
|
||||
|
||||
### `do`-`until` loop — always executes body at least once
|
||||
```
|
||||
do {
|
||||
; body
|
||||
break
|
||||
continue
|
||||
} until condition
|
||||
```
|
||||
## Syntax Pitfalls
|
||||
|
||||
### `repeat` loop — repeat a fixed number of times (most efficient)
|
||||
- Most efficient code generation — prefer over `for` when loop variable not needed
|
||||
- Omit count for infinite loop (still supports `break`)
|
||||
```
|
||||
repeat 15 {
|
||||
; body
|
||||
break
|
||||
continue
|
||||
}
|
||||
- Hex uses `$FF`, binary uses `%1010`, and casts use `expression as type`.
|
||||
- There is no automatic type widening: `byte * byte` remains a byte. Cast
|
||||
explicitly when a wider result is needed.
|
||||
- `&` is an untyped address and `&&` is a typed pointer. Complex pointer field
|
||||
assignments may require `ptr^^.field`.
|
||||
- `and`, `or`, `xor`, and `not` are logical operators. Use `&`, `|`, `^`, and
|
||||
`~` for bitwise operations.
|
||||
- `and` and `or` short-circuit, so the right operand may not be evaluated.
|
||||
- There is no `elif`, bare block, or semicolon statement separator. Semicolon
|
||||
starts a comment. Prefer one statement per line and four-space indentation.
|
||||
- `defer` executes registered statements in reverse registration order.
|
||||
- Array indexing starts at zero. Use `len(array)` rather than hardcoded sizes.
|
||||
- There is no function overloading. Call type-specific library routines such as
|
||||
`txt.print_ub` or `txt.print_w`.
|
||||
|
||||
; infinite:
|
||||
repeat {
|
||||
; body
|
||||
break if x==5
|
||||
}
|
||||
```
|
||||
## Control Flow
|
||||
|
||||
### `unroll` loop — compile-time code duplication
|
||||
- Not a real loop — duplicates body N times at compile time
|
||||
- No `break`/`continue` allowed
|
||||
- Only simple statements (assignments, calls) in body
|
||||
- Constant iteration count required
|
||||
```
|
||||
unroll 80 {
|
||||
cx16.VERA_DATA0 = 255
|
||||
}
|
||||
```
|
||||
Prog8 supports `if`/`else`, `when`, `for`, `while`, `do`/`until`, `repeat`,
|
||||
`unroll`, `break`, `continue`, `goto`, and labels. `unroll` duplicates a
|
||||
constant-count body at compile time and does not support `break` or `continue`.
|
||||
`repeat` is usually more efficient than `for` when no loop variable is needed.
|
||||
Use `if_cs`, `if_cc`, `if_z`, and `if_nz` for direct CPU-flag branches.
|
||||
|
||||
## Subroutines & Return Values
|
||||
- **Don't use `private` on subroutines and variables** (including nested ones) unless the user asks for it. Everything is public by default in Prog8 — follow that convention.
|
||||
- `inline` keyword for subroutines to suggest inlining
|
||||
- No **function overloading** (except builtins) and no **polymorphism** in general. This means you must call specific routines for different types (e.g., `txt.print_ub(val)` vs `txt.print_w(val)` instead of a generic `print(val)`). Cannot use builtin names (msw, lsw, msb, lsb, mkword, mklong, peek, peekw, peekl, etc.) as variable/sub names
|
||||
- Can return 0, 1, or multiple values: `a, b, c = routine()`. Use `void` to skip: `void routine()`, `a, void, c = routine()`
|
||||
- Nested subroutines access parent scope variables directly
|
||||
## Subroutines and Assembly
|
||||
|
||||
## Assembly Subroutines (asmsub)
|
||||
For low-level assembly that gets arguments via registers and returns values in registers.
|
||||
- **Syntax**: `[private] [inline] asmsub subname(params) [clobbers(regs)] [-> returns] { %asm {{ ... }} }`
|
||||
- **Body Restriction**: The body of an `asmsub` **must only contain** a single `%asm {{ ... }}` node. Regular Prog8 statements or nested blocks are NOT allowed.
|
||||
- **Parameters**: `type name @register` (e.g., `ubyte val @A`, `uword addr @AX`, `float f @FAC1`).
|
||||
- **Return Values**: `-> type @register` (e.g., `-> ubyte @A`, `-> bool @Pz` for immediate branch use).
|
||||
- **Registers**:
|
||||
- **8-bit**: `@A`, `@X`, `@Y`
|
||||
- **16-bit**: `@AX`, `@AY`, `@XY` (register pairs)
|
||||
- **Float**: `@FAC1`, `@FAC2` (Floating Point Accumulators)
|
||||
- **Virtual**: `@R0`–`@R15` (16-bit), `@R0R1`–`@R14R15` (32-bit combined)
|
||||
- **Status Flags (for returns)**: `@Pc` (Carry), `@Pz` (Zero), `@Pv` (Overflow), `@Pn` (Negative)
|
||||
- **Clobbers**: `clobbers (A, X, Y)` — list all hardware registers modified by the routine.
|
||||
- **Parameter names** are for documentation and type checking only. Use the registers in your assembly code.
|
||||
- **Inlining**: `inline asmsub` will paste the assembly code directly at the call site, avoiding `jsr`/`rts` overhead.
|
||||
- Subroutines can return zero, one, or multiple values:
|
||||
`a, b = routine()` or `void routine()`.
|
||||
- Avoid `private` unless requested. Prog8 symbols are public by default.
|
||||
- `asmsub` bodies contain only one `%asm {{ ... }}` node. Parameters are type
|
||||
checked and documented, but assembly must use the mapped registers. Declare
|
||||
every modified hardware register in `clobbers`.
|
||||
- `extsub` maps a signature to a fixed external address and has no body. Use
|
||||
it for ROM, kernel, drivers, or binary-library routines.
|
||||
- For inline assembly, load `asm6502-coder` for 64tass syntax, target-specific
|
||||
instructions, zero-page rules, and symbol references. Load `m68k-coder` for
|
||||
M68000 assembly.
|
||||
|
||||
## External Subroutines (extsub)
|
||||
Used to call routines at fixed memory addresses (like ROM KERNAL routines or third-party drivers).
|
||||
- **Syntax**: `[private] extsub [@bank <value>] address = subname(params) [clobbers(regs)] [-> returns]`
|
||||
- **Address**: Can be a hex literal (`$C000`) or a constant expression.
|
||||
- **Bank (optional)**: `@bank <integer>` (constant bank) or `@bank <identifier>` (variable bank).
|
||||
- **No Body**: Unlike `asmsub`, `extsub` has no `{ }` body; it just maps a signature to an address.
|
||||
- **Example**:
|
||||
```prog8
|
||||
; CX16 KERNAL CHROUT
|
||||
extsub $FFD2 = chrout(ubyte char @A) clobbers(A, X, Y)
|
||||
## Interrupts and Hardware
|
||||
|
||||
; Routine in a specific RAM bank
|
||||
extsub @bank 10 $C09F = audio_init() clobbers(A, X, Y) -> bool @Pc
|
||||
```
|
||||
- Keep IRQ handlers short. Set a flag and do substantial work in the main loop.
|
||||
- Virtual registers `R0-R15` are not preserved across IRQ handlers. Avoid them
|
||||
or save and restore them with the library routines.
|
||||
- CX16 IRQ handlers that touch VERA registers must save and restore VERA
|
||||
context.
|
||||
- Use only symbolic scratch names such as `P8ZP_SCRATCH_W1` and
|
||||
`cx16.r0`; never hardcode zeropage addresses.
|
||||
|
||||
## Inline Assembly
|
||||
When writing inline assembly (`%asm {{ }}` blocks or `asmsub` routines), load the **asm6502-coder** skill for formatting rules, symbol references, anonymous labels, and 64tass syntax details.
|
||||
## Verification
|
||||
|
||||
|
||||
|
||||
## Standard Library
|
||||
- Find routines, functions, variables, modules and signatures in the symbol dump file for the given compilation target.
|
||||
- Online location: https://prog8.readthedocs.io/en/latest/libraries.html#low-fi-variable-and-subroutine-definitions-in-all-available-library-modules they are linked there 1 for each compilation target
|
||||
- Structure: builtin functions, then module sections with variables/constants (`type name`) and subroutines (`name (params) -> returntype`)
|
||||
- Text output: `textio` module (`txt.print`, `txt.chrout`, `txt.print_b`/`_ub`/`_w`/`_uw`/`_l`/`_bool`, `txt.print_f` for floats, `txt.spc()`, `txt.nl()`). **Note: Prog8 has no function overloading**, so you cannot use `txt.print(number)` — you must call the specific routine for the type (e.g., `txt.print_ub(val)` for an unsigned byte).
|
||||
- Math: `math` module — integer trig (`sin8`, `cos8`) via fast LUTs; `math.rnd()` for random numbers
|
||||
- String conversion: `conv` module (`str_uword`, `str2word`, etc.) — for printing numbers use txt routines instead
|
||||
- Char operations: `strings` module (`isdigit`, `isxdigit`, `isupper`, `islower`, `isletter`, `isspace`, `isprint`) — use these instead of manual ASCII/PETSCII arithmetic
|
||||
- String functions return useful lengths — capture them: `len = strings.copy(dest, src)`, `len = strings.append(buf, text)`, `len = strings.upper(mystr)`
|
||||
|
||||
## Syntax & Formatting
|
||||
- Hex: `$FF` (not `0xFF`); Binary: `%1010` (not `0b1010`). Underscores for readability: `25_000_000`
|
||||
- 4-digit hex `$0000` = uword. No type suffixes (no `0L`). Cast: `expr as type`
|
||||
- Augmented assignment: `+=`, `-=`, `*=`, `/=`, `%=`, `&=`, `|=`, `^=`, `<<=`, `>>=`
|
||||
- **`;` starts a comment to end of line** — NOT a statement separator. One statement per line
|
||||
- **No `elif`**: use nested `else { if ... }`
|
||||
- Type casting: `expression as type` (e.g., `bytevar as word`). `as` has very low precedence (lower than arithmetic)
|
||||
- **No automatic type widening**: `byte*byte=byte` (overflow possible!). Cast explicitly
|
||||
- **No bare `{ }` blocks** like C/Java
|
||||
- Indentation: 4 spaces for .p8 and .asm files (no tabs)
|
||||
- Character encoding: 6502 targets use PETSCII by default (call `txt.lowercase()` at start for lowercase). Virtual target uses ISO (`%encoding iso` + `txt.iso()`)
|
||||
- **Test programs for CBM targets must only use lowercase text in string literals** (the C64/C128/PET32/CX16 targets all use PETSCII). In PETSCII, uppercase letters A-Z are the *graphics* symbols, so any uppercase text you put in a string literal is rendered as unpredictable graphics characters instead of readable letters. Use only lowercase (a-z) for any printed test output, labels, or expected-match strings on these targets; reserve uppercase for intentional PETSCII graphics. The `virtual` target (ISO) is unaffected and may use either case.
|
||||
- Array size inferred from initializer: `str[] types = ["a", "b", "c"]`
|
||||
- Enums: `Enum::Value` syntax (double colon), declared inside a block. They are syntactic sugar for a list of `const` declarations, not a type. Use enums for related values, `const` for standalone
|
||||
- Avoid `globals.XXXX` — move constants closer to where they're used
|
||||
- Member access through pointers: use `.` for both direct and pointer access. The compiler infers the type. For complex assignment targets, `^^` may be needed: `ptr^^.field = value`
|
||||
- Qualified names: must use full path from top level (e.g., `cx16.r0`, not relative)
|
||||
- Use the `virtual` target for behavioral tests when possible:
|
||||
`prog8c -target virtual -emu program.p8`.
|
||||
- Use `-check` for syntax and semantic checks without output generation.
|
||||
- Use `-noopt` to determine whether a failure is optimizer-related.
|
||||
- For IR execution, use `-vmtrace` when control flow needs inspection.
|
||||
- Do not modify a correct test program to work around a compiler crash. Reduce
|
||||
the case and fix the compiler instead.
|
||||
- When running `gradle` tests, grep for `SUCCESSFUL` to check if all tests
|
||||
passed, and `FAILED` to check for failures. Gradle does not print individual
|
||||
`PASSED` lines by default.
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
run: ./gradlew compileKotlin compileTestKotlin --console=plain
|
||||
|
||||
- name: Test
|
||||
run: ./gradlew test --continue --console=plain
|
||||
run: ./gradlew test --continue --max-workers=3 -PtestConcurrency=2 -PtestMaxHeap=8g --console=plain
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
@@ -52,4 +52,3 @@ jobs:
|
||||
with:
|
||||
name: test-results
|
||||
path: compiler/build/test-results/test/
|
||||
|
||||
@@ -15,6 +15,8 @@ out-old/
|
||||
.*cache/
|
||||
*.directory
|
||||
*.prg
|
||||
*.elf
|
||||
*.o
|
||||
*.bin
|
||||
*.p8ir
|
||||
*.bankedcalls
|
||||
|
||||
Generated
+1
-1
@@ -14,4 +14,4 @@
|
||||
<module name="prog8.codeGenNew6502.test" target="17" />
|
||||
</bytecodeTargetLevel>
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
Generated
+2
@@ -12,6 +12,8 @@
|
||||
<option value="$PROJECT_DIR$/codeCore" />
|
||||
<option value="$PROJECT_DIR$/codeGenCpu6502" />
|
||||
<option value="$PROJECT_DIR$/codeGenIntermediate" />
|
||||
<option value="$PROJECT_DIR$/codeGenM68k" />
|
||||
<option value="$PROJECT_DIR$/codeGenNew6502" />
|
||||
<option value="$PROJECT_DIR$/codeOptimizers" />
|
||||
<option value="$PROJECT_DIR$/compiler" />
|
||||
<option value="$PROJECT_DIR$/compilerAst" />
|
||||
|
||||
Generated
+1
-1
@@ -15,6 +15,6 @@
|
||||
</component>
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="externalSystemId" value="Gradle" />
|
||||
<option name="version" value="2.4.10" />
|
||||
<option name="version" value="2.4.20" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"context": {
|
||||
"fileName": "AGENTS.md"
|
||||
},
|
||||
"$version": 3,
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(prog8c *)",
|
||||
"Bash(gradle *)",
|
||||
"Bash(mkdir *)",
|
||||
"Bash(ls *)",
|
||||
"Bash(cat *)",
|
||||
"Bash(find *)",
|
||||
"Bash(sed *)",
|
||||
"Bash(grep *)",
|
||||
"Bash(xargs *)",
|
||||
"Bash(wc *)",
|
||||
"Bash(diff *)",
|
||||
"Bash(paste *)",
|
||||
"Bash(echo *)",
|
||||
"Bash(cp *)",
|
||||
"Bash(time)",
|
||||
"Bash(kotlinc *)",
|
||||
"Bash(python3 *)",
|
||||
"Bash(jar *)",
|
||||
"Bash(od *)",
|
||||
"Bash(xxd *)",
|
||||
"Bash(gcc *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@
|
||||
- ANTLR4 version 4.13 is used for the parser implementation.
|
||||
- Dependent library versions can be found in 'build.gradle.kts' and in the IntelliJ IDEA configuration files in .idea/libraries
|
||||
- The compiler main entrypoint is in the "compiler" module, in src/prog8/CompilerMain.kt
|
||||
- **Codebase knowledge graph**: For a structured overview of modules, their dependencies, key entities, the end-to-end compilation pipeline, the standard library layout, and the test structure, consult `docs/CODEBASE-KNOWLEDGE-GRAPH.md`. Read it when you need to understand how the compiler fits together before diving into specific files.
|
||||
|
||||
## Compilation Flow (High-Level)
|
||||
```
|
||||
@@ -53,7 +54,8 @@ Source → parseMainModule() → processAst() → optimizeAst() → postprocessA
|
||||
|
||||
### Target Differences
|
||||
- **CPU instruction set differences**: Only the CommanderX16 target (cx16) can use 65C02 instructions such as STZ. The other targets (C64, C128, PET32) can only use original 6502 instructions.
|
||||
- When writing or understanding assembly code, load the `asm6502-coder` skill for 64tass syntax and conventions.
|
||||
- When writing or understanding 6502/65C02 assembly code, load the `asm6502-coder` skill for 64tass syntax and conventions.
|
||||
- When writing or understanding M68K assembly code (amiga500, qemu68k targets), load the `m68k-coder` skill for vasm mot syntax and conventions.
|
||||
|
||||
|
||||
## DEBUGGING TIP: Use `-noopt` to isolate problems
|
||||
@@ -199,7 +201,7 @@ When the task involves writing or understanding `.p8` (Prog8 source) or `.p8ir`
|
||||
- **Before final verification**: Always run `gradle build` to ensure all tests pass
|
||||
- **After major refactoring**: Run `gradle build` to catch regressions
|
||||
- **When debugging test failures**: Use `gradle test --tests "*TestName*"` for specific tests
|
||||
- **When working on a standalone codegen module** (one that reads `.p8ir` files): Run `gradle :<module>:test --console=plain` instead of full `gradle build`. Compiler unit tests are irrelevant for IR-level changes. Do run full `gradle build` before final verification.
|
||||
- **When working on a standalone code gen module** (one that reads `.p8ir` files): Run `gradle :<module>:test --console=plain` instead of full `gradle build`. Compiler unit tests are irrelevant for IR-level changes. Do run full `gradle build` before final verification.
|
||||
|
||||
### Build command summary
|
||||
|
||||
@@ -242,6 +244,7 @@ When the task involves writing or understanding `.p8` (Prog8 source) or `.p8ir`
|
||||
|
||||
- Never assume your changes simply work, always test!
|
||||
- If the project does not have any testing tools, scripts, MCP tools, skills, etc. available for testing, ask the user whether testing should be skipped.
|
||||
- in prog8 code being tested, use '@shared' variables to avoid const-folding
|
||||
|
||||
### Automated Tests (gradle)
|
||||
Always use the `--console=plain` flag with these commands to avoid messing up the agent's text interface.
|
||||
@@ -281,7 +284,7 @@ gradle :compiler:compileTestKotlin --info 2>&1 | grep "^e:"
|
||||
- Unit tests use KoTest (FunSpec style)
|
||||
- Tests are in the "compiler" module's `test` directory (and some other modules)
|
||||
- Test config is in root `build.gradle.kts`; tests run in parallel
|
||||
- **When writing test programs**, add at the top: `%zeropage basicsafe` and `%option no_sysinit`
|
||||
- **When writing test programs**, add at the top: `%zeropage basicsafe` and `%option no_sysinit` - the latter ONLY when NOT writing for the amiga500 or qemu68k targets
|
||||
- When a test fails, the output shows "There were failing tests. See the report at:" - **read that HTML report**
|
||||
|
||||
### Compiler Unit Test Snippets
|
||||
|
||||
@@ -4,13 +4,14 @@ PayPal: []
|
||||
|
||||
[](https://prog8.readthedocs.io/)
|
||||
|
||||
Prog8 - Structured Programming Language for 8-bit 6502/65c02 microprocessors
|
||||
============================================================================
|
||||
Prog8 - Structured Programming Language for 6502/65c02 (8-bit) and 68000 (32-bit) microprocessors
|
||||
=================================================================================================
|
||||
|
||||
*Written by Irmen de Jong (irmen@razorvine.net)*
|
||||
|
||||
This is a structured programming language for the 8-bit 6502/6510/65c02 microprocessor from the late 1970's and 1980's
|
||||
as used in many home computers from that era. It is a medium to low level programming language,
|
||||
as used in many home computers from that era. It also supports the 32-bit Motorola 68000 CPU, for which the
|
||||
Commodore Amiga is the intended machine. It is a medium to low level programming language,
|
||||
which aims to provide many conveniences over raw assembly code (even when using a macro assembler).
|
||||
|
||||
**Want to buy me a coffee or a pizza perhaps?**
|
||||
@@ -104,9 +105,14 @@ What does Prog8 provide?
|
||||
- "c64": Commodore-64 (6502 like CPU)
|
||||
- "c128": Commodore-128 (6502 like CPU - the Z80 cpu mode is not supported)
|
||||
- "pet32": Commodore PET (limited support)
|
||||
- "amiga500": Commodore Amiga 500 (16/32-bit 68000 CPU; experimental)
|
||||
- "amiga1200": Commodore Amiga 1200 (32-bit 68020 CPU with optional FPU; experimental)
|
||||
- "qemu68k": Qemu M68k simulator (experimental)
|
||||
- via external configurable targets: Atari 800 XL, Neo6502, NES, C64 OS, Foenix F256, ...
|
||||
- If you only use standard kernal and prog8 library routines, it is possible to compile the *exact same program* for different machines (just change the compiler target flag)
|
||||
|
||||
The m68k targets (amiga500, amiga1200, qemu68k) and the optional `-newcodegen` 6502 code generator are experimental and not yet suitable for production code.
|
||||
|
||||
|
||||
|
||||
Additional required tools
|
||||
|
||||
+13
-12
@@ -41,7 +41,7 @@ rotate3d {
|
||||
for i in 0 to len(matrix_math.xcoor)-1 {
|
||||
rz = matrix_math.rotatedz[i]
|
||||
if rz >= 10 {
|
||||
persp = 600 + rz/64
|
||||
persp = 600 + (rz>>5)
|
||||
sx = matrix_math.rotatedx[i] / persp as byte + WIDTH/2
|
||||
sy = matrix_math.rotatedy[i] / persp as byte + HEIGHT/2
|
||||
;; txt.setcc(sx as ubyte, sy as ubyte, 46, 7)
|
||||
@@ -51,7 +51,7 @@ rotate3d {
|
||||
for i in 0 to len(matrix_math.xcoor)-1 {
|
||||
rz = matrix_math.rotatedz[i]
|
||||
if rz < 10 {
|
||||
persp = 600 + rz/64
|
||||
persp = 600 + (rz>>5)
|
||||
sx = matrix_math.rotatedx[i] / persp as byte + WIDTH/2
|
||||
sy = matrix_math.rotatedy[i] / persp as byte + HEIGHT/2
|
||||
;; txt.setcc(sx as ubyte, sy as ubyte, 81, 7)
|
||||
@@ -84,18 +84,19 @@ matrix_math {
|
||||
word wcosc = math.cos8(az)
|
||||
word wsinc = math.sin8(az)
|
||||
|
||||
word wcosa_sinb = wcosa*wsinb / 128
|
||||
word wsina_sinb = wsina*wsinb / 128
|
||||
; instead of (slow) /128, we simply shift 7 bits (and take the roundoff error)
|
||||
word wcosa_sinb = wcosa*wsinb >> 7
|
||||
word wsina_sinb = wsina*wsinb >> 7
|
||||
|
||||
word Axx = wcosa*wcosb / 128
|
||||
word Axy = (wcosa_sinb*wsinc - wsina*wcosc) / 128
|
||||
word Axz = (wcosa_sinb*wcosc + wsina*wsinc) / 128
|
||||
word Ayx = wsina*wcosb / 128
|
||||
word Ayy = (wsina_sinb*wsinc + wcosa*wcosc) / 128
|
||||
word Ayz = (wsina_sinb*wcosc - wcosa*wsinc) / 128
|
||||
word Axx = wcosa*wcosb >> 7
|
||||
word Axy = (wcosa_sinb*wsinc - wsina*wcosc) >> 7
|
||||
word Axz = (wcosa_sinb*wcosc + wsina*wsinc) >> 7
|
||||
word Ayx = wsina*wcosb >> 7
|
||||
word Ayy = (wsina_sinb*wsinc + wcosa*wcosc) >> 7
|
||||
word Ayz = (wsina_sinb*wcosc - wcosa*wsinc) >> 7
|
||||
word Azx = -wsinb
|
||||
word Azy = wcosb*wsinc / 128
|
||||
word Azz = wcosb*wcosc / 128
|
||||
word Azy = wcosb*wsinc >> 7
|
||||
word Azz = wcosb*wcosc >> 7
|
||||
|
||||
ubyte @zp i
|
||||
for i in 0 to len(xcoor)-1 {
|
||||
|
||||
@@ -220,10 +220,10 @@ btree {
|
||||
|
||||
arena {
|
||||
; extremely trivial arena allocator (that never frees)
|
||||
uword buffer = memory("arena", 2000, 0)
|
||||
uword next = buffer
|
||||
pointer buffer = memory("arena", 2000, 0)
|
||||
pointer next = buffer
|
||||
|
||||
sub alloc(ubyte size) -> uword {
|
||||
sub alloc(ubyte size) -> pointer {
|
||||
defer next += size
|
||||
return next
|
||||
}
|
||||
|
||||
+10
-6
@@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.10"
|
||||
kotlin("jvm") version "2.4.20"
|
||||
}
|
||||
|
||||
allprojects {
|
||||
@@ -52,8 +52,8 @@ subprojects {
|
||||
// Common test dependencies via Kotest BOM (Bill of Materials)
|
||||
// This manages all Kotest module versions centrally
|
||||
dependencies {
|
||||
testImplementation(platform("io.kotest:kotest-bom:6.2.3"))
|
||||
// implementation("com.github.irmen:ksim65:v2.1")
|
||||
testImplementation(platform("io.kotest:kotest-bom:6.2.4"))
|
||||
// implementation("org.codeberg.irmen:ksim65:v2.2")
|
||||
}
|
||||
|
||||
// Common sourceSets configuration
|
||||
@@ -72,16 +72,20 @@ subprojects {
|
||||
// Enable JUnit 5 (required for Kotest)
|
||||
useJUnitPlatform()
|
||||
|
||||
val testConcurrency = project.findProperty("testConcurrency")?.toString()?.toIntOrNull()
|
||||
?: (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
|
||||
project.findProperty("testMaxHeap")?.toString()?.let { maxHeapSize = it }
|
||||
|
||||
// Enable concurrent test execution for Kotest
|
||||
// Set parallelism to number of CPU cores
|
||||
jvmArgs("-Dkotest.framework.parallelism=${Runtime.getRuntime().availableProcessors()}")
|
||||
// Allow CI to override parallelism for memory-constrained runners
|
||||
jvmArgs("-Dkotest.framework.parallelism=$testConcurrency")
|
||||
|
||||
// Disable Kotest autoscan warning
|
||||
jvmArgs("-Dkotest.framework.classpath.scanning.autoscan.disable=true")
|
||||
|
||||
// Enable Gradle's parallel test execution (runs multiple test classes concurrently)
|
||||
// Use 50% of available processors to avoid over-subscription
|
||||
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
|
||||
maxParallelForks = testConcurrency
|
||||
|
||||
// Allow filtering tests via -PtestFilter="*TestName*" from command line
|
||||
// Example: gradle test -PtestFilter="*TestLookup*"
|
||||
|
||||
+7
-6
@@ -1,7 +1,6 @@
|
||||
package prog8.codegen.cpu6502
|
||||
package prog8.code.assembly
|
||||
|
||||
import prog8.code.GENERATED_LABEL_PREFIX
|
||||
import prog8.code.IAssemblyProgram
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.ICompilationTarget
|
||||
import prog8.code.core.IErrorReporter
|
||||
@@ -12,10 +11,10 @@ import prog8.code.target.PETTarget
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
internal class AssemblyProgram(
|
||||
override val name: String,
|
||||
outputDir: Path,
|
||||
private val compTarget: ICompilationTarget) : IAssemblyProgram {
|
||||
class AssemblyProgram6502(
|
||||
override val name: String,
|
||||
outputDir: Path,
|
||||
private val compTarget: ICompilationTarget) : IAssemblyProgram {
|
||||
|
||||
override val irInstructionCount: Int = 0
|
||||
override val irChunkCount: Int = 0
|
||||
@@ -133,6 +132,7 @@ internal class AssemblyProgram(
|
||||
|
||||
assemblerCommand = addRemainingOptions(command, binFile, assemblyFile)
|
||||
}
|
||||
else -> error("Unsupported output type: ${compTarget.defaultOutputType}")
|
||||
}
|
||||
|
||||
val proc = ProcessBuilder(assemblerCommand)
|
||||
@@ -192,3 +192,4 @@ internal class AssemblyProgram(
|
||||
viceMonListFile.toFile().appendText(breakpoints.joinToString("\n") + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package prog8.code.assembly
|
||||
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.IErrorReporter
|
||||
|
||||
interface IAssemblyProgram {
|
||||
val name: String
|
||||
val irInstructionCount: Int
|
||||
val irChunkCount: Int
|
||||
val irRegisterCount: Int
|
||||
fun assemble(options: CompilationOptions, errors: IErrorReporter): Boolean
|
||||
}
|
||||
@@ -24,7 +24,7 @@ class CallConvention(val params: List<ParamConvention>, val returns: ReturnConve
|
||||
class FParam(val name: String, vararg val possibleDatatypes: BaseDataType)
|
||||
|
||||
|
||||
private val IterableDatatypes = arrayOf(BaseDataType.STR, BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW)
|
||||
private val IterableDatatypes = arrayOf(BaseDataType.STR, BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW, BaseDataType.ARRAY_POINTER)
|
||||
private val IntegerDatatypes = arrayOf(BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.LONG)
|
||||
private val NumericDatatypes = arrayOf(BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
|
||||
|
||||
@@ -33,7 +33,7 @@ class FSignature(val pure: Boolean, // does it have side effects?
|
||||
val returnTypes: Array<BaseDataType>,
|
||||
vararg val parameters: FParam) {
|
||||
|
||||
fun callConvention(actualParamTypes: List<BaseDataType>): CallConvention {
|
||||
fun callConventionFor6502(actualParamTypes: List<BaseDataType>): CallConvention {
|
||||
val returns: ReturnConvention
|
||||
if(returnTypes.isEmpty())
|
||||
returns = ReturnConvention(null, null)
|
||||
@@ -41,7 +41,7 @@ class FSignature(val pure: Boolean, // does it have side effects?
|
||||
val returnType = returnTypes[0]
|
||||
returns = when (returnType) {
|
||||
BaseDataType.UBYTE, BaseDataType.BYTE -> ReturnConvention(returnType, RegisterOrPair.A)
|
||||
BaseDataType.UWORD, BaseDataType.WORD -> ReturnConvention(returnType, RegisterOrPair.AY)
|
||||
BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.POINTER -> ReturnConvention(returnType, RegisterOrPair.AY)
|
||||
BaseDataType.LONG -> ReturnConvention(returnType, RegisterOrPair.R14R15)
|
||||
BaseDataType.FLOAT -> ReturnConvention(returnType, RegisterOrPair.FAC1)
|
||||
in IterableDatatypes -> ReturnConvention(returnType, RegisterOrPair.AY)
|
||||
@@ -49,7 +49,7 @@ class FSignature(val pure: Boolean, // does it have side effects?
|
||||
// return type depends on arg type
|
||||
when (val paramType = actualParamTypes.first()) {
|
||||
BaseDataType.UBYTE, BaseDataType.BYTE -> ReturnConvention(paramType, RegisterOrPair.A)
|
||||
BaseDataType.UWORD, BaseDataType.WORD -> ReturnConvention(paramType, RegisterOrPair.AY)
|
||||
BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.POINTER -> ReturnConvention(paramType, RegisterOrPair.AY)
|
||||
BaseDataType.LONG -> ReturnConvention(returnType, RegisterOrPair.R14R15)
|
||||
BaseDataType.FLOAT -> ReturnConvention(paramType, RegisterOrPair.FAC1)
|
||||
in IterableDatatypes -> ReturnConvention(paramType, RegisterOrPair.AY)
|
||||
@@ -109,8 +109,8 @@ val BuiltinFunctions: Map<String, FSignature> = mapOf(
|
||||
"prog8_lib_square_word" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("value", BaseDataType.WORD, BaseDataType.UWORD)),
|
||||
"prog8_lib_square_long" to FSignature(true, arrayOf(BaseDataType.LONG), FParam("value", BaseDataType.LONG)),
|
||||
"prog8_lib_structalloc" to FSignature(true, arrayOf(BaseDataType.UWORD)),
|
||||
"prog8_lib_copylong" to FSignature(false, emptyArray(), FParam("pointer1", BaseDataType.UWORD), FParam("pointer2", BaseDataType.UWORD)),
|
||||
"prog8_lib_copyfloat" to FSignature(false, emptyArray(), FParam("pointer1", BaseDataType.UWORD), FParam("pointer2", BaseDataType.UWORD)),
|
||||
"prog8_lib_copylong" to FSignature(false, emptyArray(), FParam("pointer1", BaseDataType.POINTER, BaseDataType.UWORD, BaseDataType.LONG), FParam("pointer2", BaseDataType.POINTER, BaseDataType.UWORD, BaseDataType.LONG)),
|
||||
"prog8_lib_copyfloat" to FSignature(false, emptyArray(), FParam("pointer1", BaseDataType.POINTER, BaseDataType.UWORD, BaseDataType.LONG), FParam("pointer2", BaseDataType.POINTER, BaseDataType.UWORD, BaseDataType.LONG)),
|
||||
"abs" to FSignature(true, arrayOf(BaseDataType.UNDEFINED), FParam("value", *NumericDatatypes)),
|
||||
"abs__byte" to FSignature(true, arrayOf(BaseDataType.UBYTE), FParam("value", BaseDataType.BYTE)),
|
||||
"abs__word" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("value", BaseDataType.WORD)),
|
||||
@@ -158,33 +158,37 @@ val BuiltinFunctions: Map<String, FSignature> = mapOf(
|
||||
"max__word" to FSignature(true, arrayOf(BaseDataType.WORD), FParam("val1", BaseDataType.WORD), FParam("val2", BaseDataType.WORD)),
|
||||
"max__uword" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("val1", BaseDataType.UWORD), FParam("val2", BaseDataType.UWORD)),
|
||||
"max__long" to FSignature(true, arrayOf(BaseDataType.LONG), FParam("val1", BaseDataType.LONG), FParam("val2", BaseDataType.LONG)),
|
||||
"peek" to FSignature(true, arrayOf(BaseDataType.UBYTE), FParam("address", BaseDataType.UWORD)),
|
||||
"peekbool" to FSignature(true, arrayOf(BaseDataType.BOOL), FParam("address", BaseDataType.UWORD)),
|
||||
"peekw" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("address", BaseDataType.UWORD)),
|
||||
"peekl" to FSignature(true, arrayOf(BaseDataType.LONG), FParam("address", BaseDataType.UWORD)),
|
||||
"peekf" to FSignature(true, arrayOf(BaseDataType.FLOAT), FParam("address", BaseDataType.UWORD)),
|
||||
"poke" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD), FParam("value", BaseDataType.UBYTE, BaseDataType.BYTE)),
|
||||
"pokebool" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD), FParam("value", BaseDataType.BOOL)),
|
||||
"pokebowl" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD), FParam("value", BaseDataType.BOOL)),
|
||||
"pokew" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD), FParam("value", BaseDataType.UWORD, BaseDataType.WORD)),
|
||||
"pokel" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD), FParam("value", BaseDataType.LONG)),
|
||||
"pokef" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD), FParam("value", BaseDataType.FLOAT)),
|
||||
"pokemon" to FSignature(false, arrayOf(BaseDataType.UBYTE), FParam("address", BaseDataType.UWORD), FParam("value", BaseDataType.UBYTE)),
|
||||
"peek" to FSignature(true, arrayOf(BaseDataType.UBYTE), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER)),
|
||||
"peekbool" to FSignature(true, arrayOf(BaseDataType.BOOL), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER)),
|
||||
"peekw" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER)),
|
||||
"peekl" to FSignature(true, arrayOf(BaseDataType.LONG), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER)),
|
||||
"peekf" to FSignature(true, arrayOf(BaseDataType.FLOAT), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER)),
|
||||
"peekp" to FSignature(true, arrayOf(BaseDataType.POINTER), FParam("address", BaseDataType.POINTER, BaseDataType.UWORD, BaseDataType.LONG)),
|
||||
"poke" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER), FParam("value", BaseDataType.UBYTE, BaseDataType.BYTE)),
|
||||
"pokebool" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER), FParam("value", BaseDataType.BOOL)),
|
||||
"pokebowl" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER), FParam("value", BaseDataType.BOOL)),
|
||||
"pokew" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER), FParam("value", BaseDataType.UWORD, BaseDataType.WORD)),
|
||||
"pokel" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER), FParam("value", BaseDataType.LONG)),
|
||||
"pokef" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER), FParam("value", BaseDataType.FLOAT)),
|
||||
"pokep" to FSignature(false, emptyArray(), FParam("address", BaseDataType.POINTER, BaseDataType.UWORD, BaseDataType.LONG), FParam("value", BaseDataType.POINTER)),
|
||||
"pokemon" to FSignature(false, arrayOf(BaseDataType.UBYTE), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER), FParam("value", BaseDataType.UBYTE)),
|
||||
"rsave" to FSignature(false, emptyArray()),
|
||||
"rrestore" to FSignature(false, emptyArray()),
|
||||
"memory" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("name", BaseDataType.STR), FParam("size", BaseDataType.UWORD), FParam("alignment", BaseDataType.UWORD)),
|
||||
"memory__ref" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("name", BaseDataType.STR)),
|
||||
"callfar" to FSignature(false, arrayOf(BaseDataType.UWORD), FParam("bank", BaseDataType.UBYTE), FParam("address", BaseDataType.UWORD), FParam("arg", BaseDataType.UWORD)),
|
||||
"callfar2" to FSignature(false, arrayOf(BaseDataType.UWORD), FParam("bank", BaseDataType.UBYTE), FParam("address", BaseDataType.UWORD), FParam("argA", BaseDataType.UBYTE), FParam("argX", BaseDataType.UBYTE), FParam("argY", BaseDataType.UBYTE), FParam("argC", BaseDataType.BOOL)),
|
||||
"call" to FSignature(false, arrayOf(BaseDataType.UWORD), FParam("address", BaseDataType.UWORD)),
|
||||
"memory" to FSignature(true, arrayOf(BaseDataType.POINTER), FParam("name", BaseDataType.STR), FParam("size", BaseDataType.UWORD), FParam("alignment", BaseDataType.UWORD)),
|
||||
"memory__ref" to FSignature(true, arrayOf(BaseDataType.POINTER), FParam("name", BaseDataType.STR)),
|
||||
"callfar" to FSignature(false, arrayOf(BaseDataType.UWORD), FParam("bank", BaseDataType.UBYTE), FParam("address", BaseDataType.POINTER), FParam("arg", BaseDataType.UWORD)),
|
||||
"callfar2" to FSignature(false, arrayOf(BaseDataType.UWORD), FParam("bank", BaseDataType.UBYTE), FParam("address", BaseDataType.POINTER), FParam("argA", BaseDataType.UBYTE), FParam("argX", BaseDataType.UBYTE), FParam("argY", BaseDataType.UBYTE), FParam("argC", BaseDataType.BOOL)),
|
||||
"call" to FSignature(false, arrayOf(BaseDataType.UWORD), FParam("address", BaseDataType.POINTER)),
|
||||
"push" to FSignature(false, emptyArray(), FParam("value", BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.BOOL)),
|
||||
"pushw" to FSignature(false, emptyArray(), FParam("value", BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.POINTER)),
|
||||
"pushl" to FSignature(false, emptyArray(), FParam("value", BaseDataType.LONG)),
|
||||
"pushf" to FSignature(false, emptyArray(), FParam("value", BaseDataType.FLOAT)),
|
||||
"pushp" to FSignature(false, emptyArray(), FParam("value", BaseDataType.POINTER)),
|
||||
"pop" to FSignature(false, arrayOf(BaseDataType.UBYTE)),
|
||||
"popw" to FSignature(false, arrayOf(BaseDataType.UWORD)),
|
||||
"popl" to FSignature(false, arrayOf(BaseDataType.LONG)),
|
||||
"popf" to FSignature(false, arrayOf(BaseDataType.FLOAT)),
|
||||
"popp" to FSignature(false, arrayOf(BaseDataType.POINTER)),
|
||||
)
|
||||
|
||||
val InplaceModifyingBuiltinFunctions = setOf(
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.nio.file.Path
|
||||
import kotlin.io.path.Path
|
||||
|
||||
|
||||
@Suppress("unused")
|
||||
class CompilationOptions(val output: OutputType,
|
||||
val launcher: CbmPrgLauncherType,
|
||||
val zeropage: ZeropageType,
|
||||
@@ -17,6 +18,7 @@ class CompilationOptions(val output: OutputType,
|
||||
// these are set later, based on command line arguments or options in the source code:
|
||||
var loadAddress: UInt,
|
||||
var memtopAddress: UInt,
|
||||
var varsAddress: UInt? = null,
|
||||
var warnSymbolShadowing: Boolean = false,
|
||||
var warnImplicitTypeCast: Boolean = false,
|
||||
var optimize: Boolean = false,
|
||||
@@ -25,16 +27,16 @@ class CompilationOptions(val output: OutputType,
|
||||
var includeSourcelines: Boolean = false,
|
||||
var dumpVariables: Boolean = false,
|
||||
var dumpSymbols: Boolean = false,
|
||||
var newCodegen: Boolean = false,
|
||||
var varsHighBank: Int? = null,
|
||||
var varsGolden: Boolean = false,
|
||||
var slabsHighBank: Int? = null,
|
||||
var slabsGolden: Boolean = false,
|
||||
var breakpointCpuInstruction: String? = null,
|
||||
var ignoreFootguns: Boolean = false,
|
||||
var outputDir: Path = Path(""),
|
||||
var quiet: Boolean = false,
|
||||
var profilingInstrumentation: Boolean = false,
|
||||
var symbolDefs: Map<String, String> = emptyMap()
|
||||
var symbolDefs: Map<String, String> = emptyMap(),
|
||||
var privateSymbols: Boolean = false
|
||||
) {
|
||||
init {
|
||||
compTarget.initializeMemoryAreas(this)
|
||||
@@ -57,6 +59,7 @@ class CompilationOptions(val output: OutputType,
|
||||
private var compilerVersion: String = "unknown"
|
||||
private var loadAddress: UInt = compTarget.PROGRAM_LOAD_ADDRESS
|
||||
private var memtopAddress: UInt = compTarget.PROGRAM_MEMTOP_ADDRESS
|
||||
private var varsAddress: UInt? = null
|
||||
private var warnSymbolShadowing: Boolean = false
|
||||
private var warnImplicitTypeCast: Boolean = false
|
||||
private var optimize: Boolean = false
|
||||
@@ -67,13 +70,12 @@ class CompilationOptions(val output: OutputType,
|
||||
private var dumpSymbols: Boolean = false
|
||||
private var varsHighBank: Int? = null
|
||||
private var varsGolden: Boolean = false
|
||||
private var slabsHighBank: Int? = null
|
||||
private var slabsGolden: Boolean = false
|
||||
private var breakpointCpuInstruction: String? = null
|
||||
private var ignoreFootguns: Boolean = false
|
||||
private var outputDir: Path = Path("")
|
||||
private var quiet: Boolean = false
|
||||
private var profilingInstrumentation: Boolean = false
|
||||
private var newCodegen: Boolean = false
|
||||
private var symbolDefs: Map<String, String> = emptyMap()
|
||||
|
||||
fun output(output: OutputType) = apply { this.output = output }
|
||||
@@ -87,6 +89,7 @@ class CompilationOptions(val output: OutputType,
|
||||
fun compilerVersion(compilerVersion: String) = apply { this.compilerVersion = compilerVersion }
|
||||
fun loadAddress(loadAddress: UInt) = apply { this.loadAddress = loadAddress }
|
||||
fun memtopAddress(memtopAddress: UInt) = apply { this.memtopAddress = memtopAddress }
|
||||
fun varsAddress(varsAddress: UInt?) = apply { this.varsAddress = varsAddress }
|
||||
fun warnSymbolShadowing(warnSymbolShadowing: Boolean) = apply { this.warnSymbolShadowing = warnSymbolShadowing }
|
||||
fun warnImplicitTypeCast(warnImplicitTypeCast: Boolean) = apply { this.warnImplicitTypeCast = warnImplicitTypeCast }
|
||||
fun optimize(optimize: Boolean) = apply { this.optimize = optimize }
|
||||
@@ -97,21 +100,20 @@ class CompilationOptions(val output: OutputType,
|
||||
fun dumpSymbols(dumpSymbols: Boolean) = apply { this.dumpSymbols = dumpSymbols }
|
||||
fun varsHighBank(varsHighBank: Int?) = apply { this.varsHighBank = varsHighBank }
|
||||
fun varsGolden(varsGolden: Boolean) = apply { this.varsGolden = varsGolden }
|
||||
fun slabsHighBank(slabsHighBank: Int?) = apply { this.slabsHighBank = slabsHighBank }
|
||||
fun slabsGolden(slabsGolden: Boolean) = apply { this.slabsGolden = slabsGolden }
|
||||
fun breakpointCpuInstruction(breakpointCpuInstruction: String?) = apply { this.breakpointCpuInstruction = breakpointCpuInstruction }
|
||||
fun ignoreFootguns(ignoreFootguns: Boolean) = apply { this.ignoreFootguns = ignoreFootguns }
|
||||
fun outputDir(outputDir: Path) = apply { this.outputDir = outputDir }
|
||||
fun quiet(quiet: Boolean) = apply { this.quiet = quiet }
|
||||
fun profilingInstrumentation(profilingInstrumentation: Boolean) = apply { this.profilingInstrumentation = profilingInstrumentation }
|
||||
fun symbolDefs(symbolDefs: Map<String, String>) = apply { this.symbolDefs = symbolDefs }
|
||||
fun newCodegen(newCodegen: Boolean) = apply { this.newCodegen = newCodegen }
|
||||
|
||||
fun build(): CompilationOptions {
|
||||
return CompilationOptions(
|
||||
output, launcher, zeropage, zpReserved, zpAllowed, floats, noSysInit, romable, compTarget, compilerVersion,
|
||||
loadAddress, memtopAddress, warnSymbolShadowing, warnImplicitTypeCast, optimize, asmQuiet, asmListfile,
|
||||
includeSourcelines, dumpVariables, dumpSymbols, varsHighBank, varsGolden,
|
||||
slabsHighBank, slabsGolden, breakpointCpuInstruction, ignoreFootguns, outputDir, quiet,
|
||||
loadAddress, memtopAddress, varsAddress, warnSymbolShadowing, warnImplicitTypeCast, optimize, asmQuiet, asmListfile,
|
||||
includeSourcelines, dumpVariables, dumpSymbols, newCodegen, varsHighBank, varsGolden,
|
||||
breakpointCpuInstruction, ignoreFootguns, outputDir, quiet,
|
||||
profilingInstrumentation, symbolDefs
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ enum class BaseDataType {
|
||||
ARRAY_SPLITW, // pass by reference, split word layout, subtype is the element type (restricted to word types)
|
||||
POINTER, // typed pointer, subtype is whatever type is pointed to
|
||||
STRUCT_INSTANCE, // the actual instance of a struct (not directly supported in the language yet, but we need its type)
|
||||
ARRAY_POINTER, // array of pointers (uwords), subtype is whatever type each element points to
|
||||
ARRAY_POINTER, // array of pointers (uwords or longs), subtype is whatever type each element points to
|
||||
UNDEFINED;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ enum class BaseDataType {
|
||||
this.isByteOrBool -> false
|
||||
this.isWord -> other.isByteOrBool
|
||||
this == LONG -> other.isByteOrBool || other.isWord
|
||||
this == STR && other == UWORD || this == UWORD && other == STR -> false
|
||||
this == STR && (other == UWORD || other == LONG) || this == UWORD && other == STR -> false
|
||||
this.isArray && other.isArray -> false
|
||||
this.isArray -> other.isByteOrBool
|
||||
this == STR -> other.isByteOrBool
|
||||
@@ -38,14 +38,20 @@ enum class BaseDataType {
|
||||
else -> true
|
||||
}
|
||||
|
||||
fun equalsSize(other: BaseDataType) =
|
||||
fun equalsSize(other: BaseDataType): Boolean = equalsSize(other, null)
|
||||
fun equalsSize(other: BaseDataType, memsizer: IMemSizer?): Boolean =
|
||||
when {
|
||||
this == other -> true
|
||||
this.isArray && other.isArray -> true
|
||||
this.isByteOrBool -> other.isByteOrBool
|
||||
this.isWord -> other.isWord || other.isPointer
|
||||
this.isPointer -> other.isWord
|
||||
this == STR && other== UWORD || this== UWORD && other== STR -> true
|
||||
this.isWord -> other.isWord || (other.isPointer && (memsizer == null || memsizer.POINTER_MEM_SIZE == 2u))
|
||||
this.isLong -> other.isLong || (other.isPointer && memsizer?.POINTER_MEM_SIZE != null && memsizer.POINTER_MEM_SIZE != 2u)
|
||||
this.isPointer -> when(memsizer?.POINTER_MEM_SIZE) {
|
||||
null -> other.isWord || other.isPointer
|
||||
2u -> other.isWord || other.isPointer
|
||||
else -> other.isLong || other.isPointer
|
||||
}
|
||||
this == STR && (other == UWORD || other == LONG) || (this == UWORD || this == LONG) && other == STR -> true
|
||||
this == STR && other.isArray -> true
|
||||
this.isArray && other == STR -> true
|
||||
else -> false
|
||||
@@ -68,6 +74,15 @@ val BaseDataType.isSignedWord get() = this == BaseDataType.WORD
|
||||
val BaseDataType.isLong get() = this == BaseDataType.LONG
|
||||
val BaseDataType.isFloat get() = this == BaseDataType.FLOAT
|
||||
val BaseDataType.isInteger get() = this in setOf(BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.LONG)
|
||||
val BaseDataType.maxUnsignedValue: Double
|
||||
get() = when (this) {
|
||||
BaseDataType.UBYTE -> 255.0
|
||||
BaseDataType.BYTE -> 127.0
|
||||
BaseDataType.UWORD -> 65535.0
|
||||
BaseDataType.WORD -> 32767.0
|
||||
BaseDataType.LONG -> 2147483647.0
|
||||
else -> Double.MAX_VALUE
|
||||
}
|
||||
val BaseDataType.isIntegerOrBool get() = this in setOf(BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.LONG, BaseDataType.BOOL)
|
||||
val BaseDataType.isWordOrByteOrBool get() = this in setOf(BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.BOOL)
|
||||
val BaseDataType.isNumeric get() = this == BaseDataType.FLOAT || this.isInteger
|
||||
@@ -80,7 +95,6 @@ val BaseDataType.isArray get() = this == BaseDataType.ARRAY || this == BaseDataT
|
||||
val BaseDataType.isPointer get() = this == BaseDataType.POINTER
|
||||
val BaseDataType.isStructInstance get() = this == BaseDataType.STRUCT_INSTANCE
|
||||
val BaseDataType.isPointerArray get() = this == BaseDataType.ARRAY_POINTER
|
||||
val BaseDataType.isSplitWordArray get() = this == BaseDataType.ARRAY_SPLITW || this == BaseDataType.ARRAY_POINTER // pointer arrays are also always stored as split uwords
|
||||
val BaseDataType.isIterable get() = this in setOf(BaseDataType.STR, BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW, BaseDataType.ARRAY_POINTER)
|
||||
val BaseDataType.isPassByRef get() = this.isIterable && !this.isPointer
|
||||
val BaseDataType.isPassByValue get() = !this.isIterable || this.isPointer
|
||||
@@ -122,8 +136,10 @@ class DataType private constructor(
|
||||
require(sub!=null || subType!=null || subTypeFromAntlr!=null)
|
||||
}
|
||||
base.isArray -> {
|
||||
require(sub != null && subType==null && subTypeFromAntlr==null)
|
||||
if(base.isSplitWordArray)
|
||||
require(sub != null)
|
||||
// subType and subTypeFromAntlr allowed for arrays with struct element types
|
||||
// (from parser creating ARRAY(LONG) directly for 32-bit targets, or from VariousCleanups conversion)
|
||||
if(base == BaseDataType.ARRAY_SPLITW)
|
||||
require(sub == BaseDataType.UWORD || sub == BaseDataType.WORD)
|
||||
}
|
||||
base==BaseDataType.STR -> require(sub==BaseDataType.UBYTE) { "string subtype should be ubyte" }
|
||||
@@ -139,7 +155,12 @@ class DataType private constructor(
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is DataType) return false
|
||||
return base == other.base && sub == other.sub && (subType==other.subType || subType!!.sameas(other.subType!!))
|
||||
if (base != other.base || sub != other.sub) return false
|
||||
val st1 = subType
|
||||
val st2 = other.subType
|
||||
if (st1 == st2) return true
|
||||
if (st1 == null || st2 == null) return false
|
||||
return st1.sameas(st2)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = Objects.hash(base, sub, subType)
|
||||
@@ -174,26 +195,33 @@ class DataType private constructor(
|
||||
BaseDataType.FLOAT to DataType(BaseDataType.FLOAT, null, null),
|
||||
BaseDataType.BOOL to DataType(BaseDataType.BOOL, null, null),
|
||||
BaseDataType.STR to DataType(BaseDataType.STR, BaseDataType.UBYTE, null),
|
||||
BaseDataType.POINTER to DataType(BaseDataType.POINTER, null, null),
|
||||
BaseDataType.UNDEFINED to DataType(BaseDataType.UNDEFINED, null, null)
|
||||
)
|
||||
|
||||
fun forDt(dt: BaseDataType): DataType {
|
||||
if(dt.isStructInstance)
|
||||
TODO("cannot use struct instance as a data type (yet) - use a pointer instead")
|
||||
return DataType(BaseDataType.STRUCT_INSTANCE, null, null)
|
||||
return simpletypes.getValue(dt)
|
||||
}
|
||||
|
||||
fun arrayFor(elementDt: BaseDataType, splitwordarray: Boolean=true): DataType {
|
||||
fun arrayFor(elementDt: BaseDataType, memsizer: IMemSizer): DataType {
|
||||
// wether or not a word-array should be split-words, is determined later
|
||||
require(!elementDt.isPointer) { "use other array constructor for arrays of pointers" }
|
||||
val actualElementDt = if(elementDt==BaseDataType.STR) BaseDataType.UWORD else elementDt // array of strings is actually just an array of UWORD pointers
|
||||
return if(splitwordarray && actualElementDt.isWord)
|
||||
DataType(BaseDataType.ARRAY_SPLITW, actualElementDt, null)
|
||||
else {
|
||||
if(actualElementDt.isNumericOrBool)
|
||||
DataType(BaseDataType.ARRAY, actualElementDt, null)
|
||||
else
|
||||
throw NoSuchElementException("invalid basic element dt $elementDt")
|
||||
}
|
||||
// On 32-bit targets (m68k, virtual), str arrays become arrays of LONG (4-byte pointers)
|
||||
// via pointerBaseType. On 6502 targets they become arrays of UWORD (2-byte pointers).
|
||||
val actualElementDt = if(elementDt==BaseDataType.STR) {
|
||||
memsizer.pointerBaseType
|
||||
} else elementDt
|
||||
if(actualElementDt.isNumericOrBool)
|
||||
return DataType(BaseDataType.ARRAY, actualElementDt, null)
|
||||
else
|
||||
throw NoSuchElementException("invalid basic element dt $elementDt")
|
||||
}
|
||||
|
||||
fun splitWordArrayFor(elementDt: BaseDataType): DataType {
|
||||
require(elementDt.isWord) { "split word array element type must be word" }
|
||||
return DataType(BaseDataType.ARRAY_SPLITW, elementDt, null)
|
||||
}
|
||||
|
||||
fun arrayOfPointersTo(sub: BaseDataType): DataType = DataType(BaseDataType.ARRAY_POINTER, sub, null)
|
||||
@@ -201,6 +229,11 @@ class DataType private constructor(
|
||||
fun arrayOfPointersFromAntlrTo(sub: BaseDataType?, identifier: List<String>?): DataType =
|
||||
DataType(BaseDataType.ARRAY_POINTER, sub, null, identifier)
|
||||
|
||||
// array of struct instances: sub is STRUCT_INSTANCE, subType carries the resolved struct
|
||||
fun arrayOfStructs(structType: ISubType): DataType = DataType(BaseDataType.ARRAY, BaseDataType.STRUCT_INSTANCE, structType)
|
||||
fun arrayOfStructsFromAntlr(struct: List<String>): DataType =
|
||||
DataType(BaseDataType.ARRAY, BaseDataType.STRUCT_INSTANCE, null, subTypeFromAntlr = struct)
|
||||
|
||||
fun pointer(base: BaseDataType): DataType = DataType(BaseDataType.POINTER, base, null)
|
||||
fun pointer(dt: DataType): DataType = if(dt.isBasic)
|
||||
DataType(BaseDataType.POINTER, dt.base, null)
|
||||
@@ -217,43 +250,74 @@ class DataType private constructor(
|
||||
// DataType Methods
|
||||
// ============================================================================
|
||||
|
||||
fun elementToArray(splitwords: Boolean = true): DataType {
|
||||
return if (base == BaseDataType.UWORD || base == BaseDataType.WORD || base == BaseDataType.STR) arrayFor(base, splitwords)
|
||||
else if(base.isPointer) {
|
||||
if(subType!=null) arrayOfPointersTo(subType)
|
||||
else if(subTypeFromAntlr!=null) arrayOfPointersFromAntlrTo(sub, subTypeFromAntlr)
|
||||
else arrayOfPointersTo(sub!!)
|
||||
fun elementToArray(target: ICompilationTarget, splitwords: Boolean = true): DataType {
|
||||
fun pointerArray(): DataType =
|
||||
if (subType != null)
|
||||
arrayOfPointersTo(subType)
|
||||
else if (subTypeFromAntlr != null)
|
||||
arrayOfPointersFromAntlrTo(sub, subTypeFromAntlr)
|
||||
else
|
||||
arrayOfPointersTo(sub!!)
|
||||
|
||||
if(base == BaseDataType.STRUCT_INSTANCE) {
|
||||
val st = subType
|
||||
return if(st != null)
|
||||
arrayOfStructs(st)
|
||||
else
|
||||
arrayOfStructsFromAntlr(subTypeFromAntlr!!)
|
||||
}
|
||||
else arrayFor(base, false)
|
||||
|
||||
if(!target.cpu.is6502) {
|
||||
return if (base.isPointer)
|
||||
pointerArray()
|
||||
else
|
||||
arrayFor(base, target)
|
||||
}
|
||||
|
||||
return if (splitwords && (base == BaseDataType.UWORD || base == BaseDataType.WORD || base == BaseDataType.STR))
|
||||
splitWordArrayFor(base)
|
||||
else if(base.isPointer)
|
||||
pointerArray()
|
||||
else
|
||||
arrayFor(base, target)
|
||||
}
|
||||
|
||||
fun elementType(): DataType =
|
||||
when {
|
||||
isPointerArray -> DataType(BaseDataType.POINTER, sub, subType)
|
||||
base.isArray || base==BaseDataType.STR -> forDt(sub!!)
|
||||
base.isArray || base==BaseDataType.STR -> {
|
||||
if (sub == BaseDataType.STRUCT_INSTANCE && subType != null)
|
||||
return DataType(BaseDataType.STRUCT_INSTANCE, null, subType)
|
||||
val dt = forDt(sub!!)
|
||||
if (subType != null) DataType(BaseDataType.POINTER, null, subType) else dt
|
||||
}
|
||||
else -> throw IllegalArgumentException("not an array")
|
||||
}
|
||||
|
||||
fun typeForAddressOf(msb: Boolean): DataType {
|
||||
fun typeForUntypedAddressOf(msb: Boolean, mem: IMemSizer): DataType {
|
||||
val untypedPointerType = if(mem.memorySize(BaseDataType.POINTER)>2) LONG else UWORD
|
||||
if (isUndefined)
|
||||
return if(msb) pointer(BaseDataType.UBYTE) else UWORD
|
||||
return if(msb) pointer(BaseDataType.UBYTE) else untypedPointerType
|
||||
else {
|
||||
if (isBasic)
|
||||
return pointer(base)
|
||||
if (isString)
|
||||
return pointer(BaseDataType.UBYTE)
|
||||
if (isPointer)
|
||||
return UWORD
|
||||
return untypedPointerType
|
||||
if (isArray) {
|
||||
if (msb || isSplitWordArray)
|
||||
val isSplit = base == BaseDataType.ARRAY_SPLITW ||
|
||||
(base == BaseDataType.ARRAY_POINTER && mem.memorySize(BaseDataType.POINTER) <= 2)
|
||||
if (msb || isSplit)
|
||||
return pointer(BaseDataType.UBYTE)
|
||||
val elementDt = elementType()
|
||||
require(elementDt.isBasic)
|
||||
return pointer(elementDt)
|
||||
if (elementDt.isBasic)
|
||||
return pointer(elementDt)
|
||||
return untypedPointerType
|
||||
}
|
||||
if (subType != null)
|
||||
return pointer(this)
|
||||
return UWORD
|
||||
return untypedPointerType
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +342,7 @@ class DataType private constructor(
|
||||
BaseDataType.UBYTE -> "ubyte[]"
|
||||
BaseDataType.UWORD -> "uword[]"
|
||||
BaseDataType.LONG -> "long[]"
|
||||
BaseDataType.STRUCT_INSTANCE -> if (subType != null) "${subType!!.scopedNameString}[]" else "$subTypeFromAntlr[]"
|
||||
else -> throw IllegalArgumentException("invalid sub type")
|
||||
}
|
||||
}
|
||||
@@ -342,6 +407,7 @@ class DataType private constructor(
|
||||
BaseDataType.WORD -> "@nosplit word["
|
||||
BaseDataType.LONG -> "long["
|
||||
BaseDataType.FLOAT -> "float["
|
||||
BaseDataType.STRUCT_INSTANCE -> if (subType != null) "${subType!!.scopedNameString}[" else "$subTypeFromAntlr["
|
||||
else -> throw IllegalArgumentException("invalid sub type")
|
||||
}
|
||||
}
|
||||
@@ -366,9 +432,9 @@ class DataType private constructor(
|
||||
BaseDataType.BYTE -> targetType.base in setOf(BaseDataType.BYTE, BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
|
||||
BaseDataType.UWORD -> targetType.base in setOf(BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.FLOAT, BaseDataType.POINTER, BaseDataType.ARRAY_POINTER)
|
||||
BaseDataType.WORD -> targetType.base in setOf(BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
|
||||
BaseDataType.LONG -> targetType.base in setOf(BaseDataType.LONG, BaseDataType.FLOAT)
|
||||
BaseDataType.LONG -> targetType.base in setOf(BaseDataType.LONG, BaseDataType.FLOAT, BaseDataType.POINTER, BaseDataType.ARRAY_POINTER)
|
||||
BaseDataType.FLOAT -> targetType.base in arrayOf(BaseDataType.FLOAT)
|
||||
BaseDataType.STR -> targetType.base in setOf(BaseDataType.STR, BaseDataType.UWORD) || (targetType.isPointer && targetType.sub==BaseDataType.UBYTE)
|
||||
BaseDataType.STR -> targetType.base in setOf(BaseDataType.STR, BaseDataType.UWORD, BaseDataType.LONG) || (targetType.isPointer && targetType.sub==BaseDataType.UBYTE)
|
||||
BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW -> targetType.base in setOf(BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW) && targetType.sub == sub
|
||||
BaseDataType.POINTER -> {
|
||||
when {
|
||||
@@ -377,13 +443,14 @@ class DataType private constructor(
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
BaseDataType.ARRAY_POINTER -> targetType.base in setOf(BaseDataType.ARRAY_POINTER, targetType.base == BaseDataType.ARRAY) && targetType.sub == sub
|
||||
BaseDataType.STRUCT_INSTANCE -> false // we cannot deal with actual struct instances yet in any shape or form (only getting fields from it)
|
||||
BaseDataType.ARRAY_POINTER -> false
|
||||
BaseDataType.UNDEFINED -> false
|
||||
}
|
||||
|
||||
fun largerSizeThan(other: DataType): Boolean = base.largerSizeThan(other.base)
|
||||
fun equalsSize(other: DataType): Boolean = base.equalsSize(other.base)
|
||||
fun equalsSize(other: DataType, memsizer: IMemSizer): Boolean = base.equalsSize(other.base, memsizer)
|
||||
|
||||
/**
|
||||
* Returns the memory size in bytes.
|
||||
@@ -439,10 +506,20 @@ class DataType private constructor(
|
||||
val isBool = base == BaseDataType.BOOL
|
||||
val isFloat = base == BaseDataType.FLOAT
|
||||
val isLong = base == BaseDataType.LONG
|
||||
val isStringly = base == BaseDataType.STR || base == BaseDataType.UWORD || (base == BaseDataType.ARRAY && (sub == BaseDataType.UBYTE || sub == BaseDataType.BYTE))
|
||||
val isSplitWordArray = base.isSplitWordArray
|
||||
val isSplitUnsignedWordArray = base.isSplitWordArray && !base.isPointerArray && sub == BaseDataType.UWORD
|
||||
val isSplitSignedWordArray = base.isSplitWordArray && !base.isPointerArray && sub == BaseDataType.WORD
|
||||
fun isStringly(memsizer: IMemSizer): Boolean =
|
||||
base == BaseDataType.STR ||
|
||||
(memsizer.POINTER_MEM_SIZE == 2u && base == BaseDataType.UWORD) ||
|
||||
(memsizer.POINTER_MEM_SIZE > 2u && base == BaseDataType.LONG) ||
|
||||
(base == BaseDataType.ARRAY && (sub == BaseDataType.UBYTE || sub == BaseDataType.BYTE))
|
||||
fun isSplitWordArray(memsizer: IMemSizer): Boolean {
|
||||
return if(base== BaseDataType.ARRAY_SPLITW)
|
||||
true
|
||||
else if(base==BaseDataType.ARRAY_POINTER)
|
||||
memsizer.POINTER_MEM_SIZE<=2u
|
||||
else false
|
||||
}
|
||||
val isSplitUnsignedWordArray = base == BaseDataType.ARRAY_SPLITW && sub == BaseDataType.UWORD
|
||||
val isSplitSignedWordArray = base == BaseDataType.ARRAY_SPLITW && sub == BaseDataType.WORD
|
||||
val isIterable = base.isIterable
|
||||
val isPassByRef = base.isPassByRef
|
||||
val isPassByValue = base.isPassByValue
|
||||
|
||||
@@ -4,7 +4,11 @@ package prog8.code.core
|
||||
enum class CpuRegister {
|
||||
A,
|
||||
X,
|
||||
Y
|
||||
Y,
|
||||
// m68k:
|
||||
D0, D1, D2, D3, D4, D5, D6, D7,
|
||||
A0, A1, A2, A3, A4, A5, A6,
|
||||
FP0, FP1, FP2, FP3, FP4, FP5, FP6, FP7
|
||||
}
|
||||
|
||||
enum class RegisterOrPair {
|
||||
@@ -20,7 +24,12 @@ enum class RegisterOrPair {
|
||||
R0, R1, R2, R3, R4, R5, R6, R7,
|
||||
R8, R9, R10, R11, R12, R13, R14, R15,
|
||||
// combined virtual registers to store 32 bits longs:
|
||||
R0R1, R2R3, R4R5, R6R7, R8R9, R10R11, R12R13, R14R15;
|
||||
R0R1, R2R3, R4R5, R6R7, R8R9, R10R11, R12R13, R14R15,
|
||||
// m68k data & address registers:
|
||||
D0, D1, D2, D3, D4, D5, D6, D7,
|
||||
A0, A1, A2, A3, A4, A5, A6,
|
||||
// m68k FPU registers:
|
||||
FP0, FP1, FP2, FP3, FP4, FP5, FP6, FP7;
|
||||
|
||||
companion object {
|
||||
val names: Set<String> = entries.map { it.toString() }.toSet()
|
||||
@@ -29,6 +38,29 @@ enum class RegisterOrPair {
|
||||
CpuRegister.A -> A
|
||||
CpuRegister.X -> X
|
||||
CpuRegister.Y -> Y
|
||||
CpuRegister.D0 -> D0
|
||||
CpuRegister.D1 -> D1
|
||||
CpuRegister.D2 -> D2
|
||||
CpuRegister.D3 -> D3
|
||||
CpuRegister.D4 -> D4
|
||||
CpuRegister.D5 -> D5
|
||||
CpuRegister.D6 -> D6
|
||||
CpuRegister.D7 -> D7
|
||||
CpuRegister.A0 -> A0
|
||||
CpuRegister.A1 -> A1
|
||||
CpuRegister.A2 -> A2
|
||||
CpuRegister.A3 -> A3
|
||||
CpuRegister.A4 -> A4
|
||||
CpuRegister.A5 -> A5
|
||||
CpuRegister.A6 -> A6
|
||||
CpuRegister.FP0 -> FP0
|
||||
CpuRegister.FP1 -> FP1
|
||||
CpuRegister.FP2 -> FP2
|
||||
CpuRegister.FP3 -> FP3
|
||||
CpuRegister.FP4 -> FP4
|
||||
CpuRegister.FP5 -> FP5
|
||||
CpuRegister.FP6 -> FP6
|
||||
CpuRegister.FP7 -> FP7
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,6 +150,15 @@ val CombinedLongRegisters = setOf(
|
||||
RegisterOrPair.R14R15
|
||||
)
|
||||
|
||||
val M68kRegisters = setOf(
|
||||
RegisterOrPair.D0, RegisterOrPair.D1, RegisterOrPair.D2, RegisterOrPair.D3,
|
||||
RegisterOrPair.D4, RegisterOrPair.D5, RegisterOrPair.D6, RegisterOrPair.D7,
|
||||
RegisterOrPair.A0, RegisterOrPair.A1, RegisterOrPair.A2, RegisterOrPair.A3,
|
||||
RegisterOrPair.A4, RegisterOrPair.A5, RegisterOrPair.A6,
|
||||
RegisterOrPair.FP0, RegisterOrPair.FP1, RegisterOrPair.FP2, RegisterOrPair.FP3,
|
||||
RegisterOrPair.FP4, RegisterOrPair.FP5, RegisterOrPair.FP6, RegisterOrPair.FP7
|
||||
)
|
||||
|
||||
val CpuRegisters = setOf(
|
||||
RegisterOrPair.A, RegisterOrPair.X, RegisterOrPair.Y,
|
||||
RegisterOrPair.AX, RegisterOrPair.AY, RegisterOrPair.XY
|
||||
@@ -128,6 +169,8 @@ enum class OutputType {
|
||||
RAW,
|
||||
PRG,
|
||||
XEX,
|
||||
ELF,
|
||||
AMIGAHUNK,
|
||||
LIBRARY
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,12 @@ enum class CpuType(val statusBitsOnMultiByteOps: Boolean) {
|
||||
|
||||
/** 8-bit CMOS 65C02. Same status-bit issue as plain 6502. */
|
||||
CPU65C02(false),
|
||||
|
||||
/** 16/32 bit Motorola 68000. has single instructions that set status bits when dealing with multi byte operands */
|
||||
M68000(true),
|
||||
|
||||
/** 32 bit Motorola 68020. has single instructions that set status bits when dealing with multi byte operands */
|
||||
M68020(true),
|
||||
|
||||
/**
|
||||
* Virtual machine target. We use `false` here so the IR generator always
|
||||
@@ -44,6 +50,19 @@ enum class CpuType(val statusBitsOnMultiByteOps: Boolean) {
|
||||
* behavior consistent across all current targets.
|
||||
*/
|
||||
VIRTUAL(false)
|
||||
;
|
||||
|
||||
/** True for the 6502-family targets (plain 6502 and 65C02). */
|
||||
val is6502: Boolean
|
||||
get() = this == CPU6502 || this == CPU65C02
|
||||
val is68k: Boolean
|
||||
get() = this == M68000 || this == M68020
|
||||
/** True for targets that use the m68k-style hardware-register calling convention (D0-D7, A0-A6, FP0-FP7). */
|
||||
val usesM68kConvention: Boolean
|
||||
get() = is68k || this == VIRTUAL
|
||||
/** True for big-endian targets. The 6502 family and the VM are little-endian. */
|
||||
val isBigEndian: Boolean
|
||||
get() = this == M68000 || this == M68020
|
||||
}
|
||||
|
||||
interface ICompilationTarget: IStringEncoding, IMemSizer {
|
||||
@@ -51,7 +70,7 @@ interface ICompilationTarget: IStringEncoding, IMemSizer {
|
||||
|
||||
val FLOAT_MAX_NEGATIVE: Double
|
||||
val FLOAT_MAX_POSITIVE: Double
|
||||
val FLOAT_MEM_SIZE: UInt
|
||||
val ARRAY_SIZE_LIMIT: UInt
|
||||
val PROGRAM_LOAD_ADDRESS : UInt
|
||||
val PROGRAM_MEMTOP_ADDRESS: UInt
|
||||
val BSSHIGHRAM_START: UInt
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
package prog8.code.core
|
||||
|
||||
interface IMemSizer {
|
||||
val FLOAT_MEM_SIZE: UInt
|
||||
val POINTER_MEM_SIZE: UInt
|
||||
|
||||
val pointerType: DataType
|
||||
get() = if(POINTER_MEM_SIZE > 2u) DataType.LONG else DataType.UWORD
|
||||
|
||||
val pointerBaseType: BaseDataType
|
||||
get() = if(POINTER_MEM_SIZE > 2u) BaseDataType.LONG else BaseDataType.UWORD
|
||||
|
||||
fun memorySize(dt: DataType, numElements: Int?): Int
|
||||
|
||||
fun memorySize(dt: BaseDataType): Int {
|
||||
if(dt.isPassByRef)
|
||||
return memorySize(DataType.UWORD, null) // a pointer size
|
||||
return POINTER_MEM_SIZE.toInt()
|
||||
try {
|
||||
return memorySize(DataType.forDt(dt), null)
|
||||
} catch (x: NoSuchElementException) {
|
||||
throw IllegalArgumentException(x.message)
|
||||
}
|
||||
}
|
||||
|
||||
fun isSplitWordArray(dt: DataType): Boolean {
|
||||
return dt.isSplitWordArray(this)
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,26 @@ abstract class Zeropage(options: CompilationOptions): MemoryAllocator(options) {
|
||||
val allocatedVariables = mutableMapOf<String, VarAllocation>()
|
||||
val free = mutableSetOf<UInt>() // subclasses must set this to the appropriate free locations.
|
||||
|
||||
|
||||
/**
|
||||
* Report a warning for every scratch byte that is excluded from allocation by a %zpreserved
|
||||
* or %zpallowed directive. The compiler's code generators always use these fixed scratch
|
||||
* locations internally (P8ZP_SCRATCH_*), so a %zpreserved/%zpallowed range that excludes one
|
||||
* of them would let the compiler overwrite memory the user explicitly wants to protect.
|
||||
*/
|
||||
fun checkScratchConflicts(errors: IErrorReporter) {
|
||||
if(options.zeropage== ZeropageType.DONTUSE)
|
||||
return
|
||||
|
||||
val scratchBytes = listOf(SCRATCH_B1, SCRATCH_REG, SCRATCH_W1, SCRATCH_W1 + 1u, SCRATCH_W2, SCRATCH_W2 + 1u, SCRATCH_PTR, SCRATCH_PTR + 1u)
|
||||
for(byte in scratchBytes) {
|
||||
val preserved = options.zpReserved.any { byte in it }
|
||||
val allowed = options.zpAllowed.any { byte in it }
|
||||
if(preserved || !allowed)
|
||||
errors.warn("zero-page address $${byte.toString(16)} is a fixed scratch location used by the compiler (P8ZP_SCRATCH_*), but it is excluded by a %zpreserved or %zpallowed directive", Position.DUMMY)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeReservedFromFreePool() {
|
||||
for (reserved in options.zpReserved)
|
||||
reserve(reserved)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package prog8.code.target
|
||||
|
||||
import prog8.code.core.*
|
||||
import prog8.code.target.encodings.Encoder
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
class Amiga1200Target: ICompilationTarget,
|
||||
IStringEncoding by Encoder(false),
|
||||
IMemSizer by NormalMemSizer(4u, 4u) {
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = true
|
||||
override val defaultEncoding = Encoding.ISO
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
override val additionalAssemblerOptions = emptyList<String>()
|
||||
override val defaultOutputType = OutputType.AMIGAHUNK
|
||||
override val defaultLauncherType = CbmPrgLauncherType.NONE
|
||||
|
||||
companion object {
|
||||
const val NAME = "amiga1200"
|
||||
|
||||
/** needed to map bank numbers to libraries in extsub/CALLFAR library calls */
|
||||
val LibraryNumbers = Amiga500Target.LibraryNumbers
|
||||
}
|
||||
|
||||
override val cpu = CpuType.M68020
|
||||
|
||||
override val FLOAT_MAX_POSITIVE = Float.MAX_VALUE.toDouble()
|
||||
override val FLOAT_MAX_NEGATIVE = -Float.MAX_VALUE.toDouble()
|
||||
override val FLOAT_MEM_SIZE = 4u
|
||||
override val POINTER_MEM_SIZE = 4u
|
||||
override val ARRAY_SIZE_LIMIT = 32768u
|
||||
override val PROGRAM_LOAD_ADDRESS = 0u // amiga executables are relocatable
|
||||
override val PROGRAM_MEMTOP_ADDRESS = 0xfffffffeu // not actually used
|
||||
|
||||
override val BSSHIGHRAM_START = 0u // not used
|
||||
override val BSSHIGHRAM_END = 0u // not used
|
||||
override val BSSGOLDENRAM_START = 0u // not used
|
||||
override val BSSGOLDENRAM_END = 0u // not used
|
||||
override lateinit var zeropage: Zeropage
|
||||
|
||||
override fun getFloatAsmBytes(num: Number): String {
|
||||
TODO("float asm bytes")
|
||||
}
|
||||
|
||||
override fun convertFloatToBytes(num: Double): List<UByte> {
|
||||
TODO("convert float to bytes")
|
||||
}
|
||||
|
||||
override fun convertBytesToFloat(bytes: List<UByte>): Double {
|
||||
require(bytes.size==4) { "need 4 bytes" }
|
||||
TODO("convert bytes to float")
|
||||
}
|
||||
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
if(selectedEmulator!=1) {
|
||||
System.err.println("The amiga1200 target only supports the main emulator (Amitools's vamos).")
|
||||
return
|
||||
}
|
||||
val cpuStr = when(this.cpu) {
|
||||
CpuType.M68000 -> "68000"
|
||||
CpuType.M68020 -> "68020"
|
||||
else -> error("invalid cpu type")
|
||||
}
|
||||
val exeFile = programNameWithPath.resolveSibling("${programNameWithPath.fileName}")
|
||||
val cmd =
|
||||
listOf(
|
||||
"vamos",
|
||||
"--cpu", cpuStr,
|
||||
exeFile.toString(),
|
||||
)
|
||||
if(!quiet)
|
||||
println("Launching Amitools's Vamos...")
|
||||
val pb = ProcessBuilder(cmd).inheritIO()
|
||||
try {
|
||||
pb.start().waitFor()
|
||||
} catch(_: Exception) {
|
||||
System.err.println("Cannot find 'vamos' (Amitools's Amiga emulator). Install it from: https://github.com/cnvogelg/amitools")
|
||||
}
|
||||
}
|
||||
|
||||
override fun isIOAddress(address: UInt): Boolean =
|
||||
address in 0xbfd000u..0xbfdfffu || // CIA-B
|
||||
address in 0xbfe000u..0xbfefffu || // CIA-A
|
||||
address in 0xdff000u..0xdff1ffu || // Custom chipset (Paula, Alice, Lisa, Denise)
|
||||
address in 0xe80000u..0xefffffu // Zorro II autoconfig space
|
||||
|
||||
override fun initializeMemoryAreas(compilerOptions: CompilationOptions) {
|
||||
zeropage = Amiga1200Zeropage(compilerOptions)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class Amiga1200Zeropage(options: CompilationOptions): Zeropage(options) {
|
||||
override val SCRATCH_B1: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_REG: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_W1: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_W2: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_PTR: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package prog8.code.target
|
||||
|
||||
import prog8.code.core.*
|
||||
import prog8.code.target.encodings.Encoder
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
class Amiga500Target: ICompilationTarget,
|
||||
IStringEncoding by Encoder(false),
|
||||
IMemSizer by NormalMemSizer(4u, 4u) {
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = true
|
||||
override val defaultEncoding = Encoding.ISO
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
override val additionalAssemblerOptions = emptyList<String>()
|
||||
override val defaultOutputType = OutputType.AMIGAHUNK
|
||||
override val defaultLauncherType = CbmPrgLauncherType.NONE
|
||||
|
||||
companion object {
|
||||
const val NAME = "amiga500"
|
||||
|
||||
/** needed to map bank numbers to libraries in extsub/CALLFAR library calls */
|
||||
val LibraryNumbers = mapOf(
|
||||
1 to "Sys", // exec
|
||||
2 to "DOS",
|
||||
3 to "Gfx",
|
||||
4 to "Intuition",
|
||||
5 to "GadTools",
|
||||
6 to "Layers",
|
||||
7 to "Asl",
|
||||
8 to "ConsoleDevice",
|
||||
9 to "Utility",
|
||||
10 to "Expansion",
|
||||
11 to "Icon",
|
||||
12 to "Workbench",
|
||||
13 to "Diskfont",
|
||||
14 to "IFFParse",
|
||||
15 to "Input",
|
||||
16 to "Keymap",
|
||||
17 to "Locale",
|
||||
18 to "Timer",
|
||||
19 to "LowLevel",
|
||||
20 to "Math",
|
||||
21 to "MathIeeeSingBas",
|
||||
22 to "MathIeeeSingTrans",
|
||||
23 to "MathIeeeDoubBas",
|
||||
24 to "MathIeeeDoubTrans",
|
||||
25 to "MathTrans",
|
||||
26 to "NV",
|
||||
27 to "RealTime",
|
||||
28 to "Translator",
|
||||
29 to "RexxSys",
|
||||
30 to "Cx",
|
||||
31 to "DataTypes",
|
||||
32 to "Disk",
|
||||
33 to "AmigaGuide",
|
||||
34 to "ARexx",
|
||||
35 to "BattClock",
|
||||
36 to "BattMem"
|
||||
)
|
||||
}
|
||||
|
||||
override val cpu = CpuType.M68000
|
||||
|
||||
override val FLOAT_MAX_POSITIVE = Float.MAX_VALUE.toDouble()
|
||||
override val FLOAT_MAX_NEGATIVE = -Float.MAX_VALUE.toDouble()
|
||||
override val FLOAT_MEM_SIZE = 4u
|
||||
override val POINTER_MEM_SIZE = 4u
|
||||
override val ARRAY_SIZE_LIMIT = 32768u
|
||||
override val PROGRAM_LOAD_ADDRESS = 0u // amiga executables are relocatable
|
||||
override val PROGRAM_MEMTOP_ADDRESS = 0xfffffffeu // not actually used
|
||||
|
||||
override val BSSHIGHRAM_START = 0u // not used
|
||||
override val BSSHIGHRAM_END = 0u // not used
|
||||
override val BSSGOLDENRAM_START = 0u // not used
|
||||
override val BSSGOLDENRAM_END = 0u // not used
|
||||
override lateinit var zeropage: Zeropage
|
||||
|
||||
override fun getFloatAsmBytes(num: Number): String {
|
||||
TODO("float asm bytes")
|
||||
}
|
||||
|
||||
override fun convertFloatToBytes(num: Double): List<UByte> {
|
||||
TODO("convert float to bytes")
|
||||
}
|
||||
|
||||
override fun convertBytesToFloat(bytes: List<UByte>): Double {
|
||||
require(bytes.size==4) { "need 4 bytes" }
|
||||
TODO("convert bytes to float")
|
||||
}
|
||||
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
if(selectedEmulator!=1) {
|
||||
System.err.println("The amiga500 target only supports the main emulator (Amitools's vamos).")
|
||||
return
|
||||
}
|
||||
val cpuStr = when(this.cpu) {
|
||||
CpuType.M68000 -> "68000"
|
||||
CpuType.M68020 -> "68020"
|
||||
else -> error("invalid cpu type")
|
||||
}
|
||||
val exeFile = programNameWithPath.resolveSibling("${programNameWithPath.fileName}")
|
||||
val cmd =
|
||||
listOf(
|
||||
"vamos",
|
||||
"--cpu", cpuStr,
|
||||
exeFile.toString(),
|
||||
)
|
||||
if(!quiet)
|
||||
println("Launching Amitools's Vamos...")
|
||||
val pb = ProcessBuilder(cmd).inheritIO()
|
||||
try {
|
||||
pb.start().waitFor()
|
||||
} catch(e: Exception) {
|
||||
System.err.println("Cannot find 'vamos' (Amitools's Amiga emulator). Install it from: https://github.com/cnvogelg/amitools")
|
||||
}
|
||||
}
|
||||
|
||||
override fun isIOAddress(address: UInt): Boolean =
|
||||
address in 0xbfd000u..0xbfdfffu || // CIA-B
|
||||
address in 0xbfe000u..0xbfefffu || // CIA-A
|
||||
address in 0xdff000u..0xdff1ffu || // Custom chipset (Paula, Alice, Lisa, Denise)
|
||||
address in 0xe80000u..0xefffffu // Zorro II autoconfig space
|
||||
|
||||
override fun initializeMemoryAreas(compilerOptions: CompilationOptions) {
|
||||
zeropage = AmigaZeropage(compilerOptions)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class AmigaZeropage(options: CompilationOptions): Zeropage(options) {
|
||||
override val SCRATCH_B1: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_REG: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_W1: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_W2: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_PTR: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import java.nio.file.Path
|
||||
|
||||
class C128Target: ICompilationTarget,
|
||||
IStringEncoding by Encoder(true),
|
||||
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE) {
|
||||
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE, 2u) {
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = true
|
||||
@@ -29,6 +29,8 @@ class C128Target: ICompilationTarget,
|
||||
override val FLOAT_MAX_POSITIVE = Mflpt5.FLOAT_MAX_POSITIVE
|
||||
override val FLOAT_MAX_NEGATIVE = Mflpt5.FLOAT_MAX_NEGATIVE
|
||||
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE.toUInt()
|
||||
override val POINTER_MEM_SIZE = 2u
|
||||
override val ARRAY_SIZE_LIMIT = 256u
|
||||
override val PROGRAM_LOAD_ADDRESS = 0x1c01u
|
||||
override val PROGRAM_MEMTOP_ADDRESS = 0xc000u
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import java.nio.file.Path
|
||||
|
||||
class C64Target: ICompilationTarget,
|
||||
IStringEncoding by Encoder(true),
|
||||
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE) {
|
||||
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE, 2u) {
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = true
|
||||
@@ -31,7 +31,9 @@ class C64Target: ICompilationTarget,
|
||||
|
||||
override val FLOAT_MAX_POSITIVE = Mflpt5.FLOAT_MAX_POSITIVE
|
||||
override val FLOAT_MAX_NEGATIVE = Mflpt5.FLOAT_MAX_NEGATIVE
|
||||
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE.toUInt()
|
||||
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE
|
||||
override val POINTER_MEM_SIZE = 2u
|
||||
override val ARRAY_SIZE_LIMIT = 256u
|
||||
override val PROGRAM_LOAD_ADDRESS = 0x0801u
|
||||
override val PROGRAM_MEMTOP_ADDRESS = 0xcfe0u // $a000 if floats are used
|
||||
// note that at $cfe0-$cfff are the 16 'virtual registers' R0-R15
|
||||
@@ -97,6 +99,9 @@ val CompilationTargets = listOf(
|
||||
C128Target.NAME,
|
||||
Cx16Target.NAME,
|
||||
PETTarget.NAME,
|
||||
Qemu68kTarget.NAME,
|
||||
Amiga500Target.NAME,
|
||||
Amiga1200Target.NAME,
|
||||
VMTarget.NAME
|
||||
)
|
||||
|
||||
@@ -105,6 +110,9 @@ fun getCompilationTargetByName(name: String) = when(name.lowercase()) {
|
||||
C128Target.NAME -> C128Target()
|
||||
Cx16Target.NAME -> Cx16Target()
|
||||
PETTarget.NAME -> PETTarget()
|
||||
Qemu68kTarget.NAME -> Qemu68kTarget()
|
||||
Amiga500Target.NAME -> Amiga500Target()
|
||||
Amiga1200Target.NAME -> Amiga1200Target()
|
||||
VMTarget.NAME -> VMTarget()
|
||||
else -> throw IllegalArgumentException("invalid compilation target")
|
||||
}
|
||||
@@ -29,6 +29,7 @@ class ConfigFileTarget(
|
||||
override val libraryPath: Path,
|
||||
override val customLauncher: List<String>,
|
||||
override val additionalAssemblerOptions: List<String>,
|
||||
val varsAddress: UInt? = null,
|
||||
val ioAddresses: List<UIntRange>,
|
||||
val zpScratchB1: UInt,
|
||||
val zpScratchReg: UInt,
|
||||
@@ -38,8 +39,13 @@ class ConfigFileTarget(
|
||||
val virtualregistersStart: UInt,
|
||||
val zpFullsafe: List<UIntRange>,
|
||||
val zpKernalsafe: List<UIntRange>,
|
||||
val zpBasicsafe: List<UIntRange>
|
||||
): ICompilationTarget, IStringEncoding by Encoder(true), IMemSizer by NormalMemSizer(8) {
|
||||
val zpBasicsafe: List<UIntRange>,
|
||||
val pointerSize: Int = 2
|
||||
): ICompilationTarget, IStringEncoding by Encoder(true), IMemSizer by NormalMemSizer(8u, pointerSize.toUInt()) {
|
||||
|
||||
init {
|
||||
require(pointerSize == 2 || pointerSize == 4) { "unsupported pointer size: $pointerSize" }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -66,6 +72,12 @@ class ConfigFileTarget(
|
||||
return value.toUInt()
|
||||
}
|
||||
|
||||
private fun parsePointerSize(props: Properties): Int {
|
||||
val pointerSize = parseInt(props.getProperty("pointer_size", "2")).toInt()
|
||||
require(pointerSize == 2 || pointerSize == 4) { "unsupported pointer size: $pointerSize" }
|
||||
return pointerSize
|
||||
}
|
||||
|
||||
private fun parseAddressRanges(key: String, props: Properties): List<UIntRange> {
|
||||
val rangesStr = props.getString(key)
|
||||
if(rangesStr.isBlank())
|
||||
@@ -111,6 +123,7 @@ class ConfigFileTarget(
|
||||
(customLauncherStr+"\n").lines().map { it.trimEnd() }
|
||||
else emptyList()
|
||||
val assemblerOptionsStr = props.getProperty("assembler_options", "").trim()
|
||||
val pointerSize = parsePointerSize(props)
|
||||
val outputTypeString = props.getProperty("output_type", "PRG")
|
||||
val defaultOutputType = OutputType.valueOf(outputTypeString.uppercase())
|
||||
val launcherString = props.getProperty("launcher", "BASIC")
|
||||
@@ -120,6 +133,8 @@ class ConfigFileTarget(
|
||||
CbmPrgLauncherType.BASIC
|
||||
}
|
||||
|
||||
val varsAddress = props.getProperty("vars_address")?.let { parseInt(it) }
|
||||
|
||||
return ConfigFileTarget(
|
||||
configfile.nameWithoutExtension,
|
||||
props.getProperty("supports_banked_calls", "false").toBoolean(),
|
||||
@@ -136,6 +151,7 @@ class ConfigFileTarget(
|
||||
libraryPath,
|
||||
customLauncher,
|
||||
if(assemblerOptionsStr=="") emptyList() else assemblerOptionsStr.split(" "),
|
||||
varsAddress,
|
||||
ioAddresses,
|
||||
props.getInteger("zp_scratch_b1"),
|
||||
props.getInteger("zp_scratch_reg"),
|
||||
@@ -146,6 +162,7 @@ class ConfigFileTarget(
|
||||
zpFullsafe,
|
||||
zpKernalsafe,
|
||||
zpBasicsafe,
|
||||
pointerSize,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -154,7 +171,8 @@ class ConfigFileTarget(
|
||||
override val FLOAT_MAX_POSITIVE = 9.999999999e97
|
||||
override val FLOAT_MAX_NEGATIVE = -9.999999999e97
|
||||
override val FLOAT_MEM_SIZE = 8u
|
||||
|
||||
override val POINTER_MEM_SIZE = pointerSize.toUInt()
|
||||
override val ARRAY_SIZE_LIMIT = 256u
|
||||
override lateinit var zeropage: Zeropage
|
||||
|
||||
override fun getFloatAsmBytes(num: Number) = TODO("floats")
|
||||
|
||||
@@ -8,7 +8,7 @@ import java.nio.file.Path
|
||||
|
||||
class Cx16Target: ICompilationTarget,
|
||||
IStringEncoding by Encoder(true),
|
||||
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE) {
|
||||
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE, 2u) {
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = true
|
||||
@@ -29,6 +29,8 @@ class Cx16Target: ICompilationTarget,
|
||||
override val FLOAT_MAX_POSITIVE = Mflpt5.FLOAT_MAX_POSITIVE
|
||||
override val FLOAT_MAX_NEGATIVE = Mflpt5.FLOAT_MAX_NEGATIVE
|
||||
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE.toUInt()
|
||||
override val POINTER_MEM_SIZE = 2u
|
||||
override val ARRAY_SIZE_LIMIT = 256u
|
||||
override val PROGRAM_LOAD_ADDRESS = 0x0801u
|
||||
override val PROGRAM_MEMTOP_ADDRESS = 0x9f00u
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ data class Mflpt5(val b0: UByte, val b1: UByte, val b2: UByte, val b3: UByte, va
|
||||
companion object {
|
||||
const val FLOAT_MAX_POSITIVE = 1.7014118345e+38 // bytes: 255,127,255,255,255
|
||||
const val FLOAT_MAX_NEGATIVE = -1.7014118345e+38 // bytes: 255,255,255,255,255
|
||||
const val FLOAT_MEM_SIZE = 5
|
||||
const val FLOAT_MEM_SIZE = 5u
|
||||
|
||||
val zero = Mflpt5(0u, 0u, 0u, 0u, 0u)
|
||||
fun fromNumber(num: Number): Mflpt5 {
|
||||
@@ -73,4 +73,4 @@ data class Mflpt5(val b0: UByte, val b1: UByte, val b2: UByte, val b3: UByte, va
|
||||
val b4 = "$" + b4.toString(16).padStart(2, '0')
|
||||
return "$b0, $b1, $b2, $b3, $b4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,35 +4,41 @@ import prog8.code.core.BaseDataType
|
||||
import prog8.code.core.DataType
|
||||
import prog8.code.core.IMemSizer
|
||||
|
||||
internal class NormalMemSizer(val floatsize: Int): IMemSizer {
|
||||
internal class NormalMemSizer(override val FLOAT_MEM_SIZE: UInt, override val POINTER_MEM_SIZE: UInt): IMemSizer {
|
||||
|
||||
private val pointerSize = POINTER_MEM_SIZE.toInt()
|
||||
private val floatSize = FLOAT_MEM_SIZE.toInt()
|
||||
|
||||
override fun memorySize(dt: DataType, numElements: Int?): Int {
|
||||
if(dt.isPointerArray)
|
||||
return 2 * numElements!! // array of pointers is just array of uwords
|
||||
return pointerSize * numElements!! // array of pointers
|
||||
else if(dt.isArray) {
|
||||
if(numElements==null) return 2 // treat it as a pointer size
|
||||
if(numElements==null) return pointerSize // treat it as a pointer size
|
||||
return when(dt.sub) {
|
||||
BaseDataType.BOOL, BaseDataType.UBYTE, BaseDataType.BYTE -> numElements
|
||||
BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.STR -> numElements * 2
|
||||
BaseDataType.UWORD, BaseDataType.WORD -> numElements * 2
|
||||
BaseDataType.STR, BaseDataType.POINTER -> numElements * pointerSize
|
||||
BaseDataType.LONG -> numElements * 4
|
||||
BaseDataType.FLOAT-> numElements * floatsize
|
||||
BaseDataType.FLOAT-> numElements * floatSize
|
||||
BaseDataType.STRUCT_INSTANCE -> numElements * (dt.subType?.memsize(this) ?: throw IllegalArgumentException("struct instance array has unresolved subtype"))
|
||||
BaseDataType.UNDEFINED -> throw IllegalArgumentException("undefined has no memory size")
|
||||
else -> throw IllegalArgumentException("invalid sub type")
|
||||
}
|
||||
}
|
||||
else if (dt.isString) {
|
||||
return numElements // treat it as the size of the given string with the length
|
||||
?: 2 // treat it as the size to store a string pointer
|
||||
?: pointerSize // treat it as the size to store a string pointer
|
||||
}
|
||||
|
||||
return when {
|
||||
dt.isByteOrBool -> 1 * (numElements ?: 1)
|
||||
dt.isFloat -> floatsize * (numElements ?: 1)
|
||||
dt.isFloat -> floatSize * (numElements ?: 1)
|
||||
dt.isLong -> 4 * (numElements ?: 1)
|
||||
dt.isPointer -> 2 // pointer is just a uword
|
||||
dt.isStructInstance -> dt.subType?.memsize(this) ?: 0
|
||||
dt.isWord -> 2 * (numElements ?: 1)
|
||||
dt.isPointer -> pointerSize * (numElements ?: 1)
|
||||
dt.isStructInstance -> dt.subType?.memsize(this) ?: throw IllegalArgumentException("struct instance has unresolved subtype")
|
||||
dt.isUndefined -> throw IllegalArgumentException("undefined has no memory size")
|
||||
else -> 2 * (numElements ?: 1)
|
||||
else -> throw IllegalArgumentException("invalid dt $dt")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import java.nio.file.Path
|
||||
|
||||
class PETTarget: ICompilationTarget,
|
||||
IStringEncoding by Encoder(true),
|
||||
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE) {
|
||||
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE, 2u) {
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = false
|
||||
@@ -28,6 +28,8 @@ class PETTarget: ICompilationTarget,
|
||||
override val FLOAT_MAX_POSITIVE = Mflpt5.FLOAT_MAX_POSITIVE
|
||||
override val FLOAT_MAX_NEGATIVE = Mflpt5.FLOAT_MAX_NEGATIVE
|
||||
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE.toUInt()
|
||||
override val POINTER_MEM_SIZE = 2u
|
||||
override val ARRAY_SIZE_LIMIT = 256u
|
||||
override val PROGRAM_LOAD_ADDRESS = 0x0401u
|
||||
override val PROGRAM_MEMTOP_ADDRESS = 0x8000u
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package prog8.code.target
|
||||
|
||||
import prog8.code.core.*
|
||||
import prog8.code.target.encodings.Encoder
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* For now target a m68020 cpu
|
||||
* Eventually the goal is to be able to create programs for the Amiga A1200 (68020 cpu) or A500 (68000 cpu)
|
||||
*/
|
||||
|
||||
class Qemu68kTarget: ICompilationTarget,
|
||||
IStringEncoding by Encoder(false),
|
||||
IMemSizer by NormalMemSizer(4u, 4u) {
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = false
|
||||
override val defaultEncoding = Encoding.ISO
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
override val additionalAssemblerOptions = emptyList<String>()
|
||||
override val defaultOutputType = OutputType.ELF
|
||||
override val defaultLauncherType = CbmPrgLauncherType.NONE
|
||||
|
||||
companion object {
|
||||
const val NAME = "qemu68k"
|
||||
}
|
||||
|
||||
override val cpu = CpuType.M68020
|
||||
|
||||
override val FLOAT_MAX_POSITIVE = Float.MAX_VALUE.toDouble()
|
||||
override val FLOAT_MAX_NEGATIVE = -Float.MAX_VALUE.toDouble()
|
||||
override val FLOAT_MEM_SIZE = 4u
|
||||
override val POINTER_MEM_SIZE = 4u
|
||||
override val ARRAY_SIZE_LIMIT = 32768u
|
||||
override val PROGRAM_LOAD_ADDRESS = 0x10000u
|
||||
override val PROGRAM_MEMTOP_ADDRESS = 0x00100000u // TODO hardcoded at 1 Mb of RAM for now... it starts at $0
|
||||
|
||||
override val BSSHIGHRAM_START = 0u // not actually used
|
||||
override val BSSHIGHRAM_END = 0u // not actually used
|
||||
override val BSSGOLDENRAM_START = 0u // not actually used
|
||||
override val BSSGOLDENRAM_END = 0u // not actually used
|
||||
override lateinit var zeropage: Zeropage // not actually used
|
||||
|
||||
override fun getFloatAsmBytes(num: Number): String {
|
||||
TODO("float asm bytes")
|
||||
}
|
||||
|
||||
override fun convertFloatToBytes(num: Double): List<UByte> {
|
||||
TODO("convert float to bytes")
|
||||
}
|
||||
|
||||
override fun convertBytesToFloat(bytes: List<UByte>): Double {
|
||||
require(bytes.size==4) { "need 4 bytes" }
|
||||
TODO("convert bytes to float")
|
||||
}
|
||||
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
if(selectedEmulator!=1) {
|
||||
System.err.println("The qemu68k target only supports the main emulator (Qemu).")
|
||||
return
|
||||
}
|
||||
val elfFile = programNameWithPath.resolveSibling("${programNameWithPath.fileName}.elf")
|
||||
val binFile = programNameWithPath.resolveSibling("${programNameWithPath.fileName}.bin")
|
||||
val isElf = elfFile.toFile().exists()
|
||||
val cpuStr = this.cpu.toString().lowercase()
|
||||
|
||||
val cmd = if (isElf) {
|
||||
listOf(
|
||||
"qemu-system-m68k",
|
||||
"-M", "virt",
|
||||
"-cpu", cpuStr,
|
||||
"-m", "1M",
|
||||
"-kernel", elfFile.toString(),
|
||||
"-nographic"
|
||||
)
|
||||
} else if (binFile.toFile().exists()) {
|
||||
val loadAddr = PROGRAM_LOAD_ADDRESS.toInt()
|
||||
listOf(
|
||||
"qemu-system-m68k",
|
||||
"-M", "virt",
|
||||
"-cpu", cpuStr,
|
||||
"-m", "1M",
|
||||
"-device", "loader,file=${binFile},addr=0x${loadAddr.toString(16)},cpu-num=0",
|
||||
"-nographic"
|
||||
)
|
||||
} else {
|
||||
System.err.println("No .elf or .bin file found for ${programNameWithPath.fileName}")
|
||||
return
|
||||
}
|
||||
if(!quiet) {
|
||||
val launchMsg = if (isElf) "ELF" else "raw binary"
|
||||
println("Launching QEMU (press Ctrl-A X to exit)... (from $launchMsg)")
|
||||
}
|
||||
val pb = ProcessBuilder(cmd).inheritIO()
|
||||
try {
|
||||
pb.start().waitFor()
|
||||
} catch (_: java.io.IOException) {
|
||||
System.err.println("Cannot launch qemu-system-m68k. Install it via your package manager, e.g.:")
|
||||
System.err.println(" sudo apt install qemu-system-m68k # Debian/Ubuntu")
|
||||
System.err.println(" sudo pacman -S qemu-system-m68k # Arch Linux")
|
||||
System.err.println("or build it from source: https://www.qemu.org/download/")
|
||||
}
|
||||
}
|
||||
|
||||
override fun isIOAddress(address: UInt): Boolean = address>=0xff000000u
|
||||
|
||||
override fun initializeMemoryAreas(compilerOptions: CompilationOptions) {
|
||||
zeropage = M68kZeropage(compilerOptions)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class M68kZeropage(options: CompilationOptions): Zeropage(options) {
|
||||
override val SCRATCH_B1: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_REG: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_W1: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_W2: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_PTR: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import kotlin.io.path.readText
|
||||
|
||||
class VMTarget: ICompilationTarget,
|
||||
IStringEncoding by Encoder(false),
|
||||
IMemSizer by NormalMemSizer(FLOAT_MEM_SIZE) {
|
||||
IMemSizer by NormalMemSizer(8u, 4u) {
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = false
|
||||
@@ -23,16 +23,19 @@ class VMTarget: ICompilationTarget,
|
||||
|
||||
companion object {
|
||||
const val NAME = "virtual"
|
||||
const val FLOAT_MEM_SIZE = 8 // 64-bits double
|
||||
const val RAM_SIZE = 16 * 1024 * 1024 // 16 MB
|
||||
}
|
||||
|
||||
override val cpu = CpuType.VIRTUAL
|
||||
|
||||
override val FLOAT_MAX_POSITIVE = Double.MAX_VALUE
|
||||
override val FLOAT_MAX_NEGATIVE = -Double.MAX_VALUE
|
||||
override val FLOAT_MEM_SIZE = VMTarget.FLOAT_MEM_SIZE.toUInt()
|
||||
override val PROGRAM_LOAD_ADDRESS = 0u // not actually used
|
||||
override val PROGRAM_MEMTOP_ADDRESS = 0xffffu // not actually used
|
||||
override val FLOAT_MEM_SIZE = 8u
|
||||
override val POINTER_MEM_SIZE = 4u
|
||||
override val ARRAY_SIZE_LIMIT = 65535u
|
||||
|
||||
override val PROGRAM_LOAD_ADDRESS = 0u
|
||||
override val PROGRAM_MEMTOP_ADDRESS = 0xffffffu
|
||||
|
||||
override val BSSHIGHRAM_START = 0u // not actually used
|
||||
override val BSSHIGHRAM_END = 0u // not actually used
|
||||
|
||||
@@ -8,6 +8,9 @@ import prog8.code.core.*
|
||||
* Returns standard memory sizes for all data types.
|
||||
*/
|
||||
object DummyMemsizer : IMemSizer {
|
||||
override val FLOAT_MEM_SIZE: UInt = 4u
|
||||
override val POINTER_MEM_SIZE: UInt = 2u
|
||||
|
||||
override fun memorySize(dt: DataType, numElements: Int?): Int {
|
||||
if (dt.isPointerArray)
|
||||
return 2 * numElements!!
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package prog8tests.codecore
|
||||
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import prog8.code.core.BaseDataType
|
||||
import prog8.code.target.ConfigFileTarget
|
||||
import java.nio.file.Files
|
||||
import kotlin.io.path.writeText
|
||||
|
||||
class TestConfigFileTarget: FunSpec({
|
||||
fun loadTarget(extraProperties: String = ""): ConfigFileTarget {
|
||||
val directory = Files.createTempDirectory("prog8-config-target")
|
||||
val library = Files.createDirectories(directory.resolve("library"))
|
||||
val config = directory.resolve("target.properties")
|
||||
config.writeText(
|
||||
"""
|
||||
cpu = 65C02
|
||||
encoding = iso
|
||||
load_address = 2048
|
||||
memtop = 65535
|
||||
bss_highram_start = 0
|
||||
bss_highram_end = 0
|
||||
bss_goldenram_start = 0
|
||||
bss_goldenram_end = 0
|
||||
zp_scratch_b1 = 240
|
||||
zp_scratch_reg = 241
|
||||
zp_scratch_w1 = 242
|
||||
zp_scratch_w2 = 244
|
||||
zp_scratch_ptr = 246
|
||||
virtual_registers = 2
|
||||
zp_fullsafe = 34-239
|
||||
zp_kernalsafe = 34-239
|
||||
zp_basicsafe = 34-239
|
||||
io_regions =
|
||||
""".trimIndent() + "\nlibrary = $library\n$extraProperties"
|
||||
)
|
||||
return try {
|
||||
ConfigFileTarget.fromConfigFile(config)
|
||||
} finally {
|
||||
directory.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
test("legacy config defaults to 2-byte pointers and 256 element arrays") {
|
||||
val target = loadTarget()
|
||||
|
||||
target.POINTER_MEM_SIZE shouldBe 2u
|
||||
target.memorySize(BaseDataType.POINTER) shouldBe 2
|
||||
target.ARRAY_SIZE_LIMIT shouldBe 256u
|
||||
}
|
||||
|
||||
test("config supports 4-byte pointers") {
|
||||
val target = loadTarget("pointer_size = 4")
|
||||
|
||||
target.POINTER_MEM_SIZE shouldBe 4u
|
||||
target.memorySize(BaseDataType.POINTER) shouldBe 4
|
||||
target.ARRAY_SIZE_LIMIT shouldBe 256u
|
||||
}
|
||||
|
||||
test("config rejects unsupported pointer sizes") {
|
||||
shouldThrow<IllegalArgumentException> {
|
||||
loadTarget("pointer_size = 3")
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -23,31 +23,31 @@ class TestConversions: FunSpec({
|
||||
}
|
||||
|
||||
test("toHex for 16-255 (hex with $)") {
|
||||
16.toHex() shouldBe "\$10"
|
||||
255.toHex() shouldBe "\$ff"
|
||||
16.toHex() shouldBe $$"$10"
|
||||
255.toHex() shouldBe $$"$ff"
|
||||
}
|
||||
|
||||
test("toHex for 256-65535 (4 digit hex)") {
|
||||
256.toHex() shouldBe "\$0100"
|
||||
65535.toHex() shouldBe "\$ffff"
|
||||
256.toHex() shouldBe $$"$0100"
|
||||
65535.toHex() shouldBe $$"$ffff"
|
||||
}
|
||||
|
||||
test("toHex for larger numbers (8 digit hex)") {
|
||||
65536.toHex() shouldBe "\$00010000"
|
||||
0x12345678.toHex() shouldBe "\$12345678"
|
||||
65536.toHex() shouldBe $$"$00010000"
|
||||
0x12345678.toHex() shouldBe $$"$12345678"
|
||||
}
|
||||
|
||||
test("toHex for negative numbers") {
|
||||
(-1).toHex() shouldBe "-1"
|
||||
(-255).toHex() shouldBe "-\$ff"
|
||||
Int.MIN_VALUE.toHex() shouldBe "\$80000000"
|
||||
(-255).toHex() shouldBe $$"-$ff"
|
||||
Int.MIN_VALUE.toHex() shouldBe $$"$80000000"
|
||||
}
|
||||
|
||||
test("UInt.toHex") {
|
||||
0u.toHex() shouldBe "0"
|
||||
15u.toHex() shouldBe "15"
|
||||
255u.toHex() shouldBe "\$ff"
|
||||
65535u.toHex() shouldBe "\$ffff"
|
||||
255u.toHex() shouldBe $$"$ff"
|
||||
65535u.toHex() shouldBe $$"$ffff"
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -2,22 +2,28 @@ package prog8tests.codecore
|
||||
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.booleans.shouldBeFalse
|
||||
import io.kotest.matchers.booleans.shouldBeTrue
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.shouldNotBe
|
||||
import prog8.code.core.*
|
||||
import prog8.code.target.Amiga500Target
|
||||
import prog8.code.target.Cx16Target
|
||||
import prog8.code.target.VMTarget
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for DataType and BaseDataType classes in codeCore.
|
||||
* Focus on type predicates, relationships, and key operations.
|
||||
*/
|
||||
class TestDataType: FunSpec({
|
||||
val tgt = Cx16Target()
|
||||
|
||||
val dummyMemSizer = object : IMemSizer {
|
||||
override val FLOAT_MEM_SIZE: UInt = 4u
|
||||
override val POINTER_MEM_SIZE: UInt = 2u
|
||||
|
||||
override fun memorySize(dt: DataType, numElements: Int?): Int {
|
||||
if (dt.isPointerArray) return 2 * numElements!!
|
||||
if (dt.isArray || dt.isSplitWordArray) {
|
||||
if (dt.isArray || dt.isSplitWordArray(this)) {
|
||||
require(numElements != null)
|
||||
return when (dt.sub) {
|
||||
BaseDataType.BOOL, BaseDataType.BYTE, BaseDataType.UBYTE -> numElements
|
||||
@@ -45,61 +51,69 @@ class TestDataType: FunSpec({
|
||||
// ============================================================================
|
||||
|
||||
test("isByte extension") {
|
||||
BaseDataType.UBYTE.isByte.shouldBeTrue()
|
||||
BaseDataType.BYTE.isByte.shouldBeTrue()
|
||||
BaseDataType.UWORD.isByte.shouldBeFalse()
|
||||
BaseDataType.WORD.isByte.shouldBeFalse()
|
||||
BaseDataType.UBYTE.isByte shouldBe true
|
||||
BaseDataType.BYTE.isByte shouldBe true
|
||||
BaseDataType.UWORD.isByte shouldBe false
|
||||
BaseDataType.WORD.isByte shouldBe false
|
||||
}
|
||||
|
||||
test("isByteOrBool extension") {
|
||||
BaseDataType.UBYTE.isByteOrBool.shouldBeTrue()
|
||||
BaseDataType.BYTE.isByteOrBool.shouldBeTrue()
|
||||
BaseDataType.BOOL.isByteOrBool.shouldBeTrue()
|
||||
BaseDataType.UWORD.isByteOrBool.shouldBeFalse()
|
||||
BaseDataType.UBYTE.isByteOrBool shouldBe true
|
||||
BaseDataType.BYTE.isByteOrBool shouldBe true
|
||||
BaseDataType.BOOL.isByteOrBool shouldBe true
|
||||
BaseDataType.UWORD.isByteOrBool shouldBe false
|
||||
}
|
||||
|
||||
test("isWord extension") {
|
||||
BaseDataType.UWORD.isWord.shouldBeTrue()
|
||||
BaseDataType.WORD.isWord.shouldBeTrue()
|
||||
BaseDataType.UBYTE.isWord.shouldBeFalse()
|
||||
BaseDataType.UWORD.isWord shouldBe true
|
||||
BaseDataType.WORD.isWord shouldBe true
|
||||
BaseDataType.UBYTE.isWord shouldBe false
|
||||
}
|
||||
|
||||
test("isInteger extension") {
|
||||
BaseDataType.UBYTE.isInteger.shouldBeTrue()
|
||||
BaseDataType.WORD.isInteger.shouldBeTrue()
|
||||
BaseDataType.LONG.isInteger.shouldBeTrue()
|
||||
BaseDataType.FLOAT.isInteger.shouldBeFalse()
|
||||
BaseDataType.UBYTE.isInteger shouldBe true
|
||||
BaseDataType.WORD.isInteger shouldBe true
|
||||
BaseDataType.LONG.isInteger shouldBe true
|
||||
BaseDataType.FLOAT.isInteger shouldBe false
|
||||
}
|
||||
|
||||
test("isNumeric extension") {
|
||||
BaseDataType.UBYTE.isNumeric.shouldBeTrue()
|
||||
BaseDataType.FLOAT.isNumeric.shouldBeTrue()
|
||||
BaseDataType.BOOL.isNumeric.shouldBeFalse()
|
||||
BaseDataType.UBYTE.isNumeric shouldBe true
|
||||
BaseDataType.FLOAT.isNumeric shouldBe true
|
||||
BaseDataType.BOOL.isNumeric shouldBe false
|
||||
}
|
||||
|
||||
test("isSigned extension") {
|
||||
BaseDataType.BYTE.isSigned.shouldBeTrue()
|
||||
BaseDataType.WORD.isSigned.shouldBeTrue()
|
||||
BaseDataType.LONG.isSigned.shouldBeTrue()
|
||||
BaseDataType.UBYTE.isSigned.shouldBeFalse()
|
||||
BaseDataType.BYTE.isSigned shouldBe true
|
||||
BaseDataType.WORD.isSigned shouldBe true
|
||||
BaseDataType.LONG.isSigned shouldBe true
|
||||
BaseDataType.UBYTE.isSigned shouldBe false
|
||||
}
|
||||
|
||||
test("isArray extension") {
|
||||
BaseDataType.ARRAY.isArray.shouldBeTrue()
|
||||
BaseDataType.ARRAY_SPLITW.isArray.shouldBeTrue()
|
||||
BaseDataType.ARRAY_POINTER.isArray.shouldBeTrue()
|
||||
BaseDataType.UBYTE.isArray.shouldBeFalse()
|
||||
BaseDataType.ARRAY.isArray shouldBe true
|
||||
BaseDataType.ARRAY_SPLITW.isArray shouldBe true
|
||||
BaseDataType.ARRAY_POINTER.isArray shouldBe true
|
||||
BaseDataType.UBYTE.isArray shouldBe false
|
||||
}
|
||||
|
||||
test("isPointer extension") {
|
||||
BaseDataType.POINTER.isPointer.shouldBeTrue()
|
||||
BaseDataType.UBYTE.isPointer.shouldBeFalse()
|
||||
BaseDataType.POINTER.isPointer shouldBe true
|
||||
BaseDataType.UBYTE.isPointer shouldBe false
|
||||
}
|
||||
|
||||
test("isSplitWordArray extension") {
|
||||
BaseDataType.ARRAY_SPLITW.isSplitWordArray.shouldBeTrue()
|
||||
BaseDataType.ARRAY_POINTER.isSplitWordArray.shouldBeTrue()
|
||||
BaseDataType.ARRAY.isSplitWordArray.shouldBeFalse()
|
||||
DataType.splitWordArrayFor(BaseDataType.UWORD).isSplitWordArray(Cx16Target()) shouldBe true
|
||||
DataType.splitWordArrayFor(BaseDataType.UWORD).isSplitWordArray(VMTarget()) shouldBe true
|
||||
DataType.splitWordArrayFor(BaseDataType.UWORD).isSplitWordArray(Amiga500Target()) shouldBe true
|
||||
|
||||
DataType.arrayOfPointersTo(BaseDataType.UWORD).isSplitWordArray(Cx16Target()) shouldBe true
|
||||
DataType.arrayOfPointersTo(BaseDataType.UWORD).isSplitWordArray(VMTarget()) shouldBe false
|
||||
DataType.arrayOfPointersTo(BaseDataType.UWORD).isSplitWordArray(Amiga500Target()) shouldBe false
|
||||
|
||||
DataType.arrayFor(BaseDataType.UWORD, Cx16Target()).isSplitWordArray(Cx16Target()) shouldBe false
|
||||
DataType.arrayFor(BaseDataType.UWORD, VMTarget()).isSplitWordArray(VMTarget()) shouldBe false
|
||||
DataType.arrayFor(BaseDataType.UWORD, Amiga500Target()).isSplitWordArray(Amiga500Target()) shouldBe false
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -113,20 +127,20 @@ class TestDataType: FunSpec({
|
||||
DataType.forDt(BaseDataType.FLOAT) shouldBe DataType.FLOAT
|
||||
}
|
||||
|
||||
test("DataType.forDt throws for struct instance") {
|
||||
shouldThrow<NotImplementedError> {
|
||||
DataType.forDt(BaseDataType.STRUCT_INSTANCE)
|
||||
}
|
||||
test("DataType.forDt supports struct instance") {
|
||||
val dt = DataType.forDt(BaseDataType.STRUCT_INSTANCE)
|
||||
dt.base shouldBe BaseDataType.STRUCT_INSTANCE
|
||||
dt.isStructInstance shouldBe true
|
||||
}
|
||||
|
||||
test("DataType.arrayFor creates array types") {
|
||||
val ubyteArray = DataType.arrayFor(BaseDataType.UBYTE)
|
||||
val ubyteArray = DataType.arrayFor(BaseDataType.UBYTE, tgt)
|
||||
ubyteArray.base shouldBe BaseDataType.ARRAY
|
||||
ubyteArray.sub shouldBe BaseDataType.UBYTE
|
||||
|
||||
val wordArraySplit = DataType.arrayFor(BaseDataType.WORD)
|
||||
wordArraySplit.base shouldBe BaseDataType.ARRAY_SPLITW
|
||||
wordArraySplit.sub shouldBe BaseDataType.WORD
|
||||
val wordArray = DataType.arrayFor(BaseDataType.WORD, tgt)
|
||||
wordArray.base shouldBe BaseDataType.ARRAY
|
||||
wordArray.sub shouldBe BaseDataType.WORD
|
||||
}
|
||||
|
||||
test("DataType.pointer creates pointer types") {
|
||||
@@ -140,45 +154,45 @@ class TestDataType: FunSpec({
|
||||
// ============================================================================
|
||||
|
||||
test("DataType.isBasic property") {
|
||||
DataType.UBYTE.isBasic.shouldBeTrue()
|
||||
DataType.WORD.isBasic.shouldBeTrue()
|
||||
DataType.STR.isBasic.shouldBeFalse()
|
||||
DataType.arrayFor(BaseDataType.UBYTE).isBasic.shouldBeFalse()
|
||||
DataType.UBYTE.isBasic shouldBe true
|
||||
DataType.WORD.isBasic shouldBe true
|
||||
DataType.STR.isBasic shouldBe false
|
||||
DataType.arrayFor(BaseDataType.UBYTE, tgt).isBasic shouldBe false
|
||||
}
|
||||
|
||||
test("DataType.isByte property") {
|
||||
DataType.UBYTE.isByte.shouldBeTrue()
|
||||
DataType.BYTE.isByte.shouldBeTrue()
|
||||
DataType.WORD.isByte.shouldBeFalse()
|
||||
DataType.UBYTE.isByte shouldBe true
|
||||
DataType.BYTE.isByte shouldBe true
|
||||
DataType.WORD.isByte shouldBe false
|
||||
}
|
||||
|
||||
test("DataType.isNumeric property") {
|
||||
DataType.UBYTE.isNumeric.shouldBeTrue()
|
||||
DataType.FLOAT.isNumeric.shouldBeTrue()
|
||||
DataType.BOOL.isNumeric.shouldBeFalse()
|
||||
DataType.STR.isNumeric.shouldBeFalse()
|
||||
DataType.UBYTE.isNumeric shouldBe true
|
||||
DataType.FLOAT.isNumeric shouldBe true
|
||||
DataType.BOOL.isNumeric shouldBe false
|
||||
DataType.STR.isNumeric shouldBe false
|
||||
}
|
||||
|
||||
test("DataType.isArray property") {
|
||||
DataType.arrayFor(BaseDataType.UBYTE).isArray.shouldBeTrue()
|
||||
DataType.UBYTE.isArray.shouldBeFalse()
|
||||
DataType.arrayFor(BaseDataType.UBYTE, tgt).isArray shouldBe true
|
||||
DataType.UBYTE.isArray shouldBe false
|
||||
}
|
||||
|
||||
test("DataType.isPointer property") {
|
||||
DataType.pointer(BaseDataType.UBYTE).isPointer.shouldBeTrue()
|
||||
DataType.UBYTE.isPointer.shouldBeFalse()
|
||||
DataType.pointer(BaseDataType.UBYTE).isPointer shouldBe true
|
||||
DataType.UBYTE.isPointer shouldBe false
|
||||
}
|
||||
|
||||
test("DataType.isString property") {
|
||||
DataType.STR.isString.shouldBeTrue()
|
||||
DataType.UBYTE.isString.shouldBeFalse()
|
||||
DataType.STR.isString shouldBe true
|
||||
DataType.UBYTE.isString shouldBe false
|
||||
}
|
||||
|
||||
test("DataType.isPassByRef property") {
|
||||
DataType.STR.isPassByRef.shouldBeTrue()
|
||||
DataType.arrayFor(BaseDataType.UBYTE).isPassByRef.shouldBeTrue()
|
||||
DataType.UBYTE.isPassByRef.shouldBeFalse()
|
||||
DataType.pointer(BaseDataType.UBYTE).isPassByRef.shouldBeFalse()
|
||||
DataType.STR.isPassByRef shouldBe true
|
||||
DataType.arrayFor(BaseDataType.UBYTE, tgt).isPassByRef shouldBe true
|
||||
DataType.UBYTE.isPassByRef shouldBe false
|
||||
DataType.pointer(BaseDataType.UBYTE).isPassByRef shouldBe false
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -186,8 +200,8 @@ class TestDataType: FunSpec({
|
||||
// ============================================================================
|
||||
|
||||
test("DataType.elementType returns element type for arrays") {
|
||||
DataType.arrayFor(BaseDataType.UBYTE).elementType() shouldBe DataType.UBYTE
|
||||
DataType.arrayFor(BaseDataType.WORD).elementType() shouldBe DataType.WORD
|
||||
DataType.arrayFor(BaseDataType.UBYTE, tgt).elementType() shouldBe DataType.UBYTE
|
||||
DataType.arrayFor(BaseDataType.WORD, tgt).elementType() shouldBe DataType.WORD
|
||||
DataType.STR.elementType() shouldBe DataType.UBYTE
|
||||
}
|
||||
|
||||
@@ -226,31 +240,31 @@ class TestDataType: FunSpec({
|
||||
}
|
||||
|
||||
test("BaseDataType.largerSizeThan") {
|
||||
BaseDataType.WORD.largerSizeThan(BaseDataType.UBYTE).shouldBeTrue()
|
||||
BaseDataType.LONG.largerSizeThan(BaseDataType.WORD).shouldBeTrue()
|
||||
BaseDataType.UBYTE.largerSizeThan(BaseDataType.WORD).shouldBeFalse()
|
||||
BaseDataType.STR.largerSizeThan(BaseDataType.UBYTE).shouldBeTrue()
|
||||
BaseDataType.STR.largerSizeThan(BaseDataType.BOOL).shouldBeTrue()
|
||||
BaseDataType.STR.largerSizeThan(BaseDataType.UWORD).shouldBeFalse()
|
||||
BaseDataType.STR.largerSizeThan(BaseDataType.WORD).shouldBeFalse()
|
||||
BaseDataType.STR.largerSizeThan(BaseDataType.POINTER).shouldBeFalse()
|
||||
BaseDataType.STR.largerSizeThan(BaseDataType.LONG).shouldBeFalse()
|
||||
BaseDataType.POINTER.largerSizeThan(BaseDataType.UBYTE).shouldBeTrue()
|
||||
BaseDataType.POINTER.largerSizeThan(BaseDataType.BOOL).shouldBeTrue()
|
||||
BaseDataType.POINTER.largerSizeThan(BaseDataType.UWORD).shouldBeFalse()
|
||||
BaseDataType.POINTER.largerSizeThan(BaseDataType.STR).shouldBeFalse()
|
||||
BaseDataType.WORD.largerSizeThan(BaseDataType.UBYTE) shouldBe true
|
||||
BaseDataType.LONG.largerSizeThan(BaseDataType.WORD) shouldBe true
|
||||
BaseDataType.UBYTE.largerSizeThan(BaseDataType.WORD) shouldBe false
|
||||
BaseDataType.STR.largerSizeThan(BaseDataType.UBYTE) shouldBe true
|
||||
BaseDataType.STR.largerSizeThan(BaseDataType.BOOL) shouldBe true
|
||||
BaseDataType.STR.largerSizeThan(BaseDataType.UWORD) shouldBe false
|
||||
BaseDataType.STR.largerSizeThan(BaseDataType.WORD) shouldBe false
|
||||
BaseDataType.STR.largerSizeThan(BaseDataType.POINTER) shouldBe false
|
||||
BaseDataType.STR.largerSizeThan(BaseDataType.LONG) shouldBe false
|
||||
BaseDataType.POINTER.largerSizeThan(BaseDataType.UBYTE) shouldBe true
|
||||
BaseDataType.POINTER.largerSizeThan(BaseDataType.BOOL) shouldBe true
|
||||
BaseDataType.POINTER.largerSizeThan(BaseDataType.UWORD) shouldBe false
|
||||
BaseDataType.POINTER.largerSizeThan(BaseDataType.STR) shouldBe false
|
||||
}
|
||||
|
||||
test("BaseDataType.equalsSize") {
|
||||
BaseDataType.UBYTE.equalsSize(BaseDataType.UBYTE).shouldBeTrue()
|
||||
BaseDataType.UBYTE.equalsSize(BaseDataType.BYTE).shouldBeTrue()
|
||||
BaseDataType.WORD.equalsSize(BaseDataType.UWORD).shouldBeTrue()
|
||||
BaseDataType.UBYTE.equalsSize(BaseDataType.WORD).shouldBeFalse()
|
||||
BaseDataType.STR.equalsSize(BaseDataType.STR).shouldBeTrue()
|
||||
BaseDataType.STR.equalsSize(BaseDataType.UWORD).shouldBeTrue()
|
||||
BaseDataType.STR.equalsSize(BaseDataType.BYTE).shouldBeFalse()
|
||||
BaseDataType.POINTER.equalsSize(BaseDataType.UWORD).shouldBeTrue()
|
||||
BaseDataType.POINTER.equalsSize(BaseDataType.STR).shouldBeFalse()
|
||||
BaseDataType.UBYTE.equalsSize(BaseDataType.UBYTE) shouldBe true
|
||||
BaseDataType.UBYTE.equalsSize(BaseDataType.BYTE) shouldBe true
|
||||
BaseDataType.WORD.equalsSize(BaseDataType.UWORD) shouldBe true
|
||||
BaseDataType.UBYTE.equalsSize(BaseDataType.WORD) shouldBe false
|
||||
BaseDataType.STR.equalsSize(BaseDataType.STR) shouldBe true
|
||||
BaseDataType.STR.equalsSize(BaseDataType.UWORD) shouldBe true
|
||||
BaseDataType.STR.equalsSize(BaseDataType.BYTE) shouldBe false
|
||||
BaseDataType.POINTER.equalsSize(BaseDataType.UWORD) shouldBe true
|
||||
BaseDataType.POINTER.equalsSize(BaseDataType.STR) shouldBe false
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -258,39 +272,39 @@ class TestDataType: FunSpec({
|
||||
// ============================================================================
|
||||
|
||||
test("bool assignability") {
|
||||
DataType.BOOL.isAssignableTo(DataType.BOOL).shouldBeTrue()
|
||||
DataType.BOOL.isAssignableTo(DataType.UBYTE).shouldBeFalse()
|
||||
DataType.BOOL.isAssignableTo(DataType.BOOL) shouldBe true
|
||||
DataType.BOOL.isAssignableTo(DataType.UBYTE) shouldBe false
|
||||
}
|
||||
|
||||
test("ubyte assignability") {
|
||||
DataType.UBYTE.isAssignableTo(DataType.UBYTE).shouldBeTrue()
|
||||
DataType.UBYTE.isAssignableTo(DataType.UWORD).shouldBeTrue()
|
||||
DataType.UBYTE.isAssignableTo(DataType.LONG).shouldBeTrue()
|
||||
DataType.UBYTE.isAssignableTo(DataType.FLOAT).shouldBeTrue()
|
||||
DataType.UBYTE.isAssignableTo(DataType.UBYTE) shouldBe true
|
||||
DataType.UBYTE.isAssignableTo(DataType.UWORD) shouldBe true
|
||||
DataType.UBYTE.isAssignableTo(DataType.LONG) shouldBe true
|
||||
DataType.UBYTE.isAssignableTo(DataType.FLOAT) shouldBe true
|
||||
}
|
||||
|
||||
test("byte assignability") {
|
||||
DataType.BYTE.isAssignableTo(DataType.BYTE).shouldBeTrue()
|
||||
DataType.BYTE.isAssignableTo(DataType.WORD).shouldBeTrue()
|
||||
DataType.BYTE.isAssignableTo(DataType.LONG).shouldBeTrue()
|
||||
DataType.BYTE.isAssignableTo(DataType.BYTE) shouldBe true
|
||||
DataType.BYTE.isAssignableTo(DataType.WORD) shouldBe true
|
||||
DataType.BYTE.isAssignableTo(DataType.LONG) shouldBe true
|
||||
}
|
||||
|
||||
test("uword assignability") {
|
||||
DataType.UWORD.isAssignableTo(DataType.UWORD).shouldBeTrue()
|
||||
DataType.UWORD.isAssignableTo(DataType.LONG).shouldBeTrue()
|
||||
DataType.UWORD.isAssignableTo(DataType.FLOAT).shouldBeTrue()
|
||||
DataType.UWORD.isAssignableTo(DataType.UWORD) shouldBe true
|
||||
DataType.UWORD.isAssignableTo(DataType.LONG) shouldBe true
|
||||
DataType.UWORD.isAssignableTo(DataType.FLOAT) shouldBe true
|
||||
}
|
||||
|
||||
test("string assignability") {
|
||||
DataType.STR.isAssignableTo(DataType.STR).shouldBeTrue()
|
||||
DataType.STR.isAssignableTo(DataType.UWORD).shouldBeTrue()
|
||||
DataType.STR.isAssignableTo(DataType.STR) shouldBe true
|
||||
DataType.STR.isAssignableTo(DataType.UWORD) shouldBe true
|
||||
}
|
||||
|
||||
test("pointer assignability") {
|
||||
val ptr = DataType.pointer(BaseDataType.UBYTE)
|
||||
ptr.isAssignableTo(DataType.UWORD).shouldBeTrue()
|
||||
ptr.isAssignableTo(DataType.LONG).shouldBeTrue()
|
||||
ptr.isAssignableTo(DataType.pointer(BaseDataType.UBYTE)).shouldBeTrue()
|
||||
ptr.isAssignableTo(DataType.UWORD) shouldBe true
|
||||
ptr.isAssignableTo(DataType.LONG) shouldBe true
|
||||
ptr.isAssignableTo(DataType.pointer(BaseDataType.UBYTE)) shouldBe true
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -305,7 +319,7 @@ class TestDataType: FunSpec({
|
||||
}
|
||||
|
||||
test("DataType.toString for arrays") {
|
||||
DataType.arrayFor(BaseDataType.UBYTE).toString() shouldBe "ubyte[]"
|
||||
DataType.arrayFor(BaseDataType.WORD).toString() shouldBe "word[] (split)"
|
||||
DataType.arrayFor(BaseDataType.UBYTE, tgt).toString() shouldBe "ubyte[]"
|
||||
DataType.arrayFor(BaseDataType.WORD, tgt).toString() shouldBe "word[]"
|
||||
}
|
||||
})
|
||||
@@ -2,6 +2,8 @@ package prog8.codegen.cpu6502
|
||||
|
||||
import com.github.michaelbull.result.fold
|
||||
import prog8.code.*
|
||||
import prog8.code.assembly.AssemblyProgram6502
|
||||
import prog8.code.assembly.IAssemblyProgram
|
||||
import prog8.code.ast.*
|
||||
import prog8.code.core.*
|
||||
import prog8.code.source.ImportFileSystem
|
||||
@@ -15,8 +17,7 @@ import kotlin.io.path.writeLines
|
||||
internal const val subroutineFloatEvalResultVar1 = "prog8_float_eval_result1"
|
||||
internal const val subroutineFloatEvalResultVar2 = "prog8_float_eval_result2"
|
||||
|
||||
class AsmGen6502(val prefixSymbols: Boolean,
|
||||
private val lastGeneratedLabelSequenceNr: Int,
|
||||
class AsmGen6502(private val lastGeneratedLabelSequenceNr: Int,
|
||||
private val preassignedCallSiteIds: Map<PtAsmSub, UByte> = emptyMap()
|
||||
): ICodeGeneratorBackend {
|
||||
override fun generate(
|
||||
@@ -25,7 +26,7 @@ class AsmGen6502(val prefixSymbols: Boolean,
|
||||
options: CompilationOptions,
|
||||
errors: IErrorReporter,
|
||||
): IAssemblyProgram? {
|
||||
val st = if(prefixSymbols) prefixSymbols(program, options, symbolTable) else symbolTable
|
||||
val st = prefixSymbols(program, options, symbolTable)
|
||||
val asmgen = AsmGen6502Internal(program, st, options, errors, lastGeneratedLabelSequenceNr, preassignedCallSiteIds)
|
||||
return asmgen.compileToAssembly()
|
||||
}
|
||||
@@ -99,7 +100,7 @@ class AsmGen6502Internal (
|
||||
if(options.dumpVariables)
|
||||
dumpVariables()
|
||||
|
||||
return AssemblyProgram(program.name, options.outputDir, options.compTarget)
|
||||
return AssemblyProgram6502(program.name, options.outputDir, options.compTarget)
|
||||
} else {
|
||||
errors.report()
|
||||
return null
|
||||
@@ -295,7 +296,7 @@ class AsmGen6502Internal (
|
||||
}
|
||||
if (e is PtAddressOf && e.identifier != null && !e.isFromArrayElement && e.dereference == null) {
|
||||
var symbol = asmVariableName(e.identifier!!)
|
||||
if (e.identifier!!.type.isSplitWordArray) {
|
||||
if (e.identifier!!.type.isSplitWordArray(options.compTarget)) {
|
||||
symbol = if (e.isMsbForSplitArray) symbol + "_msb" else symbol + "_lsb"
|
||||
}
|
||||
return Pair("#<$symbol", "#>$symbol")
|
||||
@@ -441,6 +442,7 @@ class AsmGen6502Internal (
|
||||
out(" tya | pha")
|
||||
}
|
||||
}
|
||||
else -> throw AssemblyError("register not available on this target")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,6 +471,7 @@ class AsmGen6502Internal (
|
||||
out(" pla | tay")
|
||||
}
|
||||
}
|
||||
else -> throw AssemblyError("register not available on this target")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,6 +552,7 @@ class AsmGen6502Internal (
|
||||
CpuRegister.A -> {}
|
||||
CpuRegister.X -> out(" tax")
|
||||
CpuRegister.Y -> out(" tay")
|
||||
else -> throw AssemblyError("register not available on this target")
|
||||
}
|
||||
}
|
||||
expr.type.isLong -> {
|
||||
@@ -558,6 +562,7 @@ class AsmGen6502Internal (
|
||||
CpuRegister.A -> {}
|
||||
CpuRegister.X -> out(" tax")
|
||||
CpuRegister.Y -> out(" tay")
|
||||
else -> throw AssemblyError("register not available on this target")
|
||||
}
|
||||
}
|
||||
expr.type.isFloat -> {
|
||||
@@ -575,6 +580,7 @@ class AsmGen6502Internal (
|
||||
CpuRegister.A -> {}
|
||||
CpuRegister.X -> out(" tax")
|
||||
CpuRegister.Y -> out(" tay")
|
||||
else -> throw AssemblyError("register not available on this target")
|
||||
}
|
||||
}
|
||||
else -> throw AssemblyError("weird dt")
|
||||
@@ -1498,7 +1504,7 @@ $repeatLabel""")
|
||||
}
|
||||
else if(swap.target1.pointerDeref!=null && swap.target2.pointerDeref!=null) {
|
||||
val v1 = swap.target1.pointerDeref!!
|
||||
val v2 = swap.target1.pointerDeref!!
|
||||
val v2 = swap.target2.pointerDeref!!
|
||||
if(v1.derefLast && v1.chain.isEmpty() && v2.derefLast && v2.chain.isEmpty() && isZpVar(v1.startpointer) && isZpVar(v2.startpointer)) {
|
||||
// optimized case where v1 and v2 are both already zeropage pointer variables
|
||||
val name1 = asmVariableName(v1.startpointer)
|
||||
@@ -1937,7 +1943,7 @@ $repeatLabel""")
|
||||
private fun translate(ret: PtReturn) {
|
||||
val returnvalue = ret.children.singleOrNull() as? PtExpression // could be a multi-value returning functioncall
|
||||
val sub = ret.definingSub()!!
|
||||
val returnRegs = sub.returnsWhatWhere()
|
||||
val returnRegs = sub.returnsWhatWhere(options.compTarget)
|
||||
|
||||
if(returnvalue!=null) {
|
||||
|
||||
@@ -1950,7 +1956,7 @@ $repeatLabel""")
|
||||
// Get the return register specs for the called function
|
||||
val calledSub = symbolTable.lookup(fcall.name)
|
||||
val calledReturnRegs = when(calledSub) {
|
||||
is StSub -> (calledSub.astNode!! as IPtSubroutine).returnsWhatWhere()
|
||||
is StSub -> (calledSub.astNode!! as IPtSubroutine).returnsWhatWhere(options.compTarget)
|
||||
is StExtSub -> calledSub.returns.map { it.register to it.type }
|
||||
else -> throw AssemblyError("unexpected subroutine type for multi-value return ${ret.position}")
|
||||
}
|
||||
@@ -1982,7 +1988,7 @@ $repeatLabel""")
|
||||
}
|
||||
else {
|
||||
// all else take its address and assign that also to AY register pair
|
||||
val addrOfDt = returnvalue.type.typeForAddressOf(false)
|
||||
val addrOfDt = returnvalue.type.typeForUntypedAddressOf(false, program.memsizer)
|
||||
val addrofValue = PtAddressOf(addrOfDt, false, returnvalue.position)
|
||||
addrofValue.add(returnvalue)
|
||||
addrofValue.parent = ret.parent
|
||||
@@ -2161,6 +2167,13 @@ $repeatLabel""")
|
||||
|
||||
internal fun jmp(asmLabel: String, indirect: Boolean=false, indexedX: Boolean=false) {
|
||||
if(indirect) {
|
||||
// NOTE: `jmp (ptr)` / `jmp (ptr,x)` have the 6502 page-wrap bug
|
||||
// on plain 6502: if the pointer's address ends in $FF, the
|
||||
// high byte of the target is read from the same page instead
|
||||
// of the next. 65C02 is fine. Latent: only misbehaves when the
|
||||
// pointer variable (the operand, e.g. a computed-goto target)
|
||||
// is placed at a $xxFF address. The new6502 codegen has the
|
||||
// same hazard for its JUMPI/CALLI emitters.
|
||||
if(indexedX)
|
||||
out(" jmp ($asmLabel,x)")
|
||||
else
|
||||
@@ -2238,12 +2251,22 @@ $repeatLabel""")
|
||||
val constOffset = (ptrAndIndex.second as? PtNumber)?.number?.toInt()
|
||||
if(addrOf!=null && constOffset!=null) {
|
||||
if(addrOf.isFromArrayElement) {
|
||||
val arrayId = addrOf.identifier
|
||||
val arrayDt = arrayId?.type
|
||||
if(arrayDt?.sub == BaseDataType.STRUCT_INSTANCE) {
|
||||
val base = asmSymbolName(arrayId)
|
||||
out(" pha")
|
||||
assignmentAsmGen.computeStructArrayOffsetY(arrayDt, addrOf.arrayIndexExpr!!, constOffset)
|
||||
out(" pla")
|
||||
out(" sta $base,y")
|
||||
return true
|
||||
}
|
||||
TODO("address-of array element ${addrOf.position}")
|
||||
} else if(addrOf.dereference!=null) {
|
||||
throw AssemblyError("write &dereference, makes no sense at ${addrOf.position}")
|
||||
} else {
|
||||
var symbolName = asmSymbolName(addrOf.identifier!!)
|
||||
if(addrOf.identifier!!.type.isSplitWordArray) {
|
||||
if(addrOf.identifier!!.type.isSplitWordArray(options.compTarget)) {
|
||||
symbolName = if(addrOf.isMsbForSplitArray) symbolName+"_msb" else symbolName+"_lsb"
|
||||
}
|
||||
out(" sta $symbolName+${constOffset}")
|
||||
@@ -2260,18 +2283,18 @@ $repeatLabel""")
|
||||
out(" sta (${asmSymbolName(pointervar)}),y")
|
||||
} else {
|
||||
// copy the pointer var to zp first
|
||||
val saveA = evalBytevalueWillClobberA(ptrAndIndex.first) || evalBytevalueWillClobberA(ptrAndIndex.second)
|
||||
if(saveA) out(" pha")
|
||||
// assignExpressionToVariable always clobbers A, so always save it
|
||||
out(" pha")
|
||||
if(ptrAndIndex.second.isSimple()) {
|
||||
assignExpressionToVariable(ptrAndIndex.first, "P8ZP_SCRATCH_W2", DataType.UWORD)
|
||||
assignExpressionToRegister(ptrAndIndex.second, RegisterOrPair.Y)
|
||||
if(saveA) out(" pla")
|
||||
out(" pla")
|
||||
out(" sta (P8ZP_SCRATCH_W2),y")
|
||||
} else {
|
||||
pushCpuStack(BaseDataType.UBYTE, ptrAndIndex.second)
|
||||
assignExpressionToVariable(ptrAndIndex.first, "P8ZP_SCRATCH_W2", DataType.UWORD)
|
||||
restoreRegisterStack(CpuRegister.Y, true)
|
||||
if(saveA) out(" pla")
|
||||
out(" pla")
|
||||
out(" sta (P8ZP_SCRATCH_W2),y")
|
||||
}
|
||||
}
|
||||
@@ -2284,13 +2307,21 @@ $repeatLabel""")
|
||||
val constOffset = (ptrAndIndex.second as? PtNumber)?.number?.toInt()
|
||||
if(addrOf!=null && constOffset!=null) {
|
||||
if(addrOf.isFromArrayElement) {
|
||||
val arrayId = addrOf.identifier
|
||||
val arrayDt = arrayId?.type
|
||||
if(arrayDt?.sub == BaseDataType.STRUCT_INSTANCE) {
|
||||
val base = asmSymbolName(arrayId)
|
||||
assignmentAsmGen.computeStructArrayOffsetY(arrayDt, addrOf.arrayIndexExpr!!, constOffset)
|
||||
out(" lda $base,y")
|
||||
return true
|
||||
}
|
||||
TODO("address-of array element ${addrOf.position}")
|
||||
} else if(addrOf.dereference!=null) {
|
||||
if(pointerGen.readByteByAddressOfDereference(addrOf, constOffset))
|
||||
return true
|
||||
} else {
|
||||
var symbolName = asmSymbolName(addrOf.identifier!!)
|
||||
if(addrOf.identifier!!.type.isSplitWordArray) {
|
||||
if(addrOf.identifier!!.type.isSplitWordArray(options.compTarget)) {
|
||||
symbolName = if(addrOf.isMsbForSplitArray) symbolName+"_msb" else symbolName+"_lsb"
|
||||
}
|
||||
out(" lda $symbolName+${constOffset}")
|
||||
@@ -2344,7 +2375,7 @@ $repeatLabel""")
|
||||
throw AssemblyError("write &dereference, makes no sense at ${addrOf.position}")
|
||||
} else {
|
||||
var symbolName = asmSymbolName(addrOf.identifier!!)
|
||||
if(addrOf.identifier!!.type.isSplitWordArray) {
|
||||
if(addrOf.identifier!!.type.isSplitWordArray(options.compTarget)) {
|
||||
symbolName = if(addrOf.isMsbForSplitArray) symbolName+"_msb" else symbolName+"_lsb"
|
||||
}
|
||||
out(" sta $symbolName-${constOffset}")
|
||||
@@ -2381,7 +2412,7 @@ $repeatLabel""")
|
||||
TODO("read &dereference ${addrOf.position}")
|
||||
} else {
|
||||
var symbolName = asmSymbolName(addrOf.identifier!!)
|
||||
if(addrOf.identifier!!.type.isSplitWordArray) {
|
||||
if(addrOf.identifier!!.type.isSplitWordArray(options.compTarget)) {
|
||||
symbolName = if(addrOf.isMsbForSplitArray) symbolName+"_msb" else symbolName+"_lsb"
|
||||
}
|
||||
out(" lda $symbolName-${constOffset}")
|
||||
@@ -2991,6 +3022,7 @@ $repeatLabel""")
|
||||
CpuRegister.A -> {}
|
||||
CpuRegister.X -> out(" txa")
|
||||
CpuRegister.Y -> out(" tya")
|
||||
else -> throw AssemblyError("register not available on this target")
|
||||
}
|
||||
signExtendAXlsb(if(signed) BaseDataType.BYTE else BaseDataType.UBYTE)
|
||||
out("""
|
||||
@@ -3007,6 +3039,7 @@ $repeatLabel""")
|
||||
CpuRegister.A -> out(" ldy #$offset | sta ($zpPtrVar),y")
|
||||
CpuRegister.X -> out(" txa | ldy #$offset | sta ($zpPtrVar),y")
|
||||
CpuRegister.Y -> out(" tya | ldy #$offset | sta ($zpPtrVar),y")
|
||||
else -> throw AssemblyError("register not available on this target")
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -3023,6 +3056,7 @@ $repeatLabel""")
|
||||
out(" tya")
|
||||
if(isTargetCpu(CpuType.CPU65C02)) out(" sta ($zpPtrVar)") else out(" ldy #0 | sta ($zpPtrVar),y")
|
||||
}
|
||||
else -> throw AssemblyError("register not available on this target")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package prog8.codegen.cpu6502
|
||||
|
||||
import prog8.code.GENERATED_LABEL_PREFIX
|
||||
import prog8.code.StConstant
|
||||
import prog8.code.StMemVar
|
||||
import prog8.code.SymbolTable
|
||||
import prog8.code.*
|
||||
import prog8.code.core.ICompilationTarget
|
||||
|
||||
// note: see https://wiki.nesdev.org/w/index.php/6502_assembly_optimisations
|
||||
@@ -41,6 +38,15 @@ internal fun optimizeAssembly(lines: MutableList<String>, machine: ICompilationT
|
||||
linesByFour = runPass(optimizeUnneededTempvarInAdd(linesByFour), 4, linesByFour)
|
||||
linesByFour = runPass(optimizeTSBtoRegularOr(linesByFour), 4, linesByFour)
|
||||
|
||||
var linesByFive = getLinesBy(pretrimmed, lines, 5)
|
||||
linesByFive = runPass(optimizeFoldIndexOffset(linesByFive, symbolTable), 5, linesByFive)
|
||||
|
||||
var linesBySix = getLinesBy(pretrimmed, lines, 6)
|
||||
linesBySix = runPass(optimizeFoldIndexOffsetSplitWord(linesBySix, symbolTable), 6, linesBySix)
|
||||
|
||||
var linesBySeven = getLinesBy(pretrimmed, lines, 7)
|
||||
linesBySeven = runPass(optimizeFoldIndexOffsetNonSplitWord(linesBySeven, symbolTable), 7, linesBySeven)
|
||||
|
||||
var linesByFourteen = getLinesBy(pretrimmed, lines, 14)
|
||||
linesByFourteen = runPass(optimizeSameAssignments(linesByFourteen, machine, symbolTable), 14, linesByFourteen)
|
||||
linesByFourteen = runPass(optimizeSamePointerIndexingAndUselessBeq(linesByFourteen), 14, linesByFourteen)
|
||||
@@ -889,6 +895,207 @@ private fun optimizeUnneededTempvarInAdd(linesByFour: Sequence<List<TrimmedLine>
|
||||
return mods
|
||||
}
|
||||
|
||||
private fun withLabel(line: TrimmedLine, newInstruction: String): String {
|
||||
val label = keeplabelPretrimmed(line.value)
|
||||
return if (label.isEmpty()) newInstruction else "$label ${newInstruction.trimStart()}"
|
||||
}
|
||||
|
||||
private fun scaleConstant(constStr: String, scale: Int): String {
|
||||
val value = if (constStr.startsWith("$"))
|
||||
constStr.substring(1).toInt(16)
|
||||
else
|
||||
constStr.toInt()
|
||||
val scaled = value * scale
|
||||
return if (constStr.startsWith("$"))
|
||||
"$${scaled.toString(16).padStart(2, '0')}"
|
||||
else
|
||||
scaled.toString()
|
||||
}
|
||||
|
||||
private fun isArrayByteSizedAndSmall(name: String, symbolTable: SymbolTable): Boolean {
|
||||
val symbol = symbolTable.flat[name]
|
||||
return when (symbol) {
|
||||
is StStaticVariable -> {
|
||||
val len = symbol.length
|
||||
(symbol.dt.isByteArray || symbol.dt.isString) && len != null && len <= 256u
|
||||
}
|
||||
is StMemVar -> {
|
||||
val len = symbol.length
|
||||
symbol.dt.isByteArray && len != null && len <= 256u
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isArrayWordSizedAndSmall(name: String, symbolTable: SymbolTable): Boolean {
|
||||
val symbol = symbolTable.flat[name]
|
||||
return when (symbol) {
|
||||
is StStaticVariable -> {
|
||||
val len = symbol.length
|
||||
symbol.dt.isWordArray && len != null && len <= 256u
|
||||
}
|
||||
is StMemVar -> {
|
||||
val len = symbol.length
|
||||
symbol.dt.isWordArray && len != null && len <= 256u
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun optimizeFoldIndexOffset(
|
||||
linesByFive: Sequence<List<TrimmedLine>>,
|
||||
symbolTable: SymbolTable
|
||||
): List<Modification> {
|
||||
val mods = mutableListOf<Modification>()
|
||||
|
||||
for (lines in linesByFive) {
|
||||
val first = lines[0].instruction
|
||||
val second = lines[1].instruction
|
||||
val third = lines[2].instruction
|
||||
val fourth = lines[3].instruction
|
||||
val fifth = lines[4].instruction
|
||||
|
||||
if (!first.startsWith("lda ") || second != "clc" || !third.startsWith("adc "))
|
||||
continue
|
||||
if (fourth != "tay" && fourth != "tax")
|
||||
continue
|
||||
|
||||
val reg = fourth[2]
|
||||
val constOperand = third.extractOperandTrimmed()
|
||||
if (!constOperand.startsWith("#"))
|
||||
continue
|
||||
val constValue = constOperand.substring(1)
|
||||
|
||||
val fifthOperand = fifth.extractOperandTrimmed()
|
||||
if (!fifthOperand.endsWith(",$reg") || fifthOperand.startsWith("("))
|
||||
continue
|
||||
|
||||
val addrPart = fifthOperand.dropLast(2)
|
||||
if (!isArrayByteSizedAndSmall(addrPart, symbolTable)) continue
|
||||
|
||||
val mnemonic = fifth.take(3)
|
||||
val varName = first.extractOperandTrimmed()
|
||||
mods.add(Modification(lines[0].index, false, withLabel(lines[0], " ld$reg $varName")))
|
||||
mods.add(Modification(lines[1].index, true, null))
|
||||
mods.add(Modification(lines[2].index, true, null))
|
||||
mods.add(Modification(lines[3].index, true, null))
|
||||
mods.add(Modification(lines[4].index, false, withLabel(lines[4], " $mnemonic $addrPart+$constValue,$reg")))
|
||||
}
|
||||
|
||||
return mods
|
||||
}
|
||||
|
||||
private fun optimizeFoldIndexOffsetSplitWord(
|
||||
linesBySix: Sequence<List<TrimmedLine>>,
|
||||
symbolTable: SymbolTable
|
||||
): List<Modification> {
|
||||
val mods = mutableListOf<Modification>()
|
||||
|
||||
for (lines in linesBySix) {
|
||||
val first = lines[0].instruction
|
||||
val second = lines[1].instruction
|
||||
val third = lines[2].instruction
|
||||
val fourth = lines[3].instruction
|
||||
val fifth = lines[4].instruction
|
||||
val sixth = lines[5].instruction
|
||||
|
||||
if (!first.startsWith("lda ") || second != "clc" || !third.startsWith("adc "))
|
||||
continue
|
||||
if (fourth != "tay" && fourth != "tax")
|
||||
continue
|
||||
|
||||
val reg = fourth[2]
|
||||
val constOperand = third.extractOperandTrimmed()
|
||||
if (!constOperand.startsWith("#"))
|
||||
continue
|
||||
val constValue = constOperand.substring(1)
|
||||
|
||||
val fifthOp = fifth.extractOperandTrimmed()
|
||||
val sixthOp = sixth.extractOperandTrimmed()
|
||||
if (!fifthOp.endsWith(",$reg") || fifthOp.startsWith("("))
|
||||
continue
|
||||
if (!sixthOp.endsWith(",$reg") || sixthOp.startsWith("("))
|
||||
continue
|
||||
|
||||
val lsbAddr = fifthOp.dropLast(2)
|
||||
val msbAddr = sixthOp.dropLast(2)
|
||||
if (!lsbAddr.endsWith("_lsb") || !msbAddr.endsWith("_msb"))
|
||||
continue
|
||||
val baseName = lsbAddr.dropLast(4)
|
||||
if (msbAddr != baseName + "_msb")
|
||||
continue
|
||||
|
||||
if (!isArrayWordSizedAndSmall(baseName, symbolTable))
|
||||
continue
|
||||
|
||||
val lsbMnemonic = fifth.take(3)
|
||||
val msbMnemonic = sixth.take(3)
|
||||
val varName = first.extractOperandTrimmed()
|
||||
|
||||
mods.add(Modification(lines[0].index, false, withLabel(lines[0], " ld$reg $varName")))
|
||||
mods.add(Modification(lines[1].index, true, null))
|
||||
mods.add(Modification(lines[2].index, true, null))
|
||||
mods.add(Modification(lines[3].index, true, null))
|
||||
mods.add(Modification(lines[4].index, false, withLabel(lines[4], " $lsbMnemonic $lsbAddr+$constValue,$reg")))
|
||||
mods.add(Modification(lines[5].index, false, withLabel(lines[5], " $msbMnemonic $msbAddr+$constValue,$reg")))
|
||||
}
|
||||
|
||||
return mods
|
||||
}
|
||||
|
||||
private fun optimizeFoldIndexOffsetNonSplitWord(
|
||||
linesBySeven: Sequence<List<TrimmedLine>>,
|
||||
symbolTable: SymbolTable
|
||||
): List<Modification> {
|
||||
val mods = mutableListOf<Modification>()
|
||||
|
||||
for (lines in linesBySeven) {
|
||||
val first = lines[0].instruction
|
||||
val second = lines[1].instruction
|
||||
val third = lines[2].instruction
|
||||
val fourth = lines[3].instruction
|
||||
val fifth = lines[4].instruction
|
||||
val sixth = lines[5].instruction
|
||||
val seventh = lines[6].instruction
|
||||
|
||||
if (!first.startsWith("lda ") || second != "clc" || !third.startsWith("adc "))
|
||||
continue
|
||||
if (fourth != "asl a" || (fifth != "tay" && fifth != "tax"))
|
||||
continue
|
||||
|
||||
val reg = fifth[2]
|
||||
val constOperand = third.extractOperandTrimmed()
|
||||
if (!constOperand.startsWith("#"))
|
||||
continue
|
||||
val constValue = constOperand.substring(1)
|
||||
val scaledConst = scaleConstant(constValue, 2)
|
||||
|
||||
val sixthOp = sixth.extractOperandTrimmed()
|
||||
val seventhOp = seventh.extractOperandTrimmed()
|
||||
if (!sixthOp.endsWith(",$reg") || sixthOp.startsWith("("))
|
||||
continue
|
||||
if (!seventhOp.endsWith(",$reg") || seventhOp.startsWith("("))
|
||||
continue
|
||||
|
||||
val baseAddr = sixthOp.dropLast(2)
|
||||
if (seventhOp != "$baseAddr+1,$reg")
|
||||
continue
|
||||
|
||||
if (!isArrayWordSizedAndSmall(baseAddr, symbolTable))
|
||||
continue
|
||||
|
||||
val lowMnemonic = sixth.take(3)
|
||||
val highMnemonic = seventh.take(3)
|
||||
|
||||
mods.add(Modification(lines[1].index, true, null))
|
||||
mods.add(Modification(lines[2].index, true, null))
|
||||
mods.add(Modification(lines[5].index, false, withLabel(lines[5], " $lowMnemonic $baseAddr+$scaledConst,$reg")))
|
||||
mods.add(Modification(lines[6].index, false, withLabel(lines[6], " $highMnemonic $baseAddr+1+$scaledConst,$reg")))
|
||||
}
|
||||
|
||||
return mods
|
||||
}
|
||||
|
||||
private fun optimizeAddWordToSameVariableOrExtraRegisterLoadInWordStore(linesByFourteen: Sequence<List<TrimmedLine>>): List<Modification> {
|
||||
/*
|
||||
; FIRST SEQUYENCE: P8ZP_SCRATCH_PTR += AY :
|
||||
|
||||
@@ -33,8 +33,8 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
return when (fcall.name) {
|
||||
"msw" -> funcMsw(fcall, firstReturnRegister ?: RegisterOrPair.AY)
|
||||
"lsw" -> funcLsw(fcall, firstReturnRegister ?: RegisterOrPair.AY)
|
||||
"msb" -> funcMsb(fcall, firstReturnRegister ?: RegisterOrPair.A)
|
||||
"msb__long" -> funcMsbLong(fcall, firstReturnRegister ?: RegisterOrPair.A)
|
||||
"msb" -> funcMsb(fcall, false, firstReturnRegister ?: RegisterOrPair.A)
|
||||
"msb__long" -> funcMsb(fcall, true, firstReturnRegister ?: RegisterOrPair.A)
|
||||
"lsb" -> funcLsb(fcall, false, firstReturnRegister ?: RegisterOrPair.A)
|
||||
"lsb__long" -> funcLsb(fcall,true, firstReturnRegister ?: RegisterOrPair.A)
|
||||
"mkword" -> funcMkword(fcall, firstReturnRegister ?: RegisterOrPair.AY)
|
||||
@@ -376,6 +376,11 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
|
||||
val identifier = fcall.args[0] as? PtIdentifier
|
||||
if(identifier!=null) {
|
||||
// NOTE: `jmp (ptr)` below has the 6502 page-wrap bug on plain
|
||||
// 6502 if the pointer (the identifier, a variable holding a
|
||||
// routine address) lands at $xxFF. 65C02 is fine. Latent; only
|
||||
// misbehaves for variables placed at a $xxFF address. The
|
||||
// new6502 codegen's CALLI has the same hazard.
|
||||
asmgen.out("""
|
||||
; push a return address so the jmp becomes indirect jsr
|
||||
lda #>((+)-1)
|
||||
@@ -387,6 +392,9 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
return arrayOf(RegisterOrPair.AY)
|
||||
}
|
||||
|
||||
// NOTE: same 6502 page-wrap pitfall as above: the pointer is
|
||||
// staged through P8ZP_SCRATCH_W2 before the indirect jmp, so the
|
||||
// scratch word must not land at $xxFF on plain 6502. 65C02 is fine.
|
||||
asmgen.assignExpressionToVariable(fcall.args[0], asmgen.asmVariableName("P8ZP_SCRATCH_W2"), DataType.UWORD) // jump address
|
||||
asmgen.out("""
|
||||
; push a return address so the jmp becomes indirect jsr
|
||||
@@ -1137,7 +1145,7 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
val msbAdd: Int
|
||||
if(indexer.splitWords) {
|
||||
val arrayVariable = indexer.variable ?: TODO("ptr indexing ${indexer.position}")
|
||||
indexer.setChild(0, PtIdentifier(arrayVariable.name + (if(msb) "_msb" else "_lsb"), DataType.arrayFor(BaseDataType.UBYTE, false), arrayVariable.position))
|
||||
indexer.setChild(0, PtIdentifier(arrayVariable.name + (if(msb) "_msb" else "_lsb"), DataType.arrayFor(BaseDataType.UBYTE, asmgen.options.compTarget), arrayVariable.position))
|
||||
elementSize = 1
|
||||
msbAdd = 0
|
||||
} else {
|
||||
@@ -1432,7 +1440,7 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
return emptyArray()
|
||||
} else if(addressOfIdentifier!=null && (addressOfIdentifier.type.isWord || addressOfIdentifier.type.isPointer || addressOfIdentifier.type.isByteArray)) {
|
||||
var varname = asmgen.asmVariableName(addressOfIdentifier)
|
||||
if(addressOfIdentifier.type.isSplitWordArray) {
|
||||
if(addressOfIdentifier.type.isSplitWordArray(asmgen.options.compTarget)) {
|
||||
val addrOf = result.first as PtAddressOf
|
||||
varname = if(addrOf.isMsbForSplitArray) varname+"_msb" else varname+"_lsb"
|
||||
}
|
||||
@@ -1814,7 +1822,7 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
}
|
||||
} else if(addressOfIdentifier!=null && (addressOfIdentifier.type.isWord || addressOfIdentifier.type.isPointer || addressOfIdentifier.type.isByteArray)) {
|
||||
var varname = asmgen.asmVariableName(addressOfIdentifier)
|
||||
if(addressOfIdentifier.type.isSplitWordArray) {
|
||||
if(addressOfIdentifier.type.isSplitWordArray(asmgen.options.compTarget)) {
|
||||
val addrOf = result.first as PtAddressOf
|
||||
varname = if(addrOf.isMsbForSplitArray) varname+"_msb" else varname+"_lsb"
|
||||
}
|
||||
@@ -1973,11 +1981,104 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
return arrayOf(RegisterOrPair.R14R15)
|
||||
}
|
||||
|
||||
private fun inlineClampByte(value: PtExpression, minVal: Int, maxVal: Int, signed: Boolean) {
|
||||
val typeMin = if (signed) -128 else 0
|
||||
val typeMax = if (signed) 127 else 255
|
||||
assignAsmGen.assignExpressionToRegister(value, RegisterOrPair.A, signed)
|
||||
when {
|
||||
minVal == typeMin && maxVal == typeMax -> {
|
||||
// no-op: the value in A is already the clamped result
|
||||
}
|
||||
minVal == typeMin -> {
|
||||
// upper clamp only: min(x, maxVal)
|
||||
emitByteUpperClamp(maxVal, signed)
|
||||
}
|
||||
maxVal == typeMax -> {
|
||||
// lower clamp only: max(x, minVal)
|
||||
emitByteLowerClamp(minVal, signed)
|
||||
}
|
||||
else -> {
|
||||
// two-sided clamp: upper then lower, sequentially on the running value in A
|
||||
emitByteUpperClamp(maxVal, signed)
|
||||
emitByteLowerClamp(minVal, signed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitByteUpperClamp(maxVal: Int, signed: Boolean) {
|
||||
// result = min(x, maxVal); x is in A on entry
|
||||
// mirrors the proven func_clamp_byte sequence (sec/sbc + bmi means "x < max")
|
||||
val imm = "#${(maxVal and 0xff).toHex()}"
|
||||
if (signed) {
|
||||
val keep = asmgen.makeLabel("clampkeep")
|
||||
val done = asmgen.makeLabel("clampdone")
|
||||
asmgen.out(listOf(
|
||||
" tay",
|
||||
" sec",
|
||||
" sbc $imm",
|
||||
" bvc +",
|
||||
" eor #128",
|
||||
"+ bmi $keep",
|
||||
" lda $imm",
|
||||
" jmp $done",
|
||||
"$keep tya",
|
||||
done
|
||||
).joinToString("\n"))
|
||||
} else {
|
||||
val keep = asmgen.makeLabel("clampkeep")
|
||||
asmgen.out(listOf(
|
||||
" cmp $imm",
|
||||
" bcc $keep",
|
||||
" lda $imm",
|
||||
keep
|
||||
).joinToString("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitByteLowerClamp(minVal: Int, signed: Boolean) {
|
||||
// result = max(x, minVal); x is in A on entry
|
||||
// mirrors the proven func_clamp_byte sequence (sec/sbc + bpl means "x >= min")
|
||||
val imm = "#${(minVal and 0xff).toHex()}"
|
||||
if (signed) {
|
||||
val keep = asmgen.makeLabel("clampkeep")
|
||||
val done = asmgen.makeLabel("clampdone")
|
||||
asmgen.out(listOf(
|
||||
" tay",
|
||||
" sec",
|
||||
" sbc $imm",
|
||||
" bvc +",
|
||||
" eor #128",
|
||||
"+ bpl $keep",
|
||||
" lda $imm",
|
||||
" jmp $done",
|
||||
"$keep tya",
|
||||
done
|
||||
).joinToString("\n"))
|
||||
} else {
|
||||
val keep = asmgen.makeLabel("clampkeep")
|
||||
asmgen.out(listOf(
|
||||
" cmp $imm",
|
||||
" bcs $keep",
|
||||
" lda $imm",
|
||||
keep
|
||||
).joinToString("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun funcClamp(fcall: PtFunctionCall): Array<RegisterOrPair> {
|
||||
val signed = fcall.type.isSigned
|
||||
val zpCheck = { name: String -> asmgen.isZpVar(name) }
|
||||
when {
|
||||
fcall.type.isByte -> {
|
||||
if (!signed && fcall.args[1] is PtNumber && fcall.args[2] is PtNumber) {
|
||||
// unsigned (ubyte) clamp with constant bounds: emit a small inline clamp.
|
||||
// Only worthwhile for ubyte: the signed inline sequence is larger than the
|
||||
// shared jsr func_clamp_byte subroutine, so signed clamps keep using that.
|
||||
val minVal = (fcall.args[1] as PtNumber).number.toInt()
|
||||
val maxVal = (fcall.args[2] as PtNumber).number.toInt()
|
||||
inlineClampByte(fcall.args[0], minVal, maxVal, signed)
|
||||
return arrayOf(RegisterOrPair.A)
|
||||
}
|
||||
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck) && fcall.args[2].isSimple(zpCheck)) {
|
||||
// simple values (number/variable) do not clobber W1, so min/max can be evaluated first
|
||||
assignAsmGen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W1", fcall.args[1].type) // minimum
|
||||
@@ -2426,55 +2527,36 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
return arrayOf(RegisterOrPair.AY)
|
||||
}
|
||||
|
||||
private fun funcMsbLong(fcall: PtFunctionCall, resultReg: RegisterOrPair): Array<RegisterOrPair> {
|
||||
private fun funcMsb(fcall: PtFunctionCall, fromLong: Boolean, resultReg: RegisterOrPair): Array<RegisterOrPair> {
|
||||
val arg = fcall.args.single()
|
||||
if (!arg.type.isLong)
|
||||
throw AssemblyError("msb__long requires long argument")
|
||||
if (arg is PtNumber)
|
||||
throw AssemblyError("msb(const) should have been const-folded away")
|
||||
|
||||
if (arg is PtIdentifier) {
|
||||
val sourceName = asmgen.asmVariableName(arg)
|
||||
when(resultReg) {
|
||||
RegisterOrPair.A -> asmgen.out(" lda $sourceName+3")
|
||||
RegisterOrPair.X -> asmgen.out(" ldx $sourceName+3")
|
||||
RegisterOrPair.Y -> asmgen.out(" ldy $sourceName+3")
|
||||
else -> throw AssemblyError("invalid register for msb long: $resultReg ${arg.position}")
|
||||
}
|
||||
} else if(arg is PtArrayIndexer) {
|
||||
TODO("msb of long array element ${fcall.position}")
|
||||
if(fromLong) {
|
||||
if (!arg.type.isLong)
|
||||
throw AssemblyError("msb__long requires long argument")
|
||||
} else {
|
||||
asmgen.assignExpressionToRegister(arg, RegisterOrPair.R14R15, arg.type.isSigned)
|
||||
when(resultReg) {
|
||||
RegisterOrPair.A -> asmgen.out(" lda cx16.r14+3")
|
||||
RegisterOrPair.X -> asmgen.out(" ldx cx16.r14+3")
|
||||
RegisterOrPair.Y -> asmgen.out(" ldy cx16.r14+3")
|
||||
else -> throw AssemblyError("invalid register for msb long: $resultReg ${arg.position}")
|
||||
}
|
||||
if (!arg.type.isWord && !arg.type.isPointer)
|
||||
throw AssemblyError("msb requires word argument")
|
||||
}
|
||||
|
||||
return arrayOf(resultReg)
|
||||
}
|
||||
|
||||
private fun funcMsb(fcall: PtFunctionCall, resultReg: RegisterOrPair): Array<RegisterOrPair> {
|
||||
val arg = fcall.args.single()
|
||||
if (!arg.type.isWord)
|
||||
throw AssemblyError("msb requires word argument")
|
||||
if (arg is PtNumber)
|
||||
throw AssemblyError("msb(const) should have been const-folded away")
|
||||
|
||||
val offset = if(fromLong) 3 else 1
|
||||
|
||||
if (arg is PtIdentifier) {
|
||||
val sourceName = asmgen.asmVariableName(arg)
|
||||
when(resultReg) {
|
||||
RegisterOrPair.A -> asmgen.out(" lda $sourceName+1")
|
||||
RegisterOrPair.X -> asmgen.out(" ldx $sourceName+1")
|
||||
RegisterOrPair.Y -> asmgen.out(" ldy $sourceName+1")
|
||||
RegisterOrPair.AX -> asmgen.out(" lda $sourceName+1 | ldx #0")
|
||||
RegisterOrPair.AY -> asmgen.out(" lda $sourceName+1 | ldy #0")
|
||||
RegisterOrPair.XY -> asmgen.out(" ldx $sourceName+1 | ldy #0")
|
||||
else -> throw AssemblyError("invalid register for msb: $resultReg ${arg.position}")
|
||||
RegisterOrPair.A -> asmgen.out(" lda $sourceName+$offset")
|
||||
RegisterOrPair.X -> asmgen.out(" ldx $sourceName+$offset")
|
||||
RegisterOrPair.Y -> asmgen.out(" ldy $sourceName+$offset")
|
||||
RegisterOrPair.AX -> asmgen.out(" lda $sourceName+$offset | ldx #0")
|
||||
RegisterOrPair.AY -> asmgen.out(" lda $sourceName+$offset | ldy #0")
|
||||
RegisterOrPair.XY -> asmgen.out(" ldx $sourceName+$offset | ldy #0")
|
||||
else -> throw AssemblyError("invalid register for msb${if(fromLong) " long" else ""}: $resultReg ${arg.position}")
|
||||
}
|
||||
} else {
|
||||
if(arg is PtArrayIndexer) {
|
||||
if(fromLong)
|
||||
TODO("msb of long array element ${fcall.position}")
|
||||
|
||||
// just read the msb byte out of the word array
|
||||
if(arg.splitWords) {
|
||||
if(arg.variable==null)
|
||||
@@ -2496,15 +2578,15 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
RegisterOrPair.AX -> {
|
||||
asmgen.loadScaledArrayIndexIntoRegister(arg, CpuRegister.Y)
|
||||
asmgen.out(" lda $arrayVar,y | ldx #0")
|
||||
}
|
||||
}
|
||||
RegisterOrPair.AY -> {
|
||||
asmgen.loadScaledArrayIndexIntoRegister(arg, CpuRegister.Y)
|
||||
asmgen.out(" lda $arrayVar,y | ldy #0")
|
||||
}
|
||||
}
|
||||
RegisterOrPair.XY -> {
|
||||
asmgen.loadScaledArrayIndexIntoRegister(arg, CpuRegister.X)
|
||||
asmgen.out(" ldy $arrayVar,x | ldx #0")
|
||||
}
|
||||
}
|
||||
else -> throw AssemblyError("invalid register for msb: $resultReg ${arg.position}")
|
||||
}
|
||||
} else {
|
||||
@@ -2540,6 +2622,14 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
else -> throw AssemblyError("invalid register for msb: $resultReg ${arg.position}")
|
||||
}
|
||||
}
|
||||
} else if(fromLong) {
|
||||
asmgen.assignExpressionToRegister(arg, RegisterOrPair.R14R15, arg.type.isSigned)
|
||||
when(resultReg) {
|
||||
RegisterOrPair.A -> asmgen.out(" lda cx16.r14+$offset")
|
||||
RegisterOrPair.X -> asmgen.out(" ldx cx16.r14+$offset")
|
||||
RegisterOrPair.Y -> asmgen.out(" ldy cx16.r14+$offset")
|
||||
else -> throw AssemblyError("invalid register for msb long: $resultReg ${arg.position}")
|
||||
}
|
||||
} else {
|
||||
asmgen.assignExpressionToRegister(arg, RegisterOrPair.AY)
|
||||
asmgen.out(" tya")
|
||||
@@ -2554,7 +2644,7 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
if(fromLong) {
|
||||
if (!arg.type.isLong) throw AssemblyError("lsb__long requires long")
|
||||
} else {
|
||||
if (!arg.type.isWord) throw AssemblyError("lsb requires word")
|
||||
if (!arg.type.isWord && !arg.type.isPointer) throw AssemblyError("lsb requires word")
|
||||
}
|
||||
if (arg is PtNumber)
|
||||
throw AssemblyError("lsb(const) should have been const-folded away")
|
||||
@@ -2680,7 +2770,7 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
|
||||
private fun translateArguments(call: PtFunctionCall, funcname: String?, scope: IPtSubroutine?) {
|
||||
val signature = BuiltinFunctions.getValue(call.name)
|
||||
val callConv = signature.callConvention(call.args.map {
|
||||
val callConv = signature.callConventionFor6502(call.args.map {
|
||||
require(it.type.isNumericOrBool)
|
||||
it.type.base
|
||||
})
|
||||
@@ -2733,7 +2823,7 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
conv.dt==BaseDataType.LONG -> getSourceForLong(value)
|
||||
conv.dt.isPassByRef -> {
|
||||
// put the address of the argument in AY
|
||||
val addr = PtAddressOf(DataType.forDt(conv.dt).typeForAddressOf(false), false, value.position)
|
||||
val addr = PtAddressOf(DataType.forDt(conv.dt).typeForUntypedAddressOf(false, program.memsizer), false, value.position)
|
||||
addr.add(value)
|
||||
addr.parent = call
|
||||
AsmAssignSource.fromAstSource(addr, program, asmgen)
|
||||
@@ -2752,7 +2842,7 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
conv.dt==BaseDataType.LONG -> AsmAssignSource.fromAstSource(value, program, asmgen)
|
||||
conv.dt.isPassByRef -> {
|
||||
// put the address of the argument in AY
|
||||
val addr = PtAddressOf(DataType.forDt(conv.dt).typeForAddressOf(false), false,value.position)
|
||||
val addr = PtAddressOf(DataType.forDt(conv.dt).typeForUntypedAddressOf(false, program.memsizer), false,value.position)
|
||||
addr.add(value)
|
||||
addr.parent = call
|
||||
AsmAssignSource.fromAstSource(addr, program, asmgen)
|
||||
|
||||
@@ -5,6 +5,7 @@ import prog8.code.StMemVar
|
||||
import prog8.code.StStaticVariable
|
||||
import prog8.code.ast.PtForLoop
|
||||
import prog8.code.ast.PtIdentifier
|
||||
import prog8.code.ast.PtNumber
|
||||
import prog8.code.ast.PtRange
|
||||
import prog8.code.core.*
|
||||
import kotlin.math.absoluteValue
|
||||
@@ -33,15 +34,21 @@ internal class ForLoopsAsmGen(
|
||||
}
|
||||
|
||||
private fun translateForOverNonconstRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
|
||||
if(range.step.asConstInteger()!! < -1) {
|
||||
val constStep = range.step.asConstInteger()
|
||||
if(constStep != null && constStep < -1) {
|
||||
val limit = range.to.asConstInteger()
|
||||
if(limit==0)
|
||||
throw AssemblyError("for unsigned loop variable it's not possible to count down with step != -1 from a non-const value to exactly zero due to value wrapping")
|
||||
}
|
||||
|
||||
if(range.step !is PtNumber) {
|
||||
translateForOverNonconstStepRange(stmt, iterableDt, range)
|
||||
return
|
||||
}
|
||||
|
||||
when {
|
||||
iterableDt.isByteArray -> forOverNonconstByteRange(stmt, iterableDt, range)
|
||||
iterableDt.isWordArray && !iterableDt.isSplitWordArray -> forOverNonconstWordRange(stmt, iterableDt, range)
|
||||
iterableDt.isWordArray && !iterableDt.isSplitWordArray(asmgen.options.compTarget) -> forOverNonconstWordRange(stmt, iterableDt, range)
|
||||
iterableDt.isLongArray -> forOverNonconstLongRange(stmt, iterableDt, range)
|
||||
else -> throw AssemblyError("range expression can only be byte, word or long")
|
||||
}
|
||||
@@ -49,6 +56,369 @@ internal class ForLoopsAsmGen(
|
||||
asmgen.loopEndLabels.removeLast()
|
||||
}
|
||||
|
||||
private fun translateForOverNonconstStepRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
|
||||
when {
|
||||
iterableDt.isByteArray -> forOverNonconstStepByteRange(stmt, iterableDt, range)
|
||||
iterableDt.isWordArray && !iterableDt.isSplitWordArray(asmgen.options.compTarget) -> forOverNonconstStepWordRange(stmt, iterableDt, range)
|
||||
iterableDt.isLongArray -> throw AssemblyError("variable-step for loops over long ranges are not supported by the legacy 6502 code generator at ${stmt.position}")
|
||||
else -> throw AssemblyError("range expression can only be byte, word or long")
|
||||
}
|
||||
asmgen.loopEndLabels.removeLast()
|
||||
}
|
||||
|
||||
private fun forOverNonconstStepByteRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
|
||||
val loopDt = iterableDt.elementType()
|
||||
val loopvar = asmgen.asmVariableName(stmt.variable)
|
||||
val toValueVar = asmgen.createTempVarReused(loopDt.base, false, range)
|
||||
val stepVar = asmgen.createTempVarReused(range.step.type.base, false, range)
|
||||
val nextVar = asmgen.createTempVarReused(loopDt.base, false, range)
|
||||
val loopLabel = asmgen.makeLabel("for_loop")
|
||||
val descendingLabel = if(range.step.type.isSigned) asmgen.makeLabel("for_desc") else null
|
||||
val endLabel = asmgen.makeLabel("for_end")
|
||||
asmgen.loopEndLabels.add(endLabel)
|
||||
|
||||
// Keep range expression evaluation in source order and preserve each value across the next one.
|
||||
asmgen.assignExpressionToVariable(range.from, loopvar, loopDt)
|
||||
asmgen.assignExpressionToVariable(range.to, toValueVar, loopDt)
|
||||
asmgen.assignExpressionToVariable(range.step, stepVar, range.step.type)
|
||||
if (range.step.type.isSigned)
|
||||
asmgen.out(" lda $stepVar | beq $endLabel | bmi $descendingLabel")
|
||||
else
|
||||
asmgen.out(" lda $stepVar | beq $endLabel")
|
||||
|
||||
emitByteRangePrecheck(loopvar, toValueVar, loopLabel, endLabel, loopDt.isSigned, true)
|
||||
asmgen.out(loopLabel)
|
||||
asmgen.translate(stmt.statements)
|
||||
emitByteRangeUpdate(loopvar, toValueVar, stepVar, nextVar, endLabel, loopLabel, loopDt.isSigned, range.step.type.isSigned)
|
||||
|
||||
if(descendingLabel != null) {
|
||||
asmgen.out(descendingLabel)
|
||||
emitByteRangePrecheck(loopvar, toValueVar, loopLabel, endLabel, loopDt.isSigned, false)
|
||||
asmgen.out(" jmp $loopLabel")
|
||||
}
|
||||
asmgen.out(endLabel)
|
||||
}
|
||||
|
||||
private fun emitByteRangePrecheck(loopvar: String, toValueVar: String, loopLabel: String, endLabel: String, signed: Boolean, ascending: Boolean) {
|
||||
val notEqualLabel = asmgen.makeLabel("for_not_equal")
|
||||
val compareDoneLabel = asmgen.makeLabel("for_compare_done")
|
||||
if (signed) {
|
||||
asmgen.out("""
|
||||
lda $loopvar
|
||||
cmp $toValueVar
|
||||
bne $notEqualLabel
|
||||
jmp $loopLabel
|
||||
$notEqualLabel
|
||||
sec
|
||||
lda $loopvar
|
||||
sbc $toValueVar
|
||||
bvc $compareDoneLabel
|
||||
eor #$80
|
||||
$compareDoneLabel
|
||||
${if (ascending) "bpl" else "bmi"} $endLabel
|
||||
""")
|
||||
} else if (ascending) {
|
||||
asmgen.out("""
|
||||
lda $toValueVar
|
||||
cmp $loopvar
|
||||
bcc $endLabel
|
||||
jmp $loopLabel
|
||||
""")
|
||||
} else {
|
||||
asmgen.out("""
|
||||
lda $loopvar
|
||||
cmp $toValueVar
|
||||
bcc $endLabel
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitByteRangeUpdate(
|
||||
loopvar: String,
|
||||
toValueVar: String,
|
||||
stepVar: String,
|
||||
nextVar: String,
|
||||
endLabel: String,
|
||||
loopLabel: String,
|
||||
loopvarSigned: Boolean,
|
||||
stepSigned: Boolean
|
||||
) {
|
||||
val notLastLabel = asmgen.makeLabel("for_not_last")
|
||||
val ascendingCompareDoneLabel = asmgen.makeLabel("for_next_compare_done")
|
||||
val descendingCompareDoneLabel = if(stepSigned) asmgen.makeLabel("for_desc_compare_done") else null
|
||||
val descendingUpdateLabel = if(stepSigned) asmgen.makeLabel("for_desc_update") else null
|
||||
val storeNextLabel = asmgen.makeLabel("for_store_next")
|
||||
val ascendingWrapBranch = if (loopvarSigned) "bvs" else "bcs"
|
||||
val descendingWrapBranch = if (loopvarSigned) "bvs" else "bcc"
|
||||
asmgen.out("""
|
||||
lda $loopvar
|
||||
cmp $toValueVar
|
||||
bne $notLastLabel
|
||||
jmp $endLabel
|
||||
$notLastLabel
|
||||
""")
|
||||
if (stepSigned)
|
||||
asmgen.out(" lda $stepVar | bmi $descendingUpdateLabel")
|
||||
asmgen.out("""
|
||||
clc
|
||||
lda $loopvar
|
||||
adc $stepVar
|
||||
$ascendingWrapBranch $endLabel
|
||||
sta $nextVar
|
||||
""")
|
||||
asmgen.out("""
|
||||
lda $nextVar
|
||||
cmp $toValueVar
|
||||
beq $storeNextLabel
|
||||
sec
|
||||
lda $nextVar
|
||||
sbc $toValueVar
|
||||
bvc $ascendingCompareDoneLabel
|
||||
eor #$80
|
||||
$ascendingCompareDoneLabel
|
||||
""")
|
||||
if (loopvarSigned)
|
||||
asmgen.out(" bpl $endLabel")
|
||||
else
|
||||
asmgen.out(" bcs $endLabel")
|
||||
if(stepSigned) {
|
||||
asmgen.out("""
|
||||
jmp $storeNextLabel
|
||||
|
||||
$descendingUpdateLabel
|
||||
lda $loopvar
|
||||
cmp $toValueVar
|
||||
beq $endLabel
|
||||
clc
|
||||
lda $loopvar
|
||||
adc $stepVar
|
||||
$descendingWrapBranch $endLabel
|
||||
sta $nextVar
|
||||
""")
|
||||
if (loopvarSigned)
|
||||
asmgen.out("""
|
||||
sec
|
||||
lda $nextVar
|
||||
sbc $toValueVar
|
||||
bvc $descendingCompareDoneLabel
|
||||
eor #$80
|
||||
$descendingCompareDoneLabel
|
||||
bmi $endLabel
|
||||
""")
|
||||
else
|
||||
asmgen.out("""
|
||||
lda $nextVar
|
||||
cmp $toValueVar
|
||||
bcc $endLabel
|
||||
""")
|
||||
}
|
||||
asmgen.out("""
|
||||
jmp $storeNextLabel
|
||||
|
||||
$storeNextLabel
|
||||
lda $nextVar
|
||||
sta $loopvar
|
||||
jmp $loopLabel
|
||||
""")
|
||||
}
|
||||
|
||||
private fun forOverNonconstStepWordRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
|
||||
val loopDt = iterableDt.elementType()
|
||||
val loopvar = asmgen.asmVariableName(stmt.variable)
|
||||
val toValueVar = asmgen.createTempVarReused(loopDt.base, false, range)
|
||||
val stepVar = asmgen.createTempVarReused(range.step.type.base, false, range)
|
||||
val nextVar = asmgen.createTempVarReused(loopDt.base, false, range)
|
||||
val loopLabel = asmgen.makeLabel("for_loop")
|
||||
val descendingLabel = if(range.step.type.isSigned) asmgen.makeLabel("for_desc") else null
|
||||
val endLabel = asmgen.makeLabel("for_end")
|
||||
asmgen.loopEndLabels.add(endLabel)
|
||||
|
||||
// Keep range expression evaluation in source order and preserve each value across the next one.
|
||||
asmgen.assignExpressionToVariable(range.from, loopvar, loopDt)
|
||||
asmgen.assignExpressionToVariable(range.to, toValueVar, loopDt)
|
||||
asmgen.assignExpressionToVariable(range.step, stepVar, range.step.type)
|
||||
if (range.step.type.isSigned)
|
||||
asmgen.out(" lda $stepVar | ora ${stepVar}+1 | beq $endLabel | lda ${stepVar}+1 | bmi $descendingLabel")
|
||||
else
|
||||
asmgen.out(" lda $stepVar | ora ${stepVar}+1 | beq $endLabel")
|
||||
|
||||
emitWordRangePrecheck(loopvar, toValueVar, loopLabel, endLabel, loopDt.isSigned, true)
|
||||
asmgen.out(loopLabel)
|
||||
asmgen.translate(stmt.statements)
|
||||
emitWordRangeUpdate(loopvar, toValueVar, stepVar, nextVar, endLabel, loopLabel, loopDt.isSigned, range.step.type.isSigned)
|
||||
|
||||
if(descendingLabel != null) {
|
||||
asmgen.out(descendingLabel)
|
||||
emitWordRangePrecheck(loopvar, toValueVar, loopLabel, endLabel, loopDt.isSigned, false)
|
||||
asmgen.out(" jmp $loopLabel")
|
||||
}
|
||||
asmgen.out(endLabel)
|
||||
}
|
||||
|
||||
private fun emitWordRangePrecheck(loopvar: String, toValueVar: String, loopLabel: String, endLabel: String, signed: Boolean, ascending: Boolean) {
|
||||
val notEqualLabel = asmgen.makeLabel("for_not_equal")
|
||||
val compareDoneLabel = asmgen.makeLabel("for_compare_done")
|
||||
if (signed) {
|
||||
asmgen.out("""
|
||||
lda $loopvar+1
|
||||
cmp $toValueVar+1
|
||||
bne $notEqualLabel
|
||||
lda $loopvar
|
||||
cmp $toValueVar
|
||||
bne $notEqualLabel
|
||||
jmp $loopLabel
|
||||
$notEqualLabel
|
||||
sec
|
||||
lda $loopvar
|
||||
sbc $toValueVar
|
||||
lda $loopvar+1
|
||||
sbc $toValueVar+1
|
||||
bvc $compareDoneLabel
|
||||
eor #$80
|
||||
$compareDoneLabel
|
||||
${if (ascending) "bpl" else "bmi"} $endLabel
|
||||
""")
|
||||
} else if (ascending) {
|
||||
asmgen.out("""
|
||||
lda $toValueVar+1
|
||||
cmp $loopvar+1
|
||||
bcc $endLabel
|
||||
bne $loopLabel
|
||||
lda $toValueVar
|
||||
cmp $loopvar
|
||||
bcc $endLabel
|
||||
jmp $loopLabel
|
||||
""")
|
||||
} else {
|
||||
asmgen.out("""
|
||||
lda $loopvar+1
|
||||
cmp $toValueVar+1
|
||||
bcc $endLabel
|
||||
bne $loopLabel
|
||||
lda $loopvar
|
||||
cmp $toValueVar
|
||||
bcc $endLabel
|
||||
jmp $loopLabel
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitWordRangeUpdate(
|
||||
loopvar: String,
|
||||
toValueVar: String,
|
||||
stepVar: String,
|
||||
nextVar: String,
|
||||
endLabel: String,
|
||||
loopLabel: String,
|
||||
loopvarSigned: Boolean,
|
||||
stepSigned: Boolean
|
||||
) {
|
||||
val notLastLabel = asmgen.makeLabel("for_not_last")
|
||||
val ascendingCompareLabel = asmgen.makeLabel("for_next_compare")
|
||||
val ascendingCompareDoneLabel = asmgen.makeLabel("for_next_compare_done")
|
||||
val descendingCompareDoneLabel = if(stepSigned) asmgen.makeLabel("for_desc_compare_done") else null
|
||||
val descendingUpdateLabel = if(stepSigned) asmgen.makeLabel("for_desc_update") else null
|
||||
val descendingNotLastLabel = if(stepSigned) asmgen.makeLabel("for_desc_not_last") else null
|
||||
val storeNextLabel = asmgen.makeLabel("for_store_next")
|
||||
val ascendingWrapBranch = if (loopvarSigned) "bvs" else "bcs"
|
||||
val descendingWrapBranch = if (loopvarSigned) "bvs" else "bcc"
|
||||
asmgen.out("""
|
||||
lda $loopvar+1
|
||||
cmp $toValueVar+1
|
||||
bne $notLastLabel
|
||||
lda $loopvar
|
||||
cmp $toValueVar
|
||||
beq $endLabel
|
||||
$notLastLabel
|
||||
""")
|
||||
if (stepSigned)
|
||||
asmgen.out(" lda ${stepVar}+1 | bmi $descendingUpdateLabel")
|
||||
asmgen.out("""
|
||||
clc
|
||||
lda $loopvar
|
||||
adc $stepVar
|
||||
sta $nextVar
|
||||
lda $loopvar+1
|
||||
adc $stepVar+1
|
||||
$ascendingWrapBranch $endLabel
|
||||
sta $nextVar+1
|
||||
""")
|
||||
asmgen.out("""
|
||||
lda $nextVar+1
|
||||
cmp $toValueVar+1
|
||||
bne $ascendingCompareLabel
|
||||
lda $nextVar
|
||||
cmp $toValueVar
|
||||
beq $storeNextLabel
|
||||
$ascendingCompareLabel
|
||||
sec
|
||||
lda $nextVar
|
||||
sbc $toValueVar
|
||||
lda $nextVar+1
|
||||
sbc $toValueVar+1
|
||||
bvc $ascendingCompareDoneLabel
|
||||
eor #$80
|
||||
$ascendingCompareDoneLabel
|
||||
""")
|
||||
if (loopvarSigned)
|
||||
asmgen.out(" bpl $endLabel")
|
||||
else
|
||||
asmgen.out(" bcs $endLabel")
|
||||
if(stepSigned) {
|
||||
asmgen.out("""
|
||||
jmp $storeNextLabel
|
||||
|
||||
$descendingUpdateLabel
|
||||
lda $loopvar+1
|
||||
cmp $toValueVar+1
|
||||
bne $descendingNotLastLabel
|
||||
lda $loopvar
|
||||
cmp $toValueVar
|
||||
beq $endLabel
|
||||
$descendingNotLastLabel
|
||||
clc
|
||||
lda $loopvar
|
||||
adc $stepVar
|
||||
sta $nextVar
|
||||
lda $loopvar+1
|
||||
adc $stepVar+1
|
||||
$descendingWrapBranch $endLabel
|
||||
sta $nextVar+1
|
||||
""")
|
||||
if (loopvarSigned)
|
||||
asmgen.out("""
|
||||
sec
|
||||
lda $nextVar
|
||||
sbc $toValueVar
|
||||
lda $nextVar+1
|
||||
sbc $toValueVar+1
|
||||
bvc $descendingCompareDoneLabel
|
||||
eor #$80
|
||||
$descendingCompareDoneLabel
|
||||
bmi $endLabel
|
||||
""")
|
||||
else
|
||||
asmgen.out("""
|
||||
lda $nextVar+1
|
||||
cmp $toValueVar+1
|
||||
bcc $endLabel
|
||||
bne $storeNextLabel
|
||||
lda $nextVar
|
||||
cmp $toValueVar
|
||||
bcc $endLabel
|
||||
""")
|
||||
}
|
||||
else asmgen.out(" jmp $storeNextLabel")
|
||||
asmgen.out("""
|
||||
$storeNextLabel
|
||||
lda $nextVar
|
||||
sta $loopvar
|
||||
lda $nextVar+1
|
||||
sta $loopvar+1
|
||||
jmp $loopLabel
|
||||
""")
|
||||
}
|
||||
|
||||
private fun forOverNonconstLongRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
|
||||
val stepsize = range.step.asConstInteger()!!
|
||||
val loopLabel = asmgen.makeLabel("for_loop")
|
||||
@@ -157,12 +527,7 @@ internal class ForLoopsAsmGen(
|
||||
}
|
||||
|
||||
private fun forOverLongsRangeStepGreaterOne(range: PtRange, varname: String, iterableDt: DataType, loopLabel: String, endLabel: String, forloop: PtForLoop) {
|
||||
// TODO: implement 32-bit loop
|
||||
asmgen.romableError("for loops over long ranges with step != 1 not yet fully implemented", forloop.position)
|
||||
asmgen.out(loopLabel)
|
||||
asmgen.translate(forloop.statements)
|
||||
asmgen.jmp(loopLabel)
|
||||
asmgen.out(endLabel)
|
||||
TODO("legacy 6502 codegen does not support long FOR ranges with step other than 1 or -1 at ${forloop.position}")
|
||||
}
|
||||
|
||||
private fun forOverNonconstByteRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
|
||||
@@ -822,7 +1187,7 @@ $loopLabel sty $indexVar
|
||||
when {
|
||||
iterableDt.isString -> iterateStrings()
|
||||
iterableDt.isByteArray || iterableDt.isBoolArray -> iterateBytes()
|
||||
iterableDt.isSplitWordArray -> iterateSplitWords()
|
||||
iterableDt.isSplitWordArray(asmgen.options.compTarget) -> iterateSplitWords()
|
||||
iterableDt.isWordArray -> iterateWords()
|
||||
iterableDt.isLongArray -> iterateWords(true)
|
||||
iterableDt.isFloatArray -> throw AssemblyError("for loop with floating point variables is not supported")
|
||||
@@ -920,7 +1285,7 @@ $loopLabel""")
|
||||
}
|
||||
asmgen.out(endLabel)
|
||||
}
|
||||
iterableDt.isWordArray && !iterableDt.isSplitWordArray -> {
|
||||
iterableDt.isWordArray && !iterableDt.isSplitWordArray(asmgen.options.compTarget) -> {
|
||||
// loop over word range via loopvar, step >= 2 or <= -2
|
||||
when (range.step) {
|
||||
1, -1 -> {
|
||||
@@ -1091,16 +1456,15 @@ $loopLabel""")
|
||||
$loopLabel""")
|
||||
asmgen.translate(stmt.statements)
|
||||
asmgen.out("""
|
||||
lda $varname
|
||||
cmp #<${range.last}
|
||||
inc $varname
|
||||
bne +
|
||||
lda $varname+1
|
||||
cmp #>${range.last}
|
||||
beq $endLabel
|
||||
+ inc $varname
|
||||
inc $varname+1
|
||||
+ lda $varname
|
||||
cmp #<${range.last+1}
|
||||
bne $loopLabel
|
||||
inc $varname+1""")
|
||||
asmgen.jmp(loopLabel)
|
||||
lda $varname+1
|
||||
cmp #>${range.last+1}
|
||||
bne $loopLabel""")
|
||||
asmgen.out(endLabel)
|
||||
asmgen.loopEndLabels.removeLast()
|
||||
}
|
||||
@@ -1121,24 +1485,21 @@ $loopLabel""")
|
||||
if(range.last==0) {
|
||||
asmgen.out("""
|
||||
lda $varname
|
||||
bne ++
|
||||
lda $varname+1
|
||||
beq $endLabel""")
|
||||
} else {
|
||||
asmgen.out("""
|
||||
lda $varname
|
||||
cmp #<${range.last}
|
||||
bne +
|
||||
lda $varname+1
|
||||
cmp #>${range.last}
|
||||
beq $endLabel""")
|
||||
}
|
||||
asmgen.out("""
|
||||
+ lda $varname
|
||||
bne +
|
||||
dec $varname+1
|
||||
+ dec $varname""")
|
||||
asmgen.jmp(loopLabel)
|
||||
+ dec $varname
|
||||
lda $varname
|
||||
cmp #<${range.last-1}
|
||||
bne $loopLabel
|
||||
lda $varname+1
|
||||
cmp #>${range.last-1}
|
||||
bne $loopLabel""")
|
||||
asmgen.out(endLabel)
|
||||
asmgen.loopEndLabels.removeLast()
|
||||
}
|
||||
@@ -1150,7 +1511,7 @@ $loopLabel""")
|
||||
asmgen.loopEndLabels.add(endLabel)
|
||||
val varname = asmgen.asmVariableName(stmt.variable)
|
||||
val from = range.first.toLongHex()
|
||||
val to = range.last.toLongHex()
|
||||
val toPlus1 = (range.last + 1).toLongHex()
|
||||
asmgen.out("""
|
||||
lda #$${from.substring(6, 8)}
|
||||
sta $varname
|
||||
@@ -1163,26 +1524,25 @@ $loopLabel""")
|
||||
$loopLabel""")
|
||||
asmgen.translate(stmt.statements)
|
||||
asmgen.out("""
|
||||
lda $varname
|
||||
cmp #$${to.substring(6, 8)}
|
||||
inc $varname
|
||||
bne +
|
||||
lda $varname+1
|
||||
cmp #$${to.substring(4, 6)}
|
||||
bne +
|
||||
lda $varname+2
|
||||
cmp #$${to.substring(2, 4)}
|
||||
bne +
|
||||
lda $varname+3
|
||||
cmp #$${to.take(2)}
|
||||
beq $endLabel
|
||||
+ inc $varname
|
||||
bne $loopLabel
|
||||
inc $varname+1
|
||||
bne $loopLabel
|
||||
bne +
|
||||
inc $varname+2
|
||||
bne +
|
||||
inc $varname+3
|
||||
+ lda $varname
|
||||
cmp #$${toPlus1.substring(6, 8)}
|
||||
bne $loopLabel
|
||||
inc $varname+3""")
|
||||
asmgen.jmp(loopLabel)
|
||||
lda $varname+1
|
||||
cmp #$${toPlus1.substring(4, 6)}
|
||||
bne $loopLabel
|
||||
lda $varname+2
|
||||
cmp #$${toPlus1.substring(2, 4)}
|
||||
bne $loopLabel
|
||||
lda $varname+3
|
||||
cmp #$${toPlus1.take(2)}
|
||||
bne $loopLabel""")
|
||||
asmgen.out(endLabel)
|
||||
asmgen.loopEndLabels.removeLast()
|
||||
}
|
||||
@@ -1194,7 +1554,7 @@ $loopLabel""")
|
||||
asmgen.loopEndLabels.add(endLabel)
|
||||
val varname = asmgen.asmVariableName(stmt.variable)
|
||||
val from = range.first.toLongHex()
|
||||
val to = range.last.toLongHex()
|
||||
val toMinus1 = (range.last - 1).toLongHex()
|
||||
asmgen.out("""
|
||||
lda #$${from.substring(6, 8)}
|
||||
sta $varname
|
||||
@@ -1216,20 +1576,6 @@ $loopLabel""")
|
||||
bne ++
|
||||
lda $varname+3
|
||||
beq $endLabel""")
|
||||
} else {
|
||||
asmgen.out("""
|
||||
lda $varname
|
||||
cmp #$${to.substring(6, 8)}
|
||||
bne +
|
||||
lda $varname+1
|
||||
cmp #$${to.substring(4, 6)}
|
||||
bne +
|
||||
lda $varname+2
|
||||
cmp #$${to.substring(2, 4)}
|
||||
bne +
|
||||
lda $varname+3
|
||||
cmp #$${to.take(2)}
|
||||
beq $endLabel""")
|
||||
}
|
||||
asmgen.out("""
|
||||
+ lda $varname
|
||||
@@ -1241,8 +1587,19 @@ $loopLabel""")
|
||||
dec $varname+3
|
||||
+ dec $varname+2
|
||||
+ dec $varname+1
|
||||
+ dec $varname""")
|
||||
asmgen.jmp(loopLabel)
|
||||
+ dec $varname
|
||||
lda $varname
|
||||
cmp #$${toMinus1.substring(6, 8)}
|
||||
bne $loopLabel
|
||||
lda $varname+1
|
||||
cmp #$${toMinus1.substring(4, 6)}
|
||||
bne $loopLabel
|
||||
lda $varname+2
|
||||
cmp #$${toMinus1.substring(2, 4)}
|
||||
bne $loopLabel
|
||||
lda $varname+3
|
||||
cmp #$${toMinus1.take(2)}
|
||||
bne $loopLabel""")
|
||||
asmgen.out(endLabel)
|
||||
asmgen.loopEndLabels.removeLast()
|
||||
}
|
||||
|
||||
@@ -388,7 +388,7 @@ $label .byte 0 ; modified"""
|
||||
}
|
||||
val src = if(value.type.isPassByRef) {
|
||||
if(value is PtIdentifier || value is PtPointerDeref) {
|
||||
val addr = PtAddressOf(value.type.typeForAddressOf(false), false, value.position)
|
||||
val addr = PtAddressOf(value.type.typeForUntypedAddressOf(false, program.memsizer), false, value.position)
|
||||
addr.add(value)
|
||||
addr.parent = scope as PtNode
|
||||
AsmAssignSource.fromAstSource(addr, program, asmgen).adjustSignedUnsigned(target)
|
||||
|
||||
@@ -940,11 +940,10 @@ internal class IfElseAsmGen(private val program: PtProgram,
|
||||
val baseVarname = asmgen.asmVariableName(e.variable!!)
|
||||
val constIndex = e.index.asConstInteger()
|
||||
if (constIndex != null) {
|
||||
val idx = constIndex
|
||||
if (e.splitWords) {
|
||||
require(idx < 256)
|
||||
require(constIndex < 256)
|
||||
val suffix = if (long) "_3" else "_msb"
|
||||
asmgen.out(" lda $baseVarname$suffix+$idx")
|
||||
asmgen.out(" lda $baseVarname$suffix+$constIndex")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -964,7 +963,7 @@ internal class IfElseAsmGen(private val program: PtProgram,
|
||||
asmgen.out(" cpy #0")
|
||||
} else {
|
||||
var varname = asmgen.asmVariableName(e.identifier!!)
|
||||
if(e.identifier!!.type.isSplitWordArray) {
|
||||
if(e.identifier!!.type.isSplitWordArray(asmgen.options.compTarget)) {
|
||||
varname += if(e.isMsbForSplitArray) "_msb" else "_lsb"
|
||||
}
|
||||
asmgen.out(" lda #>$varname")
|
||||
@@ -1834,9 +1833,9 @@ internal class IfElseAsmGen(private val program: PtProgram,
|
||||
if (e is PtIdentifier) {
|
||||
val varname = asmgen.asmVariableName(e)
|
||||
block(varname, "$varname+1")
|
||||
} else if (e is PtAddressOf && !e.isFromArrayElement) {
|
||||
} else if (e is PtAddressOf && !e.isFromArrayElement && e.identifier != null) {
|
||||
val identifier = e.identifier!!
|
||||
val varname = if (identifier.type.isSplitWordArray) {
|
||||
val varname = if (identifier.type.isSplitWordArray(asmgen.options.compTarget)) {
|
||||
if (e.isMsbForSplitArray) identifier.name + "_msb" else identifier.name + "_lsb"
|
||||
} else {
|
||||
identifier.name
|
||||
|
||||
@@ -723,7 +723,7 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
|
||||
is PtAddressOf -> {
|
||||
val identifier = e.identifier!!
|
||||
var varname = asmgen.asmVariableName(identifier)
|
||||
if (identifier.type.isSplitWordArray) {
|
||||
if (identifier.type.isSplitWordArray(asmgen.options.compTarget)) {
|
||||
varname += if (e.isMsbForSplitArray) "_msb" else "_lsb"
|
||||
}
|
||||
asmgen.out(" lda #<$varname | ora #>$varname | bne $falseLabel")
|
||||
@@ -774,7 +774,7 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
|
||||
is PtAddressOf -> {
|
||||
val identifier = e.identifier!!
|
||||
var varname = asmgen.asmVariableName(identifier)
|
||||
if (identifier.type.isSplitWordArray) {
|
||||
if (identifier.type.isSplitWordArray(asmgen.options.compTarget)) {
|
||||
varname += if (e.isMsbForSplitArray) "_msb" else "_lsb"
|
||||
}
|
||||
asmgen.out(" lda #<$varname | ora #>$varname | beq $falseLabel")
|
||||
@@ -1100,11 +1100,10 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
|
||||
val baseVarname = asmgen.asmVariableName(e.variable!!)
|
||||
val constIndex = e.index.asConstInteger()
|
||||
if (constIndex != null) {
|
||||
val idx = constIndex
|
||||
if (e.splitWords) {
|
||||
require(idx < 256)
|
||||
require(constIndex < 256)
|
||||
val suffix = if (long) "_3" else "_msb"
|
||||
asmgen.out(" lda $baseVarname$suffix+$idx")
|
||||
asmgen.out(" lda $baseVarname$suffix+$constIndex")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1124,7 +1123,7 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
|
||||
asmgen.out(" cpy #0")
|
||||
} else {
|
||||
var varname = asmgen.asmVariableName(e.identifier!!)
|
||||
if(e.identifier!!.type.isSplitWordArray) {
|
||||
if(e.identifier!!.type.isSplitWordArray(asmgen.options.compTarget)) {
|
||||
varname += if(e.isMsbForSplitArray) "_msb" else "_lsb"
|
||||
}
|
||||
asmgen.out(" lda #>$varname")
|
||||
@@ -1156,7 +1155,7 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
|
||||
} else if (e is PtAddressOf && !e.isFromArrayElement) {
|
||||
val identifier = e.identifier!!
|
||||
var varname = asmgen.asmVariableName(identifier)
|
||||
if (identifier.type.isSplitWordArray) {
|
||||
if (identifier.type.isSplitWordArray(asmgen.options.compTarget)) {
|
||||
varname += if (e.isMsbForSplitArray) "_msb" else "_lsb"
|
||||
}
|
||||
block("#<$varname", "#>$varname")
|
||||
|
||||
@@ -173,6 +173,7 @@ internal class ProgramAndVarsGen(
|
||||
asmgen.out(" jsr p8_sys_startup.init_system")
|
||||
asmgen.out(" jsr p8_sys_startup.init_system_phase2")
|
||||
}
|
||||
else -> error("Unsupported output type: ${compTarget.defaultOutputType}")
|
||||
}
|
||||
|
||||
if (options.zeropage !in arrayOf(ZeropageType.BASICSAFE, ZeropageType.DONTUSE)) {
|
||||
@@ -223,20 +224,23 @@ internal class ProgramAndVarsGen(
|
||||
private fun footer() {
|
||||
asmgen.out(" .dsection STRUCTINSTANCES\n")
|
||||
|
||||
var relocateBssVars = false
|
||||
var relocateBssSlabs = false
|
||||
var relocatedBssStart = 0u
|
||||
var relocatedBssEnd = 0u
|
||||
var relocateVars = false
|
||||
var relocatedStart = 0u
|
||||
var relocatedEnd = 0u
|
||||
|
||||
if(options.varsGolden) {
|
||||
if(options.varsAddress != null) {
|
||||
relocateVars = true
|
||||
relocatedStart = options.varsAddress!!
|
||||
relocatedEnd = if(options.memtopAddress != 0u) options.memtopAddress else 0xFFFFu
|
||||
} else if(options.varsGolden) {
|
||||
if(options.compTarget.BSSGOLDENRAM_START == 0u ||
|
||||
options.compTarget.BSSGOLDENRAM_END == 0u ||
|
||||
options.compTarget.BSSGOLDENRAM_END <= options.compTarget.BSSGOLDENRAM_START) {
|
||||
throw AssemblyError("current compilation target hasn't got the golden ram area properly defined or it is simply not available")
|
||||
}
|
||||
relocateBssVars = true
|
||||
relocatedBssStart = options.compTarget.BSSGOLDENRAM_START
|
||||
relocatedBssEnd = options.compTarget.BSSGOLDENRAM_END
|
||||
relocateVars = true
|
||||
relocatedStart = options.compTarget.BSSGOLDENRAM_START
|
||||
relocatedEnd = options.compTarget.BSSGOLDENRAM_END
|
||||
}
|
||||
else if(options.varsHighBank!=null) {
|
||||
if(options.compTarget.BSSHIGHRAM_START == 0u ||
|
||||
@@ -244,67 +248,34 @@ internal class ProgramAndVarsGen(
|
||||
options.compTarget.BSSHIGHRAM_END <= options.compTarget.BSSHIGHRAM_START) {
|
||||
throw AssemblyError("current compilation target hasn't got the high ram area properly defined or it is simply not available")
|
||||
}
|
||||
if(options.slabsHighBank!=null && options.varsHighBank!=options.slabsHighBank)
|
||||
throw AssemblyError("slabs and vars high bank must be the same")
|
||||
relocateBssVars = true
|
||||
relocatedBssStart = options.compTarget.BSSHIGHRAM_START
|
||||
relocatedBssEnd = options.compTarget.BSSHIGHRAM_END
|
||||
}
|
||||
|
||||
if(options.slabsGolden) {
|
||||
if(options.compTarget.BSSGOLDENRAM_START == 0u ||
|
||||
options.compTarget.BSSGOLDENRAM_END == 0u ||
|
||||
options.compTarget.BSSGOLDENRAM_END <= options.compTarget.BSSGOLDENRAM_START) {
|
||||
throw AssemblyError("current compilation target hasn't got the golden ram area properly defined or it is simply not available")
|
||||
}
|
||||
relocateBssSlabs = true
|
||||
relocatedBssStart = options.compTarget.BSSGOLDENRAM_START
|
||||
relocatedBssEnd = options.compTarget.BSSGOLDENRAM_END
|
||||
}
|
||||
else if(options.slabsHighBank!=null) {
|
||||
if(options.compTarget.BSSHIGHRAM_START == 0u ||
|
||||
options.compTarget.BSSHIGHRAM_END == 0u ||
|
||||
options.compTarget.BSSHIGHRAM_END <= options.compTarget.BSSHIGHRAM_START) {
|
||||
throw AssemblyError("current compilation target hasn't got the high ram area properly defined or it is simply not available")
|
||||
}
|
||||
if(options.varsHighBank!=null && options.varsHighBank!=options.slabsHighBank)
|
||||
throw AssemblyError("slabs and vars high bank must be the same")
|
||||
relocateBssSlabs = true
|
||||
relocatedBssStart = options.compTarget.BSSHIGHRAM_START
|
||||
relocatedBssEnd = options.compTarget.BSSHIGHRAM_END
|
||||
relocateVars = true
|
||||
relocatedStart = options.compTarget.BSSHIGHRAM_START
|
||||
relocatedEnd = options.compTarget.BSSHIGHRAM_END
|
||||
}
|
||||
|
||||
asmgen.out("; bss sections")
|
||||
asmgen.out("PROG8_VARSHIGH_RAMBANK = ${options.varsHighBank ?: 1}")
|
||||
if(relocateBssVars) {
|
||||
if(!relocateBssSlabs)
|
||||
asmgen.out(" .dsection BSS_SLABS")
|
||||
if(relocateVars) {
|
||||
asmgen.out("prog8_program_end\t; end of program label for progend()")
|
||||
asmgen.out(" * = ${relocatedBssStart.toHex()}")
|
||||
asmgen.out(" * = ${relocatedStart.toHex()}")
|
||||
asmgen.out(" .dsection BSS_NOCLEAR")
|
||||
asmgen.out("prog8_bss_section_start")
|
||||
asmgen.out(" .dsection BSS")
|
||||
if(relocateBssSlabs)
|
||||
asmgen.out(" .dsection BSS_SLABS")
|
||||
asmgen.out(" .cerror * > ${relocatedBssEnd.toHex()}, \"too many variables/data for BSS section\"")
|
||||
asmgen.out(" .dsection BSS_SLABS")
|
||||
asmgen.out(" .cerror * > ${relocatedEnd.toHex()}, \"too many variables/data for BSS section\"")
|
||||
asmgen.out("prog8_bss_section_size = * - prog8_bss_section_start")
|
||||
} else {
|
||||
asmgen.out(" .dsection BSS_NOCLEAR")
|
||||
asmgen.out("prog8_bss_section_start")
|
||||
asmgen.out(" .dsection BSS")
|
||||
asmgen.out("prog8_bss_section_size = * - prog8_bss_section_start")
|
||||
if(!relocateBssSlabs)
|
||||
asmgen.out(" .dsection BSS_SLABS")
|
||||
asmgen.out(" .dsection BSS_SLABS")
|
||||
asmgen.out("prog8_program_end\t; end of program label for progend()")
|
||||
if(relocateBssSlabs) {
|
||||
asmgen.out(" * = ${relocatedBssStart.toHex()}")
|
||||
asmgen.out(" .dsection BSS_SLABS")
|
||||
asmgen.out(" .cerror * > ${relocatedBssEnd.toHex()}, \"too many data for BSS_SLABS section\"")
|
||||
}
|
||||
}
|
||||
|
||||
if(relocatedBssEnd >= options.memtopAddress)
|
||||
options.memtopAddress = relocatedBssEnd+1u
|
||||
val effectiveEnd = if(relocateVars) relocatedEnd else 0u
|
||||
if(effectiveEnd >= options.memtopAddress && effectiveEnd != 0u)
|
||||
options.memtopAddress = effectiveEnd+1u
|
||||
|
||||
asmgen.out(" ; memtop check")
|
||||
asmgen.out(" .cerror * >= ${options.memtopAddress.toHex()}, \"Program too long by \", * - ${(options.memtopAddress-1u).toHex()}, \" bytes, memtop=${options.memtopAddress.toHex()}\"")
|
||||
@@ -448,7 +419,7 @@ internal class ProgramAndVarsGen(
|
||||
asmgen.out("${StStructInstanceBlockName}_bss .block\n")
|
||||
instancesNoInit.forEach {
|
||||
val structtype: StStruct = symboltable.lookup(it.structName) as StStruct
|
||||
asmgen.out("${it.name} .fill ${structtype.size}\n")
|
||||
asmgen.out("${it.name} .fill ${structtype.size} ; struct ${it.structName}\n")
|
||||
}
|
||||
asmgen.out(" .endblock\n")
|
||||
asmgen.out(" .send BSS\n")
|
||||
@@ -644,7 +615,7 @@ internal class ProgramAndVarsGen(
|
||||
arrayVarsWithInitInZp.forEach {
|
||||
val size = it.alloc.size
|
||||
val name = asmgen.asmVariableName(it.name)
|
||||
if(it.alloc.dt.isSplitWordArray) {
|
||||
if(it.alloc.dt.isSplitWordArray(compTarget)) {
|
||||
val halfSize = size / 2
|
||||
if(halfSize>0) {
|
||||
asmgen.out("""
|
||||
@@ -723,7 +694,7 @@ internal class ProgramAndVarsGen(
|
||||
if (scopedName.startsWith("cx16.r"))
|
||||
continue // The 16 virtual registers of the cx16 are not actual variables in zp, they're memory mapped
|
||||
val variable = symboltable.flat.getValue(scopedName) as StStaticVariable
|
||||
if(variable.dt.isSplitWordArray) {
|
||||
if(variable.dt.isSplitWordArray(compTarget)) {
|
||||
val lsbAddr = zpvar.address
|
||||
val msbAddr = zpvar.address + (zpvar.size/2).toUInt()
|
||||
asmgen.out("${scopedName.substringAfterLast('.')}_lsb \t= $lsbAddr \t; zp ${zpvar.dt} (lsbs)")
|
||||
@@ -815,16 +786,17 @@ internal class ProgramAndVarsGen(
|
||||
dt.isSignedWord -> asmgen.out("${variable.name}\t.sint ?")
|
||||
dt.isLong -> asmgen.out("${variable.name}\t.dint ?")
|
||||
dt.isFloat -> asmgen.out("${variable.name}\t.fill ${compTarget.FLOAT_MEM_SIZE}")
|
||||
dt.isSplitWordArray -> {
|
||||
dt.isSplitWordArray(compTarget) -> {
|
||||
alignVar(variable.align)
|
||||
val numbytesPerHalf = compTarget.memorySize(variable.dt, variable.length!!.toInt()) / 2
|
||||
asmgen.out("${variable.name}_lsb\t.fill $numbytesPerHalf")
|
||||
asmgen.out("${variable.name}_msb\t.fill $numbytesPerHalf")
|
||||
asmgen.out("${variable.name}_lsb\t.fill $numbytesPerHalf ; split word array of ${variable.length} ${variable.dt.elementType()} (lsbs)")
|
||||
asmgen.out("${variable.name}_msb\t.fill $numbytesPerHalf ; split word array of ${variable.length} ${variable.dt.elementType()} (msbs)")
|
||||
}
|
||||
dt.isArray -> {
|
||||
alignVar(variable.align)
|
||||
val numbytes = compTarget.memorySize(variable.dt, variable.length!!.toInt())
|
||||
asmgen.out("${variable.name}\t.fill $numbytes")
|
||||
val amount = variable.length!!.toInt()
|
||||
val elementSize = compTarget.memorySize(variable.dt.elementType(), null)
|
||||
asmgen.out("${variable.name}\t.fill $amount * $elementSize ; array of $amount ${variable.dt.elementType()} ")
|
||||
}
|
||||
dt.isPointer -> asmgen.out("${variable.name}\t.word ?") // a pointer is just an uword address
|
||||
dt.isPointerArray -> {
|
||||
@@ -906,7 +878,7 @@ internal class ProgramAndVarsGen(
|
||||
asmgen.out(" .char " + chunk.joinToString())
|
||||
}
|
||||
}
|
||||
dt.isSplitWordArray -> {
|
||||
dt.isSplitWordArray(compTarget) -> {
|
||||
if(dt.elementType().isUnsignedWord || dt.elementType().isPointer) {
|
||||
val data = makeArrayFillDataUnsigned(dt, value, orNumberOfZeros)
|
||||
asmgen.out("${varname}_words := ${data.joinToString()}")
|
||||
@@ -960,6 +932,50 @@ internal class ProgramAndVarsGen(
|
||||
for (f in array.zip(floatFills))
|
||||
asmgen.out(" .byte ${f.second} ; float ${f.first}")
|
||||
}
|
||||
dt.base==BaseDataType.ARRAY && dt.sub==BaseDataType.STRUCT_INSTANCE -> {
|
||||
val struct = dt.subType as? StStruct ?: throw AssemblyError("struct type not found for $varname")
|
||||
val structSize = struct.size.toInt()
|
||||
if(value==null) {
|
||||
val totalBytes = (orNumberOfZeros ?: 0) * structSize
|
||||
if(totalBytes==0) {
|
||||
asmgen.out("$varname: .byte 0")
|
||||
} else {
|
||||
asmgen.out("$varname: .fill $totalBytes, 0")
|
||||
}
|
||||
} else {
|
||||
// initialized struct array: value contains StructInstance elements
|
||||
asmgen.out("$varname ; array of ${value.size} ${struct.name} structs")
|
||||
// expand struct field types for per-element handling
|
||||
val expandedFieldTypes = struct.fields.flatMap { field ->
|
||||
val arraySz = field.arraySize
|
||||
if(arraySz!=null) List(arraySz) { field.type } else listOf(field.type)
|
||||
}
|
||||
for(elt in value) {
|
||||
val instanceName = (elt as? StArrayElement.StructInstance)?.name ?: throw AssemblyError("expected struct instance in array init for $varname")
|
||||
val instance = symboltable.lookup(instanceName) as? StStructInstance ?: throw AssemblyError("struct instance $instanceName not found")
|
||||
val fieldValues = instance.initialValues
|
||||
require(fieldValues.size==expandedFieldTypes.size) { "field count mismatch for $instanceName" }
|
||||
for((fieldType, fieldVal) in expandedFieldTypes.zip(fieldValues)) {
|
||||
val num = when(fieldVal) {
|
||||
is StArrayElement.Number -> fieldVal.value
|
||||
is StArrayElement.BoolValue -> if(fieldVal.value) 1.0 else 0.0
|
||||
else -> throw AssemblyError("struct field value must be number for $varname (got $fieldVal)")
|
||||
}
|
||||
when {
|
||||
fieldType.isByteOrBool -> asmgen.out(" .byte ${num.toInt()}")
|
||||
fieldType.isWord -> asmgen.out(" .word ${num.toInt()}")
|
||||
fieldType.isLong -> asmgen.out(" .dword ${num.toInt()}")
|
||||
fieldType.isFloat -> {
|
||||
val bytes = compTarget.getFloatAsmBytes(num)
|
||||
asmgen.out(" .byte $bytes ; float $num")
|
||||
}
|
||||
fieldType.isPointer -> asmgen.out(" .word ${num.toInt()} ; pointer")
|
||||
else -> throw AssemblyError("unsupported struct field type $fieldType")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> throw AssemblyError("require array dt")
|
||||
}
|
||||
}
|
||||
@@ -1031,7 +1047,7 @@ internal class ProgramAndVarsGen(
|
||||
is StArrayElement.Number -> "$" + it.value.toInt().toString(16).padStart(4, '0')
|
||||
is StArrayElement.AddressOf -> {
|
||||
val symbol = symboltable.lookup(it.symbol)!!
|
||||
if(symbol is StStaticVariable && symbol.dt.isSplitWordArray)
|
||||
if(symbol is StStaticVariable && symbol.dt.isSplitWordArray(compTarget))
|
||||
asmgen.asmSymbolName(it.symbol+"_lsb")
|
||||
else
|
||||
asmgen.asmSymbolName(it.symbol)
|
||||
@@ -1072,7 +1088,7 @@ internal class ProgramAndVarsGen(
|
||||
is StArrayElement.Number -> "$" + it.value.toInt().toString(16).padStart(4, '0')
|
||||
is StArrayElement.AddressOf -> {
|
||||
val symbol = symboltable.lookup(it.symbol)!!
|
||||
if(symbol is StStaticVariable && symbol.dt.isSplitWordArray)
|
||||
if(symbol is StStaticVariable && symbol.dt.isSplitWordArray(compTarget))
|
||||
asmgen.asmSymbolName(it.symbol+"_lsb")
|
||||
else
|
||||
asmgen.asmSymbolName(it.symbol)
|
||||
|
||||
@@ -3,7 +3,6 @@ package prog8.codegen.cpu6502.assignment
|
||||
import prog8.code.StConstant
|
||||
import prog8.code.ast.*
|
||||
import prog8.code.core.*
|
||||
import prog8.code.core.BaseDataType
|
||||
import prog8.codegen.cpu6502.AsmGen6502Internal
|
||||
|
||||
|
||||
@@ -237,7 +236,7 @@ internal class AsmAssignSource(val kind: SourceStorageKind,
|
||||
if(sub is PtSub && sub.signature.returns.size>1)
|
||||
DataType.UNDEFINED // TODO list of types instead?
|
||||
else
|
||||
sub.returnsWhatWhere().firstOrNull { rr -> rr.first.registerOrPair != null || rr.first.statusflag!=null }?.second
|
||||
sub.returnsWhatWhere(asmgen.options.compTarget).firstOrNull { rr -> rr.first.registerOrPair != null || rr.first.statusflag!=null }?.second
|
||||
?: throw AssemblyError("can't translate zero return values in assignment")
|
||||
AsmAssignSource(SourceStorageKind.EXPRESSION, program, asmgen, returnType, expression = value)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -62,6 +62,63 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
|
||||
}
|
||||
}
|
||||
|
||||
private fun inplaceModificationIndexedPointerField(target: AsmAssignTarget, operator: String, value: AsmAssignSource) {
|
||||
val array = target.array!!
|
||||
val elementDt = target.datatype
|
||||
if(!elementDt.isByteOrBool && !elementDt.isWord)
|
||||
throw AssemblyError("inplace modification indexed pointer field for type $elementDt not supported")
|
||||
val indexedTarget = IndexedPtrTarget(target)
|
||||
val ptr = ptrgen.indexedEffectiveAddress(indexedTarget)
|
||||
|
||||
val elementTemp = asmgen.createTempVarReused(elementDt.base, false, array)
|
||||
if(elementDt.isByteOrBool) {
|
||||
asmgen.out("""
|
||||
ldy #0
|
||||
lda ($ptr),y
|
||||
sta $elementTemp""")
|
||||
} else {
|
||||
asmgen.out("""
|
||||
ldy #0
|
||||
lda ($ptr),y
|
||||
sta $elementTemp
|
||||
iny
|
||||
lda ($ptr),y
|
||||
sta $elementTemp+1""")
|
||||
}
|
||||
|
||||
// save the computed address on the hardware stack so later code can't clobber it
|
||||
asmgen.out("""
|
||||
lda $ptr
|
||||
pha
|
||||
lda $ptr+1
|
||||
pha""")
|
||||
|
||||
// reuse the existing variable-target logic to modify the temp
|
||||
val tempTarget = AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, elementDt, null, target.position, variableAsmName = elementTemp)
|
||||
inplaceModification(tempTarget, operator, value)
|
||||
|
||||
// restore the address and store the temp back
|
||||
asmgen.out("""
|
||||
pla
|
||||
sta $ptr+1
|
||||
pla
|
||||
sta $ptr""")
|
||||
if(elementDt.isByteOrBool) {
|
||||
asmgen.out("""
|
||||
ldy #0
|
||||
lda $elementTemp
|
||||
sta ($ptr),y""")
|
||||
} else {
|
||||
asmgen.out("""
|
||||
ldy #0
|
||||
lda $elementTemp
|
||||
sta ($ptr),y
|
||||
iny
|
||||
lda $elementTemp+1
|
||||
sta ($ptr),y""")
|
||||
}
|
||||
}
|
||||
|
||||
private fun inplaceModification(target: AsmAssignTarget, operator: String, value: AsmAssignSource) {
|
||||
|
||||
// the asm-gen code can deal with situations where you want to assign a byte into a word.
|
||||
@@ -247,7 +304,7 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
|
||||
TargetStorageKind.ARRAY -> {
|
||||
val deref = target.array!!.pointerderef
|
||||
if(deref!=null) {
|
||||
TODO("inplace modification array indexed pointer deref ${target.position}")
|
||||
inplaceModificationIndexedPointerField(target, operator, value)
|
||||
return
|
||||
}
|
||||
val targetArrayVar = target.array.variable!!
|
||||
@@ -561,6 +618,28 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
|
||||
}
|
||||
|
||||
internal fun inplacemodificationLongWithExpression(targetVar: String, operator: String, value: PtExpression) {
|
||||
// Adding an unsigned byte to a long: skip zero-extending the byte into a
|
||||
// full 4-byte long and just add it with carry propagation through the upper bytes.
|
||||
// The byte shows up here as a (widened) long-typed cast of an ubyte expression,
|
||||
// so detect it via the cast child type.
|
||||
if(operator=="+" && value is PtTypeCast && value.value.type==DataType.UBYTE) {
|
||||
assignmentAsmGen.assignExpressionToRegister(value.value, RegisterOrPair.A, false)
|
||||
asmgen.out("""
|
||||
clc
|
||||
adc $targetVar+0
|
||||
sta $targetVar+0
|
||||
lda $targetVar+1
|
||||
adc #0
|
||||
sta $targetVar+1
|
||||
lda $targetVar+2
|
||||
adc #0
|
||||
sta $targetVar+2
|
||||
lda $targetVar+3
|
||||
adc #0
|
||||
sta $targetVar+3
|
||||
""")
|
||||
return
|
||||
}
|
||||
// it's not an expression so no need to preserve R14-R15
|
||||
if(value.type.isSigned) {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R14R15, true)
|
||||
@@ -931,8 +1010,12 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
|
||||
}
|
||||
else -> {
|
||||
asmgen.out("""
|
||||
lda #0
|
||||
sta $variable
|
||||
lda $variable+3
|
||||
bpl +
|
||||
lda #255
|
||||
bne ++
|
||||
+ lda #0
|
||||
+ sta $variable
|
||||
sta $variable+1
|
||||
sta $variable+2
|
||||
sta $variable+3""")
|
||||
@@ -1670,7 +1753,7 @@ $shortcutLabel:""")
|
||||
// use the already existing optimized codegen for regular assignments x += array[index]
|
||||
val binexpr = PtBinaryExpression(operator, dt, value.position)
|
||||
binexpr.add(PtIdentifier(name, dt, value.position))
|
||||
val arrayValue = PtArrayIndexer(value.type, value.position)
|
||||
val arrayValue = PtArrayIndexer(value.type, valueVar.type.isSplitWordArray(program.memsizer), value.position)
|
||||
arrayValue.add(valueVar)
|
||||
arrayValue.add(value.index)
|
||||
binexpr.add(arrayValue)
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -219,19 +219,22 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
internal fun inplaceFloatNegate(target: PtrTarget, scope: IPtSubroutine?) {
|
||||
val (zpPtrVar, offset) = deref(target.pointer)
|
||||
// flip sign bit in second byte of 5-byte float
|
||||
asmgen.out("""
|
||||
ldy #$offset
|
||||
asmgen.out(
|
||||
$$"""
|
||||
ldy #$$offset
|
||||
iny
|
||||
lda ($zpPtrVar),y
|
||||
eor #${'$'}80
|
||||
sta ($zpPtrVar),y""")
|
||||
lda ($$zpPtrVar),y
|
||||
eor #$80
|
||||
sta ($$zpPtrVar),y"""
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
internal fun deref(pointer: PtPointerDeref, addOffsetToPointer: Boolean=false): Pair<String, UByte> {
|
||||
internal fun deref(pointer: PtPointerDeref, addOffsetToPointer: Boolean=false, loadPointerFieldValue: Boolean=false): Pair<String, UByte> {
|
||||
// walk the pointer deref chain and leaves the final pointer value in a ZP var
|
||||
// this will often be the temp var P8ZP_SCRATCH_PTR but can also be the original pointer variable if it is already in zeropage and there is nothing to add to it
|
||||
// returns the ZP var to use as a pointer, and a Y register offset (which can be zero)
|
||||
// loadPointerFieldValue: for addOffsetToPointer=true, also load the value if the final field is a pointer-typed field (rather than returning its address).
|
||||
|
||||
fun addFieldOffsetToScratchPointer(fieldoffset: UInt) {
|
||||
if(fieldoffset==0u)
|
||||
@@ -276,7 +279,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
if(addOffsetToPointer) {
|
||||
asmgen.assignExpressionToVariable(pointer.startpointer, "P8ZP_SCRATCH_PTR", DataType.UWORD)
|
||||
addFieldOffsetToScratchPointer(field.second.toUInt())
|
||||
if(pointer.derefLast)
|
||||
if(pointer.derefLast || (loadPointerFieldValue && field.first.isPointer))
|
||||
updateScratchPointer()
|
||||
return "P8ZP_SCRATCH_PTR" to 0u
|
||||
} else {
|
||||
@@ -326,6 +329,8 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
} else {
|
||||
if(addOffsetToPointer) {
|
||||
addFieldOffsetToScratchPointer(fieldinfo.second.toUInt())
|
||||
if(loadPointerFieldValue && fieldinfo.first.isPointer)
|
||||
updateScratchPointer()
|
||||
return "P8ZP_SCRATCH_PTR" to 0u
|
||||
} else
|
||||
return "P8ZP_SCRATCH_PTR" to fieldinfo.second
|
||||
@@ -357,7 +362,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
}
|
||||
// save scaled index to W2, compute base (field-corrected) into P8ZP_SCRATCH_PTR, then add
|
||||
asmgen.out(" lda P8ZP_SCRATCH_W1 | sta P8ZP_SCRATCH_W2 | lda P8ZP_SCRATCH_W1+1 | sta P8ZP_SCRATCH_W2+1")
|
||||
val (basePtr, _) = deref(target.pointer, addOffsetToPointer = true)
|
||||
val (basePtr, _) = deref(target.pointer, addOffsetToPointer = true, loadPointerFieldValue = true)
|
||||
if(basePtr != "P8ZP_SCRATCH_PTR") {
|
||||
// deref returned the original zp pointer variable; copy it so we don't mutate it
|
||||
asmgen.out("""
|
||||
@@ -450,7 +455,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
inplaceByteInc(target, amount)
|
||||
} else {
|
||||
if (target.dt.isByte) inplaceByteAdd(target, value)
|
||||
else if (target.dt.isWord) inplaceWordAdd(target, value)
|
||||
else if (target.dt.isWord || target.dt.isPointer) inplaceWordAdd(target, value)
|
||||
else if (target.dt.isFloat) inplaceFloatAddOrMul(target, "FADD", value)
|
||||
else if (target.dt.isLong) inplaceLongAdd(target, value)
|
||||
else throw AssemblyError("weird dt ${target.dt} ${target.position}")
|
||||
@@ -462,7 +467,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
inplaceByteDec(target, amount)
|
||||
} else {
|
||||
if (target.dt.isByte) inplaceByteSub(target, value)
|
||||
else if (target.dt.isWord) inplaceWordSub(target, value)
|
||||
else if (target.dt.isWord || target.dt.isPointer) inplaceWordSub(target, value)
|
||||
else if (target.dt.isFloat) inplaceFloatSubOrDiv(target, "FSUB", value)
|
||||
else if (target.dt.isLong) inplaceLongSub(target, value)
|
||||
else throw AssemblyError("weird dt ${target.position}")
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,860 @@
|
||||
package prog8.codegen.cpu6502.assignment
|
||||
|
||||
import prog8.code.ast.*
|
||||
import prog8.code.core.*
|
||||
import prog8.codegen.cpu6502.AsmGen6502Internal
|
||||
|
||||
internal class TypeCastAssignmentsGen(
|
||||
private val program: PtProgram,
|
||||
private val asmgen: AsmGen6502Internal,
|
||||
private val pointergen: PointerAssignmentsGen
|
||||
) {
|
||||
lateinit var assignmentAsmGen: AssignmentAsmGen
|
||||
|
||||
internal fun assignTypeCastedValue(target: AsmAssignTarget, targetDt: DataType, value: PtExpression, origTypeCastExpression: PtTypeCast) {
|
||||
val valueDt = value.type
|
||||
if(valueDt==targetDt)
|
||||
throw AssemblyError("type cast to identical dt should have been removed: $valueDt at ${value.position}")
|
||||
|
||||
when(value) {
|
||||
is PtIdentifier -> {
|
||||
if(targetDt.isWord) {
|
||||
if(valueDt.isUnsignedByte || valueDt.isBool) {
|
||||
assignmentAsmGen.assignVariableUByteIntoWord(target, value)
|
||||
return
|
||||
}
|
||||
if(valueDt.isSignedByte) {
|
||||
assignmentAsmGen.assignVariableByteIntoWord(target, value)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
is PtMemoryByte -> {
|
||||
if(targetDt.isWord) {
|
||||
|
||||
fun assignViaExprEval(addressExpression: PtExpression) {
|
||||
asmgen.assignExpressionToVariable(addressExpression, "P8ZP_SCRATCH_W2", DataType.UWORD)
|
||||
asmgen.loadAFromZpPointerVar("P8ZP_SCRATCH_W2")
|
||||
asmgen.out(" ldy #0")
|
||||
assignmentAsmGen.assignRegisterpairWord(target, RegisterOrPair.AY)
|
||||
}
|
||||
|
||||
when (value.address) {
|
||||
is PtNumber -> {
|
||||
val address = (value.address as PtNumber).number.toUInt()
|
||||
assignmentAsmGen.assignMemoryByteIntoWord(target, address, null)
|
||||
}
|
||||
is PtIdentifier -> {
|
||||
assignmentAsmGen.assignMemoryByteIntoWord(target, null, value.address as PtIdentifier)
|
||||
}
|
||||
is PtBinaryExpression -> {
|
||||
val addrExpr = value.address as PtBinaryExpression
|
||||
if(asmgen.tryOptimizedPointerAccessWithA(addrExpr, false)) {
|
||||
asmgen.out(" ldy #0")
|
||||
assignmentAsmGen.assignRegisterpairWord(target, RegisterOrPair.AY)
|
||||
} else {
|
||||
assignViaExprEval(value.address)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
assignViaExprEval(value.address)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
is PtNumber -> {
|
||||
if(targetDt.isPointer) {
|
||||
// assign a number to a pointer type
|
||||
require(valueDt.isInteger)
|
||||
return assignmentAsmGen.assignConstantWord(target, value.number.toInt())
|
||||
}
|
||||
else
|
||||
throw AssemblyError("literal value cast should have been const-folded away (target type=$targetDt)")
|
||||
}
|
||||
is PtBool -> throw AssemblyError("literal value cast should have been const-folded away (target type=$targetDt)")
|
||||
is PtArrayIndexer -> {
|
||||
if(targetDt.isByte && valueDt.isWord) {
|
||||
// just assign the lsb from the array value
|
||||
return assignCastWordViaLsbFunc(value, target)
|
||||
} else if(targetDt.isBool && valueDt.isWord) {
|
||||
return assignWordToBool(value, target)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
|
||||
// special case optimizations
|
||||
if(target.kind == TargetStorageKind.VARIABLE) {
|
||||
if(value is PtIdentifier && !valueDt.isUndefined)
|
||||
return assignTypeCastedIdentifier(target.asmVarname, targetDt, asmgen.asmVariableName(value), valueDt)
|
||||
|
||||
when {
|
||||
valueDt.isBool -> {
|
||||
if(targetDt.isInteger) {
|
||||
// optimization to assign boolean expression to integer target (just assign the 0 or 1 directly)
|
||||
val assignDirect = AsmAssignment(
|
||||
AsmAssignSource.fromAstSource(value, program, asmgen),
|
||||
listOf(target),
|
||||
program.memsizer,
|
||||
target.position
|
||||
)
|
||||
assignmentAsmGen.assignExpression(assignDirect, target.scope)
|
||||
} else {
|
||||
TODO("assign bool to non-integer type $targetDt ${value.position}")
|
||||
}
|
||||
}
|
||||
valueDt.isByte -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.A, valueDt.isSigned)
|
||||
assignTypeCastedRegisters(target.asmVarname, targetDt.base, RegisterOrPair.A, valueDt.base, target.position)
|
||||
}
|
||||
valueDt.isLong -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R14R15, valueDt.isSigned)
|
||||
assignTypeCastedRegisters(target.asmVarname, targetDt.base, RegisterOrPair.R14R15, valueDt.base, target.position)
|
||||
}
|
||||
valueDt.isWord || valueDt.isPointer -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.AY, valueDt.isSigned)
|
||||
assignTypeCastedRegisters(target.asmVarname, targetDt.base, RegisterOrPair.AY, valueDt.base, target.position)
|
||||
}
|
||||
valueDt.isFloat -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.FAC1, true)
|
||||
assignTypeCastedFloatFAC1(target.asmVarname, targetDt.base)
|
||||
}
|
||||
valueDt.isPassByRef -> {
|
||||
// str/array value cast (most likely to UWORD, take address-of)
|
||||
assignmentAsmGen.assignExpressionToVariable(value, target.asmVarname, targetDt)
|
||||
}
|
||||
else -> throw AssemblyError("strange dt in typecast assign to var: $valueDt --> $targetDt")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if(valueDt.isWord && origTypeCastExpression.type.isUnsignedByte) {
|
||||
val parentTc = origTypeCastExpression.parent as? PtTypeCast
|
||||
if(parentTc!=null && parentTc.type.isUnsignedWord) {
|
||||
// typecast a word value to ubyte and directly back to uword
|
||||
// generate code for lsb(value) here instead of the ubyte typecast
|
||||
return assignCastWordViaLsbFunc(value, target)
|
||||
}
|
||||
}
|
||||
|
||||
if(valueDt.isByteOrBool) {
|
||||
when(target.register) {
|
||||
RegisterOrPair.A,
|
||||
RegisterOrPair.X,
|
||||
RegisterOrPair.Y -> {
|
||||
// 'cast' an ubyte value to a byte register; no cast needed at all
|
||||
return assignmentAsmGen.assignExpressionToRegister(value, target.register, valueDt.isSigned)
|
||||
}
|
||||
RegisterOrPair.AX,
|
||||
RegisterOrPair.AY,
|
||||
RegisterOrPair.XY,
|
||||
in Cx16VirtualRegisters -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.A, false)
|
||||
assignmentAsmGen.assignRegisterByte(target, CpuRegister.A, valueDt.isSigned, true)
|
||||
return
|
||||
}
|
||||
in CombinedLongRegisters -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.A, false)
|
||||
assignmentAsmGen.assignRegisterByte(target, CpuRegister.A, valueDt.isSigned, true)
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
} else if(valueDt.isWord) {
|
||||
when(target.register) {
|
||||
RegisterOrPair.A,
|
||||
RegisterOrPair.X,
|
||||
RegisterOrPair.Y -> {
|
||||
return if(targetDt.isBool)
|
||||
assignWordToBool(value, target)
|
||||
else
|
||||
// cast an uword to a byte register, do this via lsb(value)
|
||||
// generate code for lsb(value) here instead of the ubyte typecast
|
||||
assignCastWordViaLsbFunc(value, target)
|
||||
}
|
||||
RegisterOrPair.AX,
|
||||
RegisterOrPair.AY,
|
||||
RegisterOrPair.XY,
|
||||
in Cx16VirtualRegisters -> {
|
||||
// 'cast' uword into a 16 bits register, just assign it
|
||||
return assignmentAsmGen.assignExpressionToRegister(value, target.register!!, targetDt.isSigned)
|
||||
}
|
||||
RegisterOrPair.R0R1 -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R0, false)
|
||||
return asmgen.signExtendLongVariable("cx16.r0", valueDt.base)
|
||||
}
|
||||
RegisterOrPair.R2R3 -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R2, false)
|
||||
return asmgen.signExtendLongVariable("cx16.r2", valueDt.base)
|
||||
}
|
||||
RegisterOrPair.R4R5 -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R4, false)
|
||||
return asmgen.signExtendLongVariable("cx16.r4", valueDt.base)
|
||||
}
|
||||
RegisterOrPair.R6R7 -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R6, false)
|
||||
return asmgen.signExtendLongVariable("cx16.r6", valueDt.base)
|
||||
}
|
||||
RegisterOrPair.R8R9 -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R8, false)
|
||||
return asmgen.signExtendLongVariable("cx16.r8", valueDt.base)
|
||||
}
|
||||
RegisterOrPair.R10R11 -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R10, false)
|
||||
return asmgen.signExtendLongVariable("cx16.r10", valueDt.base)
|
||||
}
|
||||
RegisterOrPair.R12R13 -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R12, false)
|
||||
return asmgen.signExtendLongVariable("cx16.r12", valueDt.base)
|
||||
}
|
||||
RegisterOrPair.R14R15 -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R14, false)
|
||||
return asmgen.signExtendLongVariable("cx16.r14", valueDt.base)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
if(target.kind==TargetStorageKind.REGISTER) {
|
||||
if(valueDt.isFloat && !target.datatype.isFloat) {
|
||||
// have to typecast the float number on the fly down to an integer
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.FAC1, targetDt.isSigned)
|
||||
assignTypeCastedFloatFAC1("P8ZP_SCRATCH_W1", targetDt.base)
|
||||
assignmentAsmGen.assignVariableToRegister("P8ZP_SCRATCH_W1", target.register!!, targetDt.isSigned, origTypeCastExpression.definingISub(), target.position)
|
||||
return
|
||||
} else {
|
||||
if(!(valueDt isAssignableTo targetDt)) {
|
||||
return if(valueDt.isWord && targetDt.isByte) {
|
||||
// word to byte, just take the lsb
|
||||
assignCastWordViaLsbFunc(value, target)
|
||||
} else if(valueDt.isWord && targetDt.isBool) {
|
||||
// word to bool
|
||||
assignWordToBool(value, target)
|
||||
} else if(valueDt.isWord && targetDt.isWord) {
|
||||
// word to word, just assign
|
||||
assignmentAsmGen.assignExpressionToRegister(value, target.register!!, valueDt.isSigned)
|
||||
} else if(valueDt.isByteOrBool && targetDt.isByte) {
|
||||
// byte to byte, just assign
|
||||
assignmentAsmGen.assignExpressionToRegister(value, target.register!!, valueDt.isSigned)
|
||||
} else if(valueDt.isByteOrBool && targetDt.isWord) {
|
||||
// byte to word, just assign
|
||||
assignmentAsmGen.assignExpressionToRegister(value, target.register!!, valueDt.isSigned)
|
||||
} else if(valueDt.isLong && targetDt.isByte) {
|
||||
// long to byte, just take the lsb
|
||||
assignCastLongToByte(value, target)
|
||||
} else if(valueDt.isLong && targetDt.isWord) {
|
||||
// long to word, just take the lsw
|
||||
assignCastViaLswFunc(value, target)
|
||||
} else if(valueDt.isPointer && targetDt.isPointer) {
|
||||
// pointer type A to pointer type B, just assign
|
||||
assignmentAsmGen.assignExpressionToRegister(value, target.register!!, valueDt.isSigned)
|
||||
} else
|
||||
throw AssemblyError("can't cast $valueDt to $targetDt, this should have been checked in the astchecker ${value.position}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(targetDt.isInteger && valueDt.isByteOrBool && valueDt.isAssignableTo(targetDt)) {
|
||||
require(targetDt.isWord || targetDt.isLong) {
|
||||
"should be byte to word or long assignment ${origTypeCastExpression.position}"
|
||||
}
|
||||
when(target.kind) {
|
||||
// TargetStorageKind.VARIABLE -> {
|
||||
// This has been handled already earlier on line 961.
|
||||
// // byte to word, just assign to registers first, then assign to variable
|
||||
// assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.AY, targetDt==BaseDataType.WORD)
|
||||
// assignTypeCastedRegisters(target.asmVarname, targetDt, RegisterOrPair.AY, targetDt)
|
||||
// return
|
||||
// }
|
||||
TargetStorageKind.ARRAY -> {
|
||||
// byte to word, just assign to registers first, then assign into array
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.AY, targetDt.isSigned)
|
||||
val deref = target.array!!.pointerderef
|
||||
if(deref!=null)
|
||||
pointergen.assignWordReg(IndexedPtrTarget(target), RegisterOrPair.AY)
|
||||
else
|
||||
assignmentAsmGen.assignRegisterpairWord(target, RegisterOrPair.AY)
|
||||
return
|
||||
}
|
||||
TargetStorageKind.REGISTER -> {
|
||||
// byte to word or long, just assign to registers
|
||||
assignmentAsmGen.assignExpressionToRegister(value, target.register!!, targetDt.isSigned)
|
||||
return
|
||||
}
|
||||
TargetStorageKind.POINTER -> {
|
||||
pointergen.assignByteToWord(PtrTarget(target), value)
|
||||
return
|
||||
}
|
||||
else -> throw AssemblyError("weird target at ${target.position}")
|
||||
}
|
||||
}
|
||||
|
||||
if(targetDt.isFloat && (target.register==RegisterOrPair.FAC1 || target.register==RegisterOrPair.FAC2)) {
|
||||
if(target.register==RegisterOrPair.FAC2)
|
||||
asmgen.pushFAC1()
|
||||
when {
|
||||
valueDt.isUnsignedByte -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.Y, false)
|
||||
asmgen.out(" jsr floats.FREADUY")
|
||||
}
|
||||
valueDt.isSignedByte -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.A, true)
|
||||
asmgen.out(" jsr floats.FREADSA")
|
||||
}
|
||||
valueDt.isUnsignedWord -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.AY, false)
|
||||
asmgen.out(" jsr floats.GIVUAYFAY")
|
||||
}
|
||||
valueDt.isSignedWord -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.AY, true)
|
||||
asmgen.out(" jsr floats.GIVAYFAY")
|
||||
}
|
||||
valueDt.isLong -> {
|
||||
when(value) {
|
||||
is PtIdentifier -> {
|
||||
val varname = asmgen.asmVariableName(value)
|
||||
asmgen.out("""
|
||||
lda #<$varname
|
||||
ldy #>$varname
|
||||
jsr floats.internal_long_AY_to_FAC""")
|
||||
}
|
||||
else -> {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R14R15, true)
|
||||
asmgen.out("""
|
||||
lda #<cx16.r14
|
||||
ldy #>cx16.r14
|
||||
jsr floats.internal_long_AY_to_FAC""")
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> throw AssemblyError("invalid dt at ${target.position}")
|
||||
}
|
||||
if(target.register==RegisterOrPair.FAC2) {
|
||||
asmgen.out(" jsr floats.MOVEF")
|
||||
asmgen.popFAC1()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if(targetDt.isUnsignedWord && valueDt.isPointer) {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.AY, false)
|
||||
assignmentAsmGen.assignRegisterpairWord(target, RegisterOrPair.AY)
|
||||
return
|
||||
}
|
||||
|
||||
// No more special optimized cases yet. Do the rest via more complex evaluation
|
||||
// note: cannot use assignTypeCastedValue because that is ourselves :P
|
||||
// NOTE: THIS MAY TURN INTO A STACK OVERFLOW ERROR IF IT CAN'T SIMPLIFY THE TYPECAST..... :-/
|
||||
asmgen.assignExpressionTo(origTypeCastExpression, target)
|
||||
}
|
||||
|
||||
|
||||
internal fun assignCastLongToByte(value: PtExpression, target: AsmAssignTarget) {
|
||||
// long to byte, can't use lsb() because that only works on words
|
||||
when(value) {
|
||||
is PtIdentifier -> {
|
||||
val longvar = asmgen.asmVariableName(value)
|
||||
asmgen.out(" lda $longvar")
|
||||
assignmentAsmGen.assignRegisterByte(target, CpuRegister.A, true, false)
|
||||
}
|
||||
is PtNumber -> throw AssemblyError("casting a long number to byte should have been const-folded away ${value.position}")
|
||||
else -> {
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R14R15, 1)
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R14R15, true)
|
||||
asmgen.out(" lda cx16.r14")
|
||||
assignmentAsmGen.assignRegisterByte(target, CpuRegister.A, true, false)
|
||||
asmgen.popLongRegisters(RegisterOrPair.R14R15, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal fun assignCastWordViaLsbFunc(value: PtExpression, target: AsmAssignTarget) {
|
||||
val lsb = PtFunctionCall("lsb", true, true,arrayOf(DataType.UBYTE), value.position)
|
||||
lsb.parent = value.parent
|
||||
lsb.add(value)
|
||||
val src = AsmAssignSource(SourceStorageKind.EXPRESSION, program, asmgen, DataType.UBYTE, expression = lsb)
|
||||
val assign = AsmAssignment(src, listOf(target), program.memsizer, value.position)
|
||||
assignmentAsmGen.translateNormalAssignment(assign, value.definingISub())
|
||||
}
|
||||
|
||||
|
||||
internal fun assignCastViaLswFunc(value: PtExpression, target: AsmAssignTarget) {
|
||||
val lsb = PtFunctionCall("lsw", true, true, arrayOf(DataType.UWORD), value.position)
|
||||
lsb.parent = value.parent
|
||||
lsb.add(value)
|
||||
val src = AsmAssignSource(SourceStorageKind.EXPRESSION, program, asmgen, DataType.UWORD, expression = lsb)
|
||||
val assign = AsmAssignment(src, listOf(target), program.memsizer, value.position)
|
||||
assignmentAsmGen.translateNormalAssignment(assign, value.definingISub())
|
||||
}
|
||||
|
||||
|
||||
internal fun assignWordToBool(value: PtExpression, target: AsmAssignTarget) {
|
||||
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.AY, false)
|
||||
asmgen.out("""
|
||||
sty P8ZP_SCRATCH_REG
|
||||
ora P8ZP_SCRATCH_REG
|
||||
beq +
|
||||
lda #1
|
||||
+""")
|
||||
assignmentAsmGen.assignRegisterByte(target, CpuRegister.A, false, false)
|
||||
}
|
||||
|
||||
|
||||
internal fun assignTypeCastedFloatFAC1(targetAsmVarName: String, targetDt: BaseDataType) {
|
||||
|
||||
if(targetDt==BaseDataType.FLOAT)
|
||||
throw AssemblyError("typecast to identical type")
|
||||
|
||||
when(targetDt) {
|
||||
BaseDataType.BOOL -> asmgen.out(" jsr floats.cast_FAC1_as_bool_into_a | sta $targetAsmVarName")
|
||||
BaseDataType.UBYTE -> asmgen.out(" jsr floats.cast_FAC1_as_uw_into_ya | sty $targetAsmVarName")
|
||||
BaseDataType.BYTE -> asmgen.out(" jsr floats.cast_FAC1_as_w_into_ay | sta $targetAsmVarName")
|
||||
BaseDataType.UWORD -> asmgen.out(" jsr floats.cast_FAC1_as_uw_into_ya | sty $targetAsmVarName | sta $targetAsmVarName+1")
|
||||
BaseDataType.WORD -> asmgen.out(" jsr floats.cast_FAC1_as_w_into_ay | sta $targetAsmVarName | sty $targetAsmVarName+1")
|
||||
BaseDataType.LONG -> {
|
||||
asmgen.out("""
|
||||
jsr floats.QINT
|
||||
lda floats.FAC_ADDR+4
|
||||
sta $targetAsmVarName
|
||||
lda floats.FAC_ADDR+3
|
||||
sta $targetAsmVarName+1
|
||||
lda floats.FAC_ADDR+2
|
||||
sta $targetAsmVarName+2
|
||||
lda floats.FAC_ADDR+1
|
||||
sta $targetAsmVarName+3
|
||||
""".trimIndent())
|
||||
}
|
||||
else -> throw AssemblyError("weird type $targetDt")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
internal fun assignTypeCastedIdentifier(targetAsmVarName: String, targetDt: DataType,
|
||||
sourceAsmVarName: String, sourceDt: DataType) {
|
||||
if(sourceDt == targetDt)
|
||||
throw AssemblyError("typecast to identical type")
|
||||
|
||||
// also see: PtExpressionAsmGen, fun translateExpression(typecast: PtTypeCast)
|
||||
when {
|
||||
sourceDt.isUnsignedByte || sourceDt.isBool -> {
|
||||
when(targetDt.base) {
|
||||
BaseDataType.BOOL -> {
|
||||
asmgen.out("""
|
||||
lda $sourceAsmVarName
|
||||
beq +
|
||||
lda #1
|
||||
+ sta $targetAsmVarName""")
|
||||
}
|
||||
BaseDataType.UBYTE, BaseDataType.BYTE -> {
|
||||
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName")
|
||||
}
|
||||
BaseDataType.UWORD, BaseDataType.WORD -> {
|
||||
if(asmgen.isTargetCpu(CpuType.CPU65C02))
|
||||
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName | stz $targetAsmVarName+1")
|
||||
else
|
||||
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName | lda #0 | sta $targetAsmVarName+1")
|
||||
}
|
||||
BaseDataType.POINTER -> TODO("byte should not be cast to pointer")
|
||||
BaseDataType.LONG -> {
|
||||
asmgen.out("""
|
||||
lda $sourceAsmVarName
|
||||
sta $targetAsmVarName
|
||||
lda #0
|
||||
sta $targetAsmVarName+1
|
||||
sta $targetAsmVarName+2
|
||||
sta $targetAsmVarName+3""")
|
||||
}
|
||||
BaseDataType.FLOAT -> {
|
||||
asmgen.out("""
|
||||
lda #<$targetAsmVarName
|
||||
ldy #>$targetAsmVarName
|
||||
sta P8ZP_SCRATCH_W2
|
||||
sty P8ZP_SCRATCH_W2+1
|
||||
ldy $sourceAsmVarName
|
||||
jsr floats.cast_from_ub""")
|
||||
}
|
||||
else -> throw AssemblyError("weird type $targetDt")
|
||||
}
|
||||
}
|
||||
sourceDt.isSignedByte -> {
|
||||
when(targetDt.base) {
|
||||
BaseDataType.UBYTE, BaseDataType.BOOL -> {
|
||||
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName")
|
||||
}
|
||||
BaseDataType.UWORD -> {
|
||||
if(asmgen.isTargetCpu(CpuType.CPU65C02))
|
||||
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName | stz $targetAsmVarName+1")
|
||||
else
|
||||
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName | lda #0 | sta $targetAsmVarName+1")
|
||||
}
|
||||
BaseDataType.WORD -> {
|
||||
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName")
|
||||
asmgen.signExtendVariableLsb(targetAsmVarName, BaseDataType.BYTE)
|
||||
}
|
||||
BaseDataType.POINTER -> TODO("byte should not be cast to pointer")
|
||||
BaseDataType.LONG -> {
|
||||
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName")
|
||||
asmgen.signExtendVariableLsb(targetAsmVarName, BaseDataType.BYTE)
|
||||
asmgen.signExtendLongVariable(targetAsmVarName, BaseDataType.WORD)
|
||||
}
|
||||
BaseDataType.FLOAT -> {
|
||||
asmgen.out("""
|
||||
lda #<$targetAsmVarName
|
||||
ldy #>$targetAsmVarName
|
||||
sta P8ZP_SCRATCH_W2
|
||||
sty P8ZP_SCRATCH_W2+1
|
||||
lda $sourceAsmVarName
|
||||
jsr floats.cast_from_b""")
|
||||
}
|
||||
else -> throw AssemblyError("weird type")
|
||||
}
|
||||
}
|
||||
sourceDt.isUnsignedWord -> {
|
||||
when(targetDt.base) {
|
||||
BaseDataType.BOOL -> {
|
||||
asmgen.out("""
|
||||
lda $sourceAsmVarName
|
||||
ora $sourceAsmVarName+1
|
||||
beq +
|
||||
lda #1
|
||||
+ sta $targetAsmVarName""")
|
||||
}
|
||||
BaseDataType.BYTE, BaseDataType.UBYTE -> {
|
||||
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName")
|
||||
}
|
||||
BaseDataType.WORD -> {
|
||||
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName | lda $sourceAsmVarName+1 | sta $targetAsmVarName+1")
|
||||
}
|
||||
BaseDataType.POINTER -> asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName | lda $sourceAsmVarName+1 | sta $targetAsmVarName+1")
|
||||
BaseDataType.LONG -> {
|
||||
asmgen.out("""
|
||||
lda $sourceAsmVarName
|
||||
sta $targetAsmVarName
|
||||
lda $sourceAsmVarName+1
|
||||
sta $targetAsmVarName+1
|
||||
lda #0
|
||||
sta $targetAsmVarName+2
|
||||
sta $targetAsmVarName+3""")
|
||||
}
|
||||
BaseDataType.FLOAT -> {
|
||||
asmgen.out("""
|
||||
lda #<$targetAsmVarName
|
||||
ldy #>$targetAsmVarName
|
||||
sta P8ZP_SCRATCH_W2
|
||||
sty P8ZP_SCRATCH_W2+1
|
||||
lda $sourceAsmVarName
|
||||
ldy $sourceAsmVarName+1
|
||||
jsr floats.cast_from_uw""")
|
||||
}
|
||||
else -> throw AssemblyError("weird type")
|
||||
}
|
||||
}
|
||||
sourceDt.isSignedWord || sourceDt.isPointer -> {
|
||||
when(targetDt.base) {
|
||||
BaseDataType.BOOL -> {
|
||||
asmgen.out("""
|
||||
lda $sourceAsmVarName
|
||||
ora $sourceAsmVarName+1
|
||||
beq +
|
||||
lda #1
|
||||
+ sta $targetAsmVarName""")
|
||||
}
|
||||
BaseDataType.BYTE, BaseDataType.UBYTE -> {
|
||||
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName")
|
||||
}
|
||||
BaseDataType.UWORD, BaseDataType.POINTER -> {
|
||||
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName | lda $sourceAsmVarName+1 | sta $targetAsmVarName+1")
|
||||
}
|
||||
BaseDataType.LONG -> {
|
||||
asmgen.out("""
|
||||
lda $sourceAsmVarName
|
||||
sta $targetAsmVarName
|
||||
lda $sourceAsmVarName+1
|
||||
sta $targetAsmVarName+1""")
|
||||
asmgen.signExtendLongVariable(targetAsmVarName, BaseDataType.WORD)
|
||||
}
|
||||
BaseDataType.FLOAT -> {
|
||||
asmgen.out("""
|
||||
lda #<$targetAsmVarName
|
||||
ldy #>$targetAsmVarName
|
||||
sta P8ZP_SCRATCH_W2
|
||||
sty P8ZP_SCRATCH_W2+1
|
||||
lda $sourceAsmVarName
|
||||
ldy $sourceAsmVarName+1
|
||||
jsr floats.cast_from_w""")
|
||||
}
|
||||
else -> throw AssemblyError("weird type")
|
||||
}
|
||||
}
|
||||
sourceDt.isFloat -> {
|
||||
asmgen.out(" lda #<$sourceAsmVarName | ldy #>$sourceAsmVarName")
|
||||
when(targetDt.base) {
|
||||
BaseDataType.BOOL -> asmgen.out(" jsr floats.cast_as_bool_into_a | sta $targetAsmVarName")
|
||||
BaseDataType.UBYTE -> asmgen.out(" jsr floats.cast_as_uw_into_ya | sty $targetAsmVarName")
|
||||
BaseDataType.BYTE -> asmgen.out(" jsr floats.cast_as_w_into_ay | sta $targetAsmVarName")
|
||||
BaseDataType.UWORD -> asmgen.out(" jsr floats.cast_as_uw_into_ya | sty $targetAsmVarName | sta $targetAsmVarName+1")
|
||||
BaseDataType.WORD -> asmgen.out(" jsr floats.cast_as_w_into_ay | sta $targetAsmVarName | sty $targetAsmVarName+1")
|
||||
BaseDataType.LONG -> {
|
||||
asmgen.out("""
|
||||
sta cx16.r0L
|
||||
sty cx16.r0H
|
||||
lda #<$targetAsmVarName
|
||||
ldy #>$targetAsmVarName
|
||||
jsr floats.cast_as_long""")
|
||||
}
|
||||
else -> throw AssemblyError("weird type")
|
||||
}
|
||||
}
|
||||
sourceDt.isString -> throw AssemblyError("cannot typecast a string value")
|
||||
sourceDt.isLong -> {
|
||||
if(targetDt.isByteOrBool) {
|
||||
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName")
|
||||
} else if(targetDt.isWord || targetDt.isPointer) {
|
||||
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName | lda $sourceAsmVarName+1 | sta $targetAsmVarName+1")
|
||||
} else if(targetDt.isFloat) {
|
||||
asmgen.out("""
|
||||
lda #<$sourceAsmVarName
|
||||
ldy #>$sourceAsmVarName
|
||||
sta cx16.r1L
|
||||
sty cx16.r1H
|
||||
lda #<$targetAsmVarName
|
||||
ldy #>$targetAsmVarName
|
||||
jsr floats.internal_long_R1_to_float_AY""")
|
||||
} else
|
||||
throw AssemblyError("weird type")
|
||||
}
|
||||
else -> throw AssemblyError("weird type")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
internal fun assignTypeCastedRegisters(targetAsmVarName: String, targetDt: BaseDataType,
|
||||
regs: RegisterOrPair, sourceDt: BaseDataType, position: Position) {
|
||||
if(sourceDt == targetDt && sourceDt != BaseDataType.POINTER)
|
||||
throw AssemblyError("typecast to identical type")
|
||||
|
||||
// also see: PtExpressionAsmGen, fun translateExpression(typecast: PtTypeCast)
|
||||
when(sourceDt) {
|
||||
BaseDataType.BOOL -> {
|
||||
if (targetDt.isByteOrBool) asmgen.out(" st${regs.toString().lowercase()} $targetAsmVarName")
|
||||
else throw AssemblyError("assign bool to non-byte variable")
|
||||
}
|
||||
BaseDataType.UBYTE -> {
|
||||
when(targetDt) {
|
||||
BaseDataType.BOOL -> {
|
||||
val compare = if(regs==RegisterOrPair.A) "cmp" else "cp${regs.toString().lowercase()}"
|
||||
asmgen.out("""
|
||||
$compare #0
|
||||
beq +
|
||||
ld${regs.toString().lowercase()} #1
|
||||
+ st${regs.toString().lowercase()} $targetAsmVarName""")
|
||||
}
|
||||
BaseDataType.BYTE -> {
|
||||
asmgen.out(" st${regs.toString().lowercase()} $targetAsmVarName")
|
||||
}
|
||||
BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.POINTER -> {
|
||||
if(asmgen.isTargetCpu(CpuType.CPU65C02))
|
||||
asmgen.out(" st${regs.toString().lowercase()} $targetAsmVarName | stz $targetAsmVarName+1")
|
||||
else
|
||||
asmgen.out(" st${regs.toString().lowercase()} $targetAsmVarName | lda #0 | sta $targetAsmVarName+1")
|
||||
}
|
||||
BaseDataType.LONG -> {
|
||||
asmgen.out("""
|
||||
st${regs.toString().lowercase()} $targetAsmVarName
|
||||
lda #0
|
||||
sta $targetAsmVarName+1
|
||||
sta $targetAsmVarName+2
|
||||
sta $targetAsmVarName+3""")
|
||||
}
|
||||
BaseDataType.FLOAT -> {
|
||||
when(regs) {
|
||||
RegisterOrPair.A -> asmgen.out(" tay")
|
||||
RegisterOrPair.X -> asmgen.out(" txa | tay")
|
||||
RegisterOrPair.Y -> {}
|
||||
else -> throw AssemblyError("non-byte regs")
|
||||
}
|
||||
asmgen.out("""
|
||||
lda #<$targetAsmVarName
|
||||
sta P8ZP_SCRATCH_W2
|
||||
lda #>$targetAsmVarName
|
||||
sta P8ZP_SCRATCH_W2+1
|
||||
jsr floats.cast_from_ub""")
|
||||
}
|
||||
else -> throw AssemblyError("weird type")
|
||||
}
|
||||
}
|
||||
BaseDataType.BYTE -> {
|
||||
when(targetDt) {
|
||||
BaseDataType.BOOL -> TODO("assign byte to bool $position")
|
||||
BaseDataType.UBYTE -> {
|
||||
asmgen.out(" st${regs.toString().lowercase()} $targetAsmVarName")
|
||||
}
|
||||
BaseDataType.UWORD -> {
|
||||
if(asmgen.isTargetCpu(CpuType.CPU65C02))
|
||||
asmgen.out(" st${regs.toString().lowercase()} $targetAsmVarName | stz $targetAsmVarName+1")
|
||||
else
|
||||
asmgen.out(" st${regs.toString().lowercase()} $targetAsmVarName | lda #0 | sta $targetAsmVarName+1")
|
||||
}
|
||||
BaseDataType.WORD, BaseDataType.POINTER -> {
|
||||
when(regs) {
|
||||
RegisterOrPair.A -> {}
|
||||
RegisterOrPair.X -> asmgen.out(" txa")
|
||||
RegisterOrPair.Y -> asmgen.out(" tya")
|
||||
else -> throw AssemblyError("non-byte regs")
|
||||
}
|
||||
asmgen.signExtendAYlsb(sourceDt)
|
||||
asmgen.out(" sta $targetAsmVarName | sty $targetAsmVarName+1")
|
||||
}
|
||||
BaseDataType.LONG -> {
|
||||
asmgen.out(" st${regs.toString().lowercase()} $targetAsmVarName")
|
||||
asmgen.signExtendLongVariable(targetAsmVarName, sourceDt)
|
||||
}
|
||||
BaseDataType.FLOAT -> {
|
||||
when(regs) {
|
||||
RegisterOrPair.A -> {}
|
||||
RegisterOrPair.X -> asmgen.out(" txa")
|
||||
RegisterOrPair.Y -> asmgen.out(" tya")
|
||||
else -> throw AssemblyError("non-byte regs")
|
||||
}
|
||||
asmgen.out("""
|
||||
ldy #<$targetAsmVarName
|
||||
sty P8ZP_SCRATCH_W2
|
||||
ldy #>$targetAsmVarName
|
||||
sty P8ZP_SCRATCH_W2+1
|
||||
jsr floats.cast_from_b""")
|
||||
}
|
||||
else -> throw AssemblyError("weird type")
|
||||
}
|
||||
}
|
||||
BaseDataType.UWORD -> {
|
||||
when(targetDt) {
|
||||
BaseDataType.BOOL -> TODO("assign uword to bool $position")
|
||||
BaseDataType.BYTE, BaseDataType.UBYTE -> {
|
||||
asmgen.out(" st${regs.toString().lowercase().first()} $targetAsmVarName")
|
||||
}
|
||||
BaseDataType.WORD, BaseDataType.POINTER -> {
|
||||
when(regs) {
|
||||
RegisterOrPair.AX -> asmgen.out(" sta $targetAsmVarName | stx $targetAsmVarName+1")
|
||||
RegisterOrPair.AY -> asmgen.out(" sta $targetAsmVarName | sty $targetAsmVarName+1")
|
||||
RegisterOrPair.XY -> asmgen.out(" stx $targetAsmVarName | sty $targetAsmVarName+1")
|
||||
else -> throw AssemblyError("non-word regs")
|
||||
}
|
||||
}
|
||||
BaseDataType.LONG -> {
|
||||
when(regs) {
|
||||
RegisterOrPair.AX -> asmgen.out(" sta $targetAsmVarName | stx $targetAsmVarName+1")
|
||||
RegisterOrPair.AY -> asmgen.out(" sta $targetAsmVarName | sty $targetAsmVarName+1")
|
||||
RegisterOrPair.XY -> asmgen.out(" stx $targetAsmVarName | sty $targetAsmVarName+1")
|
||||
else -> throw AssemblyError("non-word regs")
|
||||
}
|
||||
asmgen.out(" lda #0 | sta $targetAsmVarName+2 | sta $targetAsmVarName+3")
|
||||
}
|
||||
BaseDataType.FLOAT -> {
|
||||
if(regs!=RegisterOrPair.AY)
|
||||
throw AssemblyError("only supports AY here")
|
||||
asmgen.out("""
|
||||
tax
|
||||
lda #<$targetAsmVarName
|
||||
sta P8ZP_SCRATCH_W2
|
||||
lda #>$targetAsmVarName
|
||||
sta P8ZP_SCRATCH_W2+1
|
||||
txa
|
||||
jsr floats.cast_from_uw""")
|
||||
}
|
||||
else -> throw AssemblyError("weird type")
|
||||
}
|
||||
}
|
||||
BaseDataType.WORD -> {
|
||||
when(targetDt) {
|
||||
BaseDataType.BOOL -> TODO("assign word to bool $position")
|
||||
BaseDataType.BYTE, BaseDataType.UBYTE -> {
|
||||
asmgen.out(" st${regs.toString().lowercase().first()} $targetAsmVarName")
|
||||
}
|
||||
BaseDataType.UWORD, BaseDataType.POINTER -> {
|
||||
when(regs) {
|
||||
RegisterOrPair.AX -> asmgen.out(" sta $targetAsmVarName | stx $targetAsmVarName+1")
|
||||
RegisterOrPair.AY -> asmgen.out(" sta $targetAsmVarName | sty $targetAsmVarName+1")
|
||||
RegisterOrPair.XY -> asmgen.out(" stx $targetAsmVarName | sty $targetAsmVarName+1")
|
||||
else -> throw AssemblyError("non-word regs")
|
||||
}
|
||||
}
|
||||
BaseDataType.LONG -> {
|
||||
when(regs) {
|
||||
RegisterOrPair.AX -> asmgen.out(" sta $targetAsmVarName | stx $targetAsmVarName+1")
|
||||
RegisterOrPair.AY -> asmgen.out(" sta $targetAsmVarName | sty $targetAsmVarName+1")
|
||||
RegisterOrPair.XY -> asmgen.out(" stx $targetAsmVarName | sty $targetAsmVarName+1")
|
||||
else -> throw AssemblyError("non-word regs")
|
||||
}
|
||||
asmgen.signExtendLongVariable(targetAsmVarName, BaseDataType.WORD)
|
||||
}
|
||||
BaseDataType.FLOAT -> {
|
||||
if(regs!=RegisterOrPair.AY)
|
||||
throw AssemblyError("only supports AY here")
|
||||
asmgen.out("""
|
||||
tax
|
||||
lda #<$targetAsmVarName
|
||||
sta P8ZP_SCRATCH_W2
|
||||
lda #>$targetAsmVarName
|
||||
sta P8ZP_SCRATCH_W2+1
|
||||
txa
|
||||
jsr floats.cast_from_w""")
|
||||
}
|
||||
else -> throw AssemblyError("weird type")
|
||||
}
|
||||
}
|
||||
BaseDataType.STR -> throw AssemblyError("cannot typecast a string value")
|
||||
BaseDataType.POINTER -> {
|
||||
if(targetDt.isWord || targetDt.isPointer) {
|
||||
when(regs) {
|
||||
RegisterOrPair.AX -> asmgen.out(" sta $targetAsmVarName | stx $targetAsmVarName+1")
|
||||
RegisterOrPair.AY -> asmgen.out(" sta $targetAsmVarName | sty $targetAsmVarName+1")
|
||||
RegisterOrPair.XY -> asmgen.out(" stx $targetAsmVarName | sty $targetAsmVarName+1")
|
||||
else -> throw AssemblyError("non-word regs")
|
||||
}
|
||||
} else if(targetDt.isLong) {
|
||||
when(regs) {
|
||||
RegisterOrPair.AX -> asmgen.out(" sta $targetAsmVarName | stx $targetAsmVarName+1")
|
||||
RegisterOrPair.AY -> asmgen.out(" sta $targetAsmVarName | sty $targetAsmVarName+1")
|
||||
RegisterOrPair.XY -> asmgen.out(" stx $targetAsmVarName | sty $targetAsmVarName+1")
|
||||
else -> throw AssemblyError("non-word regs")
|
||||
}
|
||||
asmgen.out(" lda #0 | sta $targetAsmVarName+2 | sta $targetAsmVarName+3")
|
||||
} else {
|
||||
throw AssemblyError("cannot assign pointer to $targetDt")
|
||||
}
|
||||
}
|
||||
BaseDataType.LONG -> {
|
||||
val startreg = regs.startregname()
|
||||
if(targetDt.isByteOrBool) {
|
||||
asmgen.out(" lda cx16.$startreg | sta $targetAsmVarName")
|
||||
} else if(targetDt.isWord || targetDt.isPointer) {
|
||||
asmgen.out(" lda cx16.$startreg | sta $targetAsmVarName | lda cx16.$startreg+1 | sta $targetAsmVarName+1")
|
||||
} else if(targetDt.isFloat) {
|
||||
if(regs==RegisterOrPair.R0R1)
|
||||
throw AssemblyError("cannot assign long in R0R1 to float because r1 is used as a parameter $position")
|
||||
asmgen.out("""
|
||||
lda #<cx16.$startreg
|
||||
ldy #>cx16.$startreg
|
||||
sta cx16.r1L
|
||||
sty cx16.r1H
|
||||
lda #<$targetAsmVarName
|
||||
ldy #>$targetAsmVarName
|
||||
jsr floats.internal_long_R1_to_float_AY""")
|
||||
} else
|
||||
throw AssemblyError("weird type $targetDt")
|
||||
}
|
||||
else -> throw AssemblyError("weird type $sourceDt")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute 8-bit offset Y = idx*structSize + fieldOffset for a struct-array element.
|
||||
* Assumes totalBytes <=256 so offset fits in Y. Uses A/Y and multiply_bytes when needed.
|
||||
*/
|
||||
|
||||
}
|
||||
@@ -5,30 +5,33 @@ import io.kotest.matchers.shouldBe
|
||||
import prog8.code.StMemVar
|
||||
import prog8.code.SymbolTable
|
||||
import prog8.code.ast.PtProgram
|
||||
import prog8.code.core.BaseDataType
|
||||
import prog8.code.core.DataType
|
||||
import prog8.code.target.C64Target
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8.codegen.cpu6502.*
|
||||
import prog8tests.helpers.DummyMemsizer
|
||||
import prog8tests.helpers.DummyStringEncoder
|
||||
|
||||
|
||||
class TestAsmOptimizer: FunSpec({
|
||||
|
||||
val machine = C64Target()
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val symbolTable = SymbolTable(program)
|
||||
|
||||
// Add test variables with known non-IO addresses so the optimizer can resolve them
|
||||
// Non-IO addresses in typical C64 RAM range
|
||||
symbolTable.add(StMemVar("myvar", DataType.UBYTE, 0x0300u, null, null))
|
||||
symbolTable.add(StMemVar("var", DataType.UBYTE, 0x0301u, null, null))
|
||||
symbolTable.add(StMemVar("var1", DataType.UWORD, 0x0302u, null, null))
|
||||
symbolTable.add(StMemVar("var2", DataType.UWORD, 0x0304u, null, null))
|
||||
symbolTable.add(StMemVar("ptr", DataType.UWORD, 0x0306u, null, null))
|
||||
symbolTable.add(StMemVar("array", DataType.arrayFor(BaseDataType.UBYTE, machine), 0x0302u, 16u, null))
|
||||
symbolTable.add(StMemVar("wordarray", DataType.arrayFor(BaseDataType.UWORD, machine), 0x0310u, 32u, null))
|
||||
symbolTable.add(StMemVar("nswordarray", DataType.arrayFor(BaseDataType.UWORD, machine), 0x0320u, 32u, null))
|
||||
symbolTable.add(StMemVar("var1", DataType.UWORD, 0x0400u, null, null))
|
||||
symbolTable.add(StMemVar("var2", DataType.UWORD, 0x0402u, null, null))
|
||||
symbolTable.add(StMemVar("ptr", DataType.UWORD, 0x0404u, null, null))
|
||||
symbolTable.add(StMemVar("P8ZP_SCRATCH_PTR", DataType.UWORD, 0x02u, null, null))
|
||||
symbolTable.add(StMemVar("P8ZP_SCRATCH_W1", DataType.UWORD, 0x04u, null, null))
|
||||
symbolTable.add(StMemVar("A1", DataType.UWORD, 0x0310u, null, null))
|
||||
symbolTable.add(StMemVar("A2", DataType.UWORD, 0x0312u, null, null))
|
||||
symbolTable.add(StMemVar("A1", DataType.UWORD, 0x0410u, null, null))
|
||||
symbolTable.add(StMemVar("A2", DataType.UWORD, 0x0412u, null, null))
|
||||
|
||||
fun optimize(lines: MutableList<String>) {
|
||||
optimizeAssembly(lines, machine, symbolTable)
|
||||
@@ -161,13 +164,13 @@ class TestAsmOptimizer: FunSpec({
|
||||
}
|
||||
|
||||
test("optimizeJsrRts: three consecutive jsr only tail jsr+rts becomes jmp") {
|
||||
val lines = mutableListOf(" jsr \$4000", " jsr \$5000", " jsr \$6000", " rts", " nop", " nop")
|
||||
val lines = mutableListOf($$" jsr $4000", $$" jsr $5000", $$" jsr $6000", " rts", " nop", " nop")
|
||||
optimize(lines)
|
||||
lines shouldBe listOf(" jsr \$4000", " jsr \$5000", " jmp \$6000", " nop", " nop")
|
||||
lines shouldBe listOf($$" jsr $4000", $$" jsr $5000", $$" jmp $6000", " nop", " nop")
|
||||
}
|
||||
|
||||
test("optimizeJsrRts: single line with embedded newlines only tail converted defensively") {
|
||||
val lines = mutableListOf(" jsr \$4000\n jsr \$5000\n jsr \$6000", " rts", " nop", " nop")
|
||||
val lines = mutableListOf($$" jsr $4000\n jsr $5000\n jsr $6000", " rts", " nop", " nop")
|
||||
optimize(lines)
|
||||
// Before fix this became jmp/jmp/jmp via replace("jsr","jmp"); after fix only one jmp via replaceMnemonic
|
||||
val jmpCount = lines.joinToString("\n").split("jmp").size - 1
|
||||
@@ -284,6 +287,179 @@ class TestAsmOptimizer: FunSpec({
|
||||
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop")
|
||||
}
|
||||
|
||||
// --- optimizeFoldIndexOffset (byte arrays) ---
|
||||
|
||||
test("optimizeFoldIndexOffset: folds byte array lda+clc+adc+tay+lda to ldy+lda") {
|
||||
val lines = mutableListOf(
|
||||
" lda myvar", " clc", " adc #3", " tay", " lda array,y",
|
||||
" rts", " nop", " nop", " nop", " nop"
|
||||
)
|
||||
optimize(lines)
|
||||
lines[0] shouldBe " ldy myvar"
|
||||
lines[1] shouldBe " lda array+3,y"
|
||||
lines.size shouldBe 7
|
||||
}
|
||||
|
||||
test("optimizeFoldIndexOffset: folds with x register") {
|
||||
val lines = mutableListOf(
|
||||
" lda var", " clc", " adc #5", " tax", " lda array,x",
|
||||
" rts", " nop", " nop", " nop", " nop"
|
||||
)
|
||||
optimize(lines)
|
||||
lines[0] shouldBe " ldx var"
|
||||
lines[1] shouldBe " lda array+5,x"
|
||||
lines.size shouldBe 7
|
||||
}
|
||||
|
||||
test("optimizeFoldIndexOffset: works with sta instead of lda") {
|
||||
val lines = mutableListOf(
|
||||
" lda myvar", " clc", " adc #1", " tay", " sta array,y",
|
||||
" rts", " nop", " nop", " nop", " nop"
|
||||
)
|
||||
optimize(lines)
|
||||
lines[0] shouldBe " ldy myvar"
|
||||
lines[1] shouldBe " sta array+1,y"
|
||||
lines.size shouldBe 7
|
||||
}
|
||||
|
||||
test("optimizeFoldIndexOffset: does not fold indirect indexed (ptr),y") {
|
||||
val lines = mutableListOf(
|
||||
" lda myvar", " clc", " adc #3", " tay", " lda (ptr),y",
|
||||
" rts", " nop", " nop", " nop", " nop"
|
||||
)
|
||||
optimize(lines)
|
||||
lines.size shouldBe 10
|
||||
}
|
||||
|
||||
test("optimizeFoldIndexOffset: preserves hex constant format") {
|
||||
val lines = mutableListOf(
|
||||
" lda myvar", " clc", " adc #$0f", " tay", " lda array,y",
|
||||
" rts", " nop", " nop", " nop", " nop"
|
||||
)
|
||||
optimize(lines)
|
||||
lines[0] shouldBe " ldy myvar"
|
||||
lines[1] shouldBe " lda array+$0f,y"
|
||||
lines.size shouldBe 7
|
||||
}
|
||||
|
||||
test("optimizeFoldIndexOffset: does not fold when 5th op is not absolute indexed") {
|
||||
val lines = mutableListOf(
|
||||
" lda myvar", " clc", " adc #3", " tay", " nop",
|
||||
" rts", " nop", " nop", " nop", " nop"
|
||||
)
|
||||
optimize(lines)
|
||||
lines.size shouldBe 10
|
||||
}
|
||||
|
||||
test("optimizeFoldIndexOffset: works with cmp instruction") {
|
||||
val lines = mutableListOf(
|
||||
" lda myvar", " clc", " adc #7", " tay", " cmp array,y",
|
||||
" rts", " nop", " nop", " nop", " nop"
|
||||
)
|
||||
optimize(lines)
|
||||
lines[0] shouldBe " ldy myvar"
|
||||
lines[1] shouldBe " cmp array+7,y"
|
||||
lines.size shouldBe 7
|
||||
}
|
||||
|
||||
test("optimizeFoldIndexOffset: preserves label on first instruction") {
|
||||
val lines = mutableListOf(
|
||||
"mylabel: lda myvar", " clc", " adc #3", " tay", " lda array,y",
|
||||
" rts", " nop", " nop", " nop", " nop"
|
||||
)
|
||||
optimize(lines)
|
||||
lines[0] shouldBe "mylabel: ldy myvar"
|
||||
lines[1] shouldBe " lda array+3,y"
|
||||
lines.size shouldBe 7
|
||||
}
|
||||
|
||||
// --- optimizeFoldIndexOffsetSplitWord (split word arrays) ---
|
||||
|
||||
test("optimizeFoldIndexOffsetSplitWord: folds split word array access with y") {
|
||||
val lines = mutableListOf(
|
||||
" lda var", " clc", " adc #3", " tay",
|
||||
" lda wordarray_lsb,y", " ldx wordarray_msb,y",
|
||||
" rts", " nop", " nop", " nop"
|
||||
)
|
||||
optimize(lines)
|
||||
lines[0] shouldBe " ldy var"
|
||||
lines[1] shouldBe " lda wordarray_lsb+3,y"
|
||||
lines[2] shouldBe " ldx wordarray_msb+3,y"
|
||||
lines.size shouldBe 7
|
||||
}
|
||||
|
||||
test("optimizeFoldIndexOffsetSplitWord: folds split word array access with x") {
|
||||
val lines = mutableListOf(
|
||||
" lda var", " clc", " adc #3", " tax",
|
||||
" lda wordarray_lsb,x", " ldx wordarray_msb,x",
|
||||
" rts", " nop", " nop", " nop"
|
||||
)
|
||||
optimize(lines)
|
||||
lines[0] shouldBe " ldx var"
|
||||
lines[1] shouldBe " lda wordarray_lsb+3,x"
|
||||
lines[2] shouldBe " ldx wordarray_msb+3,x"
|
||||
lines.size shouldBe 7
|
||||
}
|
||||
|
||||
test("optimizeFoldIndexOffsetSplitWord: does not fold mismatched registers") {
|
||||
val lines = mutableListOf(
|
||||
" lda var", " clc", " adc #3", " tay",
|
||||
" lda wordarray_lsb,y", " ldx wordarray_msb,x",
|
||||
" rts", " nop", " nop", " nop"
|
||||
)
|
||||
optimize(lines)
|
||||
lines.size shouldBe 10
|
||||
}
|
||||
|
||||
test("optimizeFoldIndexOffsetSplitWord: does not fold non-lsb-msb pair") {
|
||||
val lines = mutableListOf(
|
||||
" lda var", " clc", " adc #3", " tay",
|
||||
" lda wordarray_lsb,y", " ldx other,y",
|
||||
" rts", " nop", " nop", " nop"
|
||||
)
|
||||
optimize(lines)
|
||||
lines.size shouldBe 10
|
||||
}
|
||||
|
||||
// --- optimizeFoldIndexOffsetNonSplitWord (non-split word arrays) ---
|
||||
|
||||
test("optimizeFoldIndexOffsetNonSplitWord: folds non-split word array access") {
|
||||
val lines = mutableListOf(
|
||||
" lda var", " clc", " adc #3", " asl a", " tay",
|
||||
" lda nswordarray,y", " ldx nswordarray+1,y",
|
||||
" rts", " nop"
|
||||
)
|
||||
optimize(lines)
|
||||
lines[0] shouldBe " lda var"
|
||||
lines[1] shouldBe " asl a"
|
||||
lines[2] shouldBe " tay"
|
||||
lines[3] shouldBe " lda nswordarray+6,y"
|
||||
lines[4] shouldBe " ldx nswordarray+1+6,y"
|
||||
lines.size shouldBe 7
|
||||
}
|
||||
|
||||
test("optimizeFoldIndexOffsetNonSplitWord: scales hex constants") {
|
||||
val lines = mutableListOf(
|
||||
" lda var", " clc", " adc #$02", " asl a", " tay",
|
||||
" lda nswordarray,y", " ldx nswordarray+1,y",
|
||||
" rts", " nop"
|
||||
)
|
||||
optimize(lines)
|
||||
lines[3] shouldBe " lda nswordarray+$04,y"
|
||||
lines[4] shouldBe " ldx nswordarray+1+$04,y"
|
||||
lines.size shouldBe 7
|
||||
}
|
||||
|
||||
test("optimizeFoldIndexOffsetNonSplitWord: does not fold without asl a") {
|
||||
val lines = mutableListOf(
|
||||
" lda var", " clc", " adc #3", " tay",
|
||||
" lda nswordarray,y", " ldx nswordarray+1,y",
|
||||
" rts", " nop", " nop"
|
||||
)
|
||||
optimize(lines)
|
||||
lines.size shouldBe 9
|
||||
}
|
||||
|
||||
// --- Edge cases ---
|
||||
|
||||
test("handles empty input") {
|
||||
|
||||
@@ -12,10 +12,9 @@ import prog8.code.ast.*
|
||||
import prog8.code.core.*
|
||||
import prog8.code.source.SourceCode
|
||||
import prog8.code.target.C64Target
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8.codegen.cpu6502.AsmGen6502
|
||||
import prog8.codegen.cpu6502.VariableAllocator
|
||||
import prog8tests.helpers.DummyMemsizer
|
||||
import prog8tests.helpers.DummyStringEncoder
|
||||
import prog8tests.helpers.ErrorReporterForTests
|
||||
import java.nio.file.Files
|
||||
import kotlin.io.path.Path
|
||||
@@ -46,13 +45,15 @@ class TestCodegen: FunSpec({
|
||||
// xx += cx16.r0
|
||||
// }
|
||||
//}
|
||||
val codegen = AsmGen6502(prefixSymbols = false, 0)
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val compTarget = C64Target()
|
||||
val codegen = AsmGen6502(0)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main",false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
|
||||
sub.add(PtVariable(
|
||||
"pi",
|
||||
DataType.UBYTE,
|
||||
false,
|
||||
ZeropageWish.DONTCARE,
|
||||
0u,
|
||||
false,
|
||||
@@ -62,7 +63,8 @@ class TestCodegen: FunSpec({
|
||||
))
|
||||
sub.add(PtVariable(
|
||||
"particleX",
|
||||
DataType.arrayFor(BaseDataType.UBYTE),
|
||||
DataType.arrayFor(BaseDataType.UBYTE, compTarget),
|
||||
false,
|
||||
ZeropageWish.DONTCARE,
|
||||
0u,
|
||||
false,
|
||||
@@ -72,7 +74,8 @@ class TestCodegen: FunSpec({
|
||||
))
|
||||
sub.add(PtVariable(
|
||||
"particleDX",
|
||||
DataType.arrayFor(BaseDataType.UBYTE),
|
||||
DataType.arrayFor(BaseDataType.UBYTE, compTarget),
|
||||
false,
|
||||
ZeropageWish.DONTCARE,
|
||||
0u,
|
||||
false,
|
||||
@@ -83,6 +86,7 @@ class TestCodegen: FunSpec({
|
||||
sub.add(PtVariable(
|
||||
"xx",
|
||||
DataType.WORD,
|
||||
false,
|
||||
ZeropageWish.DONTCARE,
|
||||
0u,
|
||||
false,
|
||||
@@ -93,14 +97,14 @@ class TestCodegen: FunSpec({
|
||||
|
||||
val assign = PtAugmentedAssign("+=", Position.DUMMY)
|
||||
val target = PtAssignTarget(false, Position.DUMMY).also {
|
||||
val targetIdx = PtArrayIndexer(DataType.UBYTE, Position.DUMMY).also { idx ->
|
||||
idx.add(PtIdentifier("main.start.particleX", DataType.arrayFor(BaseDataType.UBYTE), Position.DUMMY))
|
||||
val targetIdx = PtArrayIndexer(DataType.UBYTE, false, Position.DUMMY).also { idx ->
|
||||
idx.add(PtIdentifier("main.start.particleX", DataType.arrayFor(BaseDataType.UBYTE, compTarget), Position.DUMMY))
|
||||
idx.add(PtNumber(BaseDataType.UBYTE, 2.0, Position.DUMMY))
|
||||
}
|
||||
it.add(targetIdx)
|
||||
}
|
||||
val value = PtArrayIndexer(DataType.UBYTE, Position.DUMMY)
|
||||
value.add(PtIdentifier("main.start.particleDX", DataType.arrayFor(BaseDataType.UBYTE), Position.DUMMY))
|
||||
val value = PtArrayIndexer(DataType.UBYTE, false, Position.DUMMY)
|
||||
value.add(PtIdentifier("main.start.particleDX", DataType.arrayFor(BaseDataType.UBYTE, compTarget), Position.DUMMY))
|
||||
value.add(PtNumber(BaseDataType.UBYTE, 2.0, Position.DUMMY))
|
||||
assign.add(target)
|
||||
assign.add(value)
|
||||
@@ -166,7 +170,7 @@ class TestCodegen: FunSpec({
|
||||
}
|
||||
|
||||
test("memory mapped zp var is correctly considered to be zp var") {
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val st = SymbolTable(program)
|
||||
st.add(StMemVar("zpmemvar", DataType.WORD, 0x20u, null, null))
|
||||
st.add(StMemVar("normalmemvar", DataType.WORD, 0x9000u, null, null))
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -81,7 +81,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val tr = exprGen.translateExpression(call.args[0])
|
||||
addToResult(result, tr, tr.resultReg, -1)
|
||||
addInstr(result, IRInstruction(Opcode.PUSH, IRDataType.BYTE, reg1=tr.resultReg), null)
|
||||
addInstr(result, IRInstructions.push(IRDataType.BYTE, tr.resultReg), null)
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val tr = exprGen.translateExpression(call.args[0])
|
||||
addToResult(result, tr, tr.resultReg, -1)
|
||||
addInstr(result, IRInstruction(Opcode.PUSH, IRDataType.WORD, reg1=tr.resultReg), null)
|
||||
addInstr(result, IRInstructions.push(IRDataType.WORD, tr.resultReg), null)
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val tr = exprGen.translateExpression(call.args[0])
|
||||
addToResult(result, tr, tr.resultReg, -1)
|
||||
addInstr(result, IRInstruction(Opcode.PUSH, IRDataType.LONG, reg1=tr.resultReg), null)
|
||||
addInstr(result, IRInstructions.push(IRDataType.LONG, tr.resultReg), null)
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
|
||||
}
|
||||
|
||||
@@ -105,35 +105,35 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val tr = exprGen.translateExpression(call.args[0])
|
||||
addToResult(result, tr, -1, tr.resultFpReg)
|
||||
addInstr(result, IRInstruction(Opcode.PUSH, IRDataType.FLOAT, fpReg1 = RegisterNum(tr.resultFpReg)), null)
|
||||
addInstr(result, IRInstructions.push(IRDataType.FLOAT, tr.resultFpReg), null)
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
|
||||
}
|
||||
|
||||
private fun funcPop(): ExpressionCodeResult {
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val resultRegister = codeGen.registers.next(IRDataType.BYTE)
|
||||
addInstr(result, IRInstruction(Opcode.POP, IRDataType.BYTE, reg1=resultRegister), null)
|
||||
addInstr(result, IRInstructions.pop(IRDataType.BYTE, resultRegister), null)
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, resultRegister, -1)
|
||||
}
|
||||
|
||||
private fun funcPopW(): ExpressionCodeResult {
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val resultRegister = codeGen.registers.next(IRDataType.WORD)
|
||||
addInstr(result, IRInstruction(Opcode.POP, IRDataType.WORD, reg1=resultRegister), null)
|
||||
addInstr(result, IRInstructions.pop(IRDataType.WORD, resultRegister), null)
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, resultRegister, -1)
|
||||
}
|
||||
|
||||
private fun funcPopL(): ExpressionCodeResult {
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val resultRegister = codeGen.registers.next(IRDataType.LONG)
|
||||
addInstr(result, IRInstruction(Opcode.POP, IRDataType.LONG, reg1=resultRegister), null)
|
||||
addInstr(result, IRInstructions.pop(IRDataType.LONG, resultRegister), null)
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, resultRegister, -1)
|
||||
}
|
||||
|
||||
private fun funcPopF(): ExpressionCodeResult {
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val resultRegister = codeGen.registers.next(IRDataType.FLOAT)
|
||||
addInstr(result, IRInstruction(Opcode.POP, IRDataType.FLOAT, fpReg1 = RegisterNum(resultRegister)), null)
|
||||
addInstr(result, IRInstructions.pop(IRDataType.FLOAT, resultRegister), null)
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, -1, resultRegister)
|
||||
}
|
||||
|
||||
@@ -147,15 +147,15 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
IRDataType.LONG -> {
|
||||
val intermediateReg = codeGen.registers.next(IRDataType.LONG)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.LOADI, type, reg1=intermediateReg, reg2=trSourceAddr.resultReg, immediate = 0)
|
||||
it += IRInstruction(Opcode.STOREI, type, reg1=intermediateReg, reg2=trTargetAddr.resultReg, immediate = 0)
|
||||
it += IRInstructions.loadMemory(Opcode.LOADI, type, intermediateReg, IRMemory.indirect(trSourceAddr.resultReg))
|
||||
it += IRInstructions.storeMemory(Opcode.STOREI, type, intermediateReg, IRMemory.indirect(trTargetAddr.resultReg))
|
||||
}
|
||||
}
|
||||
IRDataType.FLOAT -> {
|
||||
val intermediateReg = codeGen.registers.next(IRDataType.FLOAT)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.LOADI, type, fpReg1=RegisterNum(intermediateReg), reg1=trSourceAddr.resultReg, immediate = 0)
|
||||
it += IRInstruction(Opcode.STOREI, type, fpReg1=RegisterNum(intermediateReg), reg1=trTargetAddr.resultReg, immediate = 0)
|
||||
it += IRInstructions.loadMemory(Opcode.LOADI, type, intermediateReg, IRMemory.indirect(trSourceAddr.resultReg))
|
||||
it += IRInstructions.storeMemory(Opcode.STOREI, type, intermediateReg, IRMemory.indirect(trTargetAddr.resultReg))
|
||||
}
|
||||
}
|
||||
else -> throw AssemblyError("invalid type $type")
|
||||
@@ -169,12 +169,12 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
addToResult(result, valueTr, valueTr.resultReg, valueTr.resultFpReg)
|
||||
return if(resultType==IRDataType.FLOAT) {
|
||||
val resultFpReg = codeGen.registers.next(IRDataType.FLOAT)
|
||||
addInstr(result, IRInstruction(Opcode.SQUARE, resultType, fpReg1 = RegisterNum(resultFpReg), fpReg2 = RegisterNum(valueTr.resultFpReg)), null)
|
||||
addInstr(result, IRInstructions.binary(Opcode.SQUARE, resultType, resultFpReg, valueTr.resultFpReg), null)
|
||||
ExpressionCodeResult(result, resultType, -1, resultFpReg)
|
||||
}
|
||||
else {
|
||||
val resultReg = codeGen.registers.next(resultType)
|
||||
addInstr(result, IRInstruction(Opcode.SQUARE, resultType, reg1 = resultReg, reg2 = valueTr.resultReg), null)
|
||||
addInstr(result, IRInstructions.binary(Opcode.SQUARE, resultType, resultReg, valueTr.resultReg), null)
|
||||
ExpressionCodeResult(result, resultType, resultReg, -1)
|
||||
}
|
||||
}
|
||||
@@ -183,12 +183,13 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val addressTr = exprGen.translateExpression(call.args[0])
|
||||
addToResult(result, addressTr, addressTr.resultReg, -1)
|
||||
addInstr(result, IRInstruction(Opcode.CALLI, reg1 = addressTr.resultReg), null)
|
||||
addInstr(result, IRInstructions.call(Opcode.CALLI, CallSite(CallTarget.Direct(codeIndirect(addressTr.resultReg)))), null)
|
||||
return if(call.void)
|
||||
ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
|
||||
else {
|
||||
val resultvalueReg = codeGen.registers.next(IRDataType.WORD)
|
||||
addInstr(result, IRInstruction(Opcode.LOADHR, IRDataType.WORD, reg1=resultvalueReg, immediate=4), null)
|
||||
val slot = if(codeGen.options.compTarget.cpu.usesM68kConvention) 10 else 4
|
||||
addInstr(result, IRInstructions.hardwareLoad(IRDataType.WORD, resultvalueReg, CallingConventionSlot(slot)), null)
|
||||
ExpressionCodeResult(result, IRDataType.WORD, resultvalueReg, -1)
|
||||
}
|
||||
}
|
||||
@@ -201,8 +202,9 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
addToResult(result, bankTr, bankTr.resultReg, -1)
|
||||
addToResult(result, addressTr, addressTr.resultReg, -1)
|
||||
addToResult(result, argumentwordTr, argumentwordTr.resultReg, -1)
|
||||
result += codeGen.makeSyscall(IMSyscall.CALLFAR, listOf(IRDataType.BYTE to bankTr.resultReg, IRDataType.WORD to addressTr.resultReg, IRDataType.WORD to argumentwordTr.resultReg), IRDataType.WORD to argumentwordTr.resultReg)
|
||||
return ExpressionCodeResult(result, IRDataType.WORD, argumentwordTr.resultReg, -1)
|
||||
val resultReg = codeGen.registers.next(IRDataType.WORD)
|
||||
result += codeGen.makeSyscall(IMSyscall.CALLFAR, listOf(IRDataType.BYTE to bankTr.resultReg, codeGen.addressDt to addressTr.resultReg, IRDataType.WORD to argumentwordTr.resultReg), IRDataType.WORD to resultReg)
|
||||
return ExpressionCodeResult(result, IRDataType.WORD, resultReg, -1)
|
||||
}
|
||||
|
||||
private fun funcCallfar2(call: PtFunctionCall): ExpressionCodeResult {
|
||||
@@ -219,15 +221,20 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
addToResult(result, argumentX, argumentX.resultReg, -1)
|
||||
addToResult(result, argumentY, argumentY.resultReg, -1)
|
||||
addToResult(result, argumentCarry, argumentCarry.resultReg, -1)
|
||||
result += codeGen.makeSyscall(IMSyscall.CALLFAR2, listOf(IRDataType.BYTE to bankTr.resultReg, IRDataType.WORD to addressTr.resultReg,
|
||||
val resultReg = codeGen.registers.next(IRDataType.WORD)
|
||||
result += codeGen.makeSyscall(IMSyscall.CALLFAR2, listOf(IRDataType.BYTE to bankTr.resultReg, codeGen.addressDt to addressTr.resultReg,
|
||||
IRDataType.BYTE to argumentA.resultReg,
|
||||
IRDataType.BYTE to argumentX.resultReg,
|
||||
IRDataType.BYTE to argumentY.resultReg,
|
||||
IRDataType.BYTE to argumentCarry.resultReg), IRDataType.WORD to addressTr.resultReg)
|
||||
return ExpressionCodeResult(result, IRDataType.WORD, addressTr.resultReg, -1)
|
||||
IRDataType.BYTE to argumentCarry.resultReg), IRDataType.WORD to resultReg)
|
||||
return ExpressionCodeResult(result, IRDataType.WORD, resultReg, -1)
|
||||
}
|
||||
|
||||
private fun funcDivmod(call: PtFunctionCall, type: IRDataType, signed: Boolean): ExpressionCodeResult {
|
||||
if(signed && codeGen.options.compTarget.cpu.is6502) {
|
||||
codeGen.errors.err("expected all ubyte or all uword arguments (no signed divmod support on 6502 yet)", call.position)
|
||||
return ExpressionCodeResult(mutableListOf(), type, -1, -1)
|
||||
}
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val number = call.args[0]
|
||||
val divident = call.args[1]
|
||||
@@ -238,33 +245,48 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
if(divident is PtNumber) {
|
||||
val tr = exprGen.translateExpression(number)
|
||||
addToResult(result, tr, tr.resultReg, -1)
|
||||
addInstr(result, IRInstruction(divmodOpcode, type, reg1 = tr.resultReg, immediate = divident.number.toInt()), null)
|
||||
divisionReg = tr.resultReg
|
||||
remainderReg = codeGen.registers.next(type)
|
||||
addInstr(result, IRInstructions.divmodImmediate(divmodOpcode, type, tr.resultReg, remainderReg, divident.number.toInt()), null)
|
||||
divisionReg = tr.resultReg
|
||||
} else {
|
||||
val numTr = exprGen.translateExpression(number)
|
||||
addToResult(result, numTr, numTr.resultReg, -1)
|
||||
val dividentTr = exprGen.translateExpression(divident)
|
||||
addToResult(result, dividentTr, dividentTr.resultReg, -1)
|
||||
addInstr(result, IRInstruction(divmodrOpcode, type, reg1 = numTr.resultReg, reg2=dividentTr.resultReg), null)
|
||||
divisionReg = numTr.resultReg
|
||||
remainderReg = dividentTr.resultReg
|
||||
addInstr(result, IRInstructions.divmodRegister(divmodrOpcode, type, numTr.resultReg, dividentTr.resultReg), null)
|
||||
divisionReg = numTr.resultReg
|
||||
}
|
||||
// DIVMOD result convention: quotient in reg1, remainder in reg2.
|
||||
// Route to the return value locations expected by the caller (via returnsWhatWhere).
|
||||
if(codeGen.options.compTarget.cpu.usesM68kConvention) {
|
||||
// m68k: first return value in D0 (slot 10), second in D1 (slot 11)
|
||||
if(type==IRDataType.BYTE) {
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstructions.hardwareStore(IRDataType.BYTE, remainderReg, CallingConventionSlot(11))
|
||||
it += IRInstructions.hardwareStore(IRDataType.BYTE, divisionReg, CallingConventionSlot(10))
|
||||
}
|
||||
} else if(type==IRDataType.WORD) {
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstructions.hardwareStore(IRDataType.WORD, remainderReg, CallingConventionSlot(11))
|
||||
it += IRInstructions.hardwareStore(IRDataType.WORD, divisionReg, CallingConventionSlot(10))
|
||||
}
|
||||
} else throw AssemblyError("invalid type for DIVMOD")
|
||||
} else {
|
||||
// 6502/cx16: quotient to A (slot 0) or AY (slot 4), remainder to cx16.r15
|
||||
// Note: STOREM must come before STOREHR, otherwise STOREM clobbers A before LOADHR reads it.
|
||||
if(type==IRDataType.BYTE) {
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, remainderReg, IRMemory.direct("cx16.r15"))
|
||||
it += IRInstructions.hardwareStore(IRDataType.BYTE, divisionReg, CallingConventionSlot(0))
|
||||
}
|
||||
} else if(type==IRDataType.WORD) {
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.WORD, remainderReg, IRMemory.direct("cx16.r15"))
|
||||
it += IRInstructions.hardwareStore(IRDataType.WORD, divisionReg, CallingConventionSlot(4))
|
||||
}
|
||||
} else throw AssemblyError("invalid type for DIVMOD")
|
||||
}
|
||||
// DIVMOD instruction result convention: on value stack, division and remainder on top.
|
||||
addInstr(result, IRInstruction(Opcode.POP, type, reg1=remainderReg), null)
|
||||
addInstr(result, IRInstruction(Opcode.POP, type, reg1=divisionReg), null)
|
||||
// return the 2 result values: division and remainder division in AY, remainder in R15
|
||||
if(type==IRDataType.BYTE) {
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.STOREHR, IRDataType.BYTE, reg1=divisionReg, immediate=0)
|
||||
it += IRInstruction(Opcode.STOREM, IRDataType.BYTE, reg1=remainderReg, labelSymbol = "cx16.r15")
|
||||
}
|
||||
} else if(type==IRDataType.WORD) {
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.STOREHR, IRDataType.WORD, reg1=divisionReg, immediate=4)
|
||||
it += IRInstruction(Opcode.STOREM, IRDataType.WORD, reg1=remainderReg, labelSymbol = "cx16.r15")
|
||||
}
|
||||
} else throw AssemblyError("invalid type for DIVMOD")
|
||||
return ExpressionCodeResult(result, type, -1, -1)
|
||||
}
|
||||
|
||||
@@ -275,7 +297,8 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
addToResult(result, left, left.resultReg, -1)
|
||||
addToResult(result, right, right.resultReg, -1)
|
||||
val resultReg = codeGen.registers.next(IRDataType.BYTE)
|
||||
result += codeGen.makeSyscall(IMSyscall.COMPARE_STRINGS, listOf(IRDataType.WORD to left.resultReg, IRDataType.WORD to right.resultReg), IRDataType.BYTE to resultReg)
|
||||
val addressDt = codeGen.addressDt
|
||||
result += codeGen.makeSyscall(IMSyscall.COMPARE_STRINGS, listOf(addressDt to left.resultReg, addressDt to right.resultReg), IRDataType.BYTE to resultReg)
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
|
||||
}
|
||||
|
||||
@@ -285,9 +308,9 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
addToResult(result, leftTr, leftTr.resultReg, -1)
|
||||
val rightTr = exprGen.translateExpression(call.args[1])
|
||||
addToResult(result, rightTr, rightTr.resultReg, -1)
|
||||
val dt = irType(call.args[0].type)
|
||||
val dt = codeGen.irType(call.args[0].type)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.CMP, dt, reg1=leftTr.resultReg, reg2=rightTr.resultReg)
|
||||
it += IRInstructions.compare(dt, leftTr.resultReg, rightTr.resultReg)
|
||||
}
|
||||
return ExpressionCodeResult(result, dt, leftTr.resultReg, -1)
|
||||
}
|
||||
@@ -306,11 +329,11 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val notNegativeLabel = codeGen.createLabelName()
|
||||
val compareReg = codeGen.registers.next(IRDataType.BYTE)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.LOADR, IRDataType.BYTE, reg1=compareReg, reg2=tr.resultReg)
|
||||
it += IRInstructions.move(IRDataType.BYTE, compareReg, tr.resultReg)
|
||||
if(needsExplicitCmpi)
|
||||
it += IRInstruction(Opcode.CMPI, IRDataType.BYTE, reg1=compareReg, immediate = 0)
|
||||
it += IRInstruction(Opcode.BSTPOS, labelSymbol = notNegativeLabel)
|
||||
it += IRInstruction(Opcode.NEG, IRDataType.BYTE, reg1=tr.resultReg)
|
||||
it += IRInstructions.compareImmediate(IRDataType.BYTE, compareReg, 0)
|
||||
it += IRInstructions.branch(Opcode.BSTPOS, codeLabel(notNegativeLabel))
|
||||
it += IRInstructions.unary(Opcode.NEG, IRDataType.BYTE, tr.resultReg)
|
||||
}
|
||||
result += IRCodeChunk(notNegativeLabel, null)
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, tr.resultReg, -1)
|
||||
@@ -319,11 +342,11 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val notNegativeLabel = codeGen.createLabelName()
|
||||
val compareReg = codeGen.registers.next(IRDataType.WORD)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.LOADR, IRDataType.WORD, reg1=compareReg, reg2=tr.resultReg)
|
||||
it += IRInstructions.move(IRDataType.WORD, compareReg, tr.resultReg)
|
||||
if(needsExplicitCmpi)
|
||||
it += IRInstruction(Opcode.CMPI, IRDataType.WORD, reg1=compareReg, immediate = 0)
|
||||
it += IRInstruction(Opcode.BSTPOS, labelSymbol = notNegativeLabel)
|
||||
it += IRInstruction(Opcode.NEG, IRDataType.WORD, reg1=tr.resultReg)
|
||||
it += IRInstructions.compareImmediate(IRDataType.WORD, compareReg, 0)
|
||||
it += IRInstructions.branch(Opcode.BSTPOS, codeLabel(notNegativeLabel))
|
||||
it += IRInstructions.unary(Opcode.NEG, IRDataType.WORD, tr.resultReg)
|
||||
}
|
||||
result += IRCodeChunk(notNegativeLabel, null)
|
||||
return ExpressionCodeResult(result, IRDataType.WORD, tr.resultReg, -1)
|
||||
@@ -332,20 +355,24 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val notNegativeLabel = codeGen.createLabelName()
|
||||
val compareReg = codeGen.registers.next(IRDataType.LONG)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.LOADR, IRDataType.LONG, reg1=compareReg, reg2=tr.resultReg)
|
||||
it += IRInstructions.move(IRDataType.LONG, compareReg, tr.resultReg)
|
||||
if(needsExplicitCmpi)
|
||||
it += IRInstruction(Opcode.CMPI, IRDataType.LONG, reg1=compareReg, immediate = 0)
|
||||
it += IRInstruction(Opcode.BSTPOS, labelSymbol = notNegativeLabel)
|
||||
it += IRInstruction(Opcode.NEG, IRDataType.LONG, reg1=tr.resultReg)
|
||||
it += IRInstructions.compareImmediate(IRDataType.LONG, compareReg, 0)
|
||||
it += IRInstructions.branch(Opcode.BSTPOS, codeLabel(notNegativeLabel))
|
||||
it += IRInstructions.unary(Opcode.NEG, IRDataType.LONG, tr.resultReg)
|
||||
}
|
||||
result += IRCodeChunk(notNegativeLabel, null)
|
||||
return ExpressionCodeResult(result, IRDataType.LONG, tr.resultReg, -1)
|
||||
}
|
||||
BaseDataType.FLOAT -> {
|
||||
val resultFpReg = codeGen.registers.next(IRDataType.FLOAT)
|
||||
addInstr(result, IRInstruction(Opcode.FABS, IRDataType.FLOAT, fpReg1 = RegisterNum(resultFpReg), fpReg2 = RegisterNum(tr.resultFpReg)), null)
|
||||
addInstr(result, IRInstructions.binary(Opcode.FABS, IRDataType.FLOAT, resultFpReg, tr.resultFpReg), null)
|
||||
return ExpressionCodeResult(result, IRDataType.FLOAT, -1, resultFpReg)
|
||||
}
|
||||
BaseDataType.POINTER -> {
|
||||
// pointer is unsigned, abs has no effect
|
||||
return ExpressionCodeResult(result, IRDataType.POINTER, tr.resultReg, -1)
|
||||
}
|
||||
else -> throw AssemblyError("weird dt")
|
||||
}
|
||||
}
|
||||
@@ -358,12 +385,12 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
if(tr.dt==IRDataType.FLOAT) {
|
||||
addToResult(result, tr, -1, tr.resultFpReg)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.SGN, tr.dt, reg1 = resultReg, fpReg1 = RegisterNum(tr.resultFpReg))
|
||||
it += IRInstructions.binary(Opcode.SGN, tr.dt, resultReg, tr.resultFpReg)
|
||||
}
|
||||
} else {
|
||||
addToResult(result, tr, tr.resultReg, -1)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.SGN, tr.dt, reg1 = resultReg, reg2 = tr.resultReg)
|
||||
it += IRInstructions.binary(Opcode.SGN, tr.dt, resultReg, tr.resultReg)
|
||||
}
|
||||
}
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
|
||||
@@ -378,7 +405,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
addToResult(result, tr, tr.resultReg, -1)
|
||||
val resultReg = codeGen.registers.next(IRDataType.BYTE)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.SQRT, IRDataType.BYTE, reg1=resultReg, reg2=tr.resultReg)
|
||||
it += IRInstructions.binary(Opcode.SQRT, IRDataType.BYTE, resultReg, tr.resultReg)
|
||||
}
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
|
||||
}
|
||||
@@ -386,7 +413,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
addToResult(result, tr, tr.resultReg, -1)
|
||||
val resultReg = codeGen.registers.next(IRDataType.BYTE) // sqrt of a word still produces just a byte result
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.SQRT, IRDataType.WORD, reg1=resultReg, reg2=tr.resultReg)
|
||||
it += IRInstructions.binary(Opcode.SQRT, IRDataType.WORD, resultReg, tr.resultReg)
|
||||
}
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
|
||||
}
|
||||
@@ -394,7 +421,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
addToResult(result, tr, tr.resultReg, -1)
|
||||
val resultReg = codeGen.registers.next(IRDataType.WORD) // sqrt of a long still produces just a word result
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.SQRT, IRDataType.LONG, reg1=resultReg, reg2=tr.resultReg)
|
||||
it += IRInstructions.binary(Opcode.SQRT, IRDataType.LONG, resultReg, tr.resultReg)
|
||||
}
|
||||
return ExpressionCodeResult(result, IRDataType.WORD, resultReg, -1)
|
||||
}
|
||||
@@ -402,7 +429,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
addToResult(result, tr, -1, tr.resultFpReg)
|
||||
val resultFpReg = codeGen.registers.next(IRDataType.FLOAT)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.SQRT, IRDataType.FLOAT, fpReg1 = RegisterNum(resultFpReg), fpReg2 = RegisterNum(tr.resultFpReg))
|
||||
it += IRInstructions.binary(Opcode.SQRT, IRDataType.FLOAT, resultFpReg, tr.resultFpReg)
|
||||
}
|
||||
return ExpressionCodeResult(result, IRDataType.FLOAT, -1, resultFpReg)
|
||||
}
|
||||
@@ -417,13 +444,13 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
// msb is 0, use EXT
|
||||
val lsbTr = exprGen.translateExpression(call.args[1])
|
||||
addToResult(result, lsbTr, lsbTr.resultReg, -1)
|
||||
addInstr(result, IRInstruction(Opcode.EXT, IRDataType.BYTE, reg1=resultReg, reg2 = lsbTr.resultReg), null)
|
||||
addInstr(result, IRInstructions.binary(Opcode.EXT, IRDataType.BYTE, resultReg, lsbTr.resultReg), null)
|
||||
} else {
|
||||
val msbTr = exprGen.translateExpression(call.args[0])
|
||||
addToResult(result, msbTr, msbTr.resultReg, -1)
|
||||
val lsbTr = exprGen.translateExpression(call.args[1])
|
||||
addToResult(result, lsbTr, lsbTr.resultReg, -1)
|
||||
addInstr(result, IRInstruction(Opcode.CONCAT, IRDataType.BYTE, reg1=resultReg, reg2 = msbTr.resultReg, reg3 = lsbTr.resultReg), null)
|
||||
addInstr(result, IRInstructions.concat(IRDataType.BYTE, resultReg, msbTr.resultReg, lsbTr.resultReg), null)
|
||||
}
|
||||
return ExpressionCodeResult(result, IRDataType.WORD, resultReg, -1)
|
||||
}
|
||||
@@ -437,23 +464,21 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
// msw is 0, use EXT
|
||||
val lswTr = exprGen.translateExpression(call.args[1])
|
||||
addToResult(result, lswTr, lswTr.resultReg, -1)
|
||||
addInstr(result, IRInstruction(Opcode.EXT, IRDataType.WORD, reg1=resultReg, reg2 = lswTr.resultReg), null)
|
||||
addInstr(result, IRInstructions.binary(Opcode.EXT, IRDataType.WORD, resultReg, lswTr.resultReg), null)
|
||||
} else {
|
||||
val mswTr = exprGen.translateExpression(call.args[0])
|
||||
addToResult(result, mswTr, mswTr.resultReg, -1)
|
||||
val lswTr = exprGen.translateExpression(call.args[1])
|
||||
addToResult(result, lswTr, lswTr.resultReg, -1)
|
||||
addInstr(result, IRInstruction(Opcode.CONCAT, IRDataType.WORD, reg1=resultReg, reg2 = mswTr.resultReg, reg3 = lswTr.resultReg), null)
|
||||
addInstr(result, IRInstructions.concat(IRDataType.WORD, resultReg, mswTr.resultReg, lswTr.resultReg), null)
|
||||
}
|
||||
} else {
|
||||
// mklong(msb, b3, b2, lsb)
|
||||
if((call.args[0] as? PtNumber)?.number == 0.0 && (call.args[1] as? PtNumber)?.number == 0.0 && (call.args[2] as? PtNumber)?.number == 0.0) {
|
||||
// use EXT.b + EXT.w
|
||||
// use EXTL.b: zero-extend byte directly to long
|
||||
val lsbTr = exprGen.translateExpression(call.args[3])
|
||||
addToResult(result, lsbTr, lsbTr.resultReg, -1)
|
||||
val wordReg = codeGen.registers.next(IRDataType.WORD)
|
||||
addInstr(result, IRInstruction(Opcode.EXT, IRDataType.BYTE, reg1=wordReg, reg2 = lsbTr.resultReg), null)
|
||||
addInstr(result, IRInstruction(Opcode.EXT, IRDataType.WORD, reg1=resultReg, reg2 = wordReg), null)
|
||||
addInstr(result, IRInstructions.binary(Opcode.EXTL, IRDataType.BYTE, resultReg, lsbTr.resultReg), null)
|
||||
} else {
|
||||
val msbTr = exprGen.translateExpression(call.args[0])
|
||||
val b2Tr = exprGen.translateExpression(call.args[1])
|
||||
@@ -465,9 +490,9 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
addToResult(result, lsbTr, lsbTr.resultReg, -1)
|
||||
val lswReg = codeGen.registers.next(IRDataType.WORD)
|
||||
val mswReg = codeGen.registers.next(IRDataType.WORD)
|
||||
addInstr(result, IRInstruction(Opcode.CONCAT, IRDataType.BYTE, reg1=mswReg, reg2 = msbTr.resultReg, reg3 = b2Tr.resultReg), null)
|
||||
addInstr(result, IRInstruction(Opcode.CONCAT, IRDataType.BYTE, reg1=lswReg, reg2 = b1Tr.resultReg, reg3 = lsbTr.resultReg), null)
|
||||
addInstr(result, IRInstruction(Opcode.CONCAT, IRDataType.WORD, reg1=resultReg, reg2 = mswReg, reg3 = lswReg), null)
|
||||
addInstr(result, IRInstructions.concat(IRDataType.BYTE, mswReg, msbTr.resultReg, b2Tr.resultReg), null)
|
||||
addInstr(result, IRInstructions.concat(IRDataType.BYTE, lswReg, b1Tr.resultReg, lsbTr.resultReg), null)
|
||||
addInstr(result, IRInstructions.concat(IRDataType.WORD, resultReg, mswReg, lswReg), null)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -475,7 +500,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
}
|
||||
|
||||
private fun funcClamp(call: PtFunctionCall): ExpressionCodeResult {
|
||||
val type = irType(call.type)
|
||||
val type = codeGen.irType(call.type)
|
||||
require(type != IRDataType.FLOAT)
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val valueTr = exprGen.translateExpression(call.args[0])
|
||||
@@ -492,17 +517,18 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
BaseDataType.LONG -> IMSyscall.CLAMP_LONG
|
||||
else -> throw AssemblyError("invalid dt")
|
||||
}
|
||||
val resultReg = codeGen.registers.next(type)
|
||||
result += codeGen.makeSyscall(syscall, listOf(
|
||||
valueTr.dt to valueTr.resultReg,
|
||||
minimumTr.dt to minimumTr.resultReg,
|
||||
maximumTr.dt to maximumTr.resultReg,
|
||||
), type to valueTr.resultReg
|
||||
), type to resultReg
|
||||
)
|
||||
return ExpressionCodeResult(result, type, valueTr.resultReg, -1)
|
||||
return ExpressionCodeResult(result, type, resultReg, -1)
|
||||
}
|
||||
|
||||
private fun funcMin(call: PtFunctionCall): ExpressionCodeResult {
|
||||
val type = irType(call.type)
|
||||
val type = codeGen.irType(call.type)
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val leftTr = exprGen.translateExpression(call.args[0])
|
||||
addToResult(result, leftTr, leftTr.resultReg, -1)
|
||||
@@ -511,17 +537,17 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val comparisonOpcode = if(call.type.isSigned) Opcode.BGTSR else Opcode.BGTR
|
||||
val after = codeGen.createLabelName()
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(comparisonOpcode, type, reg1 = rightTr.resultReg, reg2 = leftTr.resultReg, labelSymbol = after)
|
||||
it += IRInstructions.branchRegister(comparisonOpcode, type, rightTr.resultReg, leftTr.resultReg, codeLabel(after))
|
||||
// right <= left, take right
|
||||
it += IRInstruction(Opcode.LOADR, type, reg1=leftTr.resultReg, reg2=rightTr.resultReg)
|
||||
it += IRInstruction(Opcode.JUMP, labelSymbol = after)
|
||||
it += IRInstructions.move(type, leftTr.resultReg, rightTr.resultReg)
|
||||
it += IRInstructions.jump(codeLabel(after))
|
||||
}
|
||||
result += IRCodeChunk(after, null)
|
||||
return ExpressionCodeResult(result, type, leftTr.resultReg, -1)
|
||||
}
|
||||
|
||||
private fun funcMax(call: PtFunctionCall): ExpressionCodeResult {
|
||||
val type = irType(call.type)
|
||||
val type = codeGen.irType(call.type)
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val leftTr = exprGen.translateExpression(call.args[0])
|
||||
addToResult(result, leftTr, leftTr.resultReg, -1)
|
||||
@@ -530,33 +556,35 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val comparisonOpcode = if(call.type.isSigned) Opcode.BGTSR else Opcode.BGTR
|
||||
val after = codeGen.createLabelName()
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(comparisonOpcode, type, reg1 = leftTr.resultReg, reg2 = rightTr.resultReg, labelSymbol = after)
|
||||
it += IRInstructions.branchRegister(comparisonOpcode, type, leftTr.resultReg, rightTr.resultReg, codeLabel(after))
|
||||
// right >= left, take right
|
||||
it += IRInstruction(Opcode.LOADR, type, reg1=leftTr.resultReg, reg2=rightTr.resultReg)
|
||||
it += IRInstruction(Opcode.JUMP, labelSymbol = after)
|
||||
it += IRInstructions.move(type, leftTr.resultReg, rightTr.resultReg)
|
||||
it += IRInstructions.jump(codeLabel(after))
|
||||
}
|
||||
result += IRCodeChunk(after, null)
|
||||
return ExpressionCodeResult(result, type, leftTr.resultReg, -1)
|
||||
}
|
||||
|
||||
private fun funcPoke(call: PtFunctionCall, dt: IRDataType): ExpressionCodeResult {
|
||||
// Try struct-array field folding: pokew(&arr[idx]+fieldOff, value) -> STOREX arr+fieldOff,S=structSize
|
||||
tryFoldStructArrayPoke(call, dt)?.let { return it }
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
if(codeGen.isZero(call.args[1])) {
|
||||
if (call.args[0] is PtNumber) {
|
||||
val address = (call.args[0] as PtNumber).number.toUInt()
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.STOREZM, dt, address = address.toAddress())
|
||||
it += IRInstructions.storeZero(Opcode.STOREZM, dt, IRMemory.direct(address.toAddress()))
|
||||
}
|
||||
} else {
|
||||
val (address, offset) = exprGen.getAddressAndOffset(call.args[0])
|
||||
if(address!=null) {
|
||||
val tr = exprGen.translateExpression(address)
|
||||
addToResult(result, tr, tr.resultReg, -1)
|
||||
addInstr(result, IRInstruction(Opcode.STOREZI, dt, reg1 = tr.resultReg, immediate = offset!!), null)
|
||||
addInstr(result, IRInstructions.storeZero(Opcode.STOREZI, dt, IRMemory.indirect(tr.resultReg, offset!!)), null)
|
||||
} else {
|
||||
val tr = exprGen.translateExpression(call.args[0])
|
||||
addToResult(result, tr, tr.resultReg, -1)
|
||||
addInstr(result, IRInstruction(Opcode.STOREZI, dt, reg1 = tr.resultReg, immediate = 0), null)
|
||||
addInstr(result, IRInstructions.storeZero(Opcode.STOREZI, dt, IRMemory.indirect(tr.resultReg)), null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -565,9 +593,9 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val address = (call.args[0] as PtNumber).number.toUInt()
|
||||
addToResult(result, valueTr, valueTr.resultReg, valueTr.resultFpReg)
|
||||
if(dt==IRDataType.FLOAT) {
|
||||
addInstr(result, IRInstruction(Opcode.STOREM, dt, fpReg1 = RegisterNum(valueTr.resultFpReg), address = address.toAddress()), null)
|
||||
addInstr(result, IRInstructions.storeMemory(Opcode.STOREM, dt, valueTr.resultFpReg, IRMemory.direct(address.toAddress())), null)
|
||||
} else {
|
||||
addInstr(result, IRInstruction(Opcode.STOREM, dt, reg1 = valueTr.resultReg, address = address.toAddress()), null)
|
||||
addInstr(result, IRInstructions.storeMemory(Opcode.STOREM, dt, valueTr.resultReg, IRMemory.direct(address.toAddress())), null)
|
||||
}
|
||||
} else {
|
||||
val (address, offset) = exprGen.getAddressAndOffset(call.args[0])
|
||||
@@ -576,18 +604,18 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
addToResult(result, valueTr, valueTr.resultReg, valueTr.resultFpReg)
|
||||
addToResult(result, addressTr, addressTr.resultReg, -1)
|
||||
if(dt==IRDataType.FLOAT) {
|
||||
addInstr(result, IRInstruction(Opcode.STOREI, IRDataType.FLOAT, reg1 = addressTr.resultReg, fpReg1 = RegisterNum(valueTr.resultFpReg), immediate = offset!!), null)
|
||||
addInstr(result, IRInstructions.storeMemory(Opcode.STOREI, IRDataType.FLOAT, valueTr.resultFpReg, IRMemory.indirect(addressTr.resultReg, offset!!)), null)
|
||||
} else {
|
||||
addInstr(result, IRInstruction(Opcode.STOREI, dt, reg1 = valueTr.resultReg, reg2 = addressTr.resultReg, immediate = offset!!), null)
|
||||
addInstr(result, IRInstructions.storeMemory(Opcode.STOREI, dt, valueTr.resultReg, IRMemory.indirect(addressTr.resultReg, offset!!)), null)
|
||||
}
|
||||
} else {
|
||||
val addressTr = exprGen.translateExpression(call.args[0])
|
||||
addToResult(result, valueTr, valueTr.resultReg, valueTr.resultFpReg)
|
||||
addToResult(result, addressTr, addressTr.resultReg, -1)
|
||||
if(dt==IRDataType.FLOAT) {
|
||||
addInstr(result, IRInstruction(Opcode.STOREI, IRDataType.FLOAT, reg1 = addressTr.resultReg, fpReg1 = RegisterNum(valueTr.resultFpReg), immediate = 0), null)
|
||||
addInstr(result, IRInstructions.storeMemory(Opcode.STOREI, IRDataType.FLOAT, valueTr.resultFpReg, IRMemory.indirect(addressTr.resultReg)), null)
|
||||
} else {
|
||||
addInstr(result, IRInstruction(Opcode.STOREI, dt, reg1 = valueTr.resultReg, reg2 = addressTr.resultReg, immediate = 0), null)
|
||||
addInstr(result, IRInstructions.storeMemory(Opcode.STOREI, dt, valueTr.resultReg, IRMemory.indirect(addressTr.resultReg)), null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -601,26 +629,26 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
if(call.args[0] is PtNumber) {
|
||||
val resultFpRegister = codeGen.registers.next(IRDataType.FLOAT)
|
||||
val address = (call.args[0] as PtNumber).number.toUInt()
|
||||
addInstr(result, IRInstruction(Opcode.LOADM, IRDataType.FLOAT, fpReg1 = RegisterNum(resultFpRegister), address = address.toAddress()), null)
|
||||
addInstr(result, IRInstructions.loadMemory(Opcode.LOADM, IRDataType.FLOAT, resultFpRegister, IRMemory.direct(address.toAddress())), null)
|
||||
ExpressionCodeResult(result, IRDataType.FLOAT, -1, resultFpRegister)
|
||||
} else {
|
||||
val tr = exprGen.translateExpression(call.args.single())
|
||||
addToResult(result, tr, tr.resultReg, -1)
|
||||
val resultFpReg = codeGen.registers.next(IRDataType.FLOAT)
|
||||
addInstr(result, IRInstruction(Opcode.LOADI, IRDataType.FLOAT, reg1 = tr.resultReg, fpReg1 = RegisterNum(resultFpReg), immediate = 0), null)
|
||||
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, IRDataType.FLOAT, resultFpReg, IRMemory.indirect(tr.resultReg)), null)
|
||||
ExpressionCodeResult(result, IRDataType.FLOAT, -1, resultFpReg)
|
||||
}
|
||||
} else {
|
||||
if (call.args[0] is PtNumber) {
|
||||
val resultRegister = codeGen.registers.next(dt)
|
||||
val address = (call.args[0] as PtNumber).number.toUInt()
|
||||
addInstr(result, IRInstruction(Opcode.LOADM, dt, reg1 = resultRegister, address = address.toAddress()), null)
|
||||
addInstr(result, IRInstructions.loadMemory(Opcode.LOADM, dt, resultRegister, IRMemory.direct(address.toAddress())), null)
|
||||
ExpressionCodeResult(result, dt, resultRegister, -1)
|
||||
} else {
|
||||
val tr = exprGen.translateExpression(call.args.single())
|
||||
addToResult(result, tr, tr.resultReg, -1)
|
||||
val resultReg = codeGen.registers.next(dt)
|
||||
addInstr(result, IRInstruction(Opcode.LOADI, dt, reg1 = resultReg, reg2 = tr.resultReg, immediate = 0), null)
|
||||
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, dt, resultReg, IRMemory.indirect(tr.resultReg)), null)
|
||||
ExpressionCodeResult(result, dt, resultReg, -1)
|
||||
}
|
||||
}
|
||||
@@ -637,11 +665,11 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val addressNum = address.number.toUInt()
|
||||
val resultRegister = codeGen.registers.next(IRDataType.BYTE)
|
||||
if (newValueTr != null) addToResult(result, newValueTr, newValueTr.resultReg, -1)
|
||||
addInstr(result, IRInstruction(Opcode.LOADM, IRDataType.BYTE, reg1 = resultRegister, address = addressNum.toAddress()), null)
|
||||
addInstr(result, IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, resultRegister, IRMemory.direct(addressNum.toAddress())), null)
|
||||
if (newValueTr == null) {
|
||||
addInstr(result, IRInstruction(Opcode.STOREZM, IRDataType.BYTE, address = addressNum.toAddress()), null)
|
||||
addInstr(result, IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.direct(addressNum.toAddress())), null)
|
||||
} else {
|
||||
addInstr(result, IRInstruction(Opcode.STOREM, IRDataType.BYTE, reg1 = newValueTr.resultReg, address = addressNum.toAddress()), null)
|
||||
addInstr(result, IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, newValueTr.resultReg, IRMemory.direct(addressNum.toAddress())), null)
|
||||
}
|
||||
ExpressionCodeResult(result, IRDataType.BYTE, resultRegister, -1)
|
||||
} else {
|
||||
@@ -654,11 +682,11 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
if (newValueTr != null) addToResult(result, newValueTr, newValueTr.resultReg, -1)
|
||||
addToResult(result, addressTr, addressTr.resultReg, -1)
|
||||
|
||||
addInstr(result, IRInstruction(Opcode.LOADI, IRDataType.BYTE, reg1 = resultReg, reg2 = addressTr.resultReg, immediate = finalOffset), null)
|
||||
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, IRDataType.BYTE, resultReg, IRMemory.indirect(addressTr.resultReg, finalOffset)), null)
|
||||
if (newValueTr == null) {
|
||||
addInstr(result, IRInstruction(Opcode.STOREZI, IRDataType.BYTE, reg1 = addressTr.resultReg, immediate = finalOffset), null)
|
||||
addInstr(result, IRInstructions.storeZero(Opcode.STOREZI, IRDataType.BYTE, IRMemory.indirect(addressTr.resultReg, finalOffset)), null)
|
||||
} else {
|
||||
addInstr(result, IRInstruction(Opcode.STOREI, IRDataType.BYTE, reg1 = newValueTr.resultReg, reg2 = addressTr.resultReg, immediate = finalOffset), null)
|
||||
addInstr(result, IRInstructions.storeMemory(Opcode.STOREI, IRDataType.BYTE, newValueTr.resultReg, IRMemory.indirect(addressTr.resultReg, finalOffset)), null)
|
||||
}
|
||||
ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
|
||||
}
|
||||
@@ -667,10 +695,10 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
|
||||
private fun funcStructAlloc(call: PtFunctionCall): ExpressionCodeResult {
|
||||
val code = IRCodeChunk(null, null)
|
||||
val resultReg = codeGen.registers.next(IRDataType.WORD)
|
||||
val resultReg = codeGen.registers.next(IRDataType.POINTER)
|
||||
val labelname = SymbolTable.labelnameForStructInstance(call)
|
||||
code += IRInstruction(Opcode.LOAD, IRDataType.WORD, reg1=resultReg, labelSymbol = "${StStructInstanceBlockName}.$labelname")
|
||||
return ExpressionCodeResult(code, IRDataType.WORD, resultReg, -1)
|
||||
code += IRInstructions.loadAddress(IRDataType.POINTER, resultReg, "${StStructInstanceBlockName}.$labelname")
|
||||
return ExpressionCodeResult(code, IRDataType.POINTER, resultReg, -1)
|
||||
}
|
||||
|
||||
private fun funcLsb(call: PtFunctionCall, fromLong: Boolean): ExpressionCodeResult {
|
||||
@@ -679,9 +707,9 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
addToResult(result, tr, tr.resultReg, -1)
|
||||
val resultReg = codeGen.registers.next(IRDataType.BYTE)
|
||||
if(fromLong)
|
||||
addInstr(result, IRInstruction(Opcode.LSIGB, IRDataType.LONG, reg1 = resultReg, reg2 = tr.resultReg), null)
|
||||
addInstr(result, IRInstructions.binary(Opcode.LSIGB, IRDataType.LONG, resultReg, tr.resultReg), null)
|
||||
else
|
||||
addInstr(result, IRInstruction(Opcode.LSIGB, IRDataType.WORD, reg1 = resultReg, reg2 = tr.resultReg), null)
|
||||
addInstr(result, IRInstructions.binary(Opcode.LSIGB, IRDataType.WORD, resultReg, tr.resultReg), null)
|
||||
// note: if a word result is needed, the upper byte is cleared by the typecast that follows. No need to do it here.
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
|
||||
}
|
||||
@@ -691,14 +719,25 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
// TODO this can be more optimal if the argument is a variable or memory address
|
||||
val tr = exprGen.translateExpression(call.args.single())
|
||||
addToResult(result, tr, tr.resultReg, -1)
|
||||
// low byte returned in A, mid in R15, high (bank) in R14
|
||||
val byteReg = codeGen.registers.next(IRDataType.BYTE)
|
||||
addInstr(result, IRInstruction(Opcode.BSIGB, IRDataType.LONG, reg1 = byteReg, reg2 = tr.resultReg), null)
|
||||
addInstr(result, IRInstruction(Opcode.STOREM, IRDataType.BYTE, reg1 = byteReg, labelSymbol = "cx16.r14"), null)
|
||||
addInstr(result, IRInstruction(Opcode.MIDB, IRDataType.LONG, reg1 = byteReg, reg2 = tr.resultReg), null)
|
||||
addInstr(result, IRInstruction(Opcode.STOREM, IRDataType.BYTE, reg1 = byteReg, labelSymbol = "cx16.r15"), null)
|
||||
addInstr(result, IRInstruction(Opcode.LSIGB, IRDataType.LONG, reg1 = byteReg, reg2 = tr.resultReg), null)
|
||||
addInstr(result, IRInstruction(Opcode.STOREHR, IRDataType.BYTE, reg1 = byteReg, immediate = 0), null)
|
||||
if(codeGen.options.compTarget.cpu.usesM68kConvention) {
|
||||
// m68k: 3 return values in D0 (slot 10), D1 (slot 11), D2 (slot 12)
|
||||
// store high byte first, then mid, then low (to avoid clobbering)
|
||||
addInstr(result, IRInstructions.binary(Opcode.BSIGB, IRDataType.LONG, byteReg, tr.resultReg), null)
|
||||
addInstr(result, IRInstructions.hardwareStore(IRDataType.BYTE, byteReg, CallingConventionSlot(12)), null)
|
||||
addInstr(result, IRInstructions.binary(Opcode.MIDB, IRDataType.LONG, byteReg, tr.resultReg), null)
|
||||
addInstr(result, IRInstructions.hardwareStore(IRDataType.BYTE, byteReg, CallingConventionSlot(11)), null)
|
||||
addInstr(result, IRInstructions.binary(Opcode.LSIGB, IRDataType.LONG, byteReg, tr.resultReg), null)
|
||||
addInstr(result, IRInstructions.hardwareStore(IRDataType.BYTE, byteReg, CallingConventionSlot(10)), null)
|
||||
} else {
|
||||
// 6502/cx16: low byte in A (slot 0), mid in cx16.r15, high (bank) in cx16.r14
|
||||
addInstr(result, IRInstructions.binary(Opcode.BSIGB, IRDataType.LONG, byteReg, tr.resultReg), null)
|
||||
addInstr(result, IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, byteReg, IRMemory.direct("cx16.r14")), null)
|
||||
addInstr(result, IRInstructions.binary(Opcode.MIDB, IRDataType.LONG, byteReg, tr.resultReg), null)
|
||||
addInstr(result, IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, byteReg, IRMemory.direct("cx16.r15")), null)
|
||||
addInstr(result, IRInstructions.binary(Opcode.LSIGB, IRDataType.LONG, byteReg, tr.resultReg), null)
|
||||
addInstr(result, IRInstructions.hardwareStore(IRDataType.BYTE, byteReg, CallingConventionSlot(0)), null)
|
||||
}
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
|
||||
}
|
||||
|
||||
@@ -707,7 +746,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val tr = exprGen.translateExpression(call.args.single())
|
||||
addToResult(result, tr, tr.resultReg, -1)
|
||||
val resultReg = codeGen.registers.next(IRDataType.WORD)
|
||||
addInstr(result, IRInstruction(Opcode.LSIGW, IRDataType.LONG, reg1 = resultReg, reg2 = tr.resultReg), null)
|
||||
addInstr(result, IRInstructions.binary(Opcode.LSIGW, IRDataType.LONG, resultReg, tr.resultReg), null)
|
||||
return ExpressionCodeResult(result, IRDataType.WORD, resultReg, -1)
|
||||
}
|
||||
|
||||
@@ -717,9 +756,9 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
addToResult(result, tr, tr.resultReg, -1)
|
||||
val resultReg = codeGen.registers.next(IRDataType.BYTE)
|
||||
if(fromLong)
|
||||
addInstr(result, IRInstruction(Opcode.MSIGB, IRDataType.LONG, reg1 = resultReg, reg2 = tr.resultReg), null)
|
||||
addInstr(result, IRInstructions.binary(Opcode.MSIGB, IRDataType.LONG, resultReg, tr.resultReg), null)
|
||||
else
|
||||
addInstr(result, IRInstruction(Opcode.MSIGB, IRDataType.WORD, reg1 = resultReg, reg2 = tr.resultReg), null)
|
||||
addInstr(result, IRInstructions.binary(Opcode.MSIGB, IRDataType.WORD, resultReg, tr.resultReg), null)
|
||||
// note: if a word result is needed, the upper byte is cleared by the typecast that follows. No need to do it here.
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
|
||||
}
|
||||
@@ -729,14 +768,14 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val tr = exprGen.translateExpression(call.args.single())
|
||||
addToResult(result, tr, tr.resultReg, -1)
|
||||
val resultReg = codeGen.registers.next(IRDataType.WORD)
|
||||
addInstr(result, IRInstruction(Opcode.MSIGW, IRDataType.LONG, reg1 = resultReg, reg2 = tr.resultReg), null)
|
||||
addInstr(result, IRInstructions.binary(Opcode.MSIGW, IRDataType.LONG, resultReg, tr.resultReg), null)
|
||||
return ExpressionCodeResult(result, IRDataType.WORD, resultReg, -1)
|
||||
}
|
||||
|
||||
private fun funcRolRor(call: PtFunctionCall): ExpressionCodeResult {
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val arg = call.args[0]
|
||||
val vmDt = irType(arg.type)
|
||||
val vmDt = codeGen.irType(arg.type)
|
||||
val opcodeMemAndReg = when(call.name) {
|
||||
"rol" -> Opcode.ROXLM to Opcode.ROXL
|
||||
"ror" -> Opcode.ROXRM to Opcode.ROXR
|
||||
@@ -747,13 +786,13 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
|
||||
val ident = arg as? PtIdentifier
|
||||
if(ident!=null) {
|
||||
addInstr(result, IRInstruction(opcodeMemAndReg.first, vmDt, labelSymbol = ident.name), null)
|
||||
addInstr(result, IRInstructions.memoryOp(opcodeMemAndReg.first, vmDt, IRMemory.direct(ident.name)), null)
|
||||
return ExpressionCodeResult(result, vmDt, -1, -1)
|
||||
}
|
||||
|
||||
val memAddr: UInt? = (arg as? PtMemoryByte)?.address?.asConstInteger()?.toUInt()
|
||||
if(memAddr!=null) {
|
||||
addInstr(result, IRInstruction(opcodeMemAndReg.first, vmDt, address = memAddr.toAddress()), null)
|
||||
addInstr(result, IRInstructions.memoryOp(opcodeMemAndReg.first, vmDt, IRMemory.direct(memAddr.toAddress())), null)
|
||||
return ExpressionCodeResult(result, vmDt, -1, -1)
|
||||
}
|
||||
|
||||
@@ -765,19 +804,19 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
result += IRCodeChunk(null, null).also {
|
||||
when(opcodeMemAndReg.first) {
|
||||
Opcode.ROXRM, Opcode.RORM -> {
|
||||
it += IRInstruction(opcodeMemAndReg.first, IRDataType.BYTE, labelSymbol = "${variable}_msb", symbolOffset = index)
|
||||
it += IRInstruction(opcodeMemAndReg.first, IRDataType.BYTE, labelSymbol = "${variable}_lsb", symbolOffset = index)
|
||||
it += IRInstructions.memoryOp(opcodeMemAndReg.first, IRDataType.BYTE, IRMemory.direct("${variable}_msb", index))
|
||||
it += IRInstructions.memoryOp(opcodeMemAndReg.first, IRDataType.BYTE, IRMemory.direct("${variable}_lsb", index))
|
||||
}
|
||||
Opcode.ROXLM, Opcode.ROLM -> {
|
||||
it += IRInstruction(opcodeMemAndReg.first, IRDataType.BYTE, labelSymbol = "${variable}_lsb", symbolOffset = index)
|
||||
it += IRInstruction(opcodeMemAndReg.first, IRDataType.BYTE, labelSymbol = "${variable}_msb", symbolOffset = index)
|
||||
it += IRInstructions.memoryOp(opcodeMemAndReg.first, IRDataType.BYTE, IRMemory.direct("${variable}_lsb", index))
|
||||
it += IRInstructions.memoryOp(opcodeMemAndReg.first, IRDataType.BYTE, IRMemory.direct("${variable}_msb", index))
|
||||
}
|
||||
else -> throw AssemblyError("wrong rol/ror opcode")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val offset = codeGen.program.memsizer.memorySize(arr.type, index)
|
||||
addInstr(result, IRInstruction(opcodeMemAndReg.first, vmDt, labelSymbol = variable, symbolOffset = offset), null)
|
||||
addInstr(result, IRInstructions.memoryOp(opcodeMemAndReg.first, vmDt, IRMemory.direct(variable, offset)), null)
|
||||
}
|
||||
return ExpressionCodeResult(result, vmDt, -1, -1)
|
||||
}
|
||||
@@ -785,17 +824,17 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val opcode = opcodeMemAndReg.second
|
||||
val saveCarry = opcode in OpcodesThatDependOnCarry && !arg.isSimple()
|
||||
if(saveCarry)
|
||||
addInstr(result, IRInstruction(Opcode.PUSHST), null) // save Carry
|
||||
addInstr(result, IRInstructions.simple(Opcode.PUSHST), null) // save Carry
|
||||
val tr = exprGen.translateExpression(arg)
|
||||
addToResult(result, tr, tr.resultReg, -1)
|
||||
if(saveCarry)
|
||||
addInstr(result, IRInstruction(Opcode.POPST), null)
|
||||
addInstr(result, IRInstruction(opcode, vmDt, reg1 = tr.resultReg), null)
|
||||
addInstr(result, IRInstructions.simple(Opcode.POPST), null)
|
||||
addInstr(result, IRInstructions.unary(opcode, vmDt, tr.resultReg), null)
|
||||
if(saveCarry)
|
||||
addInstr(result, IRInstruction(Opcode.PUSHST), null) // save Carry
|
||||
addInstr(result, IRInstructions.simple(Opcode.PUSHST), null) // save Carry
|
||||
result += codeGen.assignRegisterTo(arg, tr.resultReg)
|
||||
if(saveCarry)
|
||||
addInstr(result, IRInstruction(Opcode.POPST), null)
|
||||
addInstr(result, IRInstructions.simple(Opcode.POPST), null)
|
||||
return ExpressionCodeResult(result, vmDt, -1, -1)
|
||||
}
|
||||
|
||||
@@ -803,22 +842,24 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val target = call.args[0]
|
||||
val isConstZeroValue = call.args[1].asConstInteger()==0
|
||||
val isBigEndian = codeGen.options.compTarget.cpu.isBigEndian
|
||||
fun byteOffset(elementSize: Int) = if(msb != isBigEndian) elementSize - 1 else 0
|
||||
when(target) {
|
||||
is PtIdentifier -> {
|
||||
val offset = if (msb) { if(target.type.isLong) 3 else 1 } else 0
|
||||
val offset = byteOffset(if(target.type.isLong) 4 else 2)
|
||||
if(isConstZeroValue) {
|
||||
result += IRCodeChunk(null, null).also {
|
||||
val pointerReg = codeGen.registers.next(IRDataType.WORD)
|
||||
it += IRInstruction(Opcode.LOAD, IRDataType.WORD, reg1 = pointerReg, labelSymbol = target.name)
|
||||
it += IRInstruction(Opcode.STOREZI, IRDataType.BYTE, reg1 = pointerReg, immediate = offset)
|
||||
val pointerReg = codeGen.registers.next(IRDataType.POINTER)
|
||||
it += IRInstructions.loadAddress(IRDataType.POINTER, pointerReg, target.name)
|
||||
it += IRInstructions.storeZero(Opcode.STOREZI, IRDataType.BYTE, IRMemory.indirect(pointerReg, offset))
|
||||
}
|
||||
} else {
|
||||
val valueTr = exprGen.translateExpression(call.args[1])
|
||||
addToResult(result, valueTr, valueTr.resultReg, -1)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
val pointerReg = codeGen.registers.next(IRDataType.WORD)
|
||||
it += IRInstruction(Opcode.LOAD, IRDataType.WORD, reg1 = pointerReg, labelSymbol = target.name)
|
||||
it += IRInstruction(Opcode.STOREI, IRDataType.BYTE, reg1 = valueTr.resultReg, reg2 = pointerReg, immediate = offset)
|
||||
val pointerReg = codeGen.registers.next(IRDataType.POINTER)
|
||||
it += IRInstructions.loadAddress(IRDataType.POINTER, pointerReg, target.name)
|
||||
it += IRInstructions.storeMemory(Opcode.STOREI, IRDataType.BYTE, valueTr.resultReg, IRMemory.indirect(pointerReg, offset))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -829,23 +870,26 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val pointerTr = exprGen.translateExpression(target.pointerderef!!)
|
||||
addToResult(result, pointerTr, pointerTr.resultReg, -1)
|
||||
val constIndex = target.index.asConstInteger()
|
||||
val elementByteOffset = byteOffset(eltSize)
|
||||
if(constIndex != null) {
|
||||
val offset = eltSize * constIndex + if(msb) eltSize - 1 else 0
|
||||
val offset = eltSize * constIndex + elementByteOffset
|
||||
if(offset > 0)
|
||||
addInstr(result, IRInstruction(Opcode.ADD, IRDataType.WORD, reg1 = pointerTr.resultReg, immediate = offset), null)
|
||||
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerTr.resultReg, offset), null)
|
||||
} else {
|
||||
val (code, indexWordReg) = codeGen.loadIndexReg(target.index, eltSize, true, false)
|
||||
result += code
|
||||
addInstr(result, IRInstruction(Opcode.ADDR, IRDataType.WORD, reg1 = pointerTr.resultReg, reg2 = indexWordReg), null)
|
||||
if(msb)
|
||||
addInstr(result, IRInstruction(Opcode.ADD, IRDataType.WORD, reg1 = pointerTr.resultReg, immediate = eltSize - 1), null)
|
||||
if(eltSize!=1)
|
||||
result += codeGen.multiplyByConst(DataType.UWORD, indexWordReg, eltSize)
|
||||
addInstr(result, IRInstructions.binary(Opcode.ADDR, IRDataType.POINTER, pointerTr.resultReg, indexWordReg), null)
|
||||
if(elementByteOffset > 0)
|
||||
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerTr.resultReg, elementByteOffset), null)
|
||||
}
|
||||
if(isConstZeroValue) {
|
||||
addInstr(result, IRInstruction(Opcode.STOREZI, IRDataType.BYTE, reg1 = pointerTr.resultReg, immediate = 0), null)
|
||||
addInstr(result, IRInstructions.storeZero(Opcode.STOREZI, IRDataType.BYTE, IRMemory.indirect(pointerTr.resultReg)), null)
|
||||
} else {
|
||||
val valueTr = exprGen.translateExpression(call.args[1])
|
||||
addToResult(result, valueTr, valueTr.resultReg, -1)
|
||||
addInstr(result, IRInstruction(Opcode.STOREI, IRDataType.BYTE, reg1 = valueTr.resultReg, reg2 = pointerTr.resultReg, immediate = 0), null)
|
||||
addInstr(result, IRInstructions.storeMemory(Opcode.STOREI, IRDataType.BYTE, valueTr.resultReg, IRMemory.indirect(pointerTr.resultReg)), null)
|
||||
}
|
||||
}
|
||||
else if(target.splitWords) {
|
||||
@@ -854,32 +898,36 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val varName = target.variable!!.name + if(msb) "_msb" else "_lsb"
|
||||
if(isConstZeroValue) {
|
||||
if(constIndex!=null) {
|
||||
val offsetReg = codeGen.registers.next(IRDataType.BYTE)
|
||||
val idxType = codeGen.options.compTarget.indexRegType
|
||||
val offsetReg = codeGen.registers.next(idxType)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=offsetReg, immediate = constIndex)
|
||||
it += IRInstruction(Opcode.STOREZX, IRDataType.BYTE, reg1=offsetReg, labelSymbol = varName)
|
||||
it += IRInstructions.load(idxType, offsetReg, constIndex)
|
||||
it += IRInstructions.storeZero(Opcode.STOREZX, IRDataType.BYTE, IRMemory.indexed(varName, offsetReg, codeGen.options.compTarget.indexRegType))
|
||||
}
|
||||
} else {
|
||||
val indexTr = exprGen.translateExpression(target.index)
|
||||
addToResult(result, indexTr, indexTr.resultReg, -1)
|
||||
val indexReg = codeGen.canonicalizeIndexReg(result, indexTr)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.STOREZX, IRDataType.BYTE, reg1=indexTr.resultReg, labelSymbol = varName)
|
||||
it += IRInstructions.storeZero(Opcode.STOREZX, IRDataType.BYTE, IRMemory.indexed(varName, indexReg, codeGen.options.compTarget.indexRegType))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val valueTr = exprGen.translateExpression(call.args[1])
|
||||
addToResult(result, valueTr, valueTr.resultReg, -1)
|
||||
if(constIndex!=null) {
|
||||
val offsetReg = codeGen.registers.next(IRDataType.BYTE)
|
||||
val idxType = codeGen.options.compTarget.indexRegType
|
||||
val offsetReg = codeGen.registers.next(idxType)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=offsetReg, immediate = constIndex)
|
||||
it += IRInstruction(Opcode.STOREX, IRDataType.BYTE, reg1=valueTr.resultReg, reg2=offsetReg, labelSymbol = varName)
|
||||
it += IRInstructions.load(idxType, offsetReg, constIndex)
|
||||
it += IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, valueTr.resultReg, IRMemory.indexed(varName, offsetReg, codeGen.options.compTarget.indexRegType))
|
||||
}
|
||||
} else {
|
||||
val indexTr = exprGen.translateExpression(target.index)
|
||||
addToResult(result, indexTr, indexTr.resultReg, -1)
|
||||
val indexReg = codeGen.canonicalizeIndexReg(result, indexTr)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.STOREX, IRDataType.BYTE, reg1=valueTr.resultReg, reg2=indexTr.resultReg, labelSymbol = varName)
|
||||
it += IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, valueTr.resultReg, IRMemory.indexed(varName, indexReg, codeGen.options.compTarget.indexRegType))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -891,50 +939,36 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val constIndex = target.index.asConstInteger()
|
||||
if(isConstZeroValue) {
|
||||
if(constIndex!=null) {
|
||||
val offsetReg = codeGen.registers.next(IRDataType.BYTE)
|
||||
val offset = eltSize*constIndex + if(msb) eltSize-1 else 0
|
||||
val offsetReg = codeGen.registers.next(if(codeGen.wordArrayIndex) IRDataType.WORD else IRDataType.BYTE)
|
||||
val offset = eltSize*constIndex + byteOffset(eltSize)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=offsetReg, immediate = offset)
|
||||
it += IRInstruction(Opcode.STOREZX, IRDataType.BYTE, reg1=offsetReg, labelSymbol = targetVariable.name)
|
||||
it += IRInstructions.load(if(codeGen.wordArrayIndex) IRDataType.WORD else IRDataType.BYTE, offsetReg, offset)
|
||||
it += IRInstructions.storeZero(Opcode.STOREZX, IRDataType.BYTE, IRMemory.indexed(targetVariable.name, offsetReg, codeGen.options.compTarget.indexRegType))
|
||||
}
|
||||
} else {
|
||||
val indexTr = exprGen.translateExpression(target.index)
|
||||
addToResult(result, indexTr, indexTr.resultReg, -1)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(target.index, eltSize, codeGen.wordArrayIndex, false)
|
||||
result += code
|
||||
result += IRCodeChunk(null, null).also {
|
||||
if(eltSize>1)
|
||||
it += codeGen.multiplyByConst(DataType.UBYTE, indexTr.resultReg, eltSize)
|
||||
if(msb) {
|
||||
it += if(target.type.isLong)
|
||||
IRInstruction(Opcode.ADD, IRDataType.BYTE, reg1 = indexTr.resultReg, immediate = 3)
|
||||
else
|
||||
IRInstruction(Opcode.INC, IRDataType.BYTE, reg1 = indexTr.resultReg)
|
||||
}
|
||||
it += IRInstruction(Opcode.STOREZX, IRDataType.BYTE, reg1=indexTr.resultReg, labelSymbol = targetVariable.name)
|
||||
val offset = byteOffset(eltSize)
|
||||
it += IRInstructions.storeZero(Opcode.STOREZX, IRDataType.BYTE, IRMemory.indexed(targetVariable.name, indexReg, codeGen.options.compTarget.indexRegType, scale = eltSize, displacement = offset))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val valueTr = exprGen.translateExpression(call.args[1])
|
||||
addToResult(result, valueTr, valueTr.resultReg, -1)
|
||||
if(constIndex!=null) {
|
||||
val offsetReg = codeGen.registers.next(IRDataType.BYTE)
|
||||
val offset = eltSize*constIndex + if(msb) eltSize-1 else 0
|
||||
val offsetReg = codeGen.registers.next(if(codeGen.wordArrayIndex) IRDataType.WORD else IRDataType.BYTE)
|
||||
val offset = eltSize*constIndex + byteOffset(eltSize)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=offsetReg, immediate = offset)
|
||||
it += IRInstruction(Opcode.STOREX, IRDataType.BYTE, reg1=valueTr.resultReg, reg2=offsetReg, labelSymbol = targetVariable.name)
|
||||
it += IRInstructions.load(if(codeGen.wordArrayIndex) IRDataType.WORD else IRDataType.BYTE, offsetReg, offset)
|
||||
it += IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, valueTr.resultReg, IRMemory.indexed(targetVariable.name, offsetReg, codeGen.options.compTarget.indexRegType))
|
||||
}
|
||||
} else {
|
||||
val indexTr = exprGen.translateExpression(target.index)
|
||||
addToResult(result, indexTr, indexTr.resultReg, -1)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(target.index, eltSize, codeGen.wordArrayIndex, false)
|
||||
result += code
|
||||
result += IRCodeChunk(null, null).also {
|
||||
if(eltSize>1)
|
||||
it += codeGen.multiplyByConst(DataType.UBYTE, indexTr.resultReg, eltSize)
|
||||
if(msb) {
|
||||
it += if(target.type.isLong)
|
||||
IRInstruction(Opcode.ADD, IRDataType.BYTE, reg1 = indexTr.resultReg, immediate = 3)
|
||||
else
|
||||
IRInstruction(Opcode.INC, IRDataType.BYTE, reg1 = indexTr.resultReg)
|
||||
}
|
||||
it += IRInstruction(Opcode.STOREX, IRDataType.BYTE, reg1=valueTr.resultReg, reg2=indexTr.resultReg, labelSymbol = targetVariable.name)
|
||||
val offset = byteOffset(eltSize)
|
||||
it += IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, valueTr.resultReg, IRMemory.indexed(targetVariable.name, indexReg, codeGen.options.compTarget.indexRegType, scale = eltSize, displacement = offset))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -944,4 +978,31 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
}
|
||||
return ExpressionCodeResult(result, IRDataType.WORD, -1, -1)
|
||||
}
|
||||
|
||||
private fun tryFoldStructArrayPoke(call: PtFunctionCall, dt: IRDataType): ExpressionCodeResult? {
|
||||
// Detect pokew(&arr[idx]+fieldOff, value) where arr is struct array with variable idx
|
||||
// and fold to single STOREX/STOREZX with scale=structSize.
|
||||
val info = extractStructArrayIndexInfo(call.args[0], codeGen) ?: return null
|
||||
val isZero = codeGen.isZero(call.args[1])
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val (idxCode, indexReg) = codeGen.loadIndexReg(info.idxExpr, info.structSize, codeGen.wordArrayIndex, false)
|
||||
result += idxCode
|
||||
return if(isZero) {
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstructions.storeZero(Opcode.STOREZX, dt, IRMemory.indexed(info.arrayName, indexReg, codeGen.options.compTarget.indexRegType, scale=info.structSize, displacement=info.fieldOffset))
|
||||
}
|
||||
ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
|
||||
} else {
|
||||
val valueTr = exprGen.translateExpression(call.args[1])
|
||||
addToResult(result, valueTr, valueTr.resultReg, valueTr.resultFpReg)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
if(dt==IRDataType.FLOAT) {
|
||||
it += IRInstructions.storeMemory(Opcode.STOREX, dt, valueTr.resultFpReg, IRMemory.indexed(info.arrayName, indexReg, codeGen.options.compTarget.indexRegType, scale=info.structSize, displacement=info.fieldOffset))
|
||||
} else {
|
||||
it += IRInstructions.storeMemory(Opcode.STOREX, dt, valueTr.resultReg, IRMemory.indexed(info.arrayName, indexReg, codeGen.options.compTarget.indexRegType, scale=info.structSize, displacement=info.fieldOffset))
|
||||
}
|
||||
}
|
||||
ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,36 @@
|
||||
package prog8.codegen.intermediate
|
||||
|
||||
import prog8.intermediate.*
|
||||
|
||||
/*
|
||||
Construction helpers for the few IR opcodes that have no dedicated IRInstructions factory,
|
||||
plus a small operand-inspection helper used by the optimizers.
|
||||
|
||||
The structured IRInstructions factory covers almost every opcode, but three opcodes have an
|
||||
operand shape that no named factory produces (a lone source register, or a source register plus
|
||||
an immediate without a destination). They are built here from schema-derived operands, so this
|
||||
module never constructs IRInstruction directly.
|
||||
*/
|
||||
|
||||
/** STOREHFACZERO / STOREHFACONE: a single source float register (no dedicated factory exists). */
|
||||
internal fun storeHwFac(opcode: Opcode, fpRegister: Int): IRInstruction =
|
||||
IRInstructions.push(IRDataType.FLOAT, fpRegister).copy(opcode = opcode)
|
||||
|
||||
/** BITTST: a source register plus a byte immediate bit number, with no destination register. */
|
||||
internal fun bitTest(type: IRDataType, register: Int, bit: Int): IRInstruction =
|
||||
IRInstructions.simple(Opcode.NOP).copy(
|
||||
opcode = Opcode.BITTST,
|
||||
type = type,
|
||||
srcA = IRInstructions.operandFor(Opcode.BITTST, type, InstructionSlot.SRC_A, register),
|
||||
immediate = IRInstructions.immediateFor(Opcode.BITTST, type, bit)
|
||||
)
|
||||
|
||||
/**
|
||||
* The name of the single symbol this instruction refers to, if any.
|
||||
* That is either the symbol a memory reference is based on, the symbol whose address is taken
|
||||
* in an immediate operand, or the label a branch/jump/call transfers control to.
|
||||
*/
|
||||
internal fun IRInstruction.referencedSymbol(): String? =
|
||||
memory?.symbolName
|
||||
?: (immediate as? ImmediateOperand.SymbolAddress)?.symbol
|
||||
?: labelTarget
|
||||
File diff suppressed because it is too large.
Load diff
@@ -11,9 +11,13 @@ class IRUnusedCodeRemover(
|
||||
fun optimize(): Int {
|
||||
var numRemoved = removeUnusedSubroutines() + removeUnusedAsmSubroutines()
|
||||
|
||||
// remove empty blocks
|
||||
// remove empty blocks (but keep blocks with %option force_output,
|
||||
// and also blocks that contain labels -- labels are addressable symbols,
|
||||
// and blocks that still have variables in the symbol table -- a block with variables is not truly empty)
|
||||
irprog.blocks.reversed().forEach { block ->
|
||||
if(block.isEmpty()) {
|
||||
val hasLabels = block.children.any { it.label != null }
|
||||
val hasVariables = irprog.st.allVariables().any { it.name.startsWith(block.label + ".") }
|
||||
if(!hasLabels && !hasVariables && block.isEmpty() && !block.options.forceOutput) {
|
||||
irprog.blocks.remove(block)
|
||||
pruneSymboltable(block.label)
|
||||
numRemoved++
|
||||
@@ -33,7 +37,7 @@ class IRUnusedCodeRemover(
|
||||
blockVars.forEach { stVar ->
|
||||
irprog.allSubs().flatMap { it.chunks }.forEach { chunk ->
|
||||
chunk.instructions.forEach { ins ->
|
||||
if(ins.labelSymbol == stVar.name) {
|
||||
if(ins.referencedSymbol() == stVar.name) {
|
||||
return // symbol occurs in an instruction
|
||||
}
|
||||
}
|
||||
@@ -70,41 +74,26 @@ class IRUnusedCodeRemover(
|
||||
private fun removeBlockInits(code: IRProgram, blockLabel: String) {
|
||||
val instructions = code.globalInits.instructions
|
||||
instructions.toTypedArray().forEach {ins ->
|
||||
if(ins.labelSymbol?.startsWith(blockLabel)==true) {
|
||||
if(ins.referencedSymbol()?.startsWith(blockLabel)==true) {
|
||||
instructions.remove(ins)
|
||||
}
|
||||
}
|
||||
|
||||
// remove stray loads
|
||||
val readRegs = mutableSetOf<Int>()
|
||||
val readFpRegs = mutableSetOf<Int>()
|
||||
val readRegs = mutableSetOf<VirtualRegister>()
|
||||
instructions.forEach { ins ->
|
||||
val readRegsCounts = mutableMapOf<RegisterNum, Int>()
|
||||
val readFpRegsCounts = mutableMapOf<RegisterNum, Int>()
|
||||
val writeRegsCounts = mutableMapOf<RegisterNum, Int>()
|
||||
val writeFpRegsCounts = mutableMapOf<RegisterNum, Int>()
|
||||
val regsTypes = mutableMapOf<RegisterNum, IRDataType>()
|
||||
ins.addUsedRegistersCounts(readRegsCounts, writeRegsCounts, readFpRegsCounts, writeFpRegsCounts, regsTypes, null)
|
||||
readRegs.addAll(readRegsCounts.keys.map { it.value })
|
||||
readFpRegs.addAll(readFpRegsCounts.keys.map { it.value })
|
||||
val readRegsCounts = mutableMapOf<VirtualRegister, Int>()
|
||||
val writeRegsCounts = mutableMapOf<VirtualRegister, Int>()
|
||||
val regsTypes = mutableMapOf<VirtualRegister, IRDataType>()
|
||||
ins.addUsedRegistersCounts(readRegsCounts, writeRegsCounts, regsTypes, null)
|
||||
readRegs.addAll(readRegsCounts.keys)
|
||||
}
|
||||
instructions.toTypedArray().forEach { ins ->
|
||||
if(ins.opcode in arrayOf(Opcode.LOAD, Opcode.LOADR, Opcode.LOADM)) {
|
||||
val reg1 = ins.reg1
|
||||
val fpReg1 = ins.fpReg1
|
||||
if(reg1!=null) {
|
||||
if(reg1 !in readRegs) {
|
||||
if(ins.labelSymbol!=null)
|
||||
code.st.removeIfExists(ins.labelSymbol!!)
|
||||
instructions.remove(ins)
|
||||
}
|
||||
}
|
||||
else if(fpReg1!=null) {
|
||||
if(fpReg1.value !in readFpRegs) {
|
||||
if(ins.labelSymbol!=null)
|
||||
code.st.removeIfExists(ins.labelSymbol!!)
|
||||
instructions.remove(ins)
|
||||
}
|
||||
val dest = ins.dest
|
||||
if(dest != null && dest.register !in readRegs) {
|
||||
ins.referencedSymbol()?.let { code.st.removeIfExists(it) }
|
||||
instructions.remove(ins)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,8 +176,8 @@ class IRUnusedCodeRemover(
|
||||
// check if asmsub is linked or called from another regular subroutine
|
||||
irprog.foreachCodeChunk { chunk ->
|
||||
chunk.instructions.forEach {
|
||||
it.labelSymbol?.let { label -> allSubs[label]?.let { cc -> linkedAsmSubs += cc } }
|
||||
// note: branchTarget can't yet point to another IRAsmSubroutine, so do nothing when it's set
|
||||
it.referencedSymbol()?.let { label -> allSubs[label]?.let { cc -> linkedAsmSubs += cc } }
|
||||
// note: codeTarget can't yet point to another IRAsmSubroutine, so do nothing when it's set
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,17 +225,26 @@ class IRUnusedCodeRemover(
|
||||
val new = mutableSetOf<IRCodeChunkBase>()
|
||||
reachable.forEach {
|
||||
it.next?.let { next -> new += next }
|
||||
if (it is IRLoopChunk) {
|
||||
// A reachable counted loop makes every body chunk reachable too.
|
||||
// The body is no longer represented by next pointers in the outer list.
|
||||
new += it.body
|
||||
}
|
||||
it.instructions.forEach { instr ->
|
||||
if (instr.branchTarget == null)
|
||||
instr.labelSymbol?.let { label ->
|
||||
val codeTarget = instr.codeTarget
|
||||
if (codeTarget == null) {
|
||||
instr.referencedSymbol()?.let { label ->
|
||||
val chunk = allLabeledChunks[label] ?: allLabeledChunks[label.substringBeforeLast('.')]
|
||||
if(chunk!=null)
|
||||
new+=chunk
|
||||
else
|
||||
allLabeledChunks[label]?.let { c -> new += c }
|
||||
}
|
||||
else
|
||||
new += instr.branchTarget!!
|
||||
}
|
||||
else {
|
||||
val chunk = irprog.resolveCodeTarget(codeTarget)
|
||||
?: codeTarget.labelName?.let { label -> allLabeledChunks[label] }
|
||||
if(chunk!=null)
|
||||
new += chunk
|
||||
}
|
||||
}
|
||||
}
|
||||
reachable += new
|
||||
@@ -282,10 +280,14 @@ class IRUnusedCodeRemover(
|
||||
irprog.foreachCodeChunk { chunk ->
|
||||
chunk.next?.let { next -> linkedChunks += next }
|
||||
chunk.instructions.forEach {
|
||||
if(it.branchTarget==null) {
|
||||
it.labelSymbol?.let { label -> allLabeledChunks[label]?.let { cc -> linkedChunks += cc } }
|
||||
val codeTarget = it.codeTarget
|
||||
if(codeTarget==null) {
|
||||
it.referencedSymbol()?.let { label -> allLabeledChunks[label]?.let { cc -> linkedChunks += cc } }
|
||||
} else {
|
||||
linkedChunks += it.branchTarget!!
|
||||
val target = irprog.resolveCodeTarget(codeTarget)
|
||||
?: codeTarget.labelName?.let { label -> allLabeledChunks[label] }
|
||||
if(target!=null)
|
||||
linkedChunks += target
|
||||
}
|
||||
}
|
||||
if (chunk.label in entrypointNames)
|
||||
@@ -295,8 +297,8 @@ class IRUnusedCodeRemover(
|
||||
// make sure that chunks that are only used as a prefix of a label, are also marked as linked
|
||||
linkedChunks.toList().forEach { chunk ->
|
||||
chunk.instructions.forEach {
|
||||
if(it.labelSymbol!=null) {
|
||||
val chunkName = it.labelSymbol!!.substringBeforeLast('.')
|
||||
it.referencedSymbol()?.let { symbol ->
|
||||
val chunkName = symbol.substringBeforeLast('.')
|
||||
allLabeledChunks[chunkName]?.let { c -> linkedChunks += c }
|
||||
}
|
||||
}
|
||||
@@ -312,7 +314,7 @@ class IRUnusedCodeRemover(
|
||||
// don't remove stuff in library modules or with %forceoutput
|
||||
// TODO this is needed to keep the subroutine body of sqrt_long from being wiped !??! Sounds like the bad solution to the problem: why is the *body* being cleared , making an empty sqrt_long subrotuine end up in the p8ir output (and eventually in the .asm, which crashes the resulting program)
|
||||
// TODO this causes other stuff to end up in the output IR that is not used......
|
||||
// note: this wasn't a problem on the virtual target, but it is a problem when using the expericodegen target (because on the virtual target, the VM simply executes the SQRT instruction directly without needing a library routine)
|
||||
// note: this wasn't a problem on the virtual target, but it is a problem when using the newcodegen targets (because on the virtual target, the VM simply executes the SQRT instruction directly without needing a library routine)
|
||||
val block = irprog.blocks.first { b -> b.children.any { it === sub } }
|
||||
if (block.library || block.options.forceOutput)
|
||||
return@foreachSub
|
||||
@@ -328,6 +330,9 @@ class IRUnusedCodeRemover(
|
||||
numRemoved++
|
||||
}
|
||||
}
|
||||
is IRLoopChunk -> {
|
||||
// keep loop at subroutine entry; don't remove
|
||||
}
|
||||
is IRInlineAsmChunk, is IRInlineBinaryChunk -> {
|
||||
sub.chunks[index] = IRCodeChunk(chunk.label, chunk.next)
|
||||
numRemoved++
|
||||
|
||||
@@ -0,0 +1,466 @@
|
||||
package prog8.codegen.intermediate
|
||||
|
||||
import prog8.intermediate.*
|
||||
|
||||
/*
|
||||
* ============================================================================
|
||||
* PROTOTYPE - NOT FOR PRODUCTION USE
|
||||
* ============================================================================
|
||||
*
|
||||
* This is a PROTOTYPE memory-slot packer and the intended reusable foundation
|
||||
* for a future hardware-register allocator targeting the m68k backend. It is
|
||||
* NOT a complete or production-ready register allocator in its current form.
|
||||
* It is DISABLED in production: its only call site in IRCodeGen.generate() is
|
||||
* commented out, and it currently runs only under TestRegisterPacker.
|
||||
*
|
||||
* What this prototype demonstrates:
|
||||
* - A working graph-coloring register allocator skeleton: CFG construction,
|
||||
* intraprocedural liveness analysis (gen/kill fixed-point), live interval
|
||||
* derivation, conflict-graph construction, greedy coloring, and IR rewrite.
|
||||
* - Coalescing of virtual registers into shared memory slots in the flat
|
||||
* `p8_regfile` BSS block (vregs with non-overlapping live ranges share a
|
||||
* slot).
|
||||
*
|
||||
* What this prototype DOES NOT do (and why it is not production-ready):
|
||||
* - No hardware-register allocation: slots are memory addresses in a flat
|
||||
* BSS regfile, not physical D/A/FP registers.
|
||||
* - No CALL clobbering: a callee packed into the same slot as a caller's
|
||||
* live value silently overwrites it (see bug 3 below).
|
||||
* - No register classes: no distinction between DATA, ADDRESS, and FPU
|
||||
* registers (required by the m68k backend).
|
||||
* - No spilling, prologue/epilogue emission, or CALL-aware interference.
|
||||
* - Known-unsound liveness for complex control flow (nested loops,
|
||||
* conditionals, early returns) - see bug 2 below.
|
||||
*
|
||||
* Intended reuse path (per ideas/m68k-register-allocation.md):
|
||||
* - Stage 1: Make the m68k backend's instruction selection location-agnostic
|
||||
* (operand() indirection returning "d3" if allocated, "p8_regfile+N" if
|
||||
* spilled) - prerequisite bulk work, no behavior change.
|
||||
* - Stage 2: Replace this packer's interference model with a correct one
|
||||
* (CALL-aware, class-constrained) and point the coloring step at real
|
||||
* D2-D6 / A2-A4 / FP2-FP7 registers. Six open design decisions remain
|
||||
* in Stage 2 (see design doc section 7.1).
|
||||
* - Stage 3: Delete subsumed peephole optimizations, update test assertions.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Known bugs / unsoundness (carried over from the removed register-packing.md):
|
||||
*
|
||||
* 1. Disjoint-interval value clobbering (observed on cx16 Fibonacci).
|
||||
* A register with multiple DISJOINT live ranges (e.g. a loop counter written
|
||||
* at loop entry and read at loop exit, with no uses in between) was split
|
||||
* into separate intervals; the packer allowed another register to share the
|
||||
* same slot during the gap, overwriting the value before its next read.
|
||||
* Fix applied (see packSubroutine interval merging): always merge ALL
|
||||
* intervals of the same register into one contiguous range, regardless of
|
||||
* overlap, so the value persists across gaps.
|
||||
*
|
||||
* 2. Complex control flow (observed on m68k TextElite).
|
||||
* The iterative gen/kill fixed-point liveness (computeLiveness) produced
|
||||
* incorrect live ranges for subroutines with complex CFGs (nested
|
||||
* conditionals, loops, early returns). Two registers with genuinely
|
||||
* overlapping live ranges were assigned the same slot, so one clobbered the
|
||||
* other at runtime (symptom: infinite loop printing spaces — galaxy map
|
||||
* data read from the wrong memory location). Root cause: the dataflow does
|
||||
* not converge to the true meet-over-all-paths solution for all loop /
|
||||
* conditional structures. The liveness MUST be validated against nested
|
||||
* loops, conditionals, early returns, and switch-like dispatch before reuse.
|
||||
*
|
||||
* 3. Cross-subroutine slot collision (fundamental design issue; see header
|
||||
* comment above). Per-subroutine packing into ONE flat shared regfile is
|
||||
* unsound: a callee packed into the same slot as a caller's live value
|
||||
* clobbers it. The packer does not model CALL clobbering. Soundness would
|
||||
* require the full-program call tree (call-graph-aware allocation, depth
|
||||
* ranges, or save/restore around calls) — which is exactly what the m68k
|
||||
* allocator with a calling convention avoids by allocating per-subroutine.
|
||||
*
|
||||
* 4. Interval-merge type loss / silent skips. When merging a register's
|
||||
* intervals, the merged interval keeps only the FIRST interval's type; if a
|
||||
* later interval has an incompatible type, the register is dropped via
|
||||
* skipRegs (and its original number is preserved via the startSlot =
|
||||
* maxReg + 1 invariant). When reusing this code, merged intervals must carry
|
||||
* the WIDEST type, not just the first, to avoid under-sized slots.
|
||||
*/
|
||||
|
||||
object RegisterPacker {
|
||||
|
||||
data class Interval(val register: VirtualRegister, val start: Int, val end: Int, val type: IRDataType)
|
||||
|
||||
fun pack(irProg: IRProgram) {
|
||||
val allRegTypes = mutableMapOf<VirtualRegister, IRDataType>()
|
||||
irProg.forEachInstruction { instr ->
|
||||
for (access in instr.registerAccesses)
|
||||
allRegTypes.putIfAbsent(access.register, access.type)
|
||||
}
|
||||
val beforeCount = allRegTypes.size
|
||||
|
||||
// Start packing slots after the highest original register number to avoid
|
||||
// collisions between packed slot numbers and original (non-packed) register numbers.
|
||||
val maxReg = allRegTypes.keys.maxOfOrNull { it.num } ?: 0
|
||||
val startSlot = maxReg + 1
|
||||
val globalSlotTypes = mutableMapOf<VirtualRegister, IRDataType>()
|
||||
irProg.foreachSub { sub -> packSubroutine(irProg, sub, allRegTypes, globalSlotTypes, startSlot) }
|
||||
|
||||
val afterTypes = rebuildTypeMap(irProg)
|
||||
val afterCount = afterTypes.size
|
||||
if (!irProg.options.quiet)
|
||||
println("Register packing: $beforeCount -> $afterCount registers")
|
||||
}
|
||||
|
||||
// Rebuild the register type map after packing, using the same type determination as the packer.
|
||||
// This avoids the strict per-instruction validation in usedRegisters() that POINTER↔LONG↔WORD
|
||||
// cross-type packing can trigger.
|
||||
fun rebuildTypeMap(irProg: IRProgram): Map<VirtualRegister, IRDataType> {
|
||||
val newTypes = mutableMapOf<VirtualRegister, IRDataType>()
|
||||
irProg.forEachInstruction { instr ->
|
||||
for (access in instr.registerAccesses)
|
||||
newTypes.putIfAbsent(access.register, access.type)
|
||||
}
|
||||
return newTypes
|
||||
}
|
||||
|
||||
private fun packSubroutine(irProg: IRProgram, sub: IRSubroutine, allRegTypes: Map<VirtualRegister, IRDataType>, globalSlotTypes: MutableMap<VirtualRegister, IRDataType>, startSlot: Int) {
|
||||
if (sub.chunks.isEmpty())
|
||||
return
|
||||
|
||||
val successors = buildCFG(sub, irProg)
|
||||
val (_, liveOut) = computeLiveness(sub, successors)
|
||||
|
||||
val codeChunks = sub.chunks.filterIsInstance<IRCodeChunk>()
|
||||
if (codeChunks.isEmpty())
|
||||
return
|
||||
|
||||
val chunkRanges = mutableMapOf<IRCodeChunk, IntRange>()
|
||||
var globalIdx = 0
|
||||
for (chunk in codeChunks) {
|
||||
val start = globalIdx
|
||||
val end = globalIdx + chunk.instructions.size - 1
|
||||
chunkRanges[chunk] = start..end
|
||||
globalIdx += chunk.instructions.size
|
||||
}
|
||||
|
||||
val registerIntervals = mutableMapOf<VirtualRegister, MutableList<Interval>>()
|
||||
val registerTypes = mutableMapOf<VirtualRegister, IRDataType>()
|
||||
|
||||
for (chunk in codeChunks) {
|
||||
val range = chunkRanges[chunk]!!
|
||||
val chunkStart = range.first
|
||||
if (chunk.instructions.isEmpty())
|
||||
continue
|
||||
|
||||
val liveSet = liveOut[chunk]?.toMutableSet() ?: mutableSetOf()
|
||||
val lastUse = mutableMapOf<VirtualRegister, Int>()
|
||||
|
||||
// Scan backward through instructions
|
||||
for (i in chunk.instructions.indices.reversed()) {
|
||||
val instr = chunk.instructions[i]
|
||||
val globalI = chunkStart + i
|
||||
val accesses = instr.registerAccesses
|
||||
|
||||
// Process READ (USE / USE_DEF) first so types are set before WRITE processing
|
||||
for (access in accesses) {
|
||||
if (access.direction != OperandDirection.DEF) {
|
||||
val r = access.register
|
||||
registerTypes.putIfAbsent(r, access.type)
|
||||
if (r !in liveSet) {
|
||||
lastUse[r] = globalI
|
||||
liveSet.add(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process WRITTEN (DEF / USE_DEF) registers: end their live range (start of interval going forward)
|
||||
for (access in accesses) {
|
||||
if (access.direction != OperandDirection.USE) {
|
||||
val r = access.register
|
||||
if (r in liveSet) {
|
||||
val end = lastUse.getOrElse(r) { globalI }
|
||||
registerIntervals.getOrPut(r) { mutableListOf() }
|
||||
.add(Interval(r, globalI, end, access.type))
|
||||
liveSet.remove(r)
|
||||
lastUse.remove(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Registers still live at start of chunk are live-in
|
||||
for (r in liveSet) {
|
||||
val end = lastUse.getOrElse(r) { chunkStart }
|
||||
registerIntervals.getOrPut(r) { mutableListOf() }
|
||||
.add(Interval(r, chunkStart, end, registerTypes.getOrElse(r) { IRDataType.BYTE }))
|
||||
}
|
||||
}
|
||||
|
||||
if (registerIntervals.isEmpty())
|
||||
return
|
||||
|
||||
// Merge adjacent/overlapping intervals for each register.
|
||||
// Also merge non-overlapping intervals of the same register into a single
|
||||
// contiguous range, because the value in the slot must persist across the
|
||||
// gap to the next use (otherwise another register packed to the same slot
|
||||
// would clobber it between intervals).
|
||||
val mergedIntervals = mutableListOf<Interval>()
|
||||
val skipRegs = mutableSetOf<VirtualRegister>()
|
||||
for ((reg, intervals) in registerIntervals) {
|
||||
val sorted = intervals.sortedBy { it.start }
|
||||
var current = sorted.first()
|
||||
for (next in sorted.drop(1)) {
|
||||
if (current.type != next.type && !typesCompatible(current.type, next.type)) {
|
||||
skipRegs.add(reg)
|
||||
}
|
||||
// Always merge: same register's intervals are merged into a single
|
||||
// contiguous range to preserve the value across gaps.
|
||||
current = Interval(reg, current.start, maxOf(current.end, next.end), current.type)
|
||||
}
|
||||
mergedIntervals.add(current)
|
||||
}
|
||||
if (skipRegs.isNotEmpty()) {
|
||||
mergedIntervals.removeAll { it.register in skipRegs }
|
||||
}
|
||||
|
||||
val conflictGraph = buildConflictGraph(sub)
|
||||
val packing = greedyColor(mergedIntervals, conflictGraph, allRegTypes, globalSlotTypes, startSlot)
|
||||
|
||||
if (packing.isNotEmpty())
|
||||
rewrite(sub, packing)
|
||||
}
|
||||
|
||||
private fun buildConflictGraph(sub: IRSubroutine): Map<VirtualRegister, Set<VirtualRegister>> {
|
||||
val conflicts = mutableMapOf<VirtualRegister, MutableSet<VirtualRegister>>()
|
||||
for (chunk in sub.chunks.filterIsInstance<IRCodeChunk>()) {
|
||||
val typeRegs = mutableMapOf<IRDataType, MutableSet<VirtualRegister>>()
|
||||
for (instr in chunk.instructions) {
|
||||
val accesses = instr.registerAccesses
|
||||
for (access in accesses)
|
||||
typeRegs.getOrPut(access.type) { mutableSetOf() }.add(access.register)
|
||||
|
||||
// Registers accessed together within a single instruction must not share a slot
|
||||
// (generalizes the old reg1/reg2-only conflict rule to every register access of
|
||||
// the instruction, now that they are all enumerable via registerAccesses).
|
||||
val regsInInstr = accesses.map { it.register }.distinct()
|
||||
for (i in regsInInstr.indices) {
|
||||
for (j in i + 1 until regsInInstr.size) {
|
||||
val r1 = regsInInstr[i]
|
||||
val r2 = regsInInstr[j]
|
||||
conflicts.getOrPut(r1) { mutableSetOf() }.add(r2)
|
||||
conflicts.getOrPut(r2) { mutableSetOf() }.add(r1)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add conflicts between registers with incompatible types in the same chunk
|
||||
val typeList = typeRegs.entries.toList()
|
||||
for (i in typeList.indices) {
|
||||
for (j in i + 1 until typeList.size) {
|
||||
val (t1, regs1) = typeList[i]
|
||||
val (t2, regs2) = typeList[j]
|
||||
if (t1 != t2 && !typesCompatible(t1, t2)) {
|
||||
for (r1 in regs1) {
|
||||
for (r2 in regs2) {
|
||||
if (r1 != r2) {
|
||||
conflicts.getOrPut(r1) { mutableSetOf() }.add(r2)
|
||||
conflicts.getOrPut(r2) { mutableSetOf() }.add(r1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return conflicts
|
||||
}
|
||||
|
||||
private fun buildCFG(sub: IRSubroutine, irProg: IRProgram): Map<IRCodeChunkBase, List<IRCodeChunkBase>> {
|
||||
val successors = mutableMapOf<IRCodeChunkBase, MutableList<IRCodeChunkBase>>()
|
||||
|
||||
val conditionals = setOf(
|
||||
Opcode.BSTCC, Opcode.BSTCS, Opcode.BSTEQ, Opcode.BSTNE,
|
||||
Opcode.BSTNEG, Opcode.BSTPOS, Opcode.BSTVC, Opcode.BSTVS,
|
||||
Opcode.BGTR, Opcode.BGT, Opcode.BLT,
|
||||
Opcode.BGTSR, Opcode.BGTS, Opcode.BLTS,
|
||||
Opcode.BGER, Opcode.BGE, Opcode.BLE,
|
||||
Opcode.BGESR, Opcode.BGES, Opcode.BLES
|
||||
)
|
||||
|
||||
for (chunk in sub.chunks) {
|
||||
val succ = mutableListOf<IRCodeChunkBase>()
|
||||
|
||||
if (chunk is IRCodeChunk) {
|
||||
val lastInstr = chunk.instructions.lastOrNull()
|
||||
val endsWithUnconditional = lastInstr != null && lastInstr.opcode in OpcodesThatBranchUnconditionally
|
||||
val endsWithConditional = lastInstr != null && lastInstr.opcode in conditionals
|
||||
|
||||
if (!endsWithUnconditional) {
|
||||
chunk.next?.let { succ.add(it) }
|
||||
}
|
||||
if (endsWithConditional) {
|
||||
lastInstr.codeTarget?.let { irProg.resolveCodeTarget(it) }?.let { target ->
|
||||
if (!succ.contains(target))
|
||||
succ.add(target)
|
||||
}
|
||||
} else if (lastInstr?.opcode == Opcode.JUMP) {
|
||||
succ.clear()
|
||||
lastInstr.codeTarget?.let { irProg.resolveCodeTarget(it) }?.let { succ.add(it) }
|
||||
}
|
||||
} else {
|
||||
chunk.next?.let { succ.add(it) }
|
||||
}
|
||||
|
||||
successors[chunk] = succ
|
||||
}
|
||||
|
||||
return successors
|
||||
}
|
||||
|
||||
private fun computeLiveness(
|
||||
sub: IRSubroutine,
|
||||
successors: Map<IRCodeChunkBase, List<IRCodeChunkBase>>
|
||||
): Pair<Map<IRCodeChunk, Set<VirtualRegister>>, Map<IRCodeChunk, Set<VirtualRegister>>> {
|
||||
|
||||
val liveIn = mutableMapOf<IRCodeChunk, MutableSet<VirtualRegister>>()
|
||||
val liveOut = mutableMapOf<IRCodeChunk, MutableSet<VirtualRegister>>()
|
||||
val gen = mutableMapOf<IRCodeChunk, MutableSet<VirtualRegister>>()
|
||||
val kill = mutableMapOf<IRCodeChunk, MutableSet<VirtualRegister>>()
|
||||
|
||||
for (chunk in sub.chunks) {
|
||||
if (chunk !is IRCodeChunk)
|
||||
continue
|
||||
|
||||
val genSet = mutableSetOf<VirtualRegister>()
|
||||
val killSet = mutableSetOf<VirtualRegister>()
|
||||
|
||||
for (instr in chunk.instructions) {
|
||||
val written = instr.definitions
|
||||
val read = instr.uses
|
||||
for (r in read) {
|
||||
if (r !in killSet)
|
||||
genSet.add(r)
|
||||
}
|
||||
for (r in written) {
|
||||
killSet.add(r)
|
||||
}
|
||||
}
|
||||
|
||||
gen[chunk] = genSet
|
||||
kill[chunk] = killSet
|
||||
liveIn[chunk] = mutableSetOf()
|
||||
liveOut[chunk] = mutableSetOf()
|
||||
}
|
||||
|
||||
var changed = true
|
||||
|
||||
while (changed) {
|
||||
changed = false
|
||||
|
||||
for (chunk in sub.chunks.reversed()) {
|
||||
if (chunk !is IRCodeChunk)
|
||||
continue
|
||||
|
||||
val newLiveOut = mutableSetOf<VirtualRegister>()
|
||||
for (succ in successors[chunk].orEmpty()) {
|
||||
if (succ is IRCodeChunk)
|
||||
newLiveOut.addAll(liveIn[succ].orEmpty())
|
||||
}
|
||||
|
||||
if (newLiveOut != liveOut[chunk]) {
|
||||
liveOut[chunk] = newLiveOut
|
||||
changed = true
|
||||
}
|
||||
|
||||
val newLiveIn = mutableSetOf<VirtualRegister>()
|
||||
newLiveIn.addAll(gen[chunk].orEmpty())
|
||||
newLiveIn.addAll(liveOut[chunk].orEmpty() - kill[chunk].orEmpty())
|
||||
|
||||
if (newLiveIn != liveIn[chunk]) {
|
||||
liveIn[chunk] = newLiveIn
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return liveIn to liveOut
|
||||
}
|
||||
|
||||
private fun greedyColor(
|
||||
intervals: List<Interval>,
|
||||
conflictGraph: Map<VirtualRegister, Set<VirtualRegister>>,
|
||||
allRegTypes: Map<VirtualRegister, IRDataType>,
|
||||
slotTypes: MutableMap<VirtualRegister, IRDataType>,
|
||||
startSlot: Int
|
||||
): Map<VirtualRegister, VirtualRegister> {
|
||||
if (intervals.isEmpty())
|
||||
return emptyMap()
|
||||
|
||||
val packing = mutableMapOf<VirtualRegister, VirtualRegister>()
|
||||
// Reserve slots for non-packed registers
|
||||
val packedRegs = intervals.map { it.register }.toSet()
|
||||
for ((reg, type) in allRegTypes) {
|
||||
if (reg !in packedRegs)
|
||||
slotTypes.putIfAbsent(reg, type)
|
||||
}
|
||||
|
||||
val sorted = intervals.sortedWith(compareBy<Interval> { it.type.ordinal }.thenBy { it.start })
|
||||
val activeSlots = mutableMapOf<VirtualRegister, Pair<VirtualRegister, Int>>()
|
||||
|
||||
for (interval in sorted) {
|
||||
activeSlots.entries.removeAll { it.value.second < interval.start }
|
||||
|
||||
var slotNum = startSlot
|
||||
var slot: VirtualRegister
|
||||
while (true) {
|
||||
// a packed slot stays in the same register file (int/float) as the register it replaces
|
||||
slot = if (interval.register is VirtualRegister.FloatReg) VirtualRegister.float(slotNum) else VirtualRegister.int(slotNum)
|
||||
if (slot !in activeSlots) {
|
||||
val existingType = slotTypes[slot]
|
||||
if (existingType == null || existingType == interval.type || typesCompatible(existingType, interval.type)) {
|
||||
if (!conflictsWithRegister(slot, interval.register, conflictGraph, packing))
|
||||
break
|
||||
}
|
||||
}
|
||||
slotNum++
|
||||
}
|
||||
|
||||
packing[interval.register] = slot
|
||||
slotTypes.putIfAbsent(slot, interval.type) // keep the first-assigned type to prevent type narrowing via typesCompatible chain
|
||||
activeSlots[slot] = Pair(interval.register, interval.end)
|
||||
}
|
||||
return packing
|
||||
}
|
||||
|
||||
private fun typesCompatible(t1: IRDataType, t2: IRDataType): Boolean {
|
||||
if (t1 == t2) return true
|
||||
return (t1 == IRDataType.POINTER && t2 in setOf(IRDataType.WORD, IRDataType.LONG)) ||
|
||||
(t2 == IRDataType.POINTER && t1 in setOf(IRDataType.WORD, IRDataType.LONG))
|
||||
}
|
||||
|
||||
private fun conflictsWithRegister(
|
||||
slot: VirtualRegister,
|
||||
register: VirtualRegister,
|
||||
conflictGraph: Map<VirtualRegister, Set<VirtualRegister>>,
|
||||
packing: Map<VirtualRegister, VirtualRegister>
|
||||
): Boolean {
|
||||
val conflictingRegs = conflictGraph[register] ?: return false
|
||||
for (cr in conflictingRegs) {
|
||||
val crSlot = packing[cr]
|
||||
if (crSlot != null && crSlot == slot)
|
||||
return true
|
||||
if (cr !in packing && cr == slot)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun rewrite(sub: IRSubroutine, packing: Map<VirtualRegister, VirtualRegister>) {
|
||||
for (chunk in sub.chunks) {
|
||||
if (chunk !is IRCodeChunk)
|
||||
continue
|
||||
|
||||
for (i in chunk.instructions.indices) {
|
||||
val instr = chunk.instructions[i]
|
||||
val newInstr = instr.mapRegisters { vr -> packing[vr] ?: vr }
|
||||
if (newInstr != instr)
|
||||
chunk.instructions[i] = newInstr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,11 @@ internal class RegisterPool {
|
||||
private val registerTypes: MutableMap<RegisterNum, IRDataType> = mutableMapOf()
|
||||
|
||||
fun getTypes(): Map<RegisterNum, IRDataType> = registerTypes
|
||||
fun resetTypes(types: Map<RegisterNum, IRDataType>) {
|
||||
// called after register packing
|
||||
registerTypes.clear()
|
||||
registerTypes.putAll(types)
|
||||
}
|
||||
|
||||
init {
|
||||
for(i in 99000..99099)
|
||||
|
||||
@@ -3,7 +3,6 @@ package prog8.codegen.intermediate
|
||||
import prog8.code.*
|
||||
import prog8.code.ast.PtStructField
|
||||
import prog8.intermediate.*
|
||||
import kotlin.collections.flatMap
|
||||
|
||||
/**
|
||||
* Converter object for transforming SymbolTable nodes to IR symbol table nodes.
|
||||
@@ -112,13 +111,14 @@ private class StToIrConverter(val romable: Boolean) {
|
||||
constant.name
|
||||
}
|
||||
}
|
||||
val noPrefix = constant.astNode?.definingBlock()?.options?.noSymbolPrefixing == true
|
||||
if(constant.value != null)
|
||||
return IRStConstant(scopedName, constant.dt, constant.value)
|
||||
return IRStConstant(scopedName, constant.dt, constant.value, noPrefix = noPrefix)
|
||||
else if(constant.memorySlab != null) {
|
||||
// For memory() constants, store reference to slab name
|
||||
// The actual address will be resolved during code generation
|
||||
val slabName = constant.memorySlab!!.name
|
||||
return IRStConstant(scopedName, constant.dt, null, slabName)
|
||||
return IRStConstant(scopedName, constant.dt, null, slabName, noPrefix)
|
||||
}
|
||||
else
|
||||
TODO("constant without value or memory slab: $constant")
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package prog8.codegen.intermediate
|
||||
|
||||
import prog8.code.ast.*
|
||||
|
||||
internal data class StructArrayIndexInfo(
|
||||
val arrayName: String,
|
||||
val idxExpr: PtExpression,
|
||||
val structSize: Int,
|
||||
val fieldOffset: Int
|
||||
)
|
||||
|
||||
internal fun extractStructArrayIndexInfo(
|
||||
addressExpr: PtExpression,
|
||||
codeGen: IRCodeGen
|
||||
): StructArrayIndexInfo? {
|
||||
fun unwrapCast(expr: PtExpression): PtExpression {
|
||||
var cur = expr
|
||||
while(cur is PtTypeCast) cur = cur.value
|
||||
return cur
|
||||
}
|
||||
fun asAddressOf(expr: PtExpression): PtAddressOf? {
|
||||
return unwrapCast(expr) as? PtAddressOf
|
||||
}
|
||||
val baseAddrOf: PtAddressOf
|
||||
val fieldOffset: Int
|
||||
val unwrapped = unwrapCast(addressExpr)
|
||||
when {
|
||||
unwrapped is PtAddressOf -> {
|
||||
baseAddrOf = unwrapped
|
||||
fieldOffset = 0
|
||||
}
|
||||
unwrapped is PtBinaryExpression && unwrapped.operator == "+" -> {
|
||||
val leftAddr = asAddressOf(unwrapped.left) ?: return null
|
||||
val rightNum = (unwrapCast(unwrapped.right) as? PtNumber)?.number?.toInt() ?: return null
|
||||
baseAddrOf = leftAddr
|
||||
fieldOffset = rightNum
|
||||
}
|
||||
else -> return null
|
||||
}
|
||||
if(!baseAddrOf.isFromArrayElement) return null
|
||||
val arrayIdent = baseAddrOf.identifier ?: return null
|
||||
val idxExpr = baseAddrOf.arrayIndexExpr ?: return null
|
||||
if(idxExpr is PtNumber) return null
|
||||
val arrayType = arrayIdent.type
|
||||
if(!arrayType.isArray) return null
|
||||
val elemType = arrayType.elementType()
|
||||
if(!elemType.isStructInstance) return null
|
||||
val structSize = codeGen.program.memsizer.memorySize(elemType, null)
|
||||
if(structSize <= 0 || structSize > 65535) return null
|
||||
if(fieldOffset < 0 || fieldOffset > 65535) return null
|
||||
return StructArrayIndexInfo(arrayIdent.name, idxExpr, structSize, fieldOffset)
|
||||
}
|
||||
@@ -2,7 +2,10 @@ package prog8.codegen.intermediate
|
||||
|
||||
import prog8.code.*
|
||||
import prog8.code.ast.*
|
||||
import prog8.code.core.*
|
||||
import prog8.code.core.AssemblyError
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.DataType
|
||||
import prog8.code.core.ISubType
|
||||
|
||||
|
||||
/**
|
||||
@@ -76,7 +79,7 @@ fun prefixSymbols(program: PtProgram, options: CompilationOptions, st: SymbolTab
|
||||
}
|
||||
}
|
||||
var lookupName = node.name
|
||||
if(node.type.isSplitWordArray && (lookupName.endsWith("_lsb") || lookupName.endsWith("_msb"))) {
|
||||
if(node.type.isSplitWordArray(program.memsizer) && (lookupName.endsWith("_lsb") || lookupName.endsWith("_msb"))) {
|
||||
lookupName = lookupName.dropLast(4)
|
||||
}
|
||||
val stNode = st.lookup(lookupName) ?:
|
||||
@@ -120,7 +123,7 @@ fun prefixSymbols(program: PtProgram, options: CompilationOptions, st: SymbolTab
|
||||
}
|
||||
else if (node is PtIdentifier) {
|
||||
var lookupName = node.name
|
||||
if(node.type.isSplitWordArray && (lookupName.endsWith("_lsb") || lookupName.endsWith("_msb"))) {
|
||||
if(node.type.isSplitWordArray(program.memsizer) && (lookupName.endsWith("_lsb") || lookupName.endsWith("_msb"))) {
|
||||
lookupName = lookupName.dropLast(4)
|
||||
}
|
||||
val stNode = st.lookup(lookupName) ?: throw AssemblyError("unknown identifier $node")
|
||||
@@ -259,7 +262,7 @@ private fun PtVariable.prefixVariableOrThis(parent: PtNode, st: SymbolTable): Pt
|
||||
else -> throw AssemblyError("weird array value element $elt")
|
||||
}
|
||||
}
|
||||
val result = PtVariable(name, type, zeropage, align, dirty, newValue, arraySize, position)
|
||||
val result = PtVariable(name, type, isSplitWordArray, zeropage, align, dirty, newValue, arraySize, position)
|
||||
result.parent = parent
|
||||
result
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package prog8.codegen.vm
|
||||
|
||||
import prog8.code.IAssemblyProgram
|
||||
import prog8.code.ICodeGeneratorBackend
|
||||
import prog8.code.SymbolTable
|
||||
import prog8.code.assembly.IAssemblyProgram
|
||||
import prog8.code.ast.PtProgram
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.codegen.intermediate.IRCodeGen
|
||||
import prog8.intermediate.IRDataType
|
||||
import prog8.intermediate.IRFileWriter
|
||||
import prog8.intermediate.IRProgram
|
||||
import prog8.intermediate.VirtualRegister
|
||||
import prog8.intermediate.dumpVariables
|
||||
|
||||
class VmCodeGen(val retainSSA: Boolean,
|
||||
private val preassignedCallSiteIds: Map<String, UByte> = emptyMap()
|
||||
@@ -22,7 +25,14 @@ class VmCodeGen(val retainSSA: Boolean,
|
||||
val irCodeGen = IRCodeGen(program, symbolTable, options, errors, retainSSA, preassignedCallSiteIds)
|
||||
val irProgram = irCodeGen.generate()
|
||||
|
||||
irProgram.verifyRegisterTypes(irCodeGen.registerTypes())
|
||||
val virtualRegisterTypes = irCodeGen.registerTypes().entries.associate { (num, type) ->
|
||||
val register: VirtualRegister = if (type == IRDataType.FLOAT) VirtualRegister.float(num.value) else VirtualRegister.int(num.value)
|
||||
register to type
|
||||
}
|
||||
irProgram.verifyRegisterTypes(virtualRegisterTypes)
|
||||
|
||||
if (options.dumpVariables)
|
||||
dumpVariables(irProgram)
|
||||
|
||||
return VmAssemblyProgram(irProgram.name, irProgram)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import prog8.intermediate.*
|
||||
import prog8tests.helpers.ErrorReporterForTests
|
||||
|
||||
class TestIRPeepholeOpt: FunSpec({
|
||||
val indexRegType = VMTarget().indexRegType
|
||||
|
||||
fun makeIRProgram(chunks: List<IRCodeChunkBase>): IRProgram {
|
||||
require(chunks.first().label=="p8b_main.p8s_start")
|
||||
val block = IRBlock("p8b_main", false, IRBlock.Options(), Position.DUMMY)
|
||||
@@ -40,9 +42,9 @@ class TestIRPeepholeOpt: FunSpec({
|
||||
|
||||
test("remove nops") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=1, immediate=42),
|
||||
IRInstruction(Opcode.NOP),
|
||||
IRInstruction(Opcode.NOP)
|
||||
IRInstructions.load(IRDataType.BYTE, 1, 42),
|
||||
IRInstructions.simple(Opcode.NOP),
|
||||
IRInstructions.simple(Opcode.NOP)
|
||||
))
|
||||
irProg.chunks().single().instructions.size shouldBe 3
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
@@ -52,13 +54,13 @@ class TestIRPeepholeOpt: FunSpec({
|
||||
|
||||
test("remove jmp to label below but keep labels") {
|
||||
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
|
||||
c1 += IRInstruction(Opcode.JUMP, labelSymbol = "label")
|
||||
c1 += IRInstructions.jump(codeLabel("label"))
|
||||
val c2 = IRCodeChunk("label", null)
|
||||
c2 += IRInstruction(Opcode.JUMP, labelSymbol = "label2")
|
||||
c2 += IRInstruction(Opcode.NOP) // removed
|
||||
c2 += IRInstructions.jump(codeLabel("label2"))
|
||||
c2 += IRInstructions.simple(Opcode.NOP) // removed
|
||||
val c3 = IRCodeChunk("label2", null)
|
||||
c3 += IRInstruction(Opcode.JUMP, labelSymbol = "label3")
|
||||
c3 += IRInstruction(Opcode.INC, IRDataType.BYTE, reg1=1)
|
||||
c3 += IRInstructions.jump(codeLabel("label3"))
|
||||
c3 += IRInstructions.unary(Opcode.INC, IRDataType.BYTE, 1)
|
||||
val c4 = IRCodeChunk("label3", null)
|
||||
val irProg = makeIRProgram(listOf(c1, c2, c3, c4))
|
||||
|
||||
@@ -84,18 +86,18 @@ class TestIRPeepholeOpt: FunSpec({
|
||||
|
||||
test("remove double sec/clc/sei/cli") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.SEC),
|
||||
IRInstruction(Opcode.SEC),
|
||||
IRInstruction(Opcode.SEC),
|
||||
IRInstruction(Opcode.CLC),
|
||||
IRInstruction(Opcode.CLC),
|
||||
IRInstruction(Opcode.CLC),
|
||||
IRInstruction(Opcode.SEI),
|
||||
IRInstruction(Opcode.SEI),
|
||||
IRInstruction(Opcode.SEI),
|
||||
IRInstruction(Opcode.CLI),
|
||||
IRInstruction(Opcode.CLI),
|
||||
IRInstruction(Opcode.CLI),
|
||||
IRInstructions.simple(Opcode.SEC),
|
||||
IRInstructions.simple(Opcode.SEC),
|
||||
IRInstructions.simple(Opcode.SEC),
|
||||
IRInstructions.simple(Opcode.CLC),
|
||||
IRInstructions.simple(Opcode.CLC),
|
||||
IRInstructions.simple(Opcode.CLC),
|
||||
IRInstructions.simple(Opcode.SEI),
|
||||
IRInstructions.simple(Opcode.SEI),
|
||||
IRInstructions.simple(Opcode.SEI),
|
||||
IRInstructions.simple(Opcode.CLI),
|
||||
IRInstructions.simple(Opcode.CLI),
|
||||
IRInstructions.simple(Opcode.CLI),
|
||||
))
|
||||
irProg.chunks().single().instructions.size shouldBe 12
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
@@ -108,18 +110,18 @@ class TestIRPeepholeOpt: FunSpec({
|
||||
|
||||
test("remove double sec/clc/sei/cli reversed") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.CLC),
|
||||
IRInstruction(Opcode.CLC),
|
||||
IRInstruction(Opcode.CLC),
|
||||
IRInstruction(Opcode.SEC),
|
||||
IRInstruction(Opcode.SEC),
|
||||
IRInstruction(Opcode.SEC),
|
||||
IRInstruction(Opcode.CLI),
|
||||
IRInstruction(Opcode.CLI),
|
||||
IRInstruction(Opcode.CLI),
|
||||
IRInstruction(Opcode.SEI),
|
||||
IRInstruction(Opcode.SEI),
|
||||
IRInstruction(Opcode.SEI),
|
||||
IRInstructions.simple(Opcode.CLC),
|
||||
IRInstructions.simple(Opcode.CLC),
|
||||
IRInstructions.simple(Opcode.CLC),
|
||||
IRInstructions.simple(Opcode.SEC),
|
||||
IRInstructions.simple(Opcode.SEC),
|
||||
IRInstructions.simple(Opcode.SEC),
|
||||
IRInstructions.simple(Opcode.CLI),
|
||||
IRInstructions.simple(Opcode.CLI),
|
||||
IRInstructions.simple(Opcode.CLI),
|
||||
IRInstructions.simple(Opcode.SEI),
|
||||
IRInstructions.simple(Opcode.SEI),
|
||||
IRInstructions.simple(Opcode.SEI),
|
||||
))
|
||||
irProg.chunks().single().instructions.size shouldBe 12
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
@@ -132,10 +134,10 @@ class TestIRPeepholeOpt: FunSpec({
|
||||
|
||||
test("push followed by pop") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.PUSH, IRDataType.BYTE, reg1=42),
|
||||
IRInstruction(Opcode.POP, IRDataType.BYTE, reg1=42),
|
||||
IRInstruction(Opcode.PUSH, IRDataType.BYTE, reg1=99),
|
||||
IRInstruction(Opcode.POP, IRDataType.BYTE, reg1=222)
|
||||
IRInstructions.push(IRDataType.BYTE, 42),
|
||||
IRInstructions.pop(IRDataType.BYTE, 42),
|
||||
IRInstructions.push(IRDataType.BYTE, 99),
|
||||
IRInstructions.pop(IRDataType.BYTE, 222)
|
||||
))
|
||||
irProg.chunks().single().instructions.size shouldBe 4
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
@@ -143,23 +145,23 @@ class TestIRPeepholeOpt: FunSpec({
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.size shouldBe 1
|
||||
instr[0].opcode shouldBe Opcode.LOADR
|
||||
instr[0].reg1 shouldBe 222
|
||||
instr[0].reg2 shouldBe 99
|
||||
instr[0].requireDest().register shouldBe VirtualRegister.int(222)
|
||||
instr[0].requireSrcA().register shouldBe VirtualRegister.int(99)
|
||||
}
|
||||
|
||||
test("remove useless div/mul, add/sub") {
|
||||
// Use different registers for each test case to avoid dead store elimination removing them
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.DIV, IRDataType.BYTE, reg1=1, immediate = 1),
|
||||
IRInstruction(Opcode.DIVS, IRDataType.BYTE, reg1=2, immediate = 1),
|
||||
IRInstruction(Opcode.MUL, IRDataType.BYTE, reg1=3, immediate = 1),
|
||||
IRInstruction(Opcode.MOD, IRDataType.BYTE, reg1=4, immediate = 1),
|
||||
IRInstruction(Opcode.DIV, IRDataType.BYTE, reg1=5, immediate = 2),
|
||||
IRInstruction(Opcode.DIVS, IRDataType.BYTE, reg1=6, immediate = 2),
|
||||
IRInstruction(Opcode.MUL, IRDataType.BYTE, reg1=7, immediate = 2),
|
||||
IRInstruction(Opcode.MOD, IRDataType.BYTE, reg1=8, immediate = 2),
|
||||
IRInstruction(Opcode.ADD, IRDataType.BYTE, reg1=9, immediate = 0),
|
||||
IRInstruction(Opcode.SUB, IRDataType.BYTE, reg1=10, immediate = 0)
|
||||
IRInstructions.binaryImmediate(Opcode.DIV, IRDataType.BYTE, 1, 1),
|
||||
IRInstructions.binaryImmediate(Opcode.DIVS, IRDataType.BYTE, 2, 1),
|
||||
IRInstructions.binaryImmediate(Opcode.MUL, IRDataType.BYTE, 3, 1),
|
||||
IRInstructions.binaryImmediate(Opcode.MOD, IRDataType.BYTE, 4, 1),
|
||||
IRInstructions.binaryImmediate(Opcode.DIV, IRDataType.BYTE, 5, 2),
|
||||
IRInstructions.binaryImmediate(Opcode.DIVS, IRDataType.BYTE, 6, 2),
|
||||
IRInstructions.binaryImmediate(Opcode.MUL, IRDataType.BYTE, 7, 2),
|
||||
IRInstructions.binaryImmediate(Opcode.MOD, IRDataType.BYTE, 8, 2),
|
||||
IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.BYTE, 9, 0),
|
||||
IRInstructions.binaryImmediate(Opcode.SUB, IRDataType.BYTE, 10, 0)
|
||||
))
|
||||
irProg.chunks().single().instructions.size shouldBe 10
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
@@ -168,11 +170,66 @@ class TestIRPeepholeOpt: FunSpec({
|
||||
irProg.chunks().single().instructions.size shouldBe 4
|
||||
}
|
||||
|
||||
test("replace integer multiply by zero with load") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.binaryImmediate(Opcode.MUL, IRDataType.BYTE, 1, 0),
|
||||
IRInstructions.binaryImmediate(Opcode.MULS, IRDataType.WORD, 2, 0),
|
||||
IRInstructions.binaryImmediate(Opcode.MULS, IRDataType.LONG, 3, 0),
|
||||
IRInstructions.binaryImmediateFloat(Opcode.MUL, 4, 0.0),
|
||||
IRInstructions.returnVoid()
|
||||
))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.count { it.opcode == Opcode.LOAD } shouldBe 3
|
||||
instr.count { it.opcode == Opcode.MUL } shouldBe 1
|
||||
}
|
||||
|
||||
test("fold adjacent integer immediate multiplications") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.binaryImmediate(Opcode.MUL, IRDataType.BYTE, 1, 7),
|
||||
IRInstructions.binaryImmediate(Opcode.MUL, IRDataType.BYTE, 1, 9),
|
||||
IRInstructions.binaryImmediate(Opcode.MULS, IRDataType.WORD, 2, -3),
|
||||
IRInstructions.binaryImmediate(Opcode.MULS, IRDataType.WORD, 2, 4),
|
||||
IRInstructions.binaryImmediate(Opcode.MULS, IRDataType.LONG, 3, 100000),
|
||||
IRInstructions.binaryImmediate(Opcode.MULS, IRDataType.LONG, 3, 30000),
|
||||
IRInstructions.returnVoid()
|
||||
))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.size shouldBe 4
|
||||
instr[0].requireImmediateInt() shouldBe 63
|
||||
instr[1].requireImmediateInt() shouldBe 65524
|
||||
instr[2].requireImmediateInt() shouldBe 3000000000L.toInt()
|
||||
}
|
||||
|
||||
test("collapse adjacent integer and float loadr chains") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.move(IRDataType.BYTE, 2, 1),
|
||||
IRInstructions.move(IRDataType.BYTE, 3, 2),
|
||||
IRInstructions.move(IRDataType.FLOAT, 5, 4),
|
||||
IRInstructions.move(IRDataType.FLOAT, 6, 5),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 3, IRMemory.direct(MemoryAddress(100u))),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.FLOAT, 6, IRMemory.direct(MemoryAddress(200u)))
|
||||
))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.map { it.opcode } shouldBe listOf(Opcode.LOADR, Opcode.LOADR, Opcode.STOREM, Opcode.STOREM)
|
||||
instr[0].requireDest().register shouldBe VirtualRegister.int(3)
|
||||
instr[0].requireSrcA().register shouldBe VirtualRegister.int(1)
|
||||
instr[1].requireDest().register shouldBe VirtualRegister.float(6)
|
||||
instr[1].requireSrcA().register shouldBe VirtualRegister.float(4)
|
||||
instr[2].requireSrcA().register shouldBe VirtualRegister.int(3)
|
||||
instr[3].requireSrcA().register shouldBe VirtualRegister.float(6)
|
||||
}
|
||||
|
||||
test("replace add/sub 1 by inc/dec") {
|
||||
// Use different registers for each test case to avoid dead store elimination removing them
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.ADD, IRDataType.BYTE, reg1=1, immediate = 1),
|
||||
IRInstruction(Opcode.SUB, IRDataType.BYTE, reg1=2, immediate = 1)
|
||||
IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.BYTE, 1, 1),
|
||||
IRInstructions.binaryImmediate(Opcode.SUB, IRDataType.BYTE, 2, 1)
|
||||
))
|
||||
irProg.chunks().single().instructions.size shouldBe 2
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
@@ -186,23 +243,23 @@ class TestIRPeepholeOpt: FunSpec({
|
||||
test("remove useless and/or/xor") {
|
||||
// Use different registers for each test case to avoid dead store elimination removing them
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.AND, IRDataType.BYTE, reg1=1, immediate = 0),
|
||||
IRInstruction(Opcode.AND, IRDataType.WORD, reg1=2, immediate = 0),
|
||||
IRInstruction(Opcode.AND, IRDataType.LONG, reg1=3, immediate = 0),
|
||||
IRInstruction(Opcode.AND, IRDataType.BYTE, reg1=4, immediate = 255),
|
||||
IRInstruction(Opcode.AND, IRDataType.WORD, reg1=5, immediate = 65535),
|
||||
IRInstruction(Opcode.AND, IRDataType.LONG, reg1=6, immediate = 2147483647),
|
||||
IRInstruction(Opcode.AND, IRDataType.LONG, reg1=7, immediate = -1),
|
||||
IRInstruction(Opcode.OR, IRDataType.BYTE, reg1=8, immediate = 0),
|
||||
IRInstruction(Opcode.OR, IRDataType.BYTE, reg1=9, immediate = 255),
|
||||
IRInstruction(Opcode.OR, IRDataType.WORD, reg1=10, immediate = 65535),
|
||||
IRInstruction(Opcode.OR, IRDataType.LONG, reg1=11, immediate = 2147483647),
|
||||
IRInstruction(Opcode.OR, IRDataType.LONG, reg1=12, immediate = -1),
|
||||
IRInstruction(Opcode.XOR, IRDataType.BYTE, reg1=13, immediate = 0),
|
||||
IRInstruction(Opcode.AND, IRDataType.BYTE, reg1=14, immediate = 200),
|
||||
IRInstruction(Opcode.AND, IRDataType.WORD, reg1=15, immediate = 60000),
|
||||
IRInstruction(Opcode.OR, IRDataType.BYTE, reg1=16, immediate = 1),
|
||||
IRInstruction(Opcode.XOR, IRDataType.BYTE, reg1=17, immediate = 1)
|
||||
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.BYTE, 1, 0),
|
||||
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.WORD, 2, 0),
|
||||
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.LONG, 3, 0),
|
||||
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.BYTE, 4, 255),
|
||||
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.WORD, 5, 65535),
|
||||
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.LONG, 6, 2147483647),
|
||||
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.LONG, 7, -1),
|
||||
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.BYTE, 8, 0),
|
||||
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.BYTE, 9, 255),
|
||||
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.WORD, 10, 65535),
|
||||
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.LONG, 11, 2147483647),
|
||||
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.LONG, 12, -1),
|
||||
IRInstructions.binaryImmediate(Opcode.XOR, IRDataType.BYTE, 13, 0),
|
||||
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.BYTE, 14, 200),
|
||||
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.WORD, 15, 60000),
|
||||
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.BYTE, 16, 1),
|
||||
IRInstructions.binaryImmediate(Opcode.XOR, IRDataType.BYTE, 17, 1)
|
||||
))
|
||||
irProg.chunks().single().instructions.size shouldBe 17
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
@@ -223,40 +280,86 @@ class TestIRPeepholeOpt: FunSpec({
|
||||
test("replace and/or/xor by constant number") {
|
||||
// Use different registers for each test case to avoid dead store elimination removing them
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.AND, IRDataType.BYTE, reg1=1, immediate = 0),
|
||||
IRInstruction(Opcode.AND, IRDataType.WORD, reg1=2, immediate = 0),
|
||||
IRInstruction(Opcode.AND, IRDataType.LONG, reg1=3, immediate = 0),
|
||||
IRInstruction(Opcode.OR, IRDataType.BYTE, reg1=4, immediate = 255),
|
||||
IRInstruction(Opcode.OR, IRDataType.WORD, reg1=5, immediate = 65535),
|
||||
IRInstruction(Opcode.OR, IRDataType.LONG, reg1=6, immediate = -1)
|
||||
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.BYTE, 1, 0),
|
||||
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.WORD, 2, 0),
|
||||
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.LONG, 3, 0),
|
||||
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.BYTE, 4, 255),
|
||||
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.WORD, 5, 65535),
|
||||
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.LONG, 6, -1)
|
||||
))
|
||||
irProg.chunks().single().instructions.size shouldBe 6
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.size shouldBe 6
|
||||
instr.map { it.opcode } shouldBe List(6) { Opcode.LOAD }
|
||||
instr.map { it.requireImmediateInt() } shouldBe listOf(0, 0, 0, 255, 65535, -1)
|
||||
}
|
||||
|
||||
test("combine adjacent immediate and/or operations") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.BYTE, 1, 0xf0),
|
||||
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.BYTE, 1, 0xcc),
|
||||
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.WORD, 2, 0x0010),
|
||||
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.WORD, 2, 0x0003),
|
||||
))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.size shouldBe 2
|
||||
instr[0].opcode shouldBe Opcode.AND
|
||||
instr[0].requireImmediateInt() shouldBe 0xc0
|
||||
instr[1].opcode shouldBe Opcode.OR
|
||||
instr[1].requireImmediateInt() shouldBe 0x0013
|
||||
}
|
||||
|
||||
test("cancel identical adjacent immediate xor operations") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.binaryImmediate(Opcode.XOR, IRDataType.BYTE, 1, 3),
|
||||
IRInstructions.binaryImmediate(Opcode.XOR, IRDataType.BYTE, 1, 3),
|
||||
IRInstructions.load(IRDataType.BYTE, 2, 42)
|
||||
))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.size shouldBe 1
|
||||
instr[0].opcode shouldBe Opcode.LOAD
|
||||
instr[1].opcode shouldBe Opcode.LOAD
|
||||
instr[2].opcode shouldBe Opcode.LOAD
|
||||
instr[3].opcode shouldBe Opcode.LOAD
|
||||
instr[4].opcode shouldBe Opcode.LOAD
|
||||
instr[5].opcode shouldBe Opcode.LOAD
|
||||
instr[0].immediate shouldBe 0
|
||||
instr[1].immediate shouldBe 0
|
||||
instr[2].immediate shouldBe 0
|
||||
instr[3].immediate shouldBe 255
|
||||
instr[4].immediate shouldBe 65535
|
||||
instr[5].immediate shouldBe -1
|
||||
instr[0].requireImmediateInt() shouldBe 42
|
||||
}
|
||||
|
||||
test("fold adjacent immediate arithmetic with inc/dec") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.BYTE, 1, 4),
|
||||
IRInstructions.unary(Opcode.INC, IRDataType.BYTE, 1),
|
||||
IRInstructions.unary(Opcode.INC, IRDataType.BYTE, 2),
|
||||
IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.BYTE, 2, 4),
|
||||
IRInstructions.binaryImmediate(Opcode.SUB, IRDataType.BYTE, 3, 4),
|
||||
IRInstructions.unary(Opcode.DEC, IRDataType.BYTE, 3),
|
||||
IRInstructions.unary(Opcode.DEC, IRDataType.BYTE, 4),
|
||||
IRInstructions.binaryImmediate(Opcode.SUB, IRDataType.BYTE, 4, 4)
|
||||
))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.size shouldBe 4
|
||||
instr[0].opcode shouldBe Opcode.ADD
|
||||
instr[0].requireImmediateInt() shouldBe 5
|
||||
instr[1].opcode shouldBe Opcode.ADD
|
||||
instr[1].requireImmediateInt() shouldBe 5
|
||||
instr[2].opcode shouldBe Opcode.SUB
|
||||
instr[2].requireImmediateInt() shouldBe 5
|
||||
instr[3].opcode shouldBe Opcode.SUB
|
||||
instr[3].requireImmediateInt() shouldBe 5
|
||||
}
|
||||
|
||||
test("replace register arithmetic by immediate operations") {
|
||||
// Test the new optimization: LOAD + SUBR/DIVR/MODR → immediate operation
|
||||
// Pattern: LOAD r1, #const followed by SUBR r2, r1 → SUB r2, #const
|
||||
|
||||
|
||||
// SUBR optimization
|
||||
val subrProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=1, immediate = 5),
|
||||
IRInstruction(Opcode.SUBR, IRDataType.BYTE, reg1=2, reg2=1)
|
||||
IRInstructions.load(IRDataType.BYTE, 1, 5),
|
||||
IRInstructions.binary(Opcode.SUBR, IRDataType.BYTE, 2, 1)
|
||||
))
|
||||
subrProg.chunks().single().instructions.size shouldBe 2
|
||||
val opt1 = IRPeepholeOptimizer(subrProg, false)
|
||||
@@ -264,12 +367,12 @@ class TestIRPeepholeOpt: FunSpec({
|
||||
val subInstr = subrProg.chunks().single().instructions
|
||||
subInstr.size shouldBe 1
|
||||
subInstr[0].opcode shouldBe Opcode.SUB
|
||||
subInstr[0].immediate shouldBe 5
|
||||
|
||||
subInstr[0].requireImmediateInt() shouldBe 5
|
||||
|
||||
// DIVR optimization
|
||||
val divrProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.LOAD, IRDataType.WORD, reg1=10, immediate = 100),
|
||||
IRInstruction(Opcode.DIVR, IRDataType.WORD, reg1=20, reg2=10)
|
||||
IRInstructions.load(IRDataType.WORD, 10, 100),
|
||||
IRInstructions.binary(Opcode.DIVR, IRDataType.WORD, 20, 10)
|
||||
))
|
||||
divrProg.chunks().single().instructions.size shouldBe 2
|
||||
val opt2 = IRPeepholeOptimizer(divrProg, false)
|
||||
@@ -277,12 +380,12 @@ class TestIRPeepholeOpt: FunSpec({
|
||||
val divInstr = divrProg.chunks().single().instructions
|
||||
divInstr.size shouldBe 1
|
||||
divInstr[0].opcode shouldBe Opcode.DIV
|
||||
divInstr[0].immediate shouldBe 100
|
||||
|
||||
divInstr[0].requireImmediateInt() shouldBe 100
|
||||
|
||||
// MODR optimization
|
||||
val modrProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=30, immediate = 7),
|
||||
IRInstruction(Opcode.MODR, IRDataType.BYTE, reg1=40, reg2=30)
|
||||
IRInstructions.load(IRDataType.BYTE, 30, 7),
|
||||
IRInstructions.binary(Opcode.MODR, IRDataType.BYTE, 40, 30)
|
||||
))
|
||||
modrProg.chunks().single().instructions.size shouldBe 2
|
||||
val opt3 = IRPeepholeOptimizer(modrProg, false)
|
||||
@@ -290,34 +393,42 @@ class TestIRPeepholeOpt: FunSpec({
|
||||
val modInstr = modrProg.chunks().single().instructions
|
||||
modInstr.size shouldBe 1
|
||||
modInstr[0].opcode shouldBe Opcode.MOD
|
||||
modInstr[0].immediate shouldBe 7
|
||||
modInstr[0].requireImmediateInt() shouldBe 7
|
||||
}
|
||||
|
||||
test("remove self identity operations") {
|
||||
// LOADR with same fp register is allowed (constructor only blocks integer reg1==reg2)
|
||||
// The IR is not SSA: a written register may also be a read-only source register.
|
||||
// Self-identity LOADR/ANDR/ORR become NOP, self-identity XORR becomes load 0.
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.LOADR, IRDataType.FLOAT, fpReg1=RegisterNum(10), fpReg2=RegisterNum(10)),
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=99, immediate=42) // keep chunk non-empty
|
||||
IRInstructions.move(IRDataType.FLOAT, 10, 10),
|
||||
IRInstructions.move(IRDataType.BYTE, 11, 11),
|
||||
IRInstructions.binary(Opcode.ANDR, IRDataType.BYTE, 12, 12),
|
||||
IRInstructions.binary(Opcode.ORR, IRDataType.BYTE, 13, 13),
|
||||
IRInstructions.binary(Opcode.XORR, IRDataType.BYTE, 14, 14),
|
||||
IRInstructions.load(IRDataType.BYTE, 99, 42) // keep chunk non-empty
|
||||
))
|
||||
irProg.chunks().single().instructions.size shouldBe 2
|
||||
irProg.chunks().single().instructions.size shouldBe 6
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.size shouldBe 1
|
||||
val instr = irProg.chunks().single().instructions.filter { it.opcode != Opcode.NOP }
|
||||
instr.size shouldBe 2
|
||||
instr[0].opcode shouldBe Opcode.LOAD
|
||||
instr[0].reg1 shouldBe 99
|
||||
instr[0].immediate shouldBe 42
|
||||
instr[0].requireDest().register shouldBe VirtualRegister.int(14)
|
||||
instr[0].requireImmediateInt() shouldBe 0
|
||||
instr[1].opcode shouldBe Opcode.LOAD
|
||||
instr[1].requireDest().register shouldBe VirtualRegister.int(99)
|
||||
instr[1].requireImmediateInt() shouldBe 42
|
||||
}
|
||||
|
||||
test("remove shift by zero") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=10, immediate=0),
|
||||
IRInstruction(Opcode.LSLN, IRDataType.BYTE, reg1=1, reg2=10),
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=11, immediate=0),
|
||||
IRInstruction(Opcode.LSRN, IRDataType.BYTE, reg1=2, reg2=11),
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=12, immediate=0),
|
||||
IRInstruction(Opcode.ASRN, IRDataType.BYTE, reg1=3, reg2=12),
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=99, immediate=42) // keep chunk non-empty
|
||||
IRInstructions.load(IRDataType.BYTE, 10, 0),
|
||||
IRInstructions.binary(Opcode.LSLN, IRDataType.BYTE, 1, 10),
|
||||
IRInstructions.load(IRDataType.BYTE, 11, 0),
|
||||
IRInstructions.binary(Opcode.LSRN, IRDataType.BYTE, 2, 11),
|
||||
IRInstructions.load(IRDataType.BYTE, 12, 0),
|
||||
IRInstructions.binary(Opcode.ASRN, IRDataType.BYTE, 3, 12),
|
||||
IRInstructions.load(IRDataType.BYTE, 99, 42) // keep chunk non-empty
|
||||
))
|
||||
irProg.chunks().single().instructions.size shouldBe 7
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
@@ -325,25 +436,68 @@ class TestIRPeepholeOpt: FunSpec({
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.size shouldBe 1
|
||||
instr[0].opcode shouldBe Opcode.LOAD
|
||||
instr[0].reg1 shouldBe 99
|
||||
instr[0].immediate shouldBe 42
|
||||
instr[0].requireDest().register shouldBe VirtualRegister.int(99)
|
||||
instr[0].requireImmediateInt() shouldBe 42
|
||||
}
|
||||
|
||||
test("replace concat with ext after harmless instruction") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.load(IRDataType.BYTE, 10, 0),
|
||||
IRInstructions.unary(Opcode.INC, IRDataType.BYTE, 20),
|
||||
IRInstructions.concat(IRDataType.BYTE, 1, 10, 11),
|
||||
IRInstructions.load(IRDataType.BYTE, 99, 42)
|
||||
))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.any {
|
||||
it.opcode == Opcode.EXT &&
|
||||
it.requireDest().register == VirtualRegister.int(1) &&
|
||||
it.requireSrcA().register == VirtualRegister.int(11)
|
||||
} shouldBe true
|
||||
instr.any { it.opcode == Opcode.CONCAT } shouldBe false
|
||||
}
|
||||
|
||||
test("do not replace concat after intervening write") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.load(IRDataType.BYTE, 10, 0),
|
||||
IRInstructions.unary(Opcode.INC, IRDataType.BYTE, 10),
|
||||
IRInstructions.concat(IRDataType.BYTE, 1, 10, 11),
|
||||
IRInstructions.load(IRDataType.BYTE, 99, 42)
|
||||
))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
irProg.chunks().single().instructions.any { it.opcode == Opcode.CONCAT } shouldBe true
|
||||
}
|
||||
|
||||
test("do not replace concat across control flow") {
|
||||
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
|
||||
c1 += IRInstructions.load(IRDataType.BYTE, 10, 0)
|
||||
c1 += IRInstructions.jump(codeLabel("after"))
|
||||
c1 += IRInstructions.concat(IRDataType.BYTE, 1, 10, 11)
|
||||
c1 += IRInstructions.load(IRDataType.BYTE, 99, 42)
|
||||
val c2 = IRCodeChunk("after", null)
|
||||
val irProg = makeIRProgram(listOf(c1, c2))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
irProg.chunks().flatMap { it.instructions }.any { it.opcode == Opcode.CONCAT } shouldBe true
|
||||
}
|
||||
|
||||
test("cancel adjacent ops") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.INV, IRDataType.BYTE, reg1=1),
|
||||
IRInstruction(Opcode.INV, IRDataType.BYTE, reg1=1),
|
||||
IRInstruction(Opcode.NEG, IRDataType.BYTE, reg1=2),
|
||||
IRInstruction(Opcode.NEG, IRDataType.BYTE, reg1=2),
|
||||
IRInstruction(Opcode.EXT, IRDataType.BYTE, reg1=3, reg2=33),
|
||||
IRInstruction(Opcode.EXT, IRDataType.BYTE, reg1=3, reg2=33),
|
||||
IRInstruction(Opcode.EXTS, IRDataType.WORD, reg1=4, reg2=44),
|
||||
IRInstruction(Opcode.EXTS, IRDataType.WORD, reg1=4, reg2=44),
|
||||
IRInstruction(Opcode.INC, IRDataType.BYTE, reg1=5),
|
||||
IRInstruction(Opcode.DEC, IRDataType.BYTE, reg1=5),
|
||||
IRInstruction(Opcode.DEC, IRDataType.BYTE, reg1=6),
|
||||
IRInstruction(Opcode.INC, IRDataType.BYTE, reg1=6),
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=99, immediate=42) // keep chunk non-empty
|
||||
IRInstructions.unary(Opcode.INV, IRDataType.BYTE, 1),
|
||||
IRInstructions.unary(Opcode.INV, IRDataType.BYTE, 1),
|
||||
IRInstructions.unary(Opcode.NEG, IRDataType.BYTE, 2),
|
||||
IRInstructions.unary(Opcode.NEG, IRDataType.BYTE, 2),
|
||||
IRInstructions.binary(Opcode.EXT, IRDataType.BYTE, 3, 33),
|
||||
IRInstructions.binary(Opcode.EXT, IRDataType.BYTE, 3, 33),
|
||||
IRInstructions.binary(Opcode.EXTS, IRDataType.WORD, 4, 44),
|
||||
IRInstructions.binary(Opcode.EXTS, IRDataType.WORD, 4, 44),
|
||||
IRInstructions.unary(Opcode.INC, IRDataType.BYTE, 5),
|
||||
IRInstructions.unary(Opcode.DEC, IRDataType.BYTE, 5),
|
||||
IRInstructions.unary(Opcode.DEC, IRDataType.BYTE, 6),
|
||||
IRInstructions.unary(Opcode.INC, IRDataType.BYTE, 6),
|
||||
IRInstructions.load(IRDataType.BYTE, 99, 42) // keep chunk non-empty
|
||||
))
|
||||
irProg.chunks().single().instructions.size shouldBe 13
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
@@ -351,12 +505,165 @@ class TestIRPeepholeOpt: FunSpec({
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.size shouldBe 3
|
||||
instr[0].opcode shouldBe Opcode.EXT
|
||||
instr[0].reg1 shouldBe 3
|
||||
instr[0].reg2 shouldBe 33
|
||||
instr[0].requireDest().register shouldBe VirtualRegister.int(3)
|
||||
instr[0].requireSrcA().register shouldBe VirtualRegister.int(33)
|
||||
instr[1].opcode shouldBe Opcode.EXTS
|
||||
instr[1].reg1 shouldBe 4
|
||||
instr[1].reg2 shouldBe 44
|
||||
instr[1].requireDest().register shouldBe VirtualRegister.int(4)
|
||||
instr[1].requireSrcA().register shouldBe VirtualRegister.int(44)
|
||||
instr[2].opcode shouldBe Opcode.LOAD
|
||||
instr[2].immediate shouldBe 42
|
||||
instr[2].requireImmediateInt() shouldBe 42
|
||||
}
|
||||
|
||||
test("coalesce redundant LOADX/STOREX to same index is removed") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 10, indexRegType)),
|
||||
IRInstructions.unary(Opcode.INC, IRDataType.BYTE, 99),
|
||||
IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 10, indexRegType)),
|
||||
IRInstructions.returnVoid()
|
||||
))
|
||||
irProg.chunks().single().instructions.count { it.opcode==Opcode.STOREX } shouldBe 1
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.count { it.opcode==Opcode.STOREX } shouldBe 0
|
||||
instr.count { it.opcode==Opcode.LOADX } shouldBe 1
|
||||
instr.size shouldBe 3
|
||||
}
|
||||
|
||||
test("do not coalesce LOADX/STOREX when index differs") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 10, indexRegType)),
|
||||
IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 11, indexRegType)),
|
||||
IRInstructions.returnVoid()
|
||||
))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.count { it.opcode==Opcode.LOADX } shouldBe 1
|
||||
instr.count { it.opcode==Opcode.STOREX } shouldBe 1
|
||||
}
|
||||
|
||||
test("do not coalesce LOADX/STOREX with intervening read of value") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 10, indexRegType)),
|
||||
IRInstructions.binary(Opcode.ADDR, IRDataType.BYTE, 1, 2),
|
||||
IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 10, indexRegType)),
|
||||
IRInstructions.returnVoid()
|
||||
))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.count { it.opcode==Opcode.STOREX } shouldBe 1
|
||||
}
|
||||
|
||||
test("do not coalesce LOADX/STOREX across chunks single-chunk") {
|
||||
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
|
||||
c1 += IRInstructions.loadMemory(Opcode.LOADX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 10, indexRegType))
|
||||
val c2 = IRCodeChunk("other", null)
|
||||
c2 += IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 10, indexRegType))
|
||||
c2 += IRInstructions.returnVoid()
|
||||
val irProg = makeIRProgram(listOf(c1, c2))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val all = irProg.chunks().flatMap { it.instructions }
|
||||
all.count { it.opcode==Opcode.LOADX } shouldBe 1
|
||||
all.count { it.opcode==Opcode.STOREX } shouldBe 1
|
||||
}
|
||||
|
||||
test("do not fold storem+loadm same symbol different offsets") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 5, IRMemory.direct("myVar", 0)),
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 6, IRMemory.direct("myVar", 1)),
|
||||
IRInstructions.returnVoid()
|
||||
))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.count { it.opcode==Opcode.STOREM } shouldBe 1
|
||||
instr.count { it.opcode==Opcode.LOADM } shouldBe 1
|
||||
instr.count { it.opcode==Opcode.LOADR } shouldBe 0
|
||||
}
|
||||
|
||||
test("do not fold storem+loadm across chunks single-chunk") {
|
||||
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
|
||||
c1 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 5, IRMemory.direct("myVar"))
|
||||
val c2 = IRCodeChunk("other", null)
|
||||
c2 += IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 6, IRMemory.direct("myVar"))
|
||||
c2 += IRInstructions.returnVoid()
|
||||
val irProg = makeIRProgram(listOf(c1, c2))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val all = irProg.chunks().flatMap { it.instructions }
|
||||
all.count { it.opcode==Opcode.STOREM } shouldBe 1
|
||||
all.count { it.opcode==Opcode.LOADM } shouldBe 1
|
||||
all.count { it.opcode==Opcode.LOADR } shouldBe 0
|
||||
}
|
||||
|
||||
test("do not fold storem+loadm different types") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 5, IRMemory.direct("myVar")),
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.WORD, 6, IRMemory.direct("myVar")),
|
||||
IRInstructions.returnVoid()
|
||||
))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.count { it.opcode==Opcode.STOREM } shouldBe 1
|
||||
instr.count { it.opcode==Opcode.LOADM } shouldBe 1
|
||||
instr.count { it.opcode==Opcode.LOADR } shouldBe 0
|
||||
}
|
||||
|
||||
test("dead store analysis sees reads via secondary operand slots") {
|
||||
// r10 is only read as the INDEX register inside the LOADX memory reference. The dead
|
||||
// store detector must see that read, otherwise the first LOAD r10 is wrongly removed
|
||||
// and the array would be indexed with an uninitialized register.
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.load(indexRegType, 10, 3),
|
||||
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 10, indexRegType)),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 1, IRMemory.direct("sym")),
|
||||
IRInstructions.load(indexRegType, 10, 7),
|
||||
IRInstructions.binaryImmediate(Opcode.ADD, indexRegType, 10, 1),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, indexRegType, 10, IRMemory.direct("sym2")),
|
||||
IRInstructions.returnVoid()
|
||||
))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.count { it.opcode == Opcode.LOADX } shouldBe 1
|
||||
instr.any { it.opcode == Opcode.LOAD && it.requireImmediateInt() == 3 } shouldBe true
|
||||
}
|
||||
|
||||
test("dead store to the same register is removed") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.load(IRDataType.BYTE, 1, 5),
|
||||
IRInstructions.load(IRDataType.BYTE, 1, 10),
|
||||
IRInstructions.unary(Opcode.INC, IRDataType.BYTE, 1),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 1, IRMemory.direct("sym")),
|
||||
IRInstructions.returnVoid()
|
||||
))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.any { it.opcode == Opcode.LOAD && it.requireImmediateInt() == 5 } shouldBe false
|
||||
instr.any { it.opcode == Opcode.LOAD && it.requireImmediateInt() == 10 } shouldBe true
|
||||
}
|
||||
|
||||
test("loadr forwarding does not delete live-out source") {
|
||||
// r1 is live-out to next chunk, so LOAD r1 should be kept even though r1 has single use in first chunk.
|
||||
// The forwarder is conservative: it keeps the source live when it may be needed in a successor chunk,
|
||||
// preventing the previous bug where the source LOAD was deleted.
|
||||
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
|
||||
c1 += IRInstructions.load(IRDataType.BYTE, 1, 42)
|
||||
c1 += IRInstructions.move(IRDataType.BYTE, 2, 1)
|
||||
c1 += IRInstructions.jump(codeLabel("next"))
|
||||
val c2 = IRCodeChunk("next", null)
|
||||
c2 += IRInstructions.move(IRDataType.BYTE, 3, 1)
|
||||
c2 += IRInstructions.returnVoid()
|
||||
val irProg = makeIRProgram(listOf(c1, c2))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val all = irProg.chunks().flatMap { it.instructions }
|
||||
// LOAD r1 must be kept because r1 is live-out; whether LOADR is forwarded to LOAD r2 is secondary.
|
||||
all.count { it.opcode == Opcode.LOAD && it.dest?.register == VirtualRegister.int(1) } shouldBe 1
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,537 @@
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.shouldNotBe
|
||||
import io.kotest.matchers.string.shouldContain
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.OutputType
|
||||
import prog8.code.core.Position
|
||||
import prog8.code.core.ZeropageType
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8.codegen.intermediate.RegisterPacker
|
||||
import prog8.intermediate.*
|
||||
|
||||
class TestRegisterPacker: FunSpec({
|
||||
|
||||
fun makeIRProgram(chunks: List<IRCodeChunkBase>): IRProgram {
|
||||
require(chunks.first().label=="p8b_main.p8s_start")
|
||||
val block = IRBlock("p8b_main", false, IRBlock.Options(), Position.DUMMY)
|
||||
val sub = IRSubroutine("p8b_main.p8s_start", emptyList(), emptyList(), Position.DUMMY)
|
||||
chunks.forEach { sub += it }
|
||||
block += sub
|
||||
val target = VMTarget()
|
||||
val options = CompilationOptions.builder(target)
|
||||
.output(OutputType.RAW)
|
||||
.zeropage(ZeropageType.DONTUSE)
|
||||
.noSysInit(true)
|
||||
.compilerVersion("99.99")
|
||||
.build()
|
||||
val prog = IRProgram("test", IRSymbolTable(), options, target)
|
||||
prog.addBlock(block)
|
||||
prog.linkChunks()
|
||||
prog.validate()
|
||||
return prog
|
||||
}
|
||||
|
||||
fun makeIRProgram(instructions: List<IRInstruction>): IRProgram {
|
||||
val chunk = IRCodeChunk("p8b_main.p8s_start", null)
|
||||
instructions.forEach { chunk += it }
|
||||
return makeIRProgram(listOf(chunk as IRCodeChunkBase))
|
||||
}
|
||||
|
||||
fun IRProgram.chunks(): List<IRCodeChunkBase> = this.allSubs().flatMap { it.chunks }.toList()
|
||||
|
||||
fun IRProgram.instructions(): List<IRInstruction> = chunks().flatMap { it.instructions }
|
||||
|
||||
fun IRProgram.registerAccesses(): List<RegisterOperand> = instructions().flatMap { it.registerAccesses }
|
||||
|
||||
test("simple coalescing") {
|
||||
// r1=[0,1], r2=[2,3] non-overlapping -> same packed slot
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.load(IRDataType.BYTE, 1, 10),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 1, IRMemory.direct("sym")),
|
||||
IRInstructions.load(IRDataType.BYTE, 2, 20),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 2, IRMemory.direct("sym2"))
|
||||
))
|
||||
RegisterPacker.pack(irProg)
|
||||
val regs = irProg.registerAccesses().map { it.register }
|
||||
// After packing, all register accesses should refer to the same register (both r1 and r2 mapped to 1 slot)
|
||||
regs.toSet().size shouldBe 1
|
||||
val used = irProg.registersUsed()
|
||||
(used.readRegs.keys + used.writeRegs.keys).size shouldBe 1
|
||||
}
|
||||
|
||||
test("no coalescing with overlapping intervals") {
|
||||
// r1=[0,3], r2=[1,4] overlap -> 2 distinct slots needed
|
||||
// LOADR uses both at same time -> destination must differ from source
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.load(IRDataType.BYTE, 1, 10),
|
||||
IRInstructions.load(IRDataType.BYTE, 2, 20),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 1, IRMemory.direct("s")),
|
||||
IRInstructions.move(IRDataType.BYTE, 1, 2)
|
||||
))
|
||||
RegisterPacker.pack(irProg)
|
||||
val instrs = irProg.instructions()
|
||||
val loadr = instrs.first { it.opcode == Opcode.LOADR }
|
||||
loadr.requireDest().register shouldNotBe loadr.requireSrcA().register
|
||||
}
|
||||
|
||||
test("cross-chunk liveness") {
|
||||
// Chunk A: def r1, def r2, use r1
|
||||
// r1=[0,2] (lives only in A), r2=defined but not used in A,
|
||||
// r2 is live-out from A because it's used in B
|
||||
// Chunk B: def r3, use r2, use r3
|
||||
// r2=[3,4] (first read in B), r3=[3,5] (lives only in B)
|
||||
// Non-overlapping: r1=[0,2] and r2=[3,4] -> can share
|
||||
// Overlapping: r2=[3,4] and r3=[3,5] -> must differ
|
||||
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
|
||||
c1 += IRInstructions.load(IRDataType.BYTE, 1, 10)
|
||||
c1 += IRInstructions.load(IRDataType.BYTE, 2, 20)
|
||||
c1 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 1, IRMemory.direct("sym"))
|
||||
|
||||
val c2 = IRCodeChunk(null, null)
|
||||
c2 += IRInstructions.load(IRDataType.BYTE, 3, 30)
|
||||
c2 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 2, IRMemory.direct("sym2"))
|
||||
c2 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 3, IRMemory.direct("sym3"))
|
||||
|
||||
val irProg = makeIRProgram(listOf(c1, c2))
|
||||
RegisterPacker.pack(irProg)
|
||||
val distinctRegs = irProg.registerAccesses().map { it.register }.toSet()
|
||||
|
||||
// r1 and r2 can share a slot (non-overlapping), r3 must differ from r2 (overlapping)
|
||||
// Results in exactly 2 distinct slots used
|
||||
distinctRegs.size shouldBe 2
|
||||
}
|
||||
|
||||
test("different types must not share the same slot number") {
|
||||
// BYTE R5=[0,1] and POINTER R1=[2,3] in same subroutine.
|
||||
// Non-overlapping but different incompatible types.
|
||||
// Packer must NOT assign both to slot 1 (would cause type conflict).
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.load(IRDataType.BYTE, 5, 10),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 5, IRMemory.direct("s")),
|
||||
IRInstructions.load(IRDataType.POINTER, 1, 0x1000),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.POINTER, 1, IRMemory.direct("s2"))
|
||||
))
|
||||
RegisterPacker.pack(irProg)
|
||||
// After packing, usedRegisters() must not throw (internal type consistency)
|
||||
// and all registers must have exactly one type each
|
||||
val chunkTypes = irProg.chunks().first().usedRegisters().regsTypes
|
||||
chunkTypes.size shouldBe 2 // exactly 2 distinct registers
|
||||
// Each register should appear with exactly one type (no conflicts)
|
||||
chunkTypes.values.toSet().size shouldBe 2 // BYTE and POINTER, each on different regs
|
||||
}
|
||||
|
||||
test("unconditional branch only one successor") {
|
||||
// Chunk A -> unconditional jump -> chunk C
|
||||
// Chunk B is unreachable from A (but still a predecessor of C via fall-through)
|
||||
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
|
||||
c1 += IRInstructions.load(IRDataType.BYTE, 1, 10)
|
||||
c1 += IRInstructions.jump(codeLabel("label_c"))
|
||||
|
||||
val c2 = IRCodeChunk("label_b", null)
|
||||
c2 += IRInstructions.load(IRDataType.BYTE, 2, 20)
|
||||
|
||||
val c3 = IRCodeChunk("label_c", null)
|
||||
c3 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 1, IRMemory.direct("sym"))
|
||||
c3 += IRInstructions.load(IRDataType.BYTE, 2, 99)
|
||||
c3 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 2, IRMemory.direct("sym2"))
|
||||
|
||||
val irProg = makeIRProgram(listOf(c1, c2, c3))
|
||||
RegisterPacker.pack(irProg)
|
||||
// r1 is live-out of A into C via JUMP; r2 is NOT live-in to C from A via the branch
|
||||
// but IS live-in to C from B's fall-through -> conservative: r2 is live-in to C
|
||||
// r1 and r3 both defined across non-overlapping chunks
|
||||
// Just verify no crash and packing happened
|
||||
val instrs = irProg.instructions()
|
||||
instrs shouldNotBe emptyList<IRInstruction>()
|
||||
}
|
||||
|
||||
test("conditional branch two successors") {
|
||||
// Chunk A -> conditional branch to C, fall-through to B
|
||||
// Liveness flows from A to both B and C
|
||||
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
|
||||
c1 += IRInstructions.compareImmediate(IRDataType.BYTE, 1, 0)
|
||||
c1 += IRInstructions.branch(Opcode.BSTNE, codeLabel("label_c"))
|
||||
|
||||
val c2 = IRCodeChunk(null, null) // fall-through (B)
|
||||
c2 += IRInstructions.load(IRDataType.BYTE, 2, 42)
|
||||
c2 += IRInstructions.jump(codeLabel("label_end"))
|
||||
|
||||
val c3 = IRCodeChunk("label_c", null)
|
||||
c3 += IRInstructions.load(IRDataType.BYTE, 2, 99)
|
||||
|
||||
val c4 = IRCodeChunk("label_end", null)
|
||||
c4 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 2, IRMemory.direct("sym"))
|
||||
|
||||
val irProg = makeIRProgram(listOf(c1, c2, c3, c4))
|
||||
RegisterPacker.pack(irProg)
|
||||
val instrs = irProg.instructions()
|
||||
instrs shouldNotBe emptyList<IRInstruction>()
|
||||
}
|
||||
|
||||
test("multi-chunk register reduction") {
|
||||
// 3 chunks with 8 registers, non-overlapping patterns
|
||||
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
|
||||
c1 += IRInstructions.load(IRDataType.BYTE, 1, 10)
|
||||
c1 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 1, IRMemory.direct("sym"))
|
||||
c1 += IRInstructions.load(IRDataType.BYTE, 2, 20)
|
||||
c1 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 2, IRMemory.direct("sym2"))
|
||||
c1 += IRInstructions.jump(codeLabel("cont"))
|
||||
|
||||
val c2 = IRCodeChunk("cont", null)
|
||||
c2 += IRInstructions.load(IRDataType.BYTE, 3, 30)
|
||||
c2 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 3, IRMemory.direct("sym3"))
|
||||
c2 += IRInstructions.load(IRDataType.BYTE, 4, 40)
|
||||
c2 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 4, IRMemory.direct("sym4"))
|
||||
c2 += IRInstructions.jump(codeLabel("cont2"))
|
||||
|
||||
val c3 = IRCodeChunk("cont2", null)
|
||||
c3 += IRInstructions.load(IRDataType.BYTE, 5, 50)
|
||||
c3 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 5, IRMemory.direct("sym5"))
|
||||
c3 += IRInstructions.load(IRDataType.BYTE, 6, 60)
|
||||
c3 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 6, IRMemory.direct("sym6"))
|
||||
|
||||
val irProg = makeIRProgram(listOf(c1, c2, c3))
|
||||
RegisterPacker.pack(irProg)
|
||||
val distinctRegs = irProg.registerAccesses().map { it.register }.toSet()
|
||||
// Each chunk defines and uses 2 regs. Within each chunk the regs are non-overlapping
|
||||
// (def rX at i, use at i+1, then def rY at i+2). Across chunks they don't overlap either
|
||||
// (sequential, jumps between chunks). So all 6 regs can share 1 slot.
|
||||
distinctRegs.size shouldBe 1
|
||||
}
|
||||
|
||||
test("floating point registers packed independently") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.loadFloat(1, 1.0),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.FLOAT, 1, IRMemory.direct("sym")),
|
||||
IRInstructions.loadFloat(2, 2.0),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.FLOAT, 2, IRMemory.direct("sym2"))
|
||||
))
|
||||
RegisterPacker.pack(irProg)
|
||||
// Both fp regs have non-overlapping intervals -> same packed slot
|
||||
val fpRegs = irProg.registerAccesses().filter { it.isFloat }.map { it.register }
|
||||
fpRegs.distinct().size shouldBe 1
|
||||
}
|
||||
|
||||
test("function call args are remapped") {
|
||||
val callSite = CallSite(
|
||||
CallTarget.SystemCall(0),
|
||||
arguments = listOf(
|
||||
Calls.argument(1, IRDataType.BYTE),
|
||||
Calls.argument(2, IRDataType.BYTE)
|
||||
),
|
||||
results = listOf(Calls.result(3, IRDataType.BYTE))
|
||||
)
|
||||
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.load(IRDataType.BYTE, 1, 10),
|
||||
IRInstructions.load(IRDataType.BYTE, 2, 20),
|
||||
// Non-overlapping: r1=[0,0], r2=[1,1], r3=live only as return from SYSCALL
|
||||
IRInstructions.call(Opcode.SYSCALL, callSite)
|
||||
))
|
||||
RegisterPacker.pack(irProg)
|
||||
val instrs = irProg.instructions()
|
||||
val syscall = instrs.first { it.opcode == Opcode.SYSCALL }
|
||||
val remapped = syscall.requireCallSite()
|
||||
// Should have same packed register numbers
|
||||
remapped.arguments.map { it.source.register }.toSet().size shouldBe 2
|
||||
remapped.results.single().destination!!.registerNumber shouldBe 3
|
||||
}
|
||||
|
||||
test("empty subroutine no crash") {
|
||||
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
|
||||
val irProg = makeIRProgram(listOf(c1))
|
||||
// Should not throw
|
||||
RegisterPacker.pack(irProg)
|
||||
}
|
||||
|
||||
test("no registers used") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.simple(Opcode.NOP),
|
||||
IRInstructions.simple(Opcode.NOP)
|
||||
))
|
||||
RegisterPacker.pack(irProg)
|
||||
irProg.instructions().size shouldBe 2
|
||||
}
|
||||
|
||||
test("call arguments with mixed types must not share same slot") {
|
||||
// Regression: call site registers with incompatible types (WORD vs LONG)
|
||||
// in the same chunk must be assigned different packed slots.
|
||||
// The conflict graph must track call site register types.
|
||||
val callSite = CallSite(
|
||||
CallTarget.SystemCall(0),
|
||||
arguments = listOf(
|
||||
Calls.argument(1, IRDataType.WORD),
|
||||
Calls.argument(2, IRDataType.LONG)
|
||||
)
|
||||
)
|
||||
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.load(IRDataType.WORD, 1, 100),
|
||||
IRInstructions.call(Opcode.SYSCALL, callSite)
|
||||
))
|
||||
RegisterPacker.pack(irProg)
|
||||
// usedRegisters() must not throw (type consistency within chunk)
|
||||
val used = irProg.registersUsed()
|
||||
val regs = (used.readRegs.keys + used.writeRegs.keys).map { it.num }.toSet()
|
||||
// Must have at least 2 distinct packed slots (WORD and LONG cannot share)
|
||||
regs.size shouldBe 2
|
||||
}
|
||||
|
||||
test("call results with mixed types must not share same slot") {
|
||||
// Same as above but for return registers
|
||||
val callSite = CallSite(
|
||||
CallTarget.SystemCall(0),
|
||||
results = listOf(
|
||||
Calls.result(1, IRDataType.WORD),
|
||||
Calls.result(2, IRDataType.LONG)
|
||||
)
|
||||
)
|
||||
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.call(Opcode.SYSCALL, callSite),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.WORD, 1, IRMemory.direct("s")),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.LONG, 2, IRMemory.direct("s2"))
|
||||
))
|
||||
RegisterPacker.pack(irProg)
|
||||
// usedRegisters() must not throw
|
||||
val used = irProg.registersUsed()
|
||||
val regs = (used.readRegs.keys + used.writeRegs.keys).map { it.num }.toSet()
|
||||
regs.size shouldBe 2
|
||||
}
|
||||
|
||||
test("WORD and LONG non-overlapping in same chunk must not share slot") {
|
||||
// Regression: two registers with incompatible types (WORD vs LONG) but
|
||||
// non-overlapping live ranges in the same chunk must NOT be assigned
|
||||
// the same packed slot, because usedRegisters() enforces a single type
|
||||
// per register number within a chunk.
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.load(IRDataType.WORD, 1, 100),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.WORD, 1, IRMemory.direct("s")),
|
||||
IRInstructions.load(IRDataType.LONG, 2, 0x10000),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.LONG, 2, IRMemory.direct("s2"))
|
||||
))
|
||||
RegisterPacker.pack(irProg)
|
||||
// usedRegisters() must not throw
|
||||
val used = irProg.registersUsed()
|
||||
val types = used.regsTypes
|
||||
// Exactly 2 registers, one WORD and one LONG (different slots)
|
||||
types.size shouldBe 2
|
||||
types.values.toSet() shouldBe setOf(IRDataType.WORD, IRDataType.LONG)
|
||||
}
|
||||
|
||||
test("cross-subroutine slot type is preserved via putIfAbsent") {
|
||||
// Regression: when a slot is first assigned a type (e.g. POINTER),
|
||||
// subsequent compatible assignments (WORD via typesCompatible) must not
|
||||
// overwrite the slot's type. Otherwise a later LONG interval would be
|
||||
// blocked (WORD vs LONG incompatible), forcing it to a different slot,
|
||||
// and registersUsed() would see the same slot with both POINTER and
|
||||
// WORD types across subroutines → should be compatible (POINTER bridges both).
|
||||
fun makeMultiSubProgram(sub1Chunks: List<IRCodeChunkBase>, sub2Chunks: List<IRCodeChunkBase>): IRProgram {
|
||||
val target = VMTarget()
|
||||
val options = CompilationOptions.builder(target)
|
||||
.output(OutputType.RAW)
|
||||
.zeropage(ZeropageType.DONTUSE)
|
||||
.noSysInit(true)
|
||||
.compilerVersion("99.99")
|
||||
.build()
|
||||
val prog = IRProgram("test", IRSymbolTable(), options, target)
|
||||
val block = IRBlock("p8b_main", false, IRBlock.Options(), Position.DUMMY)
|
||||
val sub1 = IRSubroutine("p8b_main.sub1", emptyList(), emptyList(), Position.DUMMY)
|
||||
sub1Chunks.forEach { sub1 += it }
|
||||
val sub2 = IRSubroutine("p8b_main.sub2", emptyList(), emptyList(), Position.DUMMY)
|
||||
sub2Chunks.forEach { sub2 += it }
|
||||
block += sub1
|
||||
block += sub2
|
||||
prog.addBlock(block)
|
||||
prog.linkChunks()
|
||||
prog.validate()
|
||||
return prog
|
||||
}
|
||||
|
||||
// Sub 1: POINTER interval (gets slot ≥ startSlot), and then the same slot again with WORD
|
||||
val c1 = IRCodeChunk("p8b_main.sub1", null)
|
||||
c1 += IRInstructions.load(IRDataType.POINTER, 100, 0x1000)
|
||||
c1 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.POINTER, 100, IRMemory.direct("sp"))
|
||||
c1 += IRInstructions.load(IRDataType.WORD, 101, 200)
|
||||
c1 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.WORD, 101, IRMemory.direct("sw"))
|
||||
|
||||
// Sub 2: LONG interval that should be able to share the same slot as sub 1's POINTER
|
||||
val c2 = IRCodeChunk("p8b_main.sub2", null)
|
||||
c2 += IRInstructions.load(IRDataType.LONG, 200, 0x100000)
|
||||
c2 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.LONG, 200, IRMemory.direct("sl"))
|
||||
|
||||
val irProg = makeMultiSubProgram(listOf(c1), listOf(c2))
|
||||
RegisterPacker.pack(irProg)
|
||||
// registersUsed() must not throw: POINTER and WORD compatible,
|
||||
// POINTER and LONG compatible, and putIfAbsent keeps the slot type
|
||||
// as POINTER so WORD and LONG intervals can share the same slot.
|
||||
val used = irProg.registersUsed()
|
||||
used.regsTypes.size shouldBe 2 // POINTER (or POINTER+WORD+LONG sharing) + WORD
|
||||
}
|
||||
|
||||
test("original register numbers must not collide with packed slots") {
|
||||
// Regression: pre-existing low register numbers (e.g. r5) in the program
|
||||
// must not collide with packed slot numbers. Packed slots start at
|
||||
// maxReg+1 to avoid this.
|
||||
val irProg = makeIRProgram(listOf(
|
||||
// Register 5 is used directly (not packed? actually it HAS an interval and gets packed)
|
||||
IRInstructions.load(IRDataType.BYTE, 5, 99),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 5, IRMemory.direct("s")),
|
||||
// Register 1 and 2 can be packed
|
||||
IRInstructions.load(IRDataType.WORD, 1, 100),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.WORD, 1, IRMemory.direct("s2")),
|
||||
IRInstructions.load(IRDataType.WORD, 2, 200),
|
||||
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.WORD, 2, IRMemory.direct("s3"))
|
||||
))
|
||||
RegisterPacker.pack(irProg)
|
||||
// after packing:
|
||||
// - all registers should have been packed to slots > 5 (since maxReg=5, startSlot=6)
|
||||
// - no packed slot should equal an original register number
|
||||
val used = irProg.registersUsed()
|
||||
val allRegNums = (used.readRegs.keys + used.writeRegs.keys).map { it.num }.toSet()
|
||||
// packed slots start at 6, so no register should be 5 anymore
|
||||
(5 in allRegNums) shouldBe false
|
||||
// all registers should be >= 6
|
||||
for (rn in allRegNums)
|
||||
rn shouldNotBe 5
|
||||
}
|
||||
|
||||
test("strict registersUsed throws on incompatible types across subroutines") {
|
||||
// Regression: registersUsed() in strict mode (no packing applied) must detect
|
||||
// the same register number used with incompatible types in different subroutines.
|
||||
val target = VMTarget()
|
||||
val options = CompilationOptions.builder(target)
|
||||
.output(OutputType.RAW)
|
||||
.zeropage(ZeropageType.DONTUSE)
|
||||
.noSysInit(true)
|
||||
.compilerVersion("99.99")
|
||||
.build()
|
||||
val prog = IRProgram("test", IRSymbolTable(), options, target)
|
||||
val block = IRBlock("p8b_main", false, IRBlock.Options(), Position.DUMMY)
|
||||
|
||||
// Sub 1: uses r37 as POINTER
|
||||
val sub1 = IRSubroutine("p8b_main.sub1", emptyList(), emptyList(), Position.DUMMY)
|
||||
val c1 = IRCodeChunk("p8b_main.sub1", null)
|
||||
c1 += IRInstructions.load(IRDataType.POINTER, 37, 0x1000)
|
||||
c1 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.POINTER, 37, IRMemory.direct("sp"))
|
||||
sub1 += c1
|
||||
|
||||
// Sub 2: uses same r37 as BYTE (incompatible)
|
||||
val sub2 = IRSubroutine("p8b_main.sub2", emptyList(), emptyList(), Position.DUMMY)
|
||||
val c2 = IRCodeChunk("p8b_main.sub2", null)
|
||||
c2 += IRInstructions.load(IRDataType.BYTE, 37, 42)
|
||||
c2 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 37, IRMemory.direct("sb"))
|
||||
sub2 += c2
|
||||
|
||||
block += sub1
|
||||
block += sub2
|
||||
prog.addBlock(block)
|
||||
prog.linkChunks()
|
||||
prog.validate()
|
||||
|
||||
// No packing applied - strict mode should detect the incompatibility
|
||||
val ex = shouldThrow<IllegalArgumentException> {
|
||||
prog.registersUsed()
|
||||
}
|
||||
ex.message shouldContain "register r37 given multiple types"
|
||||
ex.message shouldContain "POINTER"
|
||||
ex.message shouldContain "BYTE"
|
||||
}
|
||||
|
||||
test("intra-subroutine cross-type register is skipped by packer") {
|
||||
// When a register has non-overlapping intervals with incompatible types (e.g.
|
||||
// BYTE and LONG in different chunks), the packer skips it entirely because its
|
||||
// single-entry packing map can't handle a register with multiple types.
|
||||
// After packing, the register retains its original number and is not rewritten.
|
||||
// registersUsed() in permissive mode uses putIfAbsent and should not throw.
|
||||
val target = VMTarget()
|
||||
val options = CompilationOptions.builder(target)
|
||||
.output(OutputType.RAW)
|
||||
.zeropage(ZeropageType.DONTUSE)
|
||||
.noSysInit(true)
|
||||
.compilerVersion("99.99")
|
||||
.build()
|
||||
val prog = IRProgram("test", IRSymbolTable(), options, target)
|
||||
val block = IRBlock("p8b_main", false, IRBlock.Options(), Position.DUMMY)
|
||||
|
||||
// Single subroutine with two chunks using same register with incompatible types
|
||||
val sub = IRSubroutine("p8b_main.sub1", emptyList(), emptyList(), Position.DUMMY)
|
||||
|
||||
val c1 = IRCodeChunk("p8b_main.sub1", null)
|
||||
c1 += IRInstructions.load(IRDataType.POINTER, 37, 0x1000)
|
||||
c1 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.POINTER, 37, IRMemory.direct("sp"))
|
||||
sub += c1
|
||||
|
||||
val c2 = IRCodeChunk("p8b_main.sub1.c2", c1)
|
||||
c2 += IRInstructions.load(IRDataType.BYTE, 37, 42)
|
||||
c2 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 37, IRMemory.direct("sb"))
|
||||
sub += c2
|
||||
|
||||
block += sub
|
||||
prog.addBlock(block)
|
||||
prog.linkChunks()
|
||||
prog.validate()
|
||||
|
||||
RegisterPacker.pack(prog)
|
||||
prog.wasPackingApplied = true
|
||||
|
||||
// registersUsed() in permissive mode: putIfAbsent, should not throw
|
||||
val used = prog.registersUsed()
|
||||
// r37 should still appear (was skipped by packer)
|
||||
used.regsTypes.containsKey(VirtualRegister.int(37)) shouldBe true
|
||||
}
|
||||
|
||||
test("disjoint intervals of same register must be merged into one contiguous range") {
|
||||
// Regression: if register r1 has disjoint intervals (written in chunk 1,
|
||||
// read in chunk 3 with a gap in chunk 2), the packer must merge them into
|
||||
// a single interval spanning the gap. Otherwise another register (r2) with
|
||||
// an interval in chunk 2 could share the same slot and clobber r1's value.
|
||||
val target = VMTarget()
|
||||
val options = CompilationOptions.builder(target)
|
||||
.output(OutputType.RAW)
|
||||
.zeropage(ZeropageType.DONTUSE)
|
||||
.noSysInit(true)
|
||||
.compilerVersion("99.99")
|
||||
.build()
|
||||
val prog = IRProgram("test", IRSymbolTable(), options, target)
|
||||
val block = IRBlock("p8b_main", false, IRBlock.Options(), Position.DUMMY)
|
||||
val sub = IRSubroutine("p8b_main.sub1", emptyList(), emptyList(), Position.DUMMY)
|
||||
|
||||
// Chunk 1: write r1 (value should persist to chunk 3)
|
||||
val c1 = IRCodeChunk("p8b_main.sub1", null)
|
||||
c1 += IRInstructions.load(IRDataType.BYTE, 1, 42)
|
||||
c1 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 1, IRMemory.direct("s1"))
|
||||
sub += c1
|
||||
|
||||
// Chunk 2: write and read r2 (this interval falls in the gap between r1's uses)
|
||||
val c2 = IRCodeChunk("p8b_main.sub1.c2", c1)
|
||||
c2 += IRInstructions.load(IRDataType.BYTE, 2, 99)
|
||||
c2 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 2, IRMemory.direct("s2"))
|
||||
sub += c2
|
||||
|
||||
// Chunk 3: read r1 (uses the value from chunk 1)
|
||||
val c3 = IRCodeChunk("p8b_main.sub1.c3", c2)
|
||||
c3 += IRInstructions.move(IRDataType.BYTE, 3, 1)
|
||||
c3 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 3, IRMemory.direct("s3"))
|
||||
sub += c3
|
||||
|
||||
block += sub
|
||||
prog.addBlock(block)
|
||||
prog.linkChunks()
|
||||
prog.validate()
|
||||
|
||||
RegisterPacker.pack(prog)
|
||||
|
||||
// After packing, r1 and r2 must be in DIFFERENT slots because r1's
|
||||
// merged interval spans [0..?] covering the gap, preventing r2
|
||||
// from sharing the same slot.
|
||||
val instructions = prog.instructions()
|
||||
// Find the register operands used for the STOREM to s1 (was r1) and s2 (was r2)
|
||||
val s1Reg = instructions.first { it.memory?.symbolName=="s1" }.requireSrcA().register
|
||||
val s2Reg = instructions.first { it.memory?.symbolName=="s2" }.requireSrcA().register
|
||||
// They must be different (can't share a slot)
|
||||
s1Reg shouldNotBe s2Reg
|
||||
}
|
||||
})
|
||||
@@ -2,15 +2,15 @@ import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.shouldNotBe
|
||||
import prog8.code.*
|
||||
import prog8.code.StNodeType
|
||||
import prog8.code.SymbolTableMaker
|
||||
import prog8.code.ast.*
|
||||
import prog8.code.core.*
|
||||
import prog8.code.source.SourceCode
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8.codegen.intermediate.prefixScopedName
|
||||
import prog8.codegen.intermediate.prefixSymbols
|
||||
import prog8.codegen.intermediate.typePrefixChar
|
||||
import prog8tests.helpers.DummyMemsizer
|
||||
import prog8tests.helpers.DummyStringEncoder
|
||||
|
||||
|
||||
class TestSymbolPrefixer : FunSpec({
|
||||
@@ -59,10 +59,10 @@ class TestSymbolPrefixer : FunSpec({
|
||||
// --- full program prefixing tests ---
|
||||
|
||||
test("prefixSymbols prefixes a simple program") {
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
|
||||
val varX = PtVariable("x", DataType.UBYTE, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY)
|
||||
val varX = PtVariable("x", DataType.UBYTE, false, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY)
|
||||
sub.add(varX)
|
||||
block.add(sub)
|
||||
program.add(block)
|
||||
@@ -84,7 +84,7 @@ class TestSymbolPrefixer : FunSpec({
|
||||
}
|
||||
|
||||
test("prefixSymbols handles subroutine parameters") {
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
|
||||
val param = PtSubroutineParameter("arg", DataType.UBYTE, null, Position.DUMMY)
|
||||
@@ -100,7 +100,7 @@ class TestSymbolPrefixer : FunSpec({
|
||||
}
|
||||
|
||||
test("prefixSymbols handles constants") {
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val constant = PtConstant("SIZE", DataType.UWORD, 100.0, null, Position.DUMMY)
|
||||
block.add(constant)
|
||||
@@ -114,7 +114,7 @@ class TestSymbolPrefixer : FunSpec({
|
||||
}
|
||||
|
||||
test("prefixSymbols handles labels") {
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
|
||||
val label = PtLabel("mylabel", Position.DUMMY)
|
||||
@@ -130,7 +130,7 @@ class TestSymbolPrefixer : FunSpec({
|
||||
}
|
||||
|
||||
test("prefixSymbols handles generated labels without prefixing them") {
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
|
||||
val genLabel = PtLabel("p8_label_gen_42", Position.DUMMY)
|
||||
@@ -146,7 +146,7 @@ class TestSymbolPrefixer : FunSpec({
|
||||
}
|
||||
|
||||
test("prefixSymbols handles struct declarations") {
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val struct = PtStructDecl("Point", listOf(
|
||||
PtStructField(DataType.UBYTE, "x", null),
|
||||
@@ -163,7 +163,7 @@ class TestSymbolPrefixer : FunSpec({
|
||||
}
|
||||
|
||||
test("prefixSymbols handles asmsub") {
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val asmsub = PtAsmSub("routine", PtAsmSub.Address(null, null, 0x5000u), setOf(CpuRegister.Y), emptyList(), emptyList(), false, Position.DUMMY)
|
||||
block.add(asmsub)
|
||||
@@ -177,7 +177,7 @@ class TestSymbolPrefixer : FunSpec({
|
||||
}
|
||||
|
||||
test("prefixSymbols handles memmapped variables") {
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val memvar = PtMemMapped("reg", DataType.UBYTE, 0xd020u, null, Position.DUMMY)
|
||||
block.add(memvar)
|
||||
@@ -193,7 +193,7 @@ class TestSymbolPrefixer : FunSpec({
|
||||
// --- noSymbolPrefixing tests ---
|
||||
|
||||
test("noSymbolPrefixing blocks keep their names intact") {
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val libOptions = PtBlock.Options(noSymbolPrefixing = true)
|
||||
val libBlock = PtBlock("lib", false, SourceCode.Generated("test"), libOptions, Position.DUMMY)
|
||||
val libSub = PtSub("func", emptyList(), emptyList(), Position.DUMMY)
|
||||
@@ -209,7 +209,7 @@ class TestSymbolPrefixer : FunSpec({
|
||||
}
|
||||
|
||||
test("references from user block to noSymbolPrefixing block are not prefixed") {
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
|
||||
// Library block with no symbol prefixing
|
||||
val libOptions = PtBlock.Options(noSymbolPrefixing = true)
|
||||
@@ -240,7 +240,7 @@ class TestSymbolPrefixer : FunSpec({
|
||||
}
|
||||
|
||||
test("references from noSymbolPrefixing block to regular block get prefixed") {
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
|
||||
// Regular user block
|
||||
val userBlock = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
@@ -267,10 +267,10 @@ class TestSymbolPrefixer : FunSpec({
|
||||
}
|
||||
|
||||
test("identifier references are prefixed according to their target type") {
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
|
||||
val varX = PtVariable("x", DataType.UBYTE, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY)
|
||||
val varX = PtVariable("x", DataType.UBYTE, false, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY)
|
||||
sub.add(varX)
|
||||
|
||||
// Add an identifier reference to the variable
|
||||
@@ -290,7 +290,7 @@ class TestSymbolPrefixer : FunSpec({
|
||||
}
|
||||
|
||||
test("prefixSymbols exception for unknown identifier") {
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
|
||||
val idRef = PtIdentifier("nonexistent", DataType.UBYTE, Position.DUMMY)
|
||||
@@ -308,7 +308,7 @@ class TestSymbolPrefixer : FunSpec({
|
||||
})
|
||||
|
||||
private fun basicTestOptions(): CompilationOptions {
|
||||
val target = prog8.code.target.VMTarget()
|
||||
val target = VMTarget()
|
||||
return CompilationOptions.builder(target)
|
||||
.output(OutputType.RAW)
|
||||
.zeropage(ZeropageType.DONTUSE)
|
||||
|
||||
@@ -5,14 +5,16 @@ import prog8.code.SymbolTableMaker
|
||||
import prog8.code.ast.*
|
||||
import prog8.code.core.*
|
||||
import prog8.code.source.SourceCode
|
||||
import prog8.code.target.Cx16Target
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8.codegen.vm.VmAssemblyProgram
|
||||
import prog8.codegen.vm.VmCodeGen
|
||||
import prog8.intermediate.IRSubroutine
|
||||
import prog8.intermediate.Opcode
|
||||
import prog8.intermediate.codeAddress
|
||||
import prog8.intermediate.requireCallSite
|
||||
import prog8.intermediate.toAddress
|
||||
import prog8tests.helpers.DummyMemsizer
|
||||
import prog8tests.helpers.DummyStringEncoder
|
||||
|
||||
import prog8tests.helpers.ErrorReporterForTests
|
||||
|
||||
class TestVmCodeGen: FunSpec({
|
||||
@@ -40,13 +42,15 @@ class TestVmCodeGen: FunSpec({
|
||||
// xx += cx16.r0
|
||||
// }
|
||||
//}
|
||||
val compTarget = VMTarget()
|
||||
val codegen = VmCodeGen(false)
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
|
||||
sub.add(PtVariable(
|
||||
"pi",
|
||||
DataType.UBYTE,
|
||||
false,
|
||||
ZeropageWish.DONTCARE,
|
||||
0u,
|
||||
false,
|
||||
@@ -56,7 +60,8 @@ class TestVmCodeGen: FunSpec({
|
||||
))
|
||||
sub.add(PtVariable(
|
||||
"particleX",
|
||||
DataType.arrayFor(BaseDataType.UBYTE),
|
||||
DataType.arrayFor(BaseDataType.UBYTE, compTarget),
|
||||
false,
|
||||
ZeropageWish.DONTCARE,
|
||||
0u,
|
||||
false,
|
||||
@@ -66,7 +71,8 @@ class TestVmCodeGen: FunSpec({
|
||||
))
|
||||
sub.add(PtVariable(
|
||||
"particleDX",
|
||||
DataType.arrayFor(BaseDataType.UBYTE),
|
||||
DataType.arrayFor(BaseDataType.UBYTE, compTarget),
|
||||
false,
|
||||
ZeropageWish.DONTCARE,
|
||||
0u,
|
||||
false,
|
||||
@@ -77,6 +83,7 @@ class TestVmCodeGen: FunSpec({
|
||||
sub.add(PtVariable(
|
||||
"xx",
|
||||
DataType.WORD,
|
||||
false,
|
||||
ZeropageWish.DONTCARE,
|
||||
0u,
|
||||
false,
|
||||
@@ -87,17 +94,17 @@ class TestVmCodeGen: FunSpec({
|
||||
|
||||
val assign = PtAugmentedAssign("+=", Position.DUMMY)
|
||||
val target = PtAssignTarget(false, Position.DUMMY).also {
|
||||
val targetIdx = PtArrayIndexer(DataType.UBYTE, Position.DUMMY).also { idx ->
|
||||
val targetIdx = PtArrayIndexer(DataType.UBYTE, false, Position.DUMMY).also { idx ->
|
||||
idx.add(PtIdentifier("main.start.particleX",
|
||||
DataType.arrayFor(BaseDataType.UBYTE),
|
||||
DataType.arrayFor(BaseDataType.UBYTE, compTarget),
|
||||
Position.DUMMY))
|
||||
idx.add(PtNumber(BaseDataType.UBYTE, 2.0, Position.DUMMY))
|
||||
}
|
||||
it.add(targetIdx)
|
||||
}
|
||||
val value = PtArrayIndexer(DataType.UBYTE, Position.DUMMY)
|
||||
val value = PtArrayIndexer(DataType.UBYTE, false, Position.DUMMY)
|
||||
value.add(PtIdentifier("main.start.particleDX",
|
||||
DataType.arrayFor(BaseDataType.UBYTE),
|
||||
DataType.arrayFor(BaseDataType.UBYTE, compTarget),
|
||||
Position.DUMMY))
|
||||
value.add(PtNumber(BaseDataType.UBYTE, 2.0, Position.DUMMY))
|
||||
assign.add(target)
|
||||
@@ -152,11 +159,11 @@ class TestVmCodeGen: FunSpec({
|
||||
// Pattern: @(ptr+offset) = @(ptr+offset) + val i.e. ptr[offset] += val
|
||||
// Non-zero offsets are used to avoid potential optimization of the address expression.
|
||||
val codegen = VmCodeGen(false)
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
|
||||
sub.add(PtVariable("ptr", DataType.pointer(BaseDataType.UBYTE), ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY))
|
||||
sub.add(PtVariable("val", DataType.UBYTE, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY))
|
||||
sub.add(PtVariable("ptr", DataType.pointer(BaseDataType.UBYTE), false, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY))
|
||||
sub.add(PtVariable("val", DataType.UBYTE, false, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY))
|
||||
|
||||
fun makeMemByteAssign(op: String, offset: Int): PtAugmentedAssign {
|
||||
// Use non-zero offset to avoid optimization
|
||||
@@ -203,12 +210,12 @@ class TestVmCodeGen: FunSpec({
|
||||
andrCount shouldBe 1
|
||||
xorrCount shouldBe 1
|
||||
|
||||
// Verify that the immediate-only variants are NOT used with reg2
|
||||
val wrongAdd = instructions.any { it.opcode == Opcode.ADD && it.reg2 != null }
|
||||
val wrongSub = instructions.any { it.opcode == Opcode.SUB && it.reg2 != null }
|
||||
val wrongOr = instructions.any { it.opcode == Opcode.OR && it.reg2 != null }
|
||||
val wrongAnd = instructions.any { it.opcode == Opcode.AND && it.reg2 != null }
|
||||
val wrongXor = instructions.any { it.opcode == Opcode.XOR && it.reg2 != null }
|
||||
// Verify that the immediate-only variants are NOT used with a register source
|
||||
val wrongAdd = instructions.any { it.opcode == Opcode.ADD && it.srcA != null }
|
||||
val wrongSub = instructions.any { it.opcode == Opcode.SUB && it.srcA != null }
|
||||
val wrongOr = instructions.any { it.opcode == Opcode.OR && it.srcA != null }
|
||||
val wrongAnd = instructions.any { it.opcode == Opcode.AND && it.srcA != null }
|
||||
val wrongXor = instructions.any { it.opcode == Opcode.XOR && it.srcA != null }
|
||||
|
||||
(wrongAdd || wrongSub || wrongOr || wrongAnd || wrongXor) shouldBe false
|
||||
}
|
||||
@@ -229,12 +236,13 @@ class TestVmCodeGen: FunSpec({
|
||||
// }
|
||||
//}
|
||||
val codegen = VmCodeGen(false)
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
|
||||
sub.add(PtVariable(
|
||||
"f1",
|
||||
DataType.FLOAT,
|
||||
false,
|
||||
ZeropageWish.DONTCARE,
|
||||
0u,
|
||||
false,
|
||||
@@ -301,12 +309,13 @@ class TestVmCodeGen: FunSpec({
|
||||
// }
|
||||
//}
|
||||
val codegen = VmCodeGen(false)
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
|
||||
sub.add(PtVariable(
|
||||
"f1",
|
||||
DataType.FLOAT,
|
||||
false,
|
||||
ZeropageWish.DONTCARE,
|
||||
0u,
|
||||
false,
|
||||
@@ -369,12 +378,13 @@ class TestVmCodeGen: FunSpec({
|
||||
// }
|
||||
//}
|
||||
val codegen = VmCodeGen(false)
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
|
||||
sub.add(PtVariable(
|
||||
"f1",
|
||||
DataType.FLOAT,
|
||||
false,
|
||||
ZeropageWish.DONTCARE,
|
||||
0u,
|
||||
false,
|
||||
@@ -425,12 +435,13 @@ class TestVmCodeGen: FunSpec({
|
||||
// }
|
||||
//}
|
||||
val codegen = VmCodeGen(false)
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
|
||||
sub.add(PtVariable(
|
||||
"sb1",
|
||||
DataType.BYTE,
|
||||
false,
|
||||
ZeropageWish.DONTCARE,
|
||||
0u,
|
||||
false,
|
||||
@@ -497,12 +508,13 @@ class TestVmCodeGen: FunSpec({
|
||||
// }
|
||||
//}
|
||||
val codegen = VmCodeGen(false)
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
|
||||
sub.add(PtVariable(
|
||||
"sb1",
|
||||
DataType.BYTE,
|
||||
false,
|
||||
ZeropageWish.DONTCARE,
|
||||
0u,
|
||||
false,
|
||||
@@ -565,12 +577,13 @@ class TestVmCodeGen: FunSpec({
|
||||
// }
|
||||
//}
|
||||
val codegen = VmCodeGen(false)
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
|
||||
sub.add(PtVariable(
|
||||
"ub1",
|
||||
DataType.BYTE,
|
||||
false,
|
||||
ZeropageWish.DONTCARE,
|
||||
0u,
|
||||
false,
|
||||
@@ -614,7 +627,7 @@ class TestVmCodeGen: FunSpec({
|
||||
// }
|
||||
//}
|
||||
val codegen = VmCodeGen(false)
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val extsub = PtAsmSub("routine", PtAsmSub.Address(null, null, 0x5000u), setOf(CpuRegister.Y), emptyList(), emptyList(), false, Position.DUMMY)
|
||||
block.add(extsub)
|
||||
@@ -634,15 +647,15 @@ class TestVmCodeGen: FunSpec({
|
||||
irChunks[0].instructions.size shouldBe 1
|
||||
val callInstr = irChunks[0].instructions[0]
|
||||
callInstr.opcode shouldBe Opcode.CALL
|
||||
callInstr.address shouldBe 0x5000u.toAddress()
|
||||
callInstr.requireCallSite().codeReference shouldBe codeAddress(0x5000u.toAddress())
|
||||
}
|
||||
|
||||
test("ir codegen for target 'virtual' produces no prefixed names") {
|
||||
val codegen = VmCodeGen(false)
|
||||
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
|
||||
sub.add(PtVariable("x", DataType.UBYTE, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY))
|
||||
sub.add(PtVariable("x", DataType.UBYTE, false, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY))
|
||||
block.add(sub)
|
||||
program.add(block)
|
||||
|
||||
@@ -664,4 +677,39 @@ class TestVmCodeGen: FunSpec({
|
||||
val irVar = irProg.st.allVariables().single()
|
||||
irVar.name shouldBe "main.start.x"
|
||||
}
|
||||
|
||||
test("ir codegen for target 'cx16' via newcodegen produces prefixed names") {
|
||||
val codegen = VmCodeGen(false)
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
|
||||
sub.add(PtVariable("x", DataType.UBYTE, false, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY))
|
||||
block.add(sub)
|
||||
program.add(block)
|
||||
|
||||
val target = Cx16Target()
|
||||
val options = CompilationOptions.builder(target)
|
||||
.output(OutputType.RAW)
|
||||
.zeropage(ZeropageType.DONTUSE)
|
||||
.floats(true)
|
||||
.compilerVersion("99.99")
|
||||
.newCodegen(true)
|
||||
.build()
|
||||
val st = SymbolTableMaker(program, options).make()
|
||||
val errors = ErrorReporterForTests()
|
||||
val result = codegen.generate(program, st, options, errors) as VmAssemblyProgram
|
||||
val irProg = result.irProgram
|
||||
|
||||
// Block label should be prefixed
|
||||
val irBlock = irProg.blocks.first()
|
||||
irBlock.label shouldBe "p8b_main"
|
||||
|
||||
// Subroutine label should be fully scoped and prefixed
|
||||
val irSub = irBlock.children.single() as IRSubroutine
|
||||
irSub.label shouldBe "p8b_main.p8s_start"
|
||||
|
||||
// Variable in IR symbol table should be fully scoped and prefixed
|
||||
val irVar = irProg.st.allVariables().single()
|
||||
irVar.name shouldBe "p8b_main.p8s_start.p8v_x"
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,96 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
id("application")
|
||||
id("com.peterabeles.gversion") version "1.11.0"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":codeCore"))
|
||||
implementation(project(":simpleAst"))
|
||||
implementation(project(":intermediate"))
|
||||
implementation(project(":codeGenIntermediate"))
|
||||
implementation("com.michael-bull.kotlin-result:kotlin-result-jvm:2.3.1")
|
||||
|
||||
testImplementation(testFixtures(project(":codeCore")))
|
||||
testImplementation("io.kotest:kotest-runner-junit5")
|
||||
}
|
||||
|
||||
|
||||
val debugPort = 8000
|
||||
val debugArgs = "-agentlib:jdwp=transport=dt_socket,server=y,address=8000,suspend=n,quiet=y"
|
||||
|
||||
val serverMainClassName = "prog8.codegen.m68k.MainKt"
|
||||
val applicationName = "prog8-m68kgen"
|
||||
|
||||
application {
|
||||
mainClass.set(serverMainClassName)
|
||||
description = "New codegen based on IR files"
|
||||
applicationDistribution.into("bin") {
|
||||
filePermissions {
|
||||
user {
|
||||
read=true
|
||||
execute=true
|
||||
write=true
|
||||
}
|
||||
other.execute = true
|
||||
group.execute = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":codeCore"))
|
||||
implementation(project(":intermediate"))
|
||||
implementation("com.github.ajalt.clikt:clikt:5.1.0")
|
||||
|
||||
// Test dependencies - Kotest BOM is provided by root build.gradle.kts
|
||||
testImplementation("io.kotest:kotest-runner-junit5")
|
||||
}
|
||||
|
||||
configurations.forEach { config ->
|
||||
config.resolutionStrategy {
|
||||
preferProjectModules()
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDir("src")
|
||||
resources.srcDir("resources")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.startScripts {
|
||||
applicationName = "prog8-m68kgen"
|
||||
}
|
||||
|
||||
tasks.register<Exec>("fixFilePermissions") {
|
||||
description = "When running on macOS or Linux the start script needs executable permissions to run, this sets them"
|
||||
|
||||
onlyIf { !System.getProperty("os.name").lowercase().contains("windows") }
|
||||
commandLine("chmod", "+x", "${tasks.installDist.get().destinationDir}/bin/prog8-m68kgen")
|
||||
}
|
||||
|
||||
tasks.register<JavaExec>("debugRun") {
|
||||
description = "run with remote debugging"
|
||||
mainClass.set(serverMainClassName)
|
||||
classpath(sourceSets.main.get().runtimeClasspath)
|
||||
standardInput = System.`in`
|
||||
|
||||
jvmArgs(debugArgs)
|
||||
doLast {
|
||||
println("Using debug port $debugPort")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.installDist {
|
||||
finalizedBy("fixFilePermissions")
|
||||
}
|
||||
|
||||
tasks.build {
|
||||
finalizedBy("installDist")
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
; A working m68k kernel for use in QEMU `-M virt` m68k machine.
|
||||
; It parses the bootinfo chunks, prints stuff to the uart output, and reads input as well.
|
||||
|
||||
|
||||
section .text,code
|
||||
xref _end_of_code
|
||||
|
||||
global _start
|
||||
_start: move.l #$fffc,sp
|
||||
bsr bootinfo_parse
|
||||
move.l ram_top,d0
|
||||
beq 1$
|
||||
sub.l #256,d0
|
||||
move.l d0,sp
|
||||
1$:
|
||||
|
||||
movea.l #str_welcome,a0
|
||||
bsr puts
|
||||
|
||||
2$: bsr getchar
|
||||
move.b d0,d1
|
||||
move.b d1,(a0)
|
||||
bsr putchar
|
||||
cmpi.b #13,d1
|
||||
bne 3$
|
||||
move.b #10,d0
|
||||
bsr putchar
|
||||
3$: cmpi.b #$1b,d1
|
||||
bne 2$
|
||||
bsr do_halt
|
||||
bra 2$
|
||||
|
||||
; ============================================================
|
||||
; bootinfo_parse -- iterate bootinfo records and print them
|
||||
; ============================================================
|
||||
bootinfo_parse:
|
||||
move.l d2,-(sp)
|
||||
move.l d4,-(sp)
|
||||
moveq #0,d4
|
||||
move.l #_end_of_code,d0
|
||||
addq.l #1,d0
|
||||
lsr.l #1,d0
|
||||
lsl.l #1,d0
|
||||
movea.l d0,a2
|
||||
bi_loop:
|
||||
clr.l d0
|
||||
move.w (a2)+,d0
|
||||
beq bi_done
|
||||
move.l d0,d2
|
||||
cmpi.w #2,d2
|
||||
bne 1$
|
||||
moveq #1,d4
|
||||
1$:
|
||||
clr.l d1
|
||||
move.w (a2)+,d1
|
||||
move.l a2,a3
|
||||
lea (-4,a2,d1.l),a2
|
||||
subq.l #4,d1
|
||||
|
||||
movea.l #str_indent,a0
|
||||
bsr puts
|
||||
move.l a3,d0
|
||||
subq.l #4,d0
|
||||
bsr puthex8
|
||||
movea.l #str_colon_space,a0
|
||||
bsr puts
|
||||
|
||||
move.l d2,d0
|
||||
bsr bi_print_tag
|
||||
|
||||
bsr bi_newline
|
||||
|
||||
movea.l #str_pad,a0
|
||||
bsr puts
|
||||
move.l a3,-(sp)
|
||||
move.l d1,-(sp)
|
||||
move.l d2,-(sp)
|
||||
bsr bi_print_data
|
||||
add.w #12,sp
|
||||
|
||||
bsr bi_newline
|
||||
bra bi_loop
|
||||
|
||||
bi_done:
|
||||
tst.l d4
|
||||
bne 1$
|
||||
movea.l #str_indent,a0
|
||||
bsr puts
|
||||
movea.l #str_no_cputype,a0
|
||||
bsr puts
|
||||
bsr bi_newline
|
||||
1$: move.l (sp)+,d4
|
||||
move.l (sp)+,d2
|
||||
rts
|
||||
|
||||
bi_newline:
|
||||
movea.l #str_newline,a0
|
||||
bra puts
|
||||
|
||||
; ============================================================
|
||||
; bi_print_tag -- print tag name for value in d0
|
||||
; ============================================================
|
||||
bi_print_tag:
|
||||
cmpi.w #1,d0
|
||||
beq bi_tag_machtype
|
||||
cmpi.w #2,d0
|
||||
beq bi_tag_cputype
|
||||
cmpi.w #3,d0
|
||||
beq bi_tag_fputype
|
||||
cmpi.w #4,d0
|
||||
beq bi_tag_mmutype
|
||||
cmpi.w #5,d0
|
||||
beq bi_tag_memchunk
|
||||
cmpi.w #6,d0
|
||||
beq bi_tag_ramdisk
|
||||
cmpi.w #7,d0
|
||||
beq bi_tag_cmdline
|
||||
cmpi.w #8,d0
|
||||
beq bi_tag_rngseed
|
||||
cmpi.w #$8000,d0
|
||||
beq bi_tag_version
|
||||
cmpi.w #$8001,d0
|
||||
beq bi_tag_pic
|
||||
cmpi.w #$8002,d0
|
||||
beq bi_tag_rtc
|
||||
cmpi.w #$8003,d0
|
||||
beq bi_tag_tty
|
||||
cmpi.w #$8004,d0
|
||||
beq bi_tag_virtio
|
||||
cmpi.w #$8005,d0
|
||||
beq bi_tag_ctrl
|
||||
|
||||
movea.l #str_hex_prefix,a0
|
||||
bsr puts
|
||||
bsr puthex8
|
||||
rts
|
||||
|
||||
bi_tag_machtype: movea.l #str_machtype,a0
|
||||
bra puts
|
||||
bi_tag_cputype: movea.l #str_cputype,a0
|
||||
bra puts
|
||||
bi_tag_fputype: movea.l #str_fputype,a0
|
||||
bra puts
|
||||
bi_tag_mmutype: movea.l #str_mmutype,a0
|
||||
bra puts
|
||||
bi_tag_memchunk: movea.l #str_memchunk,a0
|
||||
bra puts
|
||||
bi_tag_ramdisk: movea.l #str_ramdisk,a0
|
||||
bra puts
|
||||
bi_tag_cmdline: movea.l #str_cmdline,a0
|
||||
bra puts
|
||||
bi_tag_rngseed: movea.l #str_rngseed,a0
|
||||
bra puts
|
||||
bi_tag_version: movea.l #str_version,a0
|
||||
bra puts
|
||||
bi_tag_pic: movea.l #str_pic,a0
|
||||
bra puts
|
||||
bi_tag_rtc: movea.l #str_rtc,a0
|
||||
bra puts
|
||||
bi_tag_tty: movea.l #str_tty,a0
|
||||
bra puts
|
||||
bi_tag_ctrl: movea.l #str_ctrl,a0
|
||||
bra puts
|
||||
bi_tag_virtio: movea.l #str_virtio,a0
|
||||
bra puts
|
||||
|
||||
; ============================================================
|
||||
; bi_print_data -- print data based on tag type
|
||||
; stack args (low to high): tag, size, data_ptr
|
||||
; ============================================================
|
||||
bi_print_data:
|
||||
move.l 4(sp),d0
|
||||
move.l 8(sp),d1
|
||||
movea.l 12(sp),a3
|
||||
|
||||
cmpi.w #1,d0
|
||||
beq bi_data_machtype
|
||||
cmpi.w #2,d0
|
||||
beq bi_data_cputype
|
||||
cmpi.w #3,d0
|
||||
beq bi_data_u32
|
||||
cmpi.w #4,d0
|
||||
beq bi_data_u32
|
||||
cmpi.w #$8000,d0
|
||||
beq bi_data_u32
|
||||
cmpi.w #5,d0
|
||||
beq bi_data_memchunk
|
||||
cmpi.w #6,d0
|
||||
beq bi_data_base_size
|
||||
cmpi.w #7,d0
|
||||
beq bi_data_str
|
||||
cmpi.w #8,d0
|
||||
beq bi_data_hex
|
||||
cmpi.w #$8001,d0
|
||||
beq bi_data_base_count
|
||||
cmpi.w #$8002,d0
|
||||
beq bi_data_base_count
|
||||
cmpi.w #$8003,d0
|
||||
beq bi_data_base_count
|
||||
cmpi.w #$8004,d0
|
||||
beq bi_data_base_count
|
||||
cmpi.w #$8005,d0
|
||||
beq bi_data_base_count
|
||||
|
||||
bi_data_hex:
|
||||
move.l d1,d3
|
||||
1$: move.b (a3)+,d0
|
||||
bsr puthex2
|
||||
subq.l #1,d3
|
||||
beq 2$
|
||||
movea.l #str_space,a0
|
||||
bsr puts
|
||||
bra 1$
|
||||
2$: rts
|
||||
|
||||
bi_data_u32:
|
||||
move.l (a3),d0
|
||||
bra puthex8
|
||||
|
||||
bi_data_memchunk:
|
||||
move.l (a3),-(sp)
|
||||
movea.l #str_mem_at,a0
|
||||
bsr puts
|
||||
move.l (sp)+,d0
|
||||
bsr puthex8
|
||||
movea.l #str_to,a0
|
||||
bsr puts
|
||||
move.l 4(a3),d0
|
||||
add.l (a3),d0
|
||||
move.l d0,ram_top
|
||||
bsr puthex8
|
||||
movea.l #str_size,a0
|
||||
bsr puts
|
||||
move.l 4(a3),d0
|
||||
bra puthex8
|
||||
|
||||
bi_data_base_size:
|
||||
movea.l #str_base,a0
|
||||
bsr puts
|
||||
move.l (a3),d0
|
||||
bsr puthex8
|
||||
movea.l #str_size,a0
|
||||
bsr puts
|
||||
move.l 4(a3),d0
|
||||
bra puthex8
|
||||
|
||||
bi_data_base_count:
|
||||
movea.l #str_base,a0
|
||||
bsr puts
|
||||
move.l (a3),d0
|
||||
bsr puthex8
|
||||
movea.l #str_count,a0
|
||||
bsr puts
|
||||
move.l 4(a3),d0
|
||||
bra puthex8
|
||||
|
||||
bi_data_str:
|
||||
movea.l a3,a0
|
||||
bra puts
|
||||
|
||||
bi_data_machtype:
|
||||
move.l (a3),d0
|
||||
cmpi.l #14,d0
|
||||
beq 1$
|
||||
bra puthex8
|
||||
1$: movea.l #str_mach_virt,a0
|
||||
bra puts
|
||||
|
||||
bi_data_cputype:
|
||||
move.l (a3),d0
|
||||
beq 6$
|
||||
btst #0,d0
|
||||
bne 1$
|
||||
btst #1,d0
|
||||
bne 2$
|
||||
btst #2,d0
|
||||
bne 3$
|
||||
btst #3,d0
|
||||
bne 4$
|
||||
btst #4,d0
|
||||
bne 5$
|
||||
bra puthex8
|
||||
1$: movea.l #str_cpu68020,a0
|
||||
bra puts
|
||||
2$: movea.l #str_cpu68030,a0
|
||||
bra puts
|
||||
3$: movea.l #str_cpu68040,a0
|
||||
bra puts
|
||||
4$: movea.l #str_cpu68060,a0
|
||||
bra puts
|
||||
5$: movea.l #str_coldfire,a0
|
||||
bra puts
|
||||
6$: movea.l #str_cpu68000,a0
|
||||
bra puts
|
||||
|
||||
; ============================================================
|
||||
puts: move.b (a0)+,d0
|
||||
beq puts_rts
|
||||
bsr putchar
|
||||
bra puts
|
||||
puts_rts:
|
||||
rts
|
||||
|
||||
; ============================================================
|
||||
puthex8:
|
||||
move.l d2,-(sp)
|
||||
move.l d3,-(sp)
|
||||
move.l d0,d2
|
||||
moveq #28,d3
|
||||
1$: move.l d2,d0
|
||||
lsr.l d3,d0
|
||||
and.b #$f,d0
|
||||
add.b #'0',d0
|
||||
cmp.b #'9',d0
|
||||
bls 2$
|
||||
add.b #7,d0
|
||||
2$: bsr putchar
|
||||
subq.l #4,d3
|
||||
bpl 1$
|
||||
move.l (sp)+,d3
|
||||
move.l (sp)+,d2
|
||||
rts
|
||||
|
||||
; ============================================================
|
||||
puthex2:
|
||||
move.l d2,-(sp)
|
||||
move.l d3,-(sp)
|
||||
move.l d0,d2
|
||||
moveq #4,d3
|
||||
1$: move.l d2,d0
|
||||
lsr.l d3,d0
|
||||
and.b #$f,d0
|
||||
add.b #'0',d0
|
||||
cmp.b #'9',d0
|
||||
bls 2$
|
||||
add.b #7,d0
|
||||
2$: bsr putchar
|
||||
subq.l #4,d3
|
||||
bpl 1$
|
||||
move.l (sp)+,d3
|
||||
move.l (sp)+,d2
|
||||
rts
|
||||
|
||||
; ============================================================
|
||||
putchar:
|
||||
movea.l #$ff008000,a1
|
||||
move.l d0,(a1)
|
||||
rts
|
||||
|
||||
; ============================================================
|
||||
getchar:
|
||||
suba.l #16,sp
|
||||
movea.l #$ff008004,a1
|
||||
1$: move.l (a1),d0
|
||||
tst.l d0
|
||||
beq 1$
|
||||
movea.l #$ff008010,a1
|
||||
move.l sp,(a1)
|
||||
movea.l #$ff008014,a1
|
||||
move.l #1,(a1)
|
||||
movea.l #$ff008008,a1
|
||||
move.l #3,(a1)
|
||||
move.b (sp),d0
|
||||
adda.l #16,sp
|
||||
rts
|
||||
|
||||
; ============================================================
|
||||
do_halt:
|
||||
movea.l #$ff009004,a1
|
||||
move.l #2,(a1)
|
||||
rts
|
||||
|
||||
; ============================================================
|
||||
; Strings
|
||||
; ============================================================
|
||||
str_indent: dc.b " ",0
|
||||
str_colon_space: dc.b ": ",0
|
||||
str_pad: dc.b " ",0
|
||||
str_newline: dc.b 10,0
|
||||
str_base: dc.b "base=0x",0
|
||||
str_size: dc.b " size=0x",0
|
||||
str_count: dc.b " count=0x",0
|
||||
str_mem_at: dc.b "mem at 0x",0
|
||||
str_to: dc.b " - 0x",0
|
||||
str_space: dc.b " ",0
|
||||
str_hex_prefix: dc.b "TAG_0x",0
|
||||
str_mach_virt: dc.b "MACH_VIRT",0
|
||||
str_cpu68000: dc.b "CPU_68000",0
|
||||
str_cpu68020: dc.b "CPU_68020",0
|
||||
str_cpu68030: dc.b "CPU_68030",0
|
||||
str_cpu68040: dc.b "CPU_68040",0
|
||||
str_cpu68060: dc.b "CPU_68060",0
|
||||
str_coldfire: dc.b "ColdFire",0
|
||||
str_no_cputype: dc.b "(no BI_CPUTYPE - assuming CPU_68000)",0
|
||||
str_machtype: dc.b "BI_MACHTYPE",0
|
||||
str_cputype: dc.b "BI_CPUTYPE",0
|
||||
str_fputype: dc.b "BI_FPUTYPE",0
|
||||
str_mmutype: dc.b "BI_MMUTYPE",0
|
||||
str_memchunk: dc.b "BI_MEMCHUNK",0
|
||||
str_ramdisk: dc.b "BI_RAMDISK",0
|
||||
str_cmdline: dc.b "BI_COMMAND_LINE",0
|
||||
str_rngseed: dc.b "BI_RNG_SEED",0
|
||||
str_version: dc.b "BI_VIRT_QEMU_VER",0
|
||||
str_pic: dc.b "BI_VIRT_GF_PIC",0
|
||||
str_rtc: dc.b "BI_VIRT_GF_RTC",0
|
||||
str_tty: dc.b "BI_VIRT_GF_TTY",0
|
||||
str_ctrl: dc.b "BI_VIRT_CTRL",0
|
||||
str_virtio: dc.b "BI_VIRT_VIRTIO",0
|
||||
str_welcome: dc.b "Hello from m68k! (ESC to halt)",10,0
|
||||
align 4
|
||||
ram_top: dc.l 0
|
||||
@@ -0,0 +1,84 @@
|
||||
# Linking the kernel
|
||||
|
||||
The kernel is linked using **vlink**, producing an ELF32 binary (`kernel.elf`) that qemu loads directly via its `-kernel` option.
|
||||
|
||||
## Toolchain
|
||||
|
||||
| Tool | Role |
|
||||
|---|---|
|
||||
| `vasmm68k_mot` | Assembles `kernel.S` → `kernel.o` |
|
||||
| `vlink` | Links `kernel.o` → `kernel.elf` using `link.ld` |
|
||||
|
||||
The `Makefile` invocation:
|
||||
|
||||
```makefile
|
||||
vlink -b elf32m68k -n -T link.ld -o kernel.elf kernel.o
|
||||
```
|
||||
|
||||
## Command-line flags
|
||||
|
||||
### `-b elf32m68k` — output format
|
||||
|
||||
Tells vlink to produce a 32-bit big-endian m68k ELF binary.
|
||||
|
||||
vlink supports many output formats; without this flag it would default to something else. Note that vlink ignores the `OUTPUT_FORMAT()` directive inside linker scripts, so the format **must** be specified on the command line.
|
||||
|
||||
### `-n` — NMAGIC / no page alignment
|
||||
|
||||
Disables page-boundary alignment of ELF segments.
|
||||
|
||||
Without `-n`, vlink aligns the LOAD segment to a full page (0x2000 = 8 KB). The resulting ELF has a large gap of padding between the ELF header and the actual code:
|
||||
|
||||
```
|
||||
Type Offset VirtAddr PhysAddr Align
|
||||
LOAD 0x002000 0x00010000 0x00010000 0x2000
|
||||
```
|
||||
|
||||
qemu's ELF loader gets confused by this layout and either fails to load the kernel or jumps to the wrong address.
|
||||
|
||||
With `-n`, segment alignment is reduced to the minimum (0x10) and the code starts right after the ELF header:
|
||||
|
||||
```
|
||||
Type Offset VirtAddr PhysAddr Align
|
||||
LOAD 0x000080 0x00010000 0x00010000 0x10
|
||||
```
|
||||
|
||||
qemu loads this cleanly — the code is at file offset 0x80, maps to virtual address 0x10000, and execution starts at `_start` as expected.
|
||||
|
||||
`-n` is the vlink equivalent of GNU ld's `-N` / `--omagic` option.
|
||||
|
||||
### `-T link.ld` — linker script
|
||||
|
||||
Passes the linker script (see below).
|
||||
|
||||
## The linker script (`link.ld`)
|
||||
|
||||
```
|
||||
ENTRY(_start)
|
||||
SECTIONS
|
||||
{
|
||||
. = 0x10000;
|
||||
.text : { *(.text) }
|
||||
.data : { *(.data) }
|
||||
.bss : { *(.bss) *(COMMON) }
|
||||
_end_of_code = .;
|
||||
}
|
||||
```
|
||||
|
||||
### Line by line
|
||||
|
||||
| Line | What it does |
|
||||
|---|---|
|
||||
| `ENTRY(_start)` | Sets the ELF entry point to `_start` — the first instruction in `kernel.S`. qemu uses this to know where to jump after loading the kernel. |
|
||||
| `SECTIONS { … }` | Opens the section layout block — the core of any linker script. Everything inside describes how to arrange the output binary. |
|
||||
| `. = 0x10000;` | Sets the **load address** to `0x10000` (64 KB). This is where qemu places the kernel in memory and begins execution. The `.` is the *location counter* — it advances automatically as sections are placed. |
|
||||
| `.text : { *(.text) }` | Collects all `.text` (code) sections from every input object file into one output `.text` section. |
|
||||
| `.data : { *(.data) }` | Same for initialised data. |
|
||||
| `.bss : { *(.bss) *(COMMON) }` | Same for uninitialised (zero-filled) data. `COMMON` catches any unresolved global variables that the assembler emits as common symbols. |
|
||||
| `_end_of_code = .;` | Defines the symbol `_end_of_code` at the address immediately after the last section. `kernel.S` references this symbol to find where the bootinfo records begin in memory (qemu appends them just past the kernel image). |
|
||||
|
||||
### Why a linker script is required
|
||||
|
||||
vlink has no command-line flag equivalent to `. = 0x10000` (setting the load address). Attempting `-Ttext=0x10000` causes a fatal error because vlink treats the `=` as part of a filename. The correct syntax `-Ttext 0x10000` (space-separated) does set the address, but then `_end_of_code` is undefined because there is no linker script to define it.
|
||||
|
||||
The linker script is therefore the minimum required: it sets the load address and defines `_end_of_code`.
|
||||
@@ -0,0 +1,456 @@
|
||||
# QEMU m68k "virt" Machine -- IO Interfaces Reference
|
||||
|
||||
All MMIO addresses and register layouts for the QEMU `-M virt` m68k machine.
|
||||
|
||||
## Memory Map
|
||||
|
||||
| Device | Base Address | Size | CPU IRQ |
|
||||
|---|---|---|---|
|
||||
| Goldfish PIC (x6) | `0xFF000000` | 6 x 0x1000 | #1-#6 |
|
||||
| Goldfish RTC (x2) | `0xFF006000` | 2 x 0x1000 | #6 (PIC#6, IRQ#1) |
|
||||
| Goldfish TTY | `0xFF008000` | 0x1000 | #1 (PIC#1, IRQ#32) |
|
||||
| Virt Ctrl | `0xFF009000` | 0x100 | #1 (PIC#1, IRQ#1) |
|
||||
| Virtio-MMIO (x128) | `0xFF010000` | 128 x 0x200 | #2-#5 (PIC#2-#5) |
|
||||
|
||||
### IRQ Hierarchy
|
||||
|
||||
```
|
||||
CPU IRQ #1 -> PIC #1 (goldfish_pic.0) -> IRQ#1 = virt-ctrl, IRQ#32 = tty
|
||||
CPU IRQ #2 -> PIC #2 (goldfish_pic.1) -> IRQ#1..#32 = virtio-mmio #1..#32
|
||||
CPU IRQ #3 -> PIC #3 (goldfish_pic.2) -> IRQ#1..#32 = virtio-mmio #33..#64
|
||||
CPU IRQ #4 -> PIC #4 (goldfish_pic.3) -> IRQ#1..#32 = virtio-mmio #65..#96
|
||||
CPU IRQ #5 -> PIC #5 (goldfish_pic.4) -> IRQ#1..#32 = virtio-mmio #97..#128
|
||||
CPU IRQ #6 -> PIC #6 (goldfish_pic.5) -> IRQ#1 = RTC, IRQ#2..#32 = unused
|
||||
CPU IRQ #7 -> NMI
|
||||
```
|
||||
|
||||
All registers are **32 bits wide**. Accesses must be 4-byte aligned.
|
||||
|
||||
---
|
||||
|
||||
## Goldfish TTY -- `0xFF008000`
|
||||
|
||||
Primary console. All access via 32-bit loads/stores.
|
||||
|
||||
### Registers
|
||||
|
||||
| Offset | Name | Access | Description |
|
||||
|---|---|---|---|
|
||||
| `0x00` | `REG_PUT_CHAR` | WO | Write one character to serial output |
|
||||
| `0x04` | `REG_BYTES_READY` | RO | Number of bytes available in RX FIFO |
|
||||
| `0x08` | `REG_CMD` | WO | Issue command |
|
||||
| `0x10` | `REG_DATA_PTR` | WO | DMA buffer address (low 32 bits) |
|
||||
| `0x14` | `REG_DATA_LEN` | WO | DMA transfer byte count |
|
||||
| `0x18` | `REG_DATA_PTR_HIGH` | WO | DMA buffer address (high 32 bits) |
|
||||
| `0x20` | `REG_VERSION` | RO | Returns 1 |
|
||||
|
||||
### Commands (write to REG_CMD)
|
||||
|
||||
| Value | Name | Description |
|
||||
|---|---|---|
|
||||
| `0` | `CMD_INT_DISABLE` | Disable interrupts |
|
||||
| `1` | `CMD_INT_ENABLE` | Enable interrupts |
|
||||
| `2` | `CMD_WRITE_BUFFER` | Write N bytes from guest memory to chardev |
|
||||
| `3` | `CMD_READ_BUFFER` | Read N bytes from chardev into guest memory |
|
||||
|
||||
### Usage: putchar (blocking, non-DMA)
|
||||
|
||||
Write the character directly to `REG_PUT_CHAR`. QEMU sends it immediately.
|
||||
|
||||
```asm
|
||||
putchar:
|
||||
movea.l #0xff008000, %a1
|
||||
move.l %d0, (%a1)
|
||||
rts
|
||||
```
|
||||
|
||||
### Usage: getchar (blocking, polled, DMA)
|
||||
|
||||
1. Poll `REG_BYTES_READY` until non-zero
|
||||
2. Set `REG_DATA_PTR` to buffer address
|
||||
3. Set `REG_DATA_LEN` to number of bytes to read (e.g. 1)
|
||||
4. Write `3` (`CMD_READ_BUFFER`) to `REG_CMD`
|
||||
5. Read character(s) from buffer
|
||||
|
||||
```asm
|
||||
getchar:
|
||||
suba.l #16, %sp
|
||||
movea.l #0xff008004, %a1 @ REG_BYTES_READY
|
||||
1: move.l (%a1), %d0
|
||||
tst.l %d0
|
||||
beq 1b @ wait until data available
|
||||
movea.l #0xff008010, %a1 @ REG_DATA_PTR
|
||||
move.l %sp, (%a1)
|
||||
movea.l #0xff008014, %a1 @ REG_DATA_LEN
|
||||
move.l #1, (%a1)
|
||||
movea.l #0xff008008, %a1 @ REG_CMD
|
||||
move.l #3, (%a1) @ CMD_READ_BUFFER
|
||||
move.b (%sp), %d0
|
||||
adda.l #16, %sp
|
||||
rts
|
||||
```
|
||||
|
||||
To use interrupts instead of polling:
|
||||
1. Write `1` (`CMD_INT_ENABLE`) to `REG_CMD` (offset `0x08`)
|
||||
2. Enable IRQ in PIC (PIC#1 at `0xFF000000`, IRQ#32)
|
||||
3. On interrupt, read `REG_BYTES_READY` then DMA data from buffer
|
||||
|
||||
---
|
||||
|
||||
## Virt Controller -- `0xFF009000`
|
||||
|
||||
Controls machine power state.
|
||||
|
||||
### Registers
|
||||
|
||||
| Offset | Name | Access | Description |
|
||||
|---|---|---|---|
|
||||
| `0x00` | `REG_FEATURES` | RO | Feature bitmask |
|
||||
| `0x04` | `REG_CMD` | WO | Issue power/reset command |
|
||||
|
||||
### Feature Bits (REG_FEATURES)
|
||||
|
||||
| Bit | Name | Description |
|
||||
|---|---|---|
|
||||
| `0` | `FEAT_POWER_CTRL` | Power control is available |
|
||||
|
||||
### Commands (write to REG_CMD)
|
||||
|
||||
| Value | Name | Description |
|
||||
|---|---|---|
|
||||
| `0` | `CMD_NOOP` | No operation |
|
||||
| `1` | `CMD_RESET` | Reset the VM |
|
||||
| `2` | `CMD_HALT` | Shut down the VM |
|
||||
| `3` | `CMD_PANIC` | Shut down with guest panic |
|
||||
|
||||
### Usage: halt
|
||||
|
||||
```asm
|
||||
halt:
|
||||
movea.l #0xff009004, %a1 @ REG_CMD
|
||||
move.l #2, (%a1) @ CMD_HALT
|
||||
rts
|
||||
```
|
||||
|
||||
### Usage: reset
|
||||
|
||||
```asm
|
||||
reset:
|
||||
movea.l #0xff009004, %a1 @ REG_CMD
|
||||
move.l #1, (%a1) @ CMD_RESET
|
||||
rts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Goldfish PIC -- `0xFF000000` (+ idx * 0x1000)
|
||||
|
||||
Programmable Interrupt Controller. Six PICs in the virt machine:
|
||||
|
||||
| Base Address | PIC # | CPU IRQ | Devices |
|
||||
|---|---|---|---|
|
||||
| `0xFF000000` | 1 | #1 | virt-ctrl (IRQ#1), tty (IRQ#32) |
|
||||
| `0xFF001000` | 2 | #2 | virtio-mmio #1..#32 |
|
||||
| `0xFF002000` | 3 | #3 | virtio-mmio #33..#64 |
|
||||
| `0xFF003000` | 4 | #4 | virtio-mmio #65..#96 |
|
||||
| `0xFF004000` | 5 | #5 | virtio-mmio #97..#128 |
|
||||
| `0xFF005000` | 6 | #6 | RTC (IRQ#1) |
|
||||
|
||||
### Registers
|
||||
|
||||
| Offset | Name | Access | Description |
|
||||
|---|---|---|---|
|
||||
| `0x00` | `REG_STATUS` | RO | Count of pending+enabled IRQs (popcount) |
|
||||
| `0x04` | `REG_IRQ_PENDING` | RO | Bitmask of pending AND enabled IRQs |
|
||||
| `0x08` | `REG_IRQ_DISABLE_ALL` | WO | Disable ALL interrupts (clears pending too) |
|
||||
| `0x0c` | `REG_DISABLE` | WO | Disable specific IRQs (bitmask) |
|
||||
| `0x10` | `REG_ENABLE` | WO | Enable specific IRQs (bitmask) |
|
||||
|
||||
Interrupts are level-sensitive. Each PIC manages 32 IRQ inputs (IRQ#1 = bit 0, IRQ#32 = bit 31).
|
||||
|
||||
### Usage: enable IRQ in PIC
|
||||
|
||||
```asm
|
||||
@ Enable IRQ#32 (tty) in PIC#1 (0xFF000000)
|
||||
movea.l #0xff000010, %a1 @ REG_ENABLE of PIC#1
|
||||
move.l #1 << 31, (%a1) @ bit 31 = IRQ#32
|
||||
```
|
||||
|
||||
### Usage: read pending IRQs
|
||||
|
||||
```asm
|
||||
movea.l #0xff000004, %a1 @ REG_IRQ_PENDING
|
||||
move.l (%a1), %d0 @ d0 = bitmask of pending IRQs
|
||||
btst #31, %d0 @ check if IRQ#32 (tty) is firing
|
||||
```
|
||||
|
||||
### Usage: disable all interrupts (mask)
|
||||
|
||||
```asm
|
||||
movea.l #0xff000008, %a1 @ REG_IRQ_DISABLE_ALL
|
||||
move.l #0, (%a1) @ any value disables all
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Goldfish RTC -- `0xFF006000`
|
||||
|
||||
Real-time clock with a 64-bit nanosecond counter. **Big-endian** on m68k (unlike other goldfish devices which are little-endian).
|
||||
|
||||
### Registers
|
||||
|
||||
| Offset | Name | Access | Description |
|
||||
|---|---|---|---|
|
||||
| `0x00` | `RTC_TIME_LOW` | RW | Time value, lower 32 bits |
|
||||
| `0x04` | `RTC_TIME_HIGH` | RW | Time value, upper 32 bits |
|
||||
| `0x08` | `RTC_ALARM_LOW` | RW | Alarm value, lower 32 bits |
|
||||
| `0x0c` | `RTC_ALARM_HIGH` | RW | Alarm value, upper 32 bits |
|
||||
| `0x10` | `RTC_IRQ_ENABLED` | RW | IRQ enable (bit 0) |
|
||||
| `0x14` | `RTC_CLEAR_ALARM` | WO | Clear the alarm |
|
||||
| `0x18` | `RTC_ALARM_STATUS` | RO | 1 if alarm is set |
|
||||
| `0x1c` | `RTC_CLEAR_INTERRUPT` | WO | Clear pending interrupt |
|
||||
|
||||
Reading `RTC_TIME_LOW` latches the high 32 bits; the subsequent read of `RTC_TIME_HIGH` returns that latched value. This ensures atomic 64-bit time reads.
|
||||
|
||||
Writing either `RTC_TIME_LOW` or `RTC_TIME_HIGH` adjusts the tick offset to set the time.
|
||||
|
||||
The RTC interrupt goes to PIC#6 (CPU IRQ #6), IRQ #1.
|
||||
|
||||
### Usage: read time
|
||||
|
||||
```asm
|
||||
movea.l #0xff006000, %a1 @ RTC_TIME_LOW
|
||||
move.l (%a1), %d0 @ d0 = low 32 bits (latches high)
|
||||
movea.l #0xff006004, %a1 @ RTC_TIME_HIGH
|
||||
move.l (%a1), %d1 @ d1 = high 32 bits
|
||||
@ now d1:d0 = 64-bit nanosecond counter
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Virtio-MMIO -- `0xFF010000` (+ idx * 0x200)
|
||||
|
||||
128 virtio-mmio transport devices. Each occupies 0x200 bytes.
|
||||
|
||||
| Start | End | Slot Range |
|
||||
|---|---|---|
|
||||
| `0xFF010000` | `0xFF01FFFF` | 128 slots (0x200 each) |
|
||||
|
||||
### Identification Registers (RO)
|
||||
|
||||
| Offset | Name | Value | Description |
|
||||
|---|---|---|---|
|
||||
| `0x000` | `MAGIC_VALUE` | `0x74726976` ("virt") |
|
||||
| `0x004` | `VERSION` | 2 (modern) | 1 = legacy |
|
||||
| `0x008` | `DEVICE_ID` | varies | 0 = no device |
|
||||
| `0x00c` | `VENDOR_ID` | `0x554D4551` ("QEMU") |
|
||||
|
||||
### Key Registers (32-bit)
|
||||
|
||||
| Offset | Name | Access | Description |
|
||||
|---|---|---|---|
|
||||
| `0x010` | `DEVICE_FEATURES` | RO | Features offered by host (low 32) |
|
||||
| `0x014` | `DEVICE_FEATURES_SEL` | WO | Select feature bits (high/low) |
|
||||
| `0x020` | `DRIVER_FEATURES` | WO | Features accepted by driver (low 32) |
|
||||
| `0x024` | `DRIVER_FEATURES_SEL` | WO | Select driver feature bits |
|
||||
| `0x030` | `QUEUE_SEL` | WO | Select virtqueue |
|
||||
| `0x034` | `QUEUE_NUM_MAX` | RO | Max queue size |
|
||||
| `0x038` | `QUEUE_NUM` | WO | Set queue size |
|
||||
| `0x044` | `QUEUE_READY` | RW | Queue ready bit |
|
||||
| `0x050` | `QUEUE_NOTIFY` | WO | Kick the virtqueue |
|
||||
| `0x060` | `INTERRUPT_STATUS` | RO | Interrupt reason bits |
|
||||
| `0x064` | `INTERRUPT_ACK` | WO | Acknowledge interrupt |
|
||||
| `0x070` | `STATUS` | RW | Device status (driver negotiation) |
|
||||
| `0x080` | `QUEUE_DESC_LOW` | WO | Descriptor table address (low) |
|
||||
| `0x084` | `QUEUE_DESC_HIGH` | WO | Descriptor table address (high) |
|
||||
| `0x090` | `QUEUE_AVAIL_LOW` | WO | Available ring address (low) |
|
||||
| `0x094` | `QUEUE_AVAIL_HIGH` | WO | Available ring address (high) |
|
||||
| `0x0a0` | `QUEUE_USED_LOW` | WO | Used ring address (low) |
|
||||
| `0x0a4` | `QUEUE_USED_HIGH` | WO | Used ring address (high) |
|
||||
| `0x0fc` | `CONFIG_GENERATION` | RO | Config space version |
|
||||
| `0x100` | `CONFIG` | RW | Device-specific config space |
|
||||
|
||||
### Interrupt Status Bits
|
||||
|
||||
| Bit | Name | Description |
|
||||
|---|---|---|
|
||||
| `0` | `VIRTIO_MMIO_INT_VRING` | Used buffer notification |
|
||||
| `1` | `VIRTIO_MMIO_INT_CONFIG` | Config change notification |
|
||||
|
||||
### Usage: detect virtio device
|
||||
|
||||
```asm
|
||||
movea.l #0xff010000, %a1 @ first virtio slot
|
||||
move.l 0x000(%a1), %d0 @ MAGIC_VALUE
|
||||
cmp.l #0x74726976, %d0 @ "virt" ?
|
||||
bne no_device
|
||||
move.l 0x008(%a1), %d0 @ DEVICE_ID
|
||||
tst.l %d0
|
||||
beq no_device
|
||||
@ valid virtio device found, d0 = device ID
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## m68k Bootinfo Protocol
|
||||
|
||||
Kernels can query device base addresses, memory layout, and machine properties via the m68k bootinfo protocol. QEMU places a bootinfo blob immediately after the kernel's ELF LOAD segment in guest physical memory.
|
||||
|
||||
### Record Format
|
||||
|
||||
Every bootinfo record has a 4-byte header followed by variable-length data:
|
||||
|
||||
```
|
||||
struct bi_record {
|
||||
uint16 tag; // tag ID (big-endian)
|
||||
uint16 size; // total record size in bytes, including 4-byte header (big-endian)
|
||||
uint8 data[size - 4]; // tag-specific data
|
||||
};
|
||||
```
|
||||
|
||||
Records are contiguous (no padding between them). The list is terminated by a sentinel record with `tag = 0` (`BI_LAST`).
|
||||
|
||||
### Locating the Bootinfo Blob
|
||||
|
||||
QEMU loads the kernel ELF at `0x10000` (the `LOAD_ADDR`). The `_end` linker symbol marks the end of the LOAD segment. QEMU places the bootinfo at the next word-aligned address:
|
||||
|
||||
```c
|
||||
parameters_base = (_end + 1) & ~1;
|
||||
```
|
||||
|
||||
When writing a kernel in assembly, use:
|
||||
|
||||
```asm
|
||||
move.l #_end, d0
|
||||
addq.l #1, d0
|
||||
lsr.l #1, d0
|
||||
lsl.l #1, d0
|
||||
movea.l d0, a2 ; a2 = bootinfo base
|
||||
```
|
||||
|
||||
### Parsing Loop
|
||||
|
||||
```asm
|
||||
movea.l d0, a2 ; a2 = bootinfo base
|
||||
1$:
|
||||
clr.l d0
|
||||
move.w (a2)+, d0 ; tag (2 bytes)
|
||||
beq 3$ ; BI_LAST sentinel
|
||||
clr.l d1
|
||||
move.w (a2)+, d1 ; size = total record size (2 bytes)
|
||||
move.l a2, a3 ; a3 -> data portion
|
||||
lea (-4, a2, d1.l), a2 ; advance to next record
|
||||
subq.l #4, d1 ; d1 = data size (total - header)
|
||||
; handle tag d0 with data at a3 (d1 bytes)
|
||||
bra 1$
|
||||
3$: ; done
|
||||
```
|
||||
|
||||
**Important**: The `size` field stores the *total* record size (including the 4-byte header), not just the data size. To advance `a2` to the next record after reading the header, compute `a2 + size - 4`. To get the data payload size, compute `size - 4`.
|
||||
|
||||
### Tag Definitions
|
||||
|
||||
#### Standard Tags (Linux/m68k)
|
||||
|
||||
Defined in Linux `include/uapi/asm-generic/bootinfo.h`:
|
||||
|
||||
| Tag | Name | Data Size | Description |
|
||||
|------|---------------------|-----------|--------------------------------------------|
|
||||
| 0x0000 | `BI_LAST` | 0 | End-of-list sentinel |
|
||||
| 0x0001 | `BI_MACHTYPE` | 4 | Machine type (enum, see below) |
|
||||
| 0x0002 | `BI_CPUTYPE` | 4 | CPU type bitmask (see below) |
|
||||
| 0x0003 | `BI_FPUTYPE` | 4 | FPU type bitmask |
|
||||
| 0x0004 | `BI_MMUTYPE` | 4 | MMU type bitmask |
|
||||
| 0x0005 | `BI_MEMCHUNK` | 8 | Memory chunk: `{uint32 base, uint32 size}` |
|
||||
| 0x0006 | `BI_RAMDISK` | 8 | Ramdisk: `{uint32 addr, uint32 size}` |
|
||||
| 0x0007 | `BI_COMMAND_LINE` | variable | Null-terminated kernel command line string |
|
||||
| 0x0008 | `BI_RNG_SEED` | 32 | Random seed for RNG initialization |
|
||||
|
||||
**BI_MACHTYPE values:**
|
||||
|
||||
| Value | Machine |
|
||||
|-------|------------|
|
||||
| 1 | Amiga |
|
||||
| 2 | Atari |
|
||||
| 3 | Mac |
|
||||
| 4 | Apollo |
|
||||
| 5 | Sun3 |
|
||||
| 6 | MVME147 |
|
||||
| 7 | MVME16x |
|
||||
| 8 | BVME6000 |
|
||||
| 9 | HP300 |
|
||||
| 10 | Q40 |
|
||||
| 11 | Sun3x |
|
||||
| 12 | M54xx |
|
||||
| 13 | M5441x |
|
||||
| 14 | **VIRT** |
|
||||
|
||||
**BI_CPUTYPE (bitmask):**
|
||||
|
||||
| Bit | CPU |
|
||||
|-----|-----------|
|
||||
| 0 | 68020 |
|
||||
| 1 | 68030 |
|
||||
| 2 | 68040 |
|
||||
| 3 | 68060 |
|
||||
| 4 | ColdFire |
|
||||
|
||||
#### Virt-Specific Tags (BI_VIRT_*)
|
||||
|
||||
Defined in `bootinfo-virt.h`:
|
||||
|
||||
| Tag | Name | Data Size | Description |
|
||||
|--------|-------------------------|-----------|--------------------------------------------------|
|
||||
| 0x8000 | `BI_VIRT_QEMU_VERSION` | 4 | QEMU version: `(major << 16) \| minor` |
|
||||
| 0x8001 | `BI_VIRT_GF_PIC_BASE` | 8 | Goldfish PIC base address + IRQ count |
|
||||
| 0x8002 | `BI_VIRT_GF_RTC_BASE` | 8 | Goldfish RTC base address + count |
|
||||
| 0x8003 | `BI_VIRT_GF_TTY_BASE` | 8 | Goldfish TTY base address + count |
|
||||
| 0x8004 | `BI_VIRT_VIRTIO_BASE` | 8 | Virtio-MMIO base address + mmio region size |
|
||||
| 0x8005 | `BI_VIRT_CTRL_BASE` | 4 | Virt controller base address |
|
||||
|
||||
For tags 0x8001-0x8004, the data is two `uint32` values: `{base address, count_or_size}`.
|
||||
|
||||
### Example: QEMU 11.0.0 on `m68020`
|
||||
|
||||
Bootinfo blob (captured via QEMU monitor `xp/32x <addr>`):
|
||||
|
||||
```
|
||||
00010490: 0x00010008 0x0000000e 0x00020008 0x00000001
|
||||
000104a0: 0x0005000c 0x00000000 0x08000000 0x80000008
|
||||
000104b0: 0x0b000000 0x8001000c 0xff000000 0x00000001
|
||||
000104c0: 0x8002000c 0xff006000 0x000000a8 0x8003000c
|
||||
000104d0: 0xff008000 0x00000027 0x8005000c 0xff009000
|
||||
000104e0: 0x00000008 0x8004000c 0xff010000 0x00000028
|
||||
000104f0: 0x00070008 0x00000000 0x00080028 0x0020ac0e
|
||||
00010500: 0xd72bb388 0x3ec5e0e0 0x5a1534b3 0xe295bd9a
|
||||
```
|
||||
|
||||
Decoded (from `xp/40x 0x10490` with QEMU 11.0.0, m68020, 128 MiB RAM):
|
||||
|
||||
| Offset | Tag | Total | Data | Meaning |
|
||||
|--------|---------------------|-------|------------------------------------------|-----------------------------------|
|
||||
| 0x10490 | BI_MACHTYPE | 8 | 0x0000000e (14) | MACH_VIRT |
|
||||
| 0x10498 | BI_CPUTYPE | 8 | 0x00000001 (bit 0) | CPU_68020 |
|
||||
| 0x104a0 | BI_MEMCHUNK | 12 | base=0x00000000, size=0x08000000 | 128 MiB RAM at 0x0 |
|
||||
| 0x104ac | BI_VIRT_QEMU_VER | 8 | 0x0B000000 (major=11, minor=0) | QEMU 11.0 |
|
||||
| 0x104b4 | BI_VIRT_GF_PIC | 12 | base=0xFF000000, count=1 | 1 PIC instance |
|
||||
| 0x104c0 | BI_VIRT_GF_RTC | 12 | base=0xFF006000, count=168 | 168 RTC regs |
|
||||
| 0x104cc | BI_VIRT_GF_TTY | 12 | base=0xFF008000, count=39 | 39 TTY channels |
|
||||
| 0x104d8 | BI_VIRT_CTRL | 12 | base=0xFF009000, count=8 | CTRL regs + feature bits |
|
||||
| 0x104e4 | BI_VIRT_VIRTIO | 12 | base=0xFF010000, count=40 | 40 VIRTIO slots |
|
||||
| 0x104f0 | BI_COMMAND_LINE | 8 | (empty) | No kernel command line |
|
||||
| 0x104f8 | BI_RNG_SEED | 40 | 36 bytes random seed | RNG initialization seed |
|
||||
| 0x10520 | BI_LAST | 4 | -- | End-of-list sentinel |
|
||||
|
||||
Key observations:
|
||||
|
||||
- The `size` field is the **total** record size including the 4-byte header.
|
||||
- Most virt device records use 12 bytes total (8 bytes data: `{base, count}`).
|
||||
- Singletons (MACHTYPE, CPUTYPE, QEMU_VERSION) use 8 bytes total (4 bytes data).
|
||||
- BI_COMMAND_LINE is always present (4 bytes of zero when empty).
|
||||
- BI_RNG_SEED: total=40 (0x28), data=36 bytes — the last record before the sentinel.
|
||||
- BI_LAST has `size=4` (a 4-byte all-zero header with tag=0).
|
||||
|
||||
### Fixed Addresses vs Bootinfo
|
||||
|
||||
Although the bootinfo protocol provides device addresses, the QEMU `-M virt` machine uses fixed addresses for all MMIO devices (see Memory Map above). A standalone kernel that skips bootinfo can hardcode these addresses directly.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,689 @@
|
||||
package prog8.codegen.m68k
|
||||
|
||||
// M68k assembly-level peephole optimizer.
|
||||
//
|
||||
// It operates on a MutableList<String> of assembly lines. Only instructions that a peephole
|
||||
// actually matches are modified or removed; every other line (including all comments) is left
|
||||
// completely untouched. This keeps the generated .asm readable and comments intact.
|
||||
//
|
||||
// To add a new peephole:
|
||||
// 1. Write a private function `optimizeXxx(window: Sequence<List<TrimmedLine>>): List<Modification>`
|
||||
// that inspects a sliding window of consecutive instructions and returns the Modifications
|
||||
// it wants to apply.
|
||||
// 2. Register it in optimizeAssembly() by adding a line:
|
||||
// byN = runPass(optimizeXxx(byN), N, byN)
|
||||
// The optimizer re-runs all passes until a full pass makes no changes (so cascading optimizations
|
||||
// are picked up too).
|
||||
|
||||
internal fun optimizeAssembly(lines: MutableList<String>) {
|
||||
val pretrimmed = lines.map { it.trimStart() }.toMutableList()
|
||||
|
||||
while (true) {
|
||||
var modified = false
|
||||
|
||||
/** Runs a single optimization pass: applies any modifications, then recomputes the line windows. */
|
||||
fun runPass(
|
||||
mods: List<Modification>,
|
||||
windowSize: Int,
|
||||
currentLines: Sequence<List<TrimmedLine>>
|
||||
): Sequence<List<TrimmedLine>> {
|
||||
if (mods.isNotEmpty()) {
|
||||
applyModifications(mods, lines, pretrimmed)
|
||||
modified = true
|
||||
return getLinesBy(pretrimmed, lines, windowSize)
|
||||
}
|
||||
return currentLines
|
||||
}
|
||||
|
||||
var by2 = getLinesBy(pretrimmed, lines, 2)
|
||||
by2 = runPass(optimizeRedundantReload(by2), 2, by2) // REGFILE-DEPENDENT
|
||||
by2 = runPass(optimizeBounceToGlobal(by2, pretrimmed), 2, by2) // REGFILE-DEPENDENT
|
||||
by2 = runPass(optimizeJmpToNextLabel(by2), 2, by2)
|
||||
by2 = runPass(optimizeTailCall(by2), 2, by2)
|
||||
by2 = runPass(optimizeRedundantTst(by2), 2, by2)
|
||||
by2 = runPass(optimizeMsigbSpill(by2, pretrimmed), 2, by2) // REGFILE-DEPENDENT
|
||||
|
||||
// DBRA peephole for repeat loops is now obsolete: counted loops are emitted as IRLoopChunk and directly translated to dbra.
|
||||
// The old REGFILE-DEPENDENT pattern (move #N,p8_regfile+slot / label: body / subq #1,slot / bne label) no longer occurs.
|
||||
|
||||
if (!modified)
|
||||
break
|
||||
}
|
||||
// M3 is a full-file scan (not a 2-window peephole) and is idempotent; run once after the fixed-point loop
|
||||
// REGFILE-DEPENDENT
|
||||
val clampMods = optimizeClampImmediate(pretrimmed, lines)
|
||||
if (clampMods.isNotEmpty()) {
|
||||
applyModifications(clampMods, lines, pretrimmed)
|
||||
}
|
||||
// M4: fuse LOADX+JUMPI - eliminate the register file store/reload between array load and indirect jump
|
||||
// REGFILE-DEPENDENT
|
||||
val fuseMods = optimizeFuseLoadxJumpi(pretrimmed, lines)
|
||||
if (fuseMods.isNotEmpty()) {
|
||||
applyModifications(fuseMods, lines, pretrimmed)
|
||||
}
|
||||
}
|
||||
|
||||
/** A single edit to apply: either remove a line (keeping its label if present) or replace it. */
|
||||
private class Modification(val lineIndex: Int, val remove: Boolean, val replacement: String?)
|
||||
|
||||
/** Pre-trimmed assembly line that also knows its original text and source line index. */
|
||||
private class TrimmedLine(val value: String, val trimmed: String, val index: Int) {
|
||||
val instruction: String = trimmed.instructionPart()
|
||||
}
|
||||
|
||||
private fun getLinesBy(pretrimmed: MutableList<String>, originalLines: MutableList<String>, windowSize: Int): Sequence<List<TrimmedLine>> =
|
||||
pretrimmed.asSequence()
|
||||
.withIndex()
|
||||
.filter { it.value.isNotBlank() && !it.value.startsWith(';') }
|
||||
.map { TrimmedLine(originalLines[it.index], it.value, it.index) }
|
||||
.windowed(windowSize, partialWindows = false)
|
||||
|
||||
private fun applyModifications(modifications: List<Modification>, lines: MutableList<String>, pretrimmed: MutableList<String>) {
|
||||
for (modification in modifications.sortedBy { it.lineIndex }.reversed()) {
|
||||
val idx = modification.lineIndex
|
||||
if (modification.remove) {
|
||||
val pretrim = pretrimmed.getOrNull(idx)
|
||||
// If the line carries a label we must keep it (other code may jump to it);
|
||||
// otherwise the line is dropped entirely.
|
||||
if (pretrim == null || pretrim.isBlank() || pretrim.startsWith(';')) {
|
||||
lines.removeAt(idx)
|
||||
} else if (hasLabel(pretrim)) {
|
||||
val label = keepLabel(pretrim)
|
||||
if (label.isNotEmpty())
|
||||
lines[idx] = label
|
||||
else
|
||||
lines.removeAt(idx)
|
||||
} else {
|
||||
lines.removeAt(idx)
|
||||
}
|
||||
pretrimmed.removeAt(idx)
|
||||
} else {
|
||||
lines[idx] = modification.replacement!!
|
||||
pretrimmed[idx] = modification.replacement.trimStart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === helpers ===
|
||||
|
||||
/** Strips a leading label (e.g. "mylabel: move.l ..." -> "move.l ..."). */
|
||||
private fun String.instructionPart(): String {
|
||||
val idx = indexOf(':')
|
||||
return if (idx >= 0) substring(idx + 1).trimStart() else this
|
||||
}
|
||||
|
||||
private fun hasLabel(line: String): Boolean =
|
||||
line.length > 1 && !line.startsWith(';') && ':' in line
|
||||
|
||||
/** Returns just the "label:" part of a line, or "" if there is no label. */
|
||||
private fun keepLabel(line: String): String {
|
||||
val idx = line.indexOf(':')
|
||||
return if (idx >= 0) line.substring(0, idx + 1) else ""
|
||||
}
|
||||
|
||||
// Deliberately only recognizes move.b/w/l (data register moves), not movea or movem:
|
||||
// movea always writes to an address register (rejected by isSafeMemoryOperand), and
|
||||
// movem uses register-list operands (also rejected). So they can never participate in
|
||||
// the patterns we optimize, and excluding them keeps moveOperands() simple.
|
||||
private fun String.isMove(): Boolean =
|
||||
startsWith("move.b") || startsWith("move.w") || startsWith("move.l")
|
||||
|
||||
/** Size suffix of a move instruction: 'b', 'w' or 'l'. */
|
||||
private fun String.moveSize(): Char = substringAfter('.').firstOrNull() ?: ' '
|
||||
|
||||
/** Splits a move instruction "move.S src, dst" into (src, dst), or null if it isn't a move. */
|
||||
private fun String.moveOperands(): Pair<String, String>? {
|
||||
val sp = indexOf(' ')
|
||||
if (sp < 0) return null
|
||||
val rest = substring(sp + 1)
|
||||
val commentIdx = rest.indexOf(';')
|
||||
val ops = (if (commentIdx >= 0) rest.substring(0, commentIdx) else rest).split(',', limit = 2)
|
||||
if (ops.size != 2) return null
|
||||
return ops[0].trim() to ops[1].trim()
|
||||
}
|
||||
|
||||
private fun String.isRegister(): Boolean =
|
||||
this.matches(Regex("^(d[0-7]|a[0-7]|fp[0-9]+|sp|pc)$"))
|
||||
|
||||
/**
|
||||
* A plain memory operand that can be safely read twice and assumed to hit the same location
|
||||
* (e.g. "p8_regfile+268", "sys.DOSBase", "$1234", "symbol"). Excludes data/address registers
|
||||
* and any indirect/indexed addressing ((a0), (a0)+, ($1234,a0), ...) whose effective address may
|
||||
* change between the two accesses.
|
||||
*/
|
||||
private fun String.isSafeMemoryOperand(): Boolean {
|
||||
if (isEmpty()) return false
|
||||
if (isRegister()) return false
|
||||
if ('(' in this || ')' in this) return false
|
||||
return true
|
||||
}
|
||||
|
||||
// === peephole passes ===
|
||||
|
||||
private fun optimizeRedundantReload(linesBy: Sequence<List<TrimmedLine>>): List<Modification> {
|
||||
// REGFILE-DEPENDENT: this pattern is produced by the current regfile-based
|
||||
// codegen (store to p8_regfile then immediately reload to d0). If values are
|
||||
// kept in registers instead of spilled to p8_regfile, this peephole will
|
||||
// rarely fire and can be removed or generalized.
|
||||
// move.S d0, MEM
|
||||
// move.S MEM, d0 -> remove the second line
|
||||
//
|
||||
// The reload is redundant: d0 already holds the value (it was just stored from d0 and is
|
||||
// unchanged between the two instructions), so dropping the reload leaves d0 intact and the
|
||||
// store to MEM stays for any later readers.
|
||||
// MEM must be a plain memory operand (see isSafeMemoryOperand) so both accesses hit the
|
||||
// exact same location.
|
||||
val mods = mutableListOf<Modification>()
|
||||
for (lines in linesBy) {
|
||||
val first = lines[0].instruction
|
||||
val second = lines[1].instruction
|
||||
// Never remove an instruction that is a jump target: arriving via the label, d0 is
|
||||
// not guaranteed to hold the stored value, so the reload would be needed there.
|
||||
if (hasLabel(lines[1].trimmed))
|
||||
continue
|
||||
if (first.isMove() && second.isMove() && first.moveSize() == second.moveSize()) {
|
||||
val (src0, dst0) = first.moveOperands() ?: continue
|
||||
val (src1, dst1) = second.moveOperands() ?: continue
|
||||
if (src0 == "d0" && dst1 == "d0" && dst0 == src1 && dst0.isSafeMemoryOperand()) {
|
||||
mods.add(Modification(lines[1].index, true, null))
|
||||
}
|
||||
}
|
||||
}
|
||||
return mods
|
||||
}
|
||||
|
||||
private fun optimizeBounceToGlobal(linesBy: Sequence<List<TrimmedLine>>, allPretrimmed: List<String>): List<Modification> {
|
||||
// REGFILE-DEPENDENT: explicitly matches p8_regfile spill followed by copy
|
||||
// to a global. Depends on the current strategy of spilling all intermediate
|
||||
// values to p8_regfile. If regfile is refactored, this bounce disappears.
|
||||
// move.S d0, p8_regfile+OFF ; spill result to regfile temp
|
||||
// move.S p8_regfile+OFF, GLOBAL ; copy temp -> global
|
||||
// ->
|
||||
// move.S d0, GLOBAL ; store directly, skip regfile bounce
|
||||
//
|
||||
// Only safe when p8_regfile+OFF is NOT read again in the remainder of the current
|
||||
// subroutine. We scan forward from the global store to the next subroutine boundary
|
||||
// to confirm this.
|
||||
val mods = mutableListOf<Modification>()
|
||||
for (lines in linesBy) {
|
||||
val first = lines[0].instruction
|
||||
val second = lines[1].instruction
|
||||
if (hasLabel(lines[0].trimmed) || hasLabel(lines[1].trimmed))
|
||||
continue
|
||||
if (first.isMove() && second.isMove() && first.moveSize() == second.moveSize()) {
|
||||
val (src0, dst0) = first.moveOperands() ?: continue
|
||||
val (src1, dst1) = second.moveOperands() ?: continue
|
||||
if (src0 == "d0" && dst1 != "d0" && dst0 == src1
|
||||
&& dst0.isRegfileSlot() && dst1.isSafeMemoryOperand()
|
||||
&& !isSlotReadAfter(dst0, lines[1].index + 1, allPretrimmed)
|
||||
) {
|
||||
val size = first.moveSize()
|
||||
val indent = lines[1].value.takeWhile { it.isWhitespace() }
|
||||
mods.add(Modification(lines[0].index, true, null)) // remove regfile spill
|
||||
mods.add(Modification(lines[1].index, false, "${indent}move.$size d0, $dst1")) // direct store
|
||||
}
|
||||
}
|
||||
}
|
||||
return mods
|
||||
}
|
||||
|
||||
private fun optimizeJmpToNextLabel(linesBy: Sequence<List<TrimmedLine>>): List<Modification> {
|
||||
// bra LABEL
|
||||
// LABEL: -> remove the branch (keep any label on the branch line itself)
|
||||
//
|
||||
// The branch is dead code because the target label immediately follows.
|
||||
// (also handles the older jmp form)
|
||||
val mods = mutableListOf<Modification>()
|
||||
for (lines in linesBy) {
|
||||
val firstInstr = lines[0].instruction
|
||||
val secondLine = lines[1].trimmed
|
||||
val branchMnemonic = when {
|
||||
firstInstr.startsWith("bra ") -> "bra"
|
||||
firstInstr.startsWith("jmp ") -> "jmp"
|
||||
else -> null
|
||||
} ?: continue
|
||||
val target = firstInstr.substringAfter("$branchMnemonic ").trim()
|
||||
// Skip indirect jumps like jmp ([p8_regfile+12]) or jmp (a0)
|
||||
if (target.startsWith("("))
|
||||
continue
|
||||
if (hasLabel(secondLine)) {
|
||||
val label = keepLabel(secondLine).removeSuffix(":")
|
||||
if (target == label) {
|
||||
if (hasLabel(lines[0].trimmed)) {
|
||||
// The branch line also carries a label - keep the label, drop the branch
|
||||
mods.add(Modification(lines[0].index, false, keepLabel(lines[0].trimmed)))
|
||||
} else {
|
||||
mods.add(Modification(lines[0].index, true, null))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return mods
|
||||
}
|
||||
|
||||
private fun optimizeTailCall(linesBy: Sequence<List<TrimmedLine>>): List<Modification> {
|
||||
// bsr LABEL
|
||||
// rts -> bra LABEL
|
||||
//
|
||||
// When a subroutine call is immediately followed by a return, replace the call with a branch.
|
||||
// The called routine will return directly to the caller's caller (tail call optimization).
|
||||
// Only handles direct label targets, not indirect (bsr (a0) etc.).
|
||||
// (also handles the older jsr form, normalizing it to bra as well)
|
||||
val mods = mutableListOf<Modification>()
|
||||
for (lines in linesBy) {
|
||||
val firstInstr = lines[0].instruction
|
||||
val secondInstr = lines[1].instruction
|
||||
// Don't optimize if the rts line has a label (it's a jump target)
|
||||
if (hasLabel(lines[1].trimmed))
|
||||
continue
|
||||
val callMnemonic = when {
|
||||
firstInstr.startsWith("bsr ") -> "bsr"
|
||||
firstInstr.startsWith("jsr ") -> "jsr"
|
||||
else -> null
|
||||
} ?: continue
|
||||
if (secondInstr == "rts") {
|
||||
val target = firstInstr.substringAfter("$callMnemonic ").trim()
|
||||
// Skip indirect/library calls like bsr (a0), jsr (a0) or jsr dos.VPrintf(a6)
|
||||
if ('(' in target)
|
||||
continue
|
||||
val indent = lines[0].value.takeWhile { it.isWhitespace() }
|
||||
if (hasLabel(lines[0].trimmed)) {
|
||||
// Keep the label, replace call with branch and remove rts
|
||||
val label = keepLabel(lines[0].trimmed)
|
||||
mods.add(Modification(lines[0].index, false, "$label\n${indent}bra $target"))
|
||||
mods.add(Modification(lines[1].index, true, null))
|
||||
} else {
|
||||
// Replace call with branch and remove rts
|
||||
mods.add(Modification(lines[0].index, false, "${indent}bra $target"))
|
||||
mods.add(Modification(lines[1].index, true, null))
|
||||
}
|
||||
}
|
||||
}
|
||||
return mods
|
||||
}
|
||||
|
||||
private fun optimizeRedundantTst(linesBy: Sequence<List<TrimmedLine>>): List<Modification> {
|
||||
// move.S SRC, DST
|
||||
// tst.S DST -> remove the tst
|
||||
//
|
||||
// andi.S #imm, DST
|
||||
// tst.S DST -> remove the tst
|
||||
//
|
||||
// ori.S #imm, DST
|
||||
// tst.S DST -> remove the tst
|
||||
//
|
||||
// On M68k, move/andi/ori/eori set N, Z based on the result. So a tst
|
||||
// immediately after to the same location is redundant.
|
||||
val mods = mutableListOf<Modification>()
|
||||
for (lines in linesBy) {
|
||||
val first = lines[0].instruction
|
||||
val second = lines[1].instruction
|
||||
// Don't remove tst if it's a jump target
|
||||
if (hasLabel(lines[1].trimmed))
|
||||
continue
|
||||
if (!second.startsWith("tst."))
|
||||
continue
|
||||
val dst0 = first.flagSettingDest() ?: continue
|
||||
if (first.moveSize() != second.moveSize())
|
||||
continue
|
||||
val tstRaw = second.substringAfter('.').substringAfter(' ').trim()
|
||||
val tstOperand = if (';' in tstRaw) tstRaw.substringBefore(';').trim() else tstRaw
|
||||
if (dst0 == tstOperand && dst0.isSafeMemoryOperand()) {
|
||||
mods.add(Modification(lines[1].index, true, null))
|
||||
}
|
||||
}
|
||||
return mods
|
||||
}
|
||||
|
||||
/**
|
||||
* If this instruction sets N/Z flags and writes to a single destination, return that destination.
|
||||
* Covers move, andi, ori, eori (the ones the codegen emits before tst).
|
||||
* Note: add/sub also set condition codes, but the codegen always inserts a move before branching,
|
||||
* so add/sub are never followed by a redundant tst.
|
||||
*/
|
||||
private fun String.flagSettingDest(): String? {
|
||||
val stripped = if (';' in this) substring(0, indexOf(';')).trim() else this
|
||||
return when {
|
||||
isMove() -> moveOperands()?.second
|
||||
startsWith("andi.") || startsWith("ori.") || startsWith("eori.") -> {
|
||||
// andi.S #imm, dst
|
||||
val sp = stripped.indexOf(' ')
|
||||
if (sp < 0) return null
|
||||
val ops = stripped.substring(sp + 1).split(',', limit = 2)
|
||||
if (ops.size != 2) return null
|
||||
ops[1].trim()
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun optimizeMsigbSpill(linesBy: Sequence<List<TrimmedLine>>, allPretrimmed: List<String>): List<Modification> {
|
||||
// REGFILE-DEPENDENT: optimizes the spill of a word/long to p8_regfile
|
||||
// followed by a byte read of its MSB. This spill is an artifact of the
|
||||
// current regfile-based codegen and big-endian layout. If values stay in
|
||||
// registers, the spill is never emitted.
|
||||
// move.w/l SYMBOL, p8_regfile+N ; spill word/long to slot
|
||||
// move.b p8_regfile+N, d0 ; read MSB
|
||||
// -> move.b SYMBOL, d0 ; big-endian: MSB is at SYMBOL+0, no spill needed
|
||||
// Keep the spill only if the slot is read again later in the subroutine.
|
||||
val mods = mutableListOf<Modification>()
|
||||
for (lines in linesBy) {
|
||||
if (hasLabel(lines[0].trimmed) || hasLabel(lines[1].trimmed)) continue
|
||||
val first = lines[0].instruction
|
||||
val second = lines[1].instruction
|
||||
if (!first.isMove() || !second.isMove()) continue
|
||||
if (second.moveSize() != 'b') continue
|
||||
val firstSize = first.moveSize()
|
||||
if (firstSize != 'w' && firstSize != 'l') continue
|
||||
val (src0, dst0) = first.moveOperands() ?: continue
|
||||
val (src1, dst1) = second.moveOperands() ?: continue
|
||||
if (dst1 != "d0") continue
|
||||
if (src1 != dst0) continue
|
||||
if (!dst0.isRegfileSlot()) continue
|
||||
if (src0.isRegfileSlot() || src0.isRegister()) continue
|
||||
if ('(' in src0 || ')' in src0) continue
|
||||
// src0 must be a plain memory operand (symbol)
|
||||
if (!src0.isSafeMemoryOperand()) continue
|
||||
// If the slot is read again later, keep the spill; otherwise we can remove it
|
||||
val indent2 = lines[1].value.takeWhile { it.isWhitespace() }
|
||||
if (isSlotReadAfter(dst0, lines[1].index + 1, allPretrimmed)) {
|
||||
// keep spill, just rewrite the byte load to read directly from the symbol
|
||||
mods.add(Modification(lines[1].index, false, "${indent2}move.b $src0,d0"))
|
||||
} else {
|
||||
mods.add(Modification(lines[0].index, true, null))
|
||||
mods.add(Modification(lines[1].index, false, "${indent2}move.b $src0,d0"))
|
||||
}
|
||||
}
|
||||
return mods
|
||||
}
|
||||
|
||||
private fun optimizeClampImmediate(allPretrimmed: List<String>, lines: MutableList<String>): List<Modification> {
|
||||
// REGFILE-DEPENDENT: folds clamp bounds that were materialized as
|
||||
// move #imm,p8_regfile+SLOT. Depends on the current codegen that spills
|
||||
// clamp immediates to regfile slots before the compare block. If immediates
|
||||
// are kept in registers or encoded as cmpi, this pattern disappears.
|
||||
// M3: fold clamp bounds that were materialized as move #imm,SLOT.
|
||||
// Only fold when we can identify the full clamp block, to avoid touching unrelated compares.
|
||||
// Pattern (filtered lines, b/w/l size must match):
|
||||
// cmp.S SLOT_MIN,d0
|
||||
// bge/bhs .clamp_max
|
||||
// move.S SLOT_MIN,d0
|
||||
// bra .clamp_done
|
||||
// .clamp_max:
|
||||
// cmp.S SLOT_MAX,d0
|
||||
// ble/bls .clamp_done
|
||||
// move.S SLOT_MAX,d0
|
||||
// .clamp_done:
|
||||
// where SLOT_MIN/MAX were defined as move.S #imm,SLOT within a few lines before the block.
|
||||
val filtered = allPretrimmed.withIndex()
|
||||
.filter { it.value.isNotBlank() && !it.value.trimStart().startsWith(';') }
|
||||
.map { TrimmedLine(lines[it.index], it.value.trimStart(), it.index) }
|
||||
|
||||
fun findDefine(slot: String, size: Char, useIdx: Int, limit: Int = 6): Pair<Int,String>? {
|
||||
var count = 0
|
||||
for (k in useIdx-1 downTo 0) {
|
||||
if (count >= limit) break
|
||||
count++
|
||||
val cand = filtered[k]
|
||||
val instr = cand.instruction
|
||||
if (!instr.isMove()) continue
|
||||
if (instr.moveSize() != size) continue
|
||||
val (src, dst) = instr.moveOperands() ?: continue
|
||||
if (dst != slot) continue
|
||||
if (!src.startsWith("#")) continue
|
||||
var overwritten = false
|
||||
for (t in k+1 until useIdx) {
|
||||
val mid = filtered[t]
|
||||
if (mid.instruction.isMove()) {
|
||||
val ops = mid.instruction.moveOperands()
|
||||
if (ops != null && ops.second == slot) { overwritten = true; break }
|
||||
}
|
||||
}
|
||||
if (!overwritten) return cand.index to src
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val mods = mutableListOf<Modification>()
|
||||
// Scan for clamp blocks
|
||||
for (idx in filtered.indices) {
|
||||
val instr = filtered[idx].instruction
|
||||
if (!instr.startsWith("cmp.")) continue
|
||||
// need at least 8 more filtered lines for a full block
|
||||
if (idx + 8 >= filtered.size) continue
|
||||
val sizeMin = instr[4]
|
||||
if (sizeMin != 'b' && sizeMin != 'w' && sizeMin != 'l') continue
|
||||
val cmpMinParts = instr.substringAfter(' ').trim().split(',', limit = 2).map { it.trim().substringBefore(';').trim() }
|
||||
if (cmpMinParts.size != 2 || cmpMinParts[1] != "d0" || !cmpMinParts[0].isRegfileSlot()) continue
|
||||
val slotMin = cmpMinParts[0]
|
||||
|
||||
val bgeInstr = filtered[idx+1].instruction
|
||||
if (!bgeInstr.startsWith("bge ") && !bgeInstr.startsWith("bhs ")) continue
|
||||
// bge target should be the next label
|
||||
val labelMax = bgeInstr.substringAfter(' ').trim().substringBefore(';').trim()
|
||||
|
||||
val moveMinInstr = filtered[idx+2].instruction
|
||||
if (!moveMinInstr.startsWith("move.")) continue
|
||||
if (moveMinInstr.moveSize() != sizeMin) continue
|
||||
val moveMinOps = moveMinInstr.moveOperands() ?: continue
|
||||
if (moveMinOps.first != slotMin || moveMinOps.second != "d0") continue
|
||||
|
||||
val braInstr = filtered[idx+3].instruction
|
||||
if (!braInstr.startsWith("bra ")) continue
|
||||
val labelDone = braInstr.substringAfter(' ').trim().substringBefore(';').trim()
|
||||
|
||||
val labelMaxLine = filtered[idx+4].trimmed
|
||||
if (!labelMaxLine.startsWith("$labelMax:")) continue
|
||||
|
||||
val cmpMaxInstr = filtered[idx+5].instruction
|
||||
if (!cmpMaxInstr.startsWith("cmp.")) continue
|
||||
if (cmpMaxInstr[4] != sizeMin) continue
|
||||
val cmpMaxParts = cmpMaxInstr.substringAfter(' ').trim().split(',', limit = 2).map { it.trim().substringBefore(';').trim() }
|
||||
if (cmpMaxParts.size != 2 || cmpMaxParts[1] != "d0" || !cmpMaxParts[0].isRegfileSlot()) continue
|
||||
val slotMax = cmpMaxParts[0]
|
||||
|
||||
val bleInstr = filtered[idx+6].instruction
|
||||
if (!bleInstr.startsWith("ble ") && !bleInstr.startsWith("bls ")) continue
|
||||
val bleTarget = bleInstr.substringAfter(' ').trim().substringBefore(';').trim()
|
||||
if (bleTarget != labelDone) continue
|
||||
|
||||
val moveMaxInstr = filtered[idx+7].instruction
|
||||
if (!moveMaxInstr.startsWith("move.")) continue
|
||||
if (moveMaxInstr.moveSize() != sizeMin) continue
|
||||
val moveMaxOps = moveMaxInstr.moveOperands() ?: continue
|
||||
if (moveMaxOps.first != slotMax || moveMaxOps.second != "d0") continue
|
||||
|
||||
val labelDoneLine = filtered[idx+8].trimmed
|
||||
if (!labelDoneLine.startsWith("$labelDone:")) continue
|
||||
|
||||
// Found a clamp block; try to fold each bound if its slot has an immediate define
|
||||
val defineMin = findDefine(slotMin, sizeMin, idx)
|
||||
if (defineMin != null) {
|
||||
val immMin = defineMin.second
|
||||
val indentCmp = filtered[idx].value.takeWhile { it.isWhitespace() }
|
||||
val indentMove = filtered[idx+2].value.takeWhile { it.isWhitespace() }
|
||||
mods.add(Modification(filtered[idx].index, false, "${indentCmp}cmpi.$sizeMin $immMin,d0"))
|
||||
mods.add(Modification(filtered[idx+2].index, false, "${indentMove}move.$sizeMin $immMin,d0"))
|
||||
// If the slot is not read after the clamp block, the defining move is dead
|
||||
val defineIdx = defineMin.first
|
||||
if (!isSlotReadAfter(slotMin, filtered[idx+8].index + 1, allPretrimmed)) {
|
||||
mods.add(Modification(defineIdx, true, null))
|
||||
}
|
||||
}
|
||||
val defineMax = findDefine(slotMax, sizeMin, idx+5)
|
||||
if (defineMax != null) {
|
||||
val immMax = defineMax.second
|
||||
val indentCmp2 = filtered[idx+5].value.takeWhile { it.isWhitespace() }
|
||||
val indentMove2 = filtered[idx+7].value.takeWhile { it.isWhitespace() }
|
||||
mods.add(Modification(filtered[idx+5].index, false, "${indentCmp2}cmpi.$sizeMin $immMax,d0"))
|
||||
mods.add(Modification(filtered[idx+7].index, false, "${indentMove2}move.$sizeMin $immMax,d0"))
|
||||
val defineIdxMax = defineMax.first
|
||||
if (!isSlotReadAfter(slotMax, filtered[idx+8].index + 1, allPretrimmed)) {
|
||||
mods.add(Modification(defineIdxMax, true, null))
|
||||
}
|
||||
}
|
||||
}
|
||||
return mods
|
||||
}
|
||||
|
||||
// REGFILE-DEPENDENT helper: identifies operands that refer to the current
|
||||
// p8_regfile spill area. If the regfile is refactored, this predicate and
|
||||
// all peepholes that use it must be updated.
|
||||
/** True if this operand is a prog8 register-file slot (e.g. "p8_regfile+268", "p8_fregfile+12"). */
|
||||
private fun String.isRegfileSlot(): Boolean =
|
||||
startsWith("p8_regfile+") || startsWith("p8_fregfile+")
|
||||
|
||||
// REGFILE-DEPENDENT helper: scans for future reads of a regfile slot.
|
||||
// Used by bounce/msigb/clamp/fuse peepholes to decide if a spill can be removed.
|
||||
/**
|
||||
* Scans forward from [startIndex] to the next subroutine boundary (or end of file) and
|
||||
* returns true if [slot] is referenced in any instruction line in that range.
|
||||
* A "reference" means the slot string appears as a standalone operand (not as a prefix
|
||||
* of a longer offset like p8_regfile+640 when checking p8_regfile+64).
|
||||
*/
|
||||
private fun isSlotReadAfter(slot: String, startIndex: Int, allPretrimmed: List<String>): Boolean {
|
||||
for (i in startIndex until allPretrimmed.size) {
|
||||
val line = allPretrimmed[i]
|
||||
if (line.startsWith("; ---- Subroutine:") || line.startsWith("; End of subroutine:")
|
||||
|| line.startsWith("prog8_program_end"))
|
||||
break
|
||||
if (line.isBlank() || line.startsWith(';')) continue
|
||||
if (isSlotReferencedInLine(slot, line)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// REGFILE-DEPENDENT helper
|
||||
private fun isSlotReferencedInLine(slot: String, line: String): Boolean {
|
||||
var from = 0
|
||||
while (true) {
|
||||
val idx = line.indexOf(slot, from)
|
||||
if (idx < 0) return false
|
||||
val after = idx + slot.length
|
||||
// Ensure it's not a prefix of a longer offset (e.g. p8_regfile+6 vs p8_regfile+64)
|
||||
if (after < line.length && line[after].isDigit()) {
|
||||
from = after
|
||||
continue
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* REGFILE-DEPENDENT: fuses the LOADX+JUMPI / LOADX+CALLI sequence for
|
||||
* `on .. goto` / `on .. call` / `when` dispatch. The codegen emits an index
|
||||
* load, table lookup into d0, spill to p8_regfile, reload to a0 and indirect
|
||||
* jump/call. If the regfile is refactored so the table lookup can target a0
|
||||
* (or an address register) directly, this spill/reload disappears and the
|
||||
* peephole becomes dead code.
|
||||
*
|
||||
* Current codegen (byte index, 68000-style JUMPI/CALLI):
|
||||
* moveq #0, d0
|
||||
* move.b p8_regfile+N, d0 ; or move.w p8_regfile+N,d0 for word index
|
||||
* lea table_label, a0
|
||||
* move.l (a0,d0.w), d0 ; table lookup (unscaled, see scaled-indexing-IR.md)
|
||||
* move.l d0, p8_regfile+M ; spill (LOADX tail)
|
||||
* movea.l p8_regfile+M, a0 ; reload (JUMPI/CALLI head, 68000)
|
||||
* jmp/jsr (a0)
|
||||
* or for 68020+ JUMPI/CALLI:
|
||||
* jmp/jsr ([p8_regfile+M]) ; memory-indirect
|
||||
*
|
||||
* Fused (preserves current unscaled semantics; no *4 scaling):
|
||||
* moveq #0, d0
|
||||
* move.b p8_regfile+N, d0
|
||||
* lea table_label, a0
|
||||
* move.l (a0,d0.w), a0
|
||||
* jmp/jsr (a0)
|
||||
*/
|
||||
private fun optimizeFuseLoadxJumpi(allPretrimmed: List<String>, lines: MutableList<String>): List<Modification> {
|
||||
val filtered = allPretrimmed.withIndex()
|
||||
.filter { it.value.isNotBlank() && !it.value.trimStart().startsWith(';') }
|
||||
.map { TrimmedLine(lines[it.index], it.value.trimStart(), it.index) }
|
||||
|
||||
val mods = mutableListOf<Modification>()
|
||||
val alreadyModified = mutableSetOf<Int>()
|
||||
|
||||
fun isLoadIndexed(instr: String): Boolean {
|
||||
// matches "move.l (a0,d0.w), d0" with flexible spacing
|
||||
if (!instr.startsWith("move.l")) return false
|
||||
if ("(a0,d0.w)" !in instr) return false
|
||||
return instr.trim().endsWith(", d0") || instr.trim().endsWith(",d0")
|
||||
}
|
||||
|
||||
fun isSpill(instr: String): String? {
|
||||
// "move.l d0, p8_regfile+M"
|
||||
if (!instr.startsWith("move.l")) return null
|
||||
val ops = instr.moveOperands() ?: return null
|
||||
if (ops.first != "d0") return null
|
||||
if (!ops.second.isRegfileSlot()) return null
|
||||
return ops.second.removePrefix("p8_regfile+").substringBefore(',').substringBefore(' ').trim()
|
||||
}
|
||||
|
||||
for (leaIdx in filtered.indices) {
|
||||
if (filtered[leaIdx].index in alreadyModified) continue
|
||||
val leaInstr = filtered[leaIdx].instruction
|
||||
if (!leaInstr.startsWith("lea ")) continue
|
||||
if (!leaInstr.endsWith(",a0")) continue
|
||||
|
||||
// The index is usually already in d0 at this point (scaled via lsl.w #2
|
||||
// or zero-extended). Earlier peepholes may have removed the explicit
|
||||
// reload of the scaled index from p8_regfile, so we do not require a
|
||||
// preceding moveq/move.b or move.w pattern. We only verify the lea
|
||||
// itself and the following LOADX+JUMPI spill/jump sequence.
|
||||
|
||||
// Need at least: lea, loadIndexed, spill, jump/reload
|
||||
if (leaIdx + 2 >= filtered.size) continue
|
||||
val loadIdx = leaIdx + 1
|
||||
val spillIdx = leaIdx + 2
|
||||
if (filtered[loadIdx].index in alreadyModified || filtered[spillIdx].index in alreadyModified) continue
|
||||
if (!isLoadIndexed(filtered[loadIdx].instruction)) continue
|
||||
val slotM = isSpill(filtered[spillIdx].instruction) ?: continue
|
||||
|
||||
// Check jump/call variant: 68000 uses movea reload + jmp/jsr (a0),
|
||||
// 68020 uses memory-indirect jmp/jsr ([slot])
|
||||
val nextIdx = leaIdx + 3
|
||||
if (nextIdx >= filtered.size) continue
|
||||
val nextInstr = filtered[nextIdx].instruction
|
||||
val is68000Reload = nextInstr.startsWith("movea.l") && slotM in nextInstr && nextInstr.trim().endsWith(",a0")
|
||||
val is68020MemIndirectJmp = nextInstr.startsWith("jmp") && "([p8_regfile+$slotM" in nextInstr.replace(" ", "")
|
||||
val is68020MemIndirectJsr = nextInstr.startsWith("jsr") && "([p8_regfile+$slotM" in nextInstr.replace(" ", "")
|
||||
|
||||
if (is68000Reload) {
|
||||
// Need jmp/jsr (a0) after the reload
|
||||
if (nextIdx + 1 >= filtered.size) continue
|
||||
val jmpIdx = nextIdx + 1
|
||||
if (filtered[jmpIdx].index in alreadyModified) continue
|
||||
val jmpInstr = filtered[jmpIdx].instruction
|
||||
val isJmp = jmpInstr == "jmp (a0)" || jmpInstr.replace(" ", "") == "jmp(a0)"
|
||||
val isJsr = jmpInstr.startsWith("jsr") && "(a0)" in jmpInstr.replace(" ", "")
|
||||
if (!isJmp && !isJsr) continue
|
||||
// No labels on lea, load, spill, reload, jmp/jsr
|
||||
if ((0..1).any { hasLabel(filtered[loadIdx + it].trimmed) }) continue
|
||||
if (hasLabel(filtered[leaIdx].trimmed)) continue
|
||||
if (hasLabel(filtered[spillIdx].trimmed) || hasLabel(filtered[nextIdx].trimmed) || hasLabel(filtered[jmpIdx].trimmed)) continue
|
||||
|
||||
val loadIndent = lines[filtered[loadIdx].index].takeWhile { it.isWhitespace() }
|
||||
mods.add(Modification(filtered[loadIdx].index, false, "${loadIndent}move.l (a0,d0.w), a0"))
|
||||
mods.add(Modification(filtered[spillIdx].index, true, null))
|
||||
mods.add(Modification(filtered[nextIdx].index, true, null))
|
||||
alreadyModified.add(filtered[loadIdx].index)
|
||||
alreadyModified.add(filtered[spillIdx].index)
|
||||
alreadyModified.add(filtered[nextIdx].index)
|
||||
// jmp/jsr stays as is
|
||||
} else if (is68020MemIndirectJmp || is68020MemIndirectJsr) {
|
||||
if (hasLabel(filtered[spillIdx].trimmed) || hasLabel(filtered[nextIdx].trimmed)) continue
|
||||
if (hasLabel(filtered[leaIdx].trimmed) || hasLabel(filtered[loadIdx].trimmed)) continue
|
||||
val loadIndent = lines[filtered[loadIdx].index].takeWhile { it.isWhitespace() }
|
||||
val jmpIndent = lines[filtered[nextIdx].index].takeWhile { it.isWhitespace() }
|
||||
val mnemonic = if (is68020MemIndirectJsr) "jsr" else "jmp"
|
||||
mods.add(Modification(filtered[loadIdx].index, false, "${loadIndent}move.l (a0,d0.w), a0"))
|
||||
mods.add(Modification(filtered[spillIdx].index, true, null))
|
||||
mods.add(Modification(filtered[nextIdx].index, false, "${jmpIndent}$mnemonic (a0)"))
|
||||
alreadyModified.add(filtered[loadIdx].index)
|
||||
alreadyModified.add(filtered[spillIdx].index)
|
||||
alreadyModified.add(filtered[nextIdx].index)
|
||||
}
|
||||
}
|
||||
|
||||
return mods
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package prog8.codegen.m68k
|
||||
|
||||
import prog8.code.assembly.IAssemblyProgram
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.CpuType
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.code.core.OutputType
|
||||
import prog8.code.target.Amiga1200Target
|
||||
import prog8.code.target.Amiga500Target
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
class AssemblyProgramM68k(override val name: String, private val outputDir: Path) : IAssemblyProgram {
|
||||
|
||||
override val irInstructionCount: Int = 0
|
||||
override val irChunkCount: Int = 0
|
||||
override val irRegisterCount: Int = 0
|
||||
|
||||
private val assemblyFile = outputDir.resolve("$name.asm")
|
||||
|
||||
fun elfFile(): Path = outputDir.resolve("$name.elf")
|
||||
|
||||
private fun runProcess(command: List<String>, quiet: Boolean, tool: String? = null): Boolean {
|
||||
val proc = ProcessBuilder(command).redirectErrorStream(true)
|
||||
if (quiet)
|
||||
proc.redirectOutput(ProcessBuilder.Redirect.DISCARD)
|
||||
val process = try {
|
||||
proc.start()
|
||||
} catch (e: Exception) {
|
||||
when {
|
||||
tool=="vasm" -> {
|
||||
System.err.println("Cannot find '${command[0]}' (vasm assembler for m68k). Install it via your package manager if it's on there, or build it from source: http://sun.hasenbraten.de/vasm/")
|
||||
}
|
||||
tool=="vlink" -> {
|
||||
System.err.println("Cannot find 'vlink' (linker). Install it via your package manager if it's on there, or build it from source: http://sun.hasenbraten.de/vlink/")
|
||||
}
|
||||
else -> {
|
||||
System.err.println("process failed to start: ${e.message}")
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (!quiet) {
|
||||
process.inputStream.bufferedReader().use { reader ->
|
||||
reader.forEachLine { println(it) }
|
||||
}
|
||||
}
|
||||
return process.waitFor() == 0
|
||||
}
|
||||
|
||||
override fun assemble(options: CompilationOptions, errors: IErrorReporter): Boolean {
|
||||
val cpu = when(options.compTarget.cpu) {
|
||||
CpuType.M68000 -> "68000"
|
||||
CpuType.M68020 -> "68020"
|
||||
else -> error("invalid cpu type for m68k codegen ${options.compTarget.cpu}")
|
||||
}
|
||||
val assemblerCpu = if(options.compTarget.name == "amiga500" && options.floats) "68020" else cpu
|
||||
|
||||
when(options.output) {
|
||||
OutputType.ELF -> {
|
||||
// Step 1: assemble to ELF object file
|
||||
val objFile = outputDir.resolve("$name.o")
|
||||
val listFile = outputDir.resolve("$name.list")
|
||||
val assembleCmd = mutableListOf(
|
||||
"vasmm68k_mot",
|
||||
"-m$assemblerCpu",
|
||||
"-m68881", // enable FPU
|
||||
"-Felf",
|
||||
"-opt-speed",
|
||||
"-warnunaligned",
|
||||
"-ldots",
|
||||
"-spaces",
|
||||
"-o", objFile.toString(),
|
||||
assemblyFile.toString()
|
||||
)
|
||||
if (options.asmListfile)
|
||||
assembleCmd.addAll(listOf("-L", listFile.toString()))
|
||||
if (options.asmQuiet)
|
||||
assembleCmd.add("-quiet")
|
||||
if (!runProcess(assembleCmd, options.quiet, "vasm"))
|
||||
return false
|
||||
// clean up any leftover ELF/obj files from previous builds
|
||||
Files.deleteIfExists(outputDir.resolve("$name.bin"))
|
||||
|
||||
// Step 2: write linker script and link to ELF executable
|
||||
val linkScript = outputDir.resolve("$name.link.ld")
|
||||
val elfFile = elfFile()
|
||||
val resourceUrl = AssemblyProgramM68k::class.java.getResource("/prog8lib/qemu68k/link.ld")
|
||||
?: error("cannot find /prog8lib/qemu68k/link.ld resource")
|
||||
Files.writeString(linkScript, resourceUrl.readText())
|
||||
|
||||
val linkCmd = listOf(
|
||||
"vlink",
|
||||
"-b", "elf32m68k",
|
||||
"-n",
|
||||
"-T", linkScript.toString(),
|
||||
"-o", elfFile.toString(),
|
||||
objFile.toString()
|
||||
)
|
||||
val linkOk = runProcess(linkCmd, options.quiet, "vlink")
|
||||
Files.deleteIfExists(linkScript)
|
||||
if(linkOk && !options.quiet)
|
||||
println("Executable written to '$elfFile'")
|
||||
return linkOk
|
||||
}
|
||||
OutputType.AMIGAHUNK -> {
|
||||
// Step 1: assemble directly to AmigaHunk executable file
|
||||
val exefile = outputDir.resolve(name)
|
||||
val listfile = outputDir.resolve("$name.list")
|
||||
val assembleCmd = when(options.compTarget) {
|
||||
is Amiga500Target -> {
|
||||
// amiga 500 with kickstart 1.3
|
||||
mutableListOf(
|
||||
"vasmm68k_mot",
|
||||
"-m$assemblerCpu",
|
||||
*if(options.floats) arrayOf("-m68881") else emptyArray(),
|
||||
"-Fhunkexe",
|
||||
"-kick1hunks", // old hunk format compatible with AmigaDOS 1.3
|
||||
"-opt-speed",
|
||||
"-warnunaligned",
|
||||
"-ldots",
|
||||
"-spaces",
|
||||
"-nosym", // no debug symbols
|
||||
"-o", exefile.toString(),
|
||||
assemblyFile.toString()
|
||||
).also { cmd ->
|
||||
if (options.asmListfile) {
|
||||
cmd.add("-L")
|
||||
cmd.add(listfile.toString())
|
||||
cmd.add("-Lns")
|
||||
}
|
||||
if (options.asmQuiet)
|
||||
cmd.add("-quiet")
|
||||
}
|
||||
}
|
||||
is Amiga1200Target -> {
|
||||
// amiga 1200 with 68020 and optional FPU
|
||||
mutableListOf(
|
||||
"vasmm68k_mot",
|
||||
"-m$assemblerCpu",
|
||||
"-m68881", // enable FPU
|
||||
"-Fhunkexe",
|
||||
"-opt-speed",
|
||||
"-warnunaligned",
|
||||
"-ldots",
|
||||
"-spaces",
|
||||
"-nosym", // no debug symbols
|
||||
"-o", exefile.toString(),
|
||||
assemblyFile.toString()
|
||||
).also { cmd ->
|
||||
if (options.asmListfile) {
|
||||
cmd.add("-L")
|
||||
cmd.add(listfile.toString())
|
||||
}
|
||||
if (options.asmQuiet)
|
||||
cmd.add("-quiet")
|
||||
}
|
||||
}
|
||||
else -> error("unsupported target for AMIGAHUNK output: ${options.compTarget.name}")
|
||||
}
|
||||
if (!runProcess(assembleCmd, options.quiet, "vasm"))
|
||||
return false
|
||||
if(!options.quiet)
|
||||
println("Executable written to '$exefile'")
|
||||
return true
|
||||
}
|
||||
else -> error("Unsupported output type: ${options.output}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,820 @@
|
||||
package prog8.codegen.m68k
|
||||
|
||||
import prog8.code.core.CpuType
|
||||
import prog8.intermediate.*
|
||||
|
||||
internal fun AsmGen.translateArithmetic(insn: IRInstruction) {
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
|
||||
if (type == IRDataType.FLOAT) {
|
||||
translateFloatArithmetic(insn)
|
||||
return
|
||||
}
|
||||
|
||||
when (insn.opcode) {
|
||||
Opcode.INC -> {
|
||||
val reg = insn.requireIntDest().intNumber
|
||||
emitLine("addq${dtSuffix(type)} #1, ${regAddr(reg)}")
|
||||
invalidateD0CacheForSlot(reg)
|
||||
}
|
||||
|
||||
Opcode.INCM -> {
|
||||
val target = resolveMemory(insn.requireMemory())
|
||||
emitLine("addq${dtSuffix(type)} #1, $target")
|
||||
invalidateD0CacheForAddress(target)
|
||||
}
|
||||
|
||||
Opcode.DEC -> {
|
||||
val reg = insn.requireIntDest().intNumber
|
||||
emitLine("subq${dtSuffix(type)} #1, ${regAddr(reg)}")
|
||||
invalidateD0CacheForSlot(reg)
|
||||
}
|
||||
|
||||
Opcode.DECM -> {
|
||||
val target = resolveMemory(insn.requireMemory())
|
||||
emitLine("subq${dtSuffix(type)} #1, $target")
|
||||
invalidateD0CacheForAddress(target)
|
||||
}
|
||||
|
||||
Opcode.NEG -> {
|
||||
val reg = insn.requireIntDest().intNumber
|
||||
emitLine("neg${dtSuffix(type)} ${regAddr(reg)}")
|
||||
invalidateD0CacheForSlot(reg)
|
||||
}
|
||||
|
||||
Opcode.NEGM -> {
|
||||
val target = resolveMemory(insn.requireMemory())
|
||||
emitLine("neg${dtSuffix(type)} $target")
|
||||
invalidateD0CacheForAddress(target)
|
||||
}
|
||||
|
||||
Opcode.ADDR -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
loadRegOrZeroExtendToD0(srcReg, type)
|
||||
emitLine("add${dtSuffix(type)} d0, ${regAddr(dstReg)}")
|
||||
invalidateD0CacheForSlot(dstReg)
|
||||
}
|
||||
|
||||
Opcode.ADD -> {
|
||||
val reg = insn.requireIntDest().intNumber
|
||||
val value = insn.requireImmediateInt()
|
||||
if(value in 1..8) {
|
||||
emitLine("addq${dtSuffix(type)} #$value, ${regAddr(reg)}")
|
||||
} else {
|
||||
emitLine("add${dtSuffix(type)} #${immVal(value, type)}, ${regAddr(reg)}")
|
||||
}
|
||||
invalidateD0CacheForSlot(reg)
|
||||
}
|
||||
|
||||
Opcode.ADDM -> {
|
||||
val reg = insn.requireIntSourceA().intNumber
|
||||
val target = resolveMemory(insn.requireMemory())
|
||||
val sv = dtSuffix(type)
|
||||
emitLoadD0(reg, type)
|
||||
emitLine("add$sv d0, $target")
|
||||
invalidateD0CacheForAddress(target)
|
||||
}
|
||||
|
||||
Opcode.ADDIM -> {
|
||||
val value = insn.requireImmediateInt()
|
||||
val target = resolveMemory(insn.requireMemory())
|
||||
val sv = dtSuffix(type)
|
||||
if(value in 1..8) {
|
||||
emitLine("addq$sv #$value,$target")
|
||||
} else {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
require(value in 2..255)
|
||||
emitLine("add$sv #$value,$target")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
require(value in 2..65535)
|
||||
emitLine("add$sv #$value,$target")
|
||||
}
|
||||
IRDataType.LONG, IRDataType.POINTER -> {
|
||||
require(value in 2..0x7fffffff)
|
||||
emitLine("add$sv #$value,$target")
|
||||
}
|
||||
}
|
||||
}
|
||||
invalidateD0CacheForAddress(target)
|
||||
}
|
||||
|
||||
Opcode.SUBR -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
loadRegOrZeroExtendToD0(srcReg, type)
|
||||
emitLine("sub${dtSuffix(type)} d0, ${regAddr(dstReg)}")
|
||||
invalidateD0CacheForSlot(dstReg)
|
||||
}
|
||||
|
||||
Opcode.SUB -> {
|
||||
val reg = insn.requireIntDest().intNumber
|
||||
val value = insn.requireImmediateInt()
|
||||
if(value in 1..8) {
|
||||
emitLine("subq${dtSuffix(type)} #$value, ${regAddr(reg)}")
|
||||
} else {
|
||||
emitLine("sub${dtSuffix(type)} #${immVal(value, type)}, ${regAddr(reg)}")
|
||||
}
|
||||
invalidateD0CacheForSlot(reg)
|
||||
}
|
||||
|
||||
Opcode.SUBM -> {
|
||||
val reg = insn.requireIntSourceA().intNumber
|
||||
val target = resolveMemory(insn.requireMemory())
|
||||
val sv = dtSuffix(type)
|
||||
emitLoadD0(reg, type)
|
||||
emitLine("sub$sv d0, $target")
|
||||
invalidateD0CacheForAddress(target)
|
||||
}
|
||||
|
||||
Opcode.SUBIM -> {
|
||||
val value = insn.requireImmediateInt()
|
||||
val target = resolveMemory(insn.requireMemory())
|
||||
val sv = dtSuffix(type)
|
||||
if(value in 1..8) {
|
||||
emitLine("subq$sv #$value,$target")
|
||||
} else {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
require(value in 2..255)
|
||||
emitLine("sub$sv #$value,$target")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
require(value in 2..65535)
|
||||
emitLine("sub$sv #$value,$target")
|
||||
}
|
||||
IRDataType.LONG, IRDataType.POINTER -> {
|
||||
require(value in 2..0x7fffffff)
|
||||
emitLine("sub$sv #$value,$target")
|
||||
}
|
||||
}
|
||||
}
|
||||
invalidateD0CacheForAddress(target)
|
||||
}
|
||||
|
||||
// --- Multiply (unsigned) ---
|
||||
// M68k has no mulu.b or muls.b; use .w with zero-extension for byte.
|
||||
|
||||
Opcode.MULR -> emitMulOp(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type, unsigned=true, imm=null, target=null)
|
||||
Opcode.MUL -> emitMulOp(insn.requireIntDest().intNumber, null, type, unsigned=true, imm=insn.requireImmediateInt(), target=null)
|
||||
Opcode.MULM -> emitMulOp(insn.requireIntSourceA().intNumber, null, type, unsigned=true, imm=null, target=resolveMemory(insn.requireMemory()))
|
||||
|
||||
// --- Multiply (signed) ---
|
||||
|
||||
Opcode.MULSR -> emitMulOp(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type, unsigned=false, imm=null, target=null)
|
||||
Opcode.MULS -> emitMulOp(insn.requireIntDest().intNumber, null, type, unsigned=false, imm=insn.requireImmediateInt(), target=null)
|
||||
Opcode.MULSM -> emitMulOp(insn.requireIntSourceA().intNumber, null, type, unsigned=false, imm=null, target=resolveMemory(insn.requireMemory()))
|
||||
|
||||
// --- Divide (unsigned) ---
|
||||
|
||||
Opcode.DIVR -> emitDivOp(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type, unsigned=true, imm=null, target=null)
|
||||
Opcode.DIV -> emitDivOp(insn.requireIntDest().intNumber, null, type, unsigned=true, imm=insn.requireImmediateInt(), target=null)
|
||||
Opcode.DIVM -> emitDivOp(insn.requireIntSourceA().intNumber, null, type, unsigned=true, imm=null, target=resolveMemory(insn.requireMemory()))
|
||||
|
||||
// --- Divide (signed) ---
|
||||
|
||||
Opcode.DIVSR -> emitDivOp(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type, unsigned=false, imm=null, target=null)
|
||||
Opcode.DIVS -> emitDivOp(insn.requireIntDest().intNumber, null, type, unsigned=false, imm=insn.requireImmediateInt(), target=null)
|
||||
Opcode.DIVSM -> emitDivOp(insn.requireIntSourceA().intNumber, null, type, unsigned=false, imm=null, target=resolveMemory(insn.requireMemory()))
|
||||
|
||||
// --- Modulus ---
|
||||
// Use divu.w/divs.w remainder (upper 16 bits) for byte/word; divul/divsl for long (68020+)
|
||||
|
||||
Opcode.MODR -> emitModOp(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type, unsigned=true, imm=null)
|
||||
Opcode.MOD -> emitModOp(insn.requireIntDest().intNumber, null, type, unsigned=true, imm=insn.requireImmediateInt())
|
||||
Opcode.MODSR -> emitModOp(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type, unsigned=false, imm=null)
|
||||
Opcode.MODS -> emitModOp(insn.requireIntDest().intNumber, null, type, unsigned=false, imm=insn.requireImmediateInt())
|
||||
|
||||
// --- DIVMOD (unsigned) ---
|
||||
// 68020+ divul.l gives quotient + remainder in one instruction
|
||||
|
||||
Opcode.DIVMODR -> emitDivModOp(insn.requireIntDest().intNumber, insn.requireDestB().intNumber, type, unsigned=true, imm=null)
|
||||
Opcode.DIVMOD -> emitDivModOp(insn.requireIntDest().intNumber, insn.requireDestB().intNumber, type, unsigned=true, imm=insn.requireImmediateInt())
|
||||
Opcode.SDIVMODR -> emitDivModOp(insn.requireIntDest().intNumber, insn.requireDestB().intNumber, type, unsigned=false, imm=null)
|
||||
Opcode.SDIVMOD -> emitDivModOp(insn.requireIntDest().intNumber, insn.requireDestB().intNumber, type, unsigned=false, imm=insn.requireImmediateInt())
|
||||
|
||||
// --- Integer sqrt/square ---
|
||||
|
||||
Opcode.SQRT -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("move.b ${regAddr(srcReg)}, math._sqrt_ub.value", "sqrt byte")
|
||||
invalidateD0Cache()
|
||||
emitLine("bsr math._sqrt_ub")
|
||||
emitStoreD0(dstReg, IRDataType.BYTE)
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("move.w ${regAddr(srcReg)}, math._sqrt_uw.value", "sqrt word")
|
||||
invalidateD0Cache()
|
||||
emitLine("bsr math._sqrt_uw")
|
||||
emitStoreD0(dstReg, IRDataType.BYTE)
|
||||
}
|
||||
IRDataType.LONG, IRDataType.POINTER -> {
|
||||
emitLine("move.l ${regAddr(srcReg)}, math._sqrt_l.value", "sqrt long")
|
||||
invalidateD0Cache()
|
||||
emitLine("bsr math._sqrt_l")
|
||||
emitStoreD0(dstReg, IRDataType.WORD)
|
||||
}
|
||||
}
|
||||
}
|
||||
Opcode.SQUARE -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLoadD0(srcReg, IRDataType.BYTE)
|
||||
emitLine($$"and.l #\$ff, d0", "zero extend")
|
||||
invalidateD0Cache()
|
||||
emitLine("mulu.w d0, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.BYTE)
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLoadD0(srcReg, IRDataType.WORD)
|
||||
emitLine("mulu.w d0, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.WORD)
|
||||
}
|
||||
IRDataType.LONG, IRDataType.POINTER -> {
|
||||
emitLoadD0(srcReg, IRDataType.LONG)
|
||||
emitLine("mulu.l d0, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Opcode.CMP -> {
|
||||
val leftReg = insn.requireIntSourceA().intNumber
|
||||
val rightReg = insn.requireSrcB().intNumber
|
||||
val s = dtSuffix(type)
|
||||
emitLoadD0(leftReg, type)
|
||||
emitLine("cmp$s ${regAddr(rightReg)}, d0")
|
||||
}
|
||||
|
||||
Opcode.CMPI -> {
|
||||
val reg = insn.requireIntSourceA().intNumber
|
||||
val masked = immVal(insn.requireImmediateInt(), type)
|
||||
// cmpi.x #0, operand can be replaced by the faster tst.x operand
|
||||
if(masked==0)
|
||||
emitLine("tst${dtSuffix(type)} ${regAddr(reg)}")
|
||||
else
|
||||
emitLine("cmpi${dtSuffix(type)} #$masked, ${regAddr(reg)}")
|
||||
}
|
||||
|
||||
else -> error("Unknown arithmetic opcode: ${insn.opcode}")
|
||||
}
|
||||
}
|
||||
|
||||
// === Multiply / Divide / Modulus / DIVMOD for all int sizes (68030-aware) ===
|
||||
|
||||
// emit a call to one of the 68000 long math helper routines (utility.library fast path + software fallback)
|
||||
private fun AsmGen.emitLongMathCall(dstReg: Int, srcReg: Int?, imm: Int?, target: String?, routine: String, resultReg: String) {
|
||||
when {
|
||||
srcReg != null -> {
|
||||
emitLoadD0(dstReg, IRDataType.LONG)
|
||||
emitLine("move.l ${regAddr(srcReg)}, d1")
|
||||
}
|
||||
imm != null -> {
|
||||
emitLoadD0(dstReg, IRDataType.LONG)
|
||||
emitLine("move.l #$imm, d1")
|
||||
}
|
||||
target != null -> {
|
||||
emitLoadD0FromAddress(target, IRDataType.LONG)
|
||||
emitLine("move.l ${regAddr(dstReg)}, d1")
|
||||
}
|
||||
}
|
||||
invalidateD0Cache()
|
||||
emitLine("bsr $routine")
|
||||
val storeTarget = target ?: regAddr(dstReg)
|
||||
if (resultReg == "d0") {
|
||||
emitStoreD0ToAddress(storeTarget, IRDataType.LONG)
|
||||
} else {
|
||||
invalidateD0CacheForAddress(storeTarget)
|
||||
emitLine("move.l $resultReg, $storeTarget")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.emitMulOp(dstReg: Int, srcReg: Int?, type: IRDataType, unsigned: Boolean, imm: Int?, target: String?) {
|
||||
val op = if (unsigned) "mulu" else "muls"
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
// No .b multiply on M68k: zero-extend to word, mulu.w/muls.w, store low byte
|
||||
when {
|
||||
srcReg != null -> {
|
||||
emitLoadD0(dstReg, IRDataType.BYTE)
|
||||
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
|
||||
invalidateD0Cache()
|
||||
emitLine("move.w d0, d2")
|
||||
emitLoadD0(srcReg, IRDataType.BYTE)
|
||||
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
|
||||
invalidateD0Cache()
|
||||
emitLine("$op.w d2, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.BYTE)
|
||||
}
|
||||
imm != null -> {
|
||||
emitLoadD0(dstReg, IRDataType.BYTE)
|
||||
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
|
||||
invalidateD0Cache()
|
||||
emitLine("$op.w #${imm.and(0xffff)}, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.BYTE)
|
||||
}
|
||||
target != null -> {
|
||||
emitLine("move.b $target, d1")
|
||||
if (unsigned) emitLine($$"and.l #$ff, d1") else emitSignExtendByteToLong("d1")
|
||||
emitLine("move.w d1, d2")
|
||||
emitLoadD0(dstReg, IRDataType.BYTE)
|
||||
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
|
||||
invalidateD0Cache()
|
||||
emitLine("$op.w d2, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0ToAddress(target, IRDataType.BYTE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IRDataType.WORD -> {
|
||||
// mulu.w/muls.w is native (16x16→32, lower 16 are result)
|
||||
// destination must be a data register, not memory
|
||||
when {
|
||||
srcReg != null -> {
|
||||
emitLoadD0(dstReg, IRDataType.WORD)
|
||||
emitLine("$op.w ${regAddr(srcReg)}, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.WORD)
|
||||
}
|
||||
imm != null -> {
|
||||
emitLoadD0(dstReg, IRDataType.WORD)
|
||||
emitLine("$op.w #${imm.and(0xffff)}, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.WORD)
|
||||
}
|
||||
target != null -> {
|
||||
emitLoadD0(dstReg, IRDataType.WORD)
|
||||
emitLine("$op.w $target, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0ToAddress(target, IRDataType.WORD)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IRDataType.LONG -> {
|
||||
if(program.options.compTarget.cpu < CpuType.M68020) {
|
||||
// 68000: no mulu.l/muls.l; use a helper routine (utility.library or software fallback)
|
||||
val routine = if (unsigned) "p8_umult32" else "p8_smult32"
|
||||
emitLongMathCall(dstReg, srcReg, imm, target, routine, "d0")
|
||||
} else {
|
||||
// 68020+ mulu.l/muls.l (32x32→64, lower 32 are result)
|
||||
// destination must be a data register, not memory
|
||||
when {
|
||||
srcReg != null -> {
|
||||
emitLoadD0(dstReg, IRDataType.LONG)
|
||||
emitLine("$op.l ${regAddr(srcReg)}, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.LONG)
|
||||
}
|
||||
imm != null -> {
|
||||
emitLoadD0(dstReg, IRDataType.LONG)
|
||||
emitLine("$op.l #${imm}, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.LONG)
|
||||
}
|
||||
target != null -> {
|
||||
emitLoadD0(dstReg, IRDataType.LONG)
|
||||
emitLine("$op.l $target, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0ToAddress(target, IRDataType.LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> TODO("MUL for ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.emitDivOp(dstReg: Int, srcReg: Int?, type: IRDataType, unsigned: Boolean, imm: Int?, target: String?) {
|
||||
val op = if (unsigned) "divu" else "divs"
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
// No .b divide on M68k: extend to word, divu.w/divs.w, take quotient from low byte
|
||||
when {
|
||||
srcReg != null -> {
|
||||
emitLoadD0(dstReg, IRDataType.BYTE)
|
||||
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
|
||||
invalidateD0Cache()
|
||||
emitLine("move.b ${regAddr(srcReg)}, d1")
|
||||
if (unsigned) emitLine($$"and.l #$ff, d1") else emitSignExtendByteToLong("d1")
|
||||
emitLine("$op.w d1, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.BYTE)
|
||||
}
|
||||
imm != null -> {
|
||||
emitLoadD0(dstReg, IRDataType.BYTE)
|
||||
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
|
||||
invalidateD0Cache()
|
||||
emitLine("$op.w #${imm.and(0xffff)}, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.BYTE)
|
||||
}
|
||||
target != null -> {
|
||||
emitLoadD0FromAddress(target, IRDataType.BYTE)
|
||||
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
|
||||
invalidateD0Cache()
|
||||
emitLine("move.b ${regAddr(dstReg)}, d1")
|
||||
if (unsigned) emitLine($$"and.l #$ff, d1") else emitSignExtendByteToLong("d1")
|
||||
emitLine("$op.w d1, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0ToAddress(target, IRDataType.BYTE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IRDataType.WORD -> {
|
||||
// divu.w/divs.w divide a 32-bit dividend (Dd) by a 16-bit divisor.
|
||||
// divs.w requires the 32-bit dividend to be SIGN-extended; for unsigned it
|
||||
// must be ZERO-extended. A plain `move.w` does not extend the upper word, so
|
||||
// extend explicitly here (this is what was wrong: signed dividends ended up
|
||||
// zero-extended, e.g. -1000 became +64536).
|
||||
fun loadDividend() {
|
||||
emitLoadD0(dstReg, IRDataType.WORD)
|
||||
if (unsigned) {
|
||||
emitLine($$"and.l #$ffff, d0", "zero-extend upper word for divu.w")
|
||||
invalidateD0Cache()
|
||||
} else {
|
||||
emitLine("ext.l d0", "sign-extend upper word for divs.w (signed 32-bit dividend)")
|
||||
invalidateD0Cache()
|
||||
}
|
||||
}
|
||||
when {
|
||||
srcReg != null -> {
|
||||
loadDividend()
|
||||
emitLine("$op.w ${regAddr(srcReg)}, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.WORD)
|
||||
}
|
||||
imm != null -> {
|
||||
loadDividend()
|
||||
emitLine("$op.w #${imm.and(0xffff)}, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.WORD)
|
||||
}
|
||||
target != null -> {
|
||||
emitLoadD0FromAddress(target, IRDataType.WORD)
|
||||
if (unsigned) {
|
||||
emitLine($$"and.l #$ffff, d0", "zero-extend upper word for divu.w")
|
||||
invalidateD0Cache()
|
||||
} else {
|
||||
emitLine("ext.l d0", "sign-extend upper word for divs.w (signed 32-bit dividend)")
|
||||
invalidateD0Cache()
|
||||
}
|
||||
emitLine("$op.w ${regAddr(dstReg)}, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0ToAddress(target, IRDataType.WORD)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IRDataType.LONG -> {
|
||||
if(program.options.compTarget.cpu < CpuType.M68020) {
|
||||
// 68000: no divu.l/divs.l; use a helper routine (utility.library or software fallback)
|
||||
val routine = if (unsigned) "p8_udivmod32" else "p8_sdivmod32"
|
||||
emitLongMathCall(dstReg, srcReg, imm, target, routine, "d0")
|
||||
} else {
|
||||
// 68020+ divu.l/divs.l (32/32→32)
|
||||
when {
|
||||
srcReg != null -> {
|
||||
emitLoadD0(dstReg, IRDataType.LONG)
|
||||
emitLine("$op.l ${regAddr(srcReg)}, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.LONG)
|
||||
}
|
||||
imm != null -> {
|
||||
emitLoadD0(dstReg, IRDataType.LONG)
|
||||
emitLine("$op.l #${imm}, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.LONG)
|
||||
}
|
||||
target != null -> {
|
||||
emitLoadD0FromAddress(target, IRDataType.LONG)
|
||||
emitLine("$op.l ${regAddr(dstReg)}, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0ToAddress(target, IRDataType.LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> TODO("DIV for ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.emitModOp(dstReg: Int, srcReg: Int?, type: IRDataType, unsigned: Boolean, imm: Int?) {
|
||||
val opDiv = if (unsigned) "divu" else "divs"
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
// divu.w gives quotient low word, remainder high word. Swap to get remainder.
|
||||
when {
|
||||
srcReg != null -> {
|
||||
emitLoadD0(dstReg, IRDataType.BYTE)
|
||||
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
|
||||
invalidateD0Cache()
|
||||
emitLine("move.b ${regAddr(srcReg)}, d1")
|
||||
if (unsigned) emitLine($$"and.l #$ff, d1") else emitSignExtendByteToLong("d1")
|
||||
emitLine("$opDiv.w d1, d0")
|
||||
invalidateD0Cache()
|
||||
}
|
||||
imm != null -> {
|
||||
emitLoadD0(dstReg, IRDataType.BYTE)
|
||||
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
|
||||
invalidateD0Cache()
|
||||
emitLine("$opDiv.w #${imm.and(0xffff)}, d0")
|
||||
invalidateD0Cache()
|
||||
}
|
||||
}
|
||||
// After divu.w/divs.w: quotient in lower word, remainder in upper word of d0.
|
||||
emitLine("swap d0", "remainder to low word")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.BYTE)
|
||||
}
|
||||
|
||||
IRDataType.WORD -> {
|
||||
// divu.w/divs.w: remainder in upper 16 bits, swap to get it.
|
||||
// divs.w requires the 32-bit dividend to be SIGN-extended; for unsigned it
|
||||
// must be ZERO-extended. A plain `move.w` does not extend the upper word, so
|
||||
// extend explicitly (signed dividends were wrongly zero-extended before).
|
||||
fun loadDividend() {
|
||||
emitLoadD0(dstReg, IRDataType.WORD)
|
||||
if (unsigned) {
|
||||
emitLine($$"and.l #$ffff, d0", "zero-extend upper word for divu.w")
|
||||
invalidateD0Cache()
|
||||
} else {
|
||||
emitLine("ext.l d0", "sign-extend upper word for divs.w (signed 32-bit dividend)")
|
||||
invalidateD0Cache()
|
||||
}
|
||||
}
|
||||
when {
|
||||
srcReg != null -> {
|
||||
loadDividend()
|
||||
emitLine("$opDiv.w ${regAddr(srcReg)}, d0")
|
||||
invalidateD0Cache()
|
||||
}
|
||||
imm != null -> {
|
||||
loadDividend()
|
||||
emitLine("$opDiv.w #${imm.and(0xffff)}, d0")
|
||||
invalidateD0Cache()
|
||||
}
|
||||
}
|
||||
emitLine("swap d0", "remainder to low word")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.WORD)
|
||||
}
|
||||
|
||||
IRDataType.LONG -> {
|
||||
if(program.options.compTarget.cpu < CpuType.M68020) {
|
||||
// 68000: no divul.l/divsl.l; use a helper routine (utility.library or software fallback)
|
||||
val routine = if (unsigned) "p8_udivmod32" else "p8_sdivmod32"
|
||||
emitLongMathCall(dstReg, srcReg, imm, null, routine, "d1")
|
||||
} else {
|
||||
// 68020+ divul.l <ea>, Dr, Dq: Dr=remainder, Dq=quotient
|
||||
val opLong = if (unsigned) "divul.l" else "divsl.l"
|
||||
when {
|
||||
srcReg != null -> {
|
||||
emitLoadD0(dstReg, IRDataType.LONG)
|
||||
emitLine("$opLong ${regAddr(srcReg)}, d1:d0")
|
||||
invalidateD0Cache()
|
||||
invalidateD0CacheForSlot(dstReg)
|
||||
emitLine("move.l d1, ${regAddr(dstReg)}", "remainder")
|
||||
}
|
||||
imm != null -> {
|
||||
emitLoadD0(dstReg, IRDataType.LONG)
|
||||
emitLine("move.l #${imm}, d2", "divisor")
|
||||
emitLine("$opLong d2, d1:d0")
|
||||
invalidateD0Cache()
|
||||
invalidateD0CacheForSlot(dstReg)
|
||||
emitLine("move.l d1, ${regAddr(dstReg)}", "remainder")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> TODO("MOD for ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.emitDivModOp(dstReg: Int, remainderReg: Int, type: IRDataType, unsigned: Boolean, imm: Int?) {
|
||||
val opLong = if (unsigned) "divul.l" else "divsl.l"
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
val opDiv = if (unsigned) "divu.w" else "divs.w"
|
||||
emitLoadD0(dstReg, IRDataType.BYTE)
|
||||
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
|
||||
invalidateD0Cache()
|
||||
if(imm!=null) {
|
||||
if (unsigned)
|
||||
emitLine("move.l #${imm.and(0xff)}, d1")
|
||||
else
|
||||
emitLine("moveq #${imm.and(0xff)}, d1")
|
||||
if (!unsigned) emitSignExtendByteToLong("d1")
|
||||
} else {
|
||||
emitLine("move.b ${regAddr(remainderReg)}, d1")
|
||||
if (unsigned) emitLine($$"and.l #$ff, d1") else emitSignExtendByteToLong("d1")
|
||||
}
|
||||
emitLine("$opDiv d1, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.BYTE)
|
||||
emitLine("swap d0", "remainder to low word")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(remainderReg, IRDataType.BYTE)
|
||||
}
|
||||
|
||||
IRDataType.WORD -> {
|
||||
val opDiv = if (unsigned) "divu.w" else "divs.w"
|
||||
if (unsigned) {
|
||||
emitLine("moveq #0, d0", "clear upper word for divu.w")
|
||||
invalidateD0Cache()
|
||||
}
|
||||
emitLoadD0(dstReg, IRDataType.WORD)
|
||||
if (!unsigned) {
|
||||
emitLine("ext.l d0", "sign-extend for divs.w")
|
||||
invalidateD0Cache()
|
||||
}
|
||||
if(imm!=null) {
|
||||
emitLine("$opDiv #${imm.and(0xffff)}, d0")
|
||||
} else {
|
||||
emitLine("$opDiv ${regAddr(remainderReg)}, d0")
|
||||
}
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.WORD)
|
||||
emitLine("swap d0", "remainder to low word")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(remainderReg, IRDataType.WORD)
|
||||
}
|
||||
|
||||
IRDataType.LONG -> {
|
||||
if(program.options.compTarget.cpu < CpuType.M68020) {
|
||||
// 68000: no divul.l/divsl.l; use a helper routine (utility.library or software fallback)
|
||||
val routine = if (unsigned) "p8_udivmod32" else "p8_sdivmod32"
|
||||
emitLoadD0(dstReg, IRDataType.LONG)
|
||||
if(imm!=null) {
|
||||
emitLine("move.l #${imm}, d1", "divisor")
|
||||
} else {
|
||||
emitLine("move.l ${regAddr(remainderReg)}, d1", "divisor")
|
||||
}
|
||||
invalidateD0Cache()
|
||||
emitLine("bsr $routine")
|
||||
emitStoreD0(dstReg, IRDataType.LONG)
|
||||
invalidateD0CacheForSlot(remainderReg)
|
||||
emitLine("move.l d1, ${regAddr(remainderReg)}", "remainder")
|
||||
} else {
|
||||
emitLoadD0(dstReg, IRDataType.LONG)
|
||||
if(imm!=null) {
|
||||
emitLine("move.l #${imm}, d1", "divisor")
|
||||
} else {
|
||||
emitLine("move.l ${regAddr(remainderReg)}, d1", "divisor")
|
||||
}
|
||||
emitLine("$opLong d1, d0")
|
||||
invalidateD0Cache()
|
||||
emitStoreD0(dstReg, IRDataType.LONG)
|
||||
invalidateD0CacheForSlot(remainderReg)
|
||||
emitLine("move.l d1, ${regAddr(remainderReg)}", "remainder")
|
||||
}
|
||||
}
|
||||
|
||||
else -> TODO("DIVMOD for ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
// === Float arithmetic via 68881 FPU ===
|
||||
// Float virtual registers live in the memory regfile. Physical fp0 (FP_ACC) and
|
||||
// fp1 (FP_SRC) are used as scratch during FPU operations.
|
||||
|
||||
private fun AsmGen.emitFloatBinaryOp(op: String, dst: RegisterNum, src: RegisterNum) {
|
||||
emitLine("fmove.s ${floatRegFileAddr(dst)}, $FP_ACC")
|
||||
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_SRC")
|
||||
emitLine("$op $FP_SRC, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
|
||||
private fun AsmGen.emitFloatOpWithConstant(fpReg: RegisterNum, op: String, value: Double) {
|
||||
emitLine("fmove.s ${floatRegFileAddr(fpReg)}, $FP_ACC")
|
||||
val native = nativeFloatConst(value)
|
||||
if (native != null) {
|
||||
emitLine("fmovecr #$native, $FP_SRC")
|
||||
} else {
|
||||
val label = makeFloatConstLabel(value)
|
||||
emitLine("lea $label, a0")
|
||||
emitLine("fmove.s (a0), $FP_SRC")
|
||||
}
|
||||
emitLine("$op $FP_SRC, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(fpReg)}")
|
||||
}
|
||||
|
||||
private fun AsmGen.translateFloatArithmetic(insn: IRInstruction) {
|
||||
when (insn.opcode) {
|
||||
Opcode.INCM -> emitFloatMemUnary("fadd.s", resolveMemory(insn.requireMemory(), forFloat = true), immediateStr = "#1.0")
|
||||
Opcode.DECM -> emitFloatMemUnary("fsub.s", resolveMemory(insn.requireMemory(), forFloat = true), immediateStr = "#1.0")
|
||||
Opcode.NEGM -> emitFloatMemUnary("fneg", resolveMemory(insn.requireMemory(), forFloat = true))
|
||||
|
||||
Opcode.INC -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(dst)}, $FP_ACC")
|
||||
emitLine("fadd.s #1.0, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
Opcode.DEC -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(dst)}, $FP_ACC")
|
||||
emitLine("fsub.s #1.0, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
Opcode.NEG -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(dst)}, $FP_ACC")
|
||||
emitLine("fneg $FP_ACC, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
|
||||
Opcode.ADDR -> emitFloatBinaryOp("fadd", insn.requireFloatDest().floatNumber, insn.requireFloatSourceA().floatNumber)
|
||||
Opcode.ADD -> emitFloatOpWithConstant(insn.requireFloatDest().floatNumber, "fadd", insn.requireImmediateFloat())
|
||||
|
||||
Opcode.SUBR -> emitFloatBinaryOp("fsub", insn.requireFloatDest().floatNumber, insn.requireFloatSourceA().floatNumber)
|
||||
Opcode.SUB -> emitFloatOpWithConstant(insn.requireFloatDest().floatNumber, "fsub", insn.requireImmediateFloat())
|
||||
|
||||
Opcode.MULR, Opcode.MULSR -> emitFloatBinaryOp("fmul", insn.requireFloatDest().floatNumber, insn.requireFloatSourceA().floatNumber)
|
||||
Opcode.MUL, Opcode.MULS -> emitFloatOpWithConstant(insn.requireFloatDest().floatNumber, "fmul", insn.requireImmediateFloat())
|
||||
|
||||
Opcode.DIVR, Opcode.DIVSR -> emitFloatBinaryOp("fdiv", insn.requireFloatDest().floatNumber, insn.requireFloatSourceA().floatNumber)
|
||||
Opcode.DIV, Opcode.DIVS -> emitFloatOpWithConstant(insn.requireFloatDest().floatNumber, "fdiv", insn.requireImmediateFloat())
|
||||
|
||||
Opcode.SQRT -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
val src = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
|
||||
emitLine("fsqrt $FP_ACC, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
|
||||
Opcode.SQUARE -> emitFloatBinaryOp("fmul", insn.requireFloatDest().floatNumber, insn.requireFloatSourceA().floatNumber)
|
||||
|
||||
Opcode.ADDM -> emitFloatMemBinary("fadd", insn.requireFloatSourceA().floatNumber, resolveMemory(insn.requireMemory(), forFloat = true))
|
||||
Opcode.SUBM -> emitFloatMemBinary("fsub", insn.requireFloatSourceA().floatNumber, resolveMemory(insn.requireMemory(), forFloat = true))
|
||||
Opcode.MULM, Opcode.MULSM -> emitFloatMemBinary("fmul", insn.requireFloatSourceA().floatNumber, resolveMemory(insn.requireMemory(), forFloat = true))
|
||||
Opcode.DIVM, Opcode.DIVSM -> emitFloatMemBinary("fdiv", insn.requireFloatSourceA().floatNumber, resolveMemory(insn.requireMemory(), forFloat = true))
|
||||
|
||||
Opcode.ADDIM -> emitFloatMemBinaryImmediate("fadd", insn.requireImmediateFloat(), resolveMemory(insn.requireMemory(), forFloat = true))
|
||||
Opcode.SUBIM -> emitFloatMemBinaryImmediate("fsub", insn.requireImmediateFloat(), resolveMemory(insn.requireMemory(), forFloat = true))
|
||||
|
||||
else -> TODO("FLOAT arithmetic: ${insn.opcode}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.emitFloatMemBinaryImmediate(op: String, value: Double, target: String) {
|
||||
emitLine("fmove.s $target,$FP_ACC")
|
||||
when (value) {
|
||||
0.0 -> { emitLine("fmove.s #0.0,$FP_SRC"); emitLine("$op $FP_SRC,$FP_ACC") }
|
||||
1.0 -> {
|
||||
if (op == "fadd" || op == "fsub") {
|
||||
emitLine("f${op.substring(1)}.s #1.0,$FP_ACC")
|
||||
} else {
|
||||
return // mul/div by 1.0 is no-op, just keep loaded value
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val lbl = makeFloatConstLabel(value)
|
||||
emitLine("lea $lbl,a0")
|
||||
emitLine("fmove.s (a0),$FP_SRC")
|
||||
emitLine("$op $FP_SRC,$FP_ACC")
|
||||
}
|
||||
}
|
||||
emitLine("fmove.s $FP_ACC,$target")
|
||||
}
|
||||
|
||||
private fun AsmGen.emitFloatMemUnary(op: String, target: String, immediateStr: String? = null) {
|
||||
emitLine("fmove.s $target, fp0")
|
||||
if (immediateStr != null)
|
||||
emitLine("$op $immediateStr, fp0")
|
||||
else
|
||||
emitLine("$op fp0, fp0")
|
||||
emitLine("fmove.s fp0, $target")
|
||||
}
|
||||
|
||||
private fun AsmGen.emitFloatMemBinary(op: String, fpReg1: RegisterNum, target: String) {
|
||||
emitLine("fmove.s $target, $FP_ACC")
|
||||
emitLine("fmove.s ${floatRegFileAddr(fpReg1)}, $FP_SRC")
|
||||
emitLine("$op $FP_SRC, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, $target")
|
||||
}
|
||||
|
||||
private fun immVal(value: Int, type: IRDataType): Int = when(type) {
|
||||
IRDataType.BYTE -> value and 0xff
|
||||
IRDataType.WORD -> value and 0xffff
|
||||
IRDataType.LONG, IRDataType.POINTER -> value
|
||||
else -> value and 0xffff
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
/*
|
||||
* Bitwise and logical IR instruction translations for the M68k code generator.
|
||||
*
|
||||
* Handles: AND, OR, XOR (register/imm/memory), INV (bitwise not),
|
||||
* shifts (LSL, LSR, ASR) by constant or variable count,
|
||||
* rotates (ROL, ROR, ROXL, ROXR) and bit manipulation (BITTST, BITSET, BITCLR, BITTOG).
|
||||
*
|
||||
* M68k has native .B/.W/.L support for all logical operations,
|
||||
* including immediate and register-count shifts.
|
||||
* Rotates ROL/ROR are true rotates (bit wraps around, no carry involvement for entry).
|
||||
* ROXL/ROXR rotate through the extend (X/C) bit.
|
||||
*/
|
||||
|
||||
package prog8.codegen.m68k
|
||||
|
||||
import prog8.code.target.Qemu68kTarget
|
||||
import prog8.intermediate.IRDataType
|
||||
import prog8.intermediate.IRInstruction
|
||||
import prog8.intermediate.Opcode
|
||||
import prog8.intermediate.intNumber
|
||||
import prog8.intermediate.requireImmediateInt
|
||||
import prog8.intermediate.requireIntDest
|
||||
import prog8.intermediate.requireIntSourceA
|
||||
import prog8.intermediate.requireMemory
|
||||
|
||||
internal fun AsmGen.translateBitwise(insn: IRInstruction) {
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
|
||||
when (insn.opcode) {
|
||||
Opcode.ANDR -> andRegisters(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type)
|
||||
Opcode.AND -> andImmediate(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type)
|
||||
Opcode.ANDM -> andMemory(insn.requireIntSourceA().intNumber, resolveMemory(insn.requireMemory()), type)
|
||||
|
||||
Opcode.ORR -> orRegisters(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type)
|
||||
Opcode.OR -> orImmediate(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type)
|
||||
Opcode.ORM -> orMemory(insn.requireIntSourceA().intNumber, resolveMemory(insn.requireMemory()), type)
|
||||
|
||||
Opcode.XORR -> xorRegisters(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type)
|
||||
Opcode.XOR -> xorImmediate(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type)
|
||||
Opcode.XORM -> xorMemory(insn.requireIntSourceA().intNumber, resolveMemory(insn.requireMemory()), type)
|
||||
|
||||
Opcode.INV -> invertRegister(insn.requireIntDest().intNumber, type)
|
||||
Opcode.INVM -> invertMemory(resolveMemory(insn.requireMemory()), type)
|
||||
|
||||
Opcode.ASRN -> arithmeticShiftRightVar(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type)
|
||||
Opcode.LSRN -> logicalShiftRightVar(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type)
|
||||
Opcode.LSLN -> logicalShiftLeftVar(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type)
|
||||
|
||||
Opcode.ASRNM -> shiftMemoryVar(resolveMemory(insn.requireMemory()), insn.requireIntSourceA().intNumber, type, isArithmetic = true)
|
||||
Opcode.LSRNM -> shiftMemoryVar(resolveMemory(insn.requireMemory()), insn.requireIntSourceA().intNumber, type, isArithmetic = false)
|
||||
Opcode.LSLNM -> shiftMemoryLeftVar(resolveMemory(insn.requireMemory()), insn.requireIntSourceA().intNumber, type)
|
||||
|
||||
// Immediate-count shifts (LSLI/LSRI/ASRI): the count is a literal in the immediate operand.
|
||||
Opcode.ASRI -> shiftRegister(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type, isArithmetic = true, isLeft = false)
|
||||
Opcode.LSRI -> shiftRegister(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type, isArithmetic = false, isLeft = false)
|
||||
Opcode.LSLI -> shiftRegister(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type, isArithmetic = false, isLeft = true)
|
||||
|
||||
Opcode.ASR -> shiftRegister(insn.requireIntDest().intNumber, 1, type, isArithmetic = true, isLeft = false)
|
||||
Opcode.ASRM -> memoryShiftRotate(resolveMemory(insn.requireMemory()), 1, type, isArithmetic = true, isLeft = false, isRotate = false, throughCarry = false)
|
||||
Opcode.LSR -> shiftRegister(insn.requireIntDest().intNumber, 1, type, isArithmetic = false, isLeft = false)
|
||||
Opcode.LSRM -> memoryShiftRotate(resolveMemory(insn.requireMemory()), 1, type, isArithmetic = false, isLeft = false, isRotate = false, throughCarry = false)
|
||||
Opcode.LSL -> shiftRegister(insn.requireIntDest().intNumber, 1, type, isArithmetic = false, isLeft = true)
|
||||
Opcode.LSLM -> memoryShiftRotate(resolveMemory(insn.requireMemory()), 1, type, isArithmetic = false, isLeft = true, isRotate = false, throughCarry = false)
|
||||
|
||||
Opcode.ROR -> rotateRight(insn.requireIntDest().intNumber, type)
|
||||
Opcode.RORM -> memoryShiftRotate(resolveMemory(insn.requireMemory()), 1, type, isArithmetic = false, isLeft = false, isRotate = true, throughCarry = false)
|
||||
Opcode.ROL -> rotateLeft(insn.requireIntDest().intNumber, type)
|
||||
Opcode.ROLM -> memoryShiftRotate(resolveMemory(insn.requireMemory()), 1, type, isArithmetic = false, isLeft = true, isRotate = true, throughCarry = false)
|
||||
Opcode.ROXR -> rotateRightThroughCarry(insn.requireIntDest().intNumber, type)
|
||||
Opcode.ROXRM -> memoryShiftRotate(resolveMemory(insn.requireMemory()), 1, type, isArithmetic = false, isLeft = false, isRotate = true, throughCarry = true)
|
||||
Opcode.ROXL -> rotateLeftThroughCarry(insn.requireIntDest().intNumber, type)
|
||||
Opcode.ROXLM -> memoryShiftRotate(resolveMemory(insn.requireMemory()), 1, type, isArithmetic = false, isLeft = true, isRotate = true, throughCarry = true)
|
||||
|
||||
Opcode.BITTST -> bitTest(insn.requireIntSourceA().intNumber, insn.requireImmediateInt(), type)
|
||||
Opcode.BITSET -> bitSet(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type)
|
||||
Opcode.BITCLR -> bitClear(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type)
|
||||
Opcode.BITTOG -> bitToggle(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type)
|
||||
|
||||
else -> error("Unknown bitwise opcode: ${insn.opcode}")
|
||||
}
|
||||
}
|
||||
|
||||
// === AND ===
|
||||
|
||||
private fun AsmGen.andRegisters(dstReg: Int, srcReg: Int, type: IRDataType) {
|
||||
val s = dtSuffix(type)
|
||||
emitLoadD0(srcReg, type)
|
||||
emitLine("and$s d0, ${regAddr(dstReg)}")
|
||||
invalidateD0CacheForSlot(dstReg)
|
||||
}
|
||||
|
||||
private fun AsmGen.andImmediate(dstReg: Int, value: Int, type: IRDataType) {
|
||||
val s = dtSuffix(type)
|
||||
val mask = when (type) {
|
||||
IRDataType.BYTE -> value and 0xff
|
||||
IRDataType.WORD -> value and 0xffff
|
||||
IRDataType.LONG -> value
|
||||
else -> error("unsupported type for AND immediate")
|
||||
}
|
||||
emitLine("andi$s #$mask, ${regAddr(dstReg)}")
|
||||
invalidateD0CacheForSlot(dstReg)
|
||||
}
|
||||
|
||||
private fun AsmGen.andMemory(dstReg: Int, sourceAddress: String, type: IRDataType) {
|
||||
val s = dtSuffix(type)
|
||||
emitLoadD0(dstReg, type)
|
||||
emitLine("and$s d0, $sourceAddress")
|
||||
invalidateD0CacheForAddress(sourceAddress)
|
||||
}
|
||||
|
||||
// === OR ===
|
||||
|
||||
private fun AsmGen.orRegisters(dstReg: Int, srcReg: Int, type: IRDataType) {
|
||||
val s = dtSuffix(type)
|
||||
emitLoadD0(srcReg, type)
|
||||
emitLine("or$s d0, ${regAddr(dstReg)}")
|
||||
invalidateD0CacheForSlot(dstReg)
|
||||
}
|
||||
|
||||
private fun AsmGen.orImmediate(dstReg: Int, value: Int, type: IRDataType) {
|
||||
val s = dtSuffix(type)
|
||||
val mask = when (type) {
|
||||
IRDataType.BYTE -> value and 0xff
|
||||
IRDataType.WORD -> value and 0xffff
|
||||
IRDataType.LONG -> value
|
||||
else -> error("unsupported type for OR immediate")
|
||||
}
|
||||
emitLine("ori$s #$mask, ${regAddr(dstReg)}")
|
||||
invalidateD0CacheForSlot(dstReg)
|
||||
}
|
||||
|
||||
private fun AsmGen.orMemory(dstReg: Int, sourceAddress: String, type: IRDataType) {
|
||||
val s = dtSuffix(type)
|
||||
emitLoadD0(dstReg, type)
|
||||
emitLine("or$s d0, $sourceAddress")
|
||||
invalidateD0CacheForAddress(sourceAddress)
|
||||
}
|
||||
|
||||
// === XOR ===
|
||||
|
||||
private fun AsmGen.xorRegisters(dstReg: Int, srcReg: Int, type: IRDataType) {
|
||||
val s = dtSuffix(type)
|
||||
emitLoadD0(srcReg, type)
|
||||
emitLine("eor$s d0, ${regAddr(dstReg)}")
|
||||
invalidateD0CacheForSlot(dstReg)
|
||||
}
|
||||
|
||||
private fun AsmGen.xorImmediate(dstReg: Int, value: Int, type: IRDataType) {
|
||||
val s = dtSuffix(type)
|
||||
val mask = when (type) {
|
||||
IRDataType.BYTE -> value and 0xff
|
||||
IRDataType.WORD -> value and 0xffff
|
||||
IRDataType.LONG -> value
|
||||
else -> error("unsupported type for XOR immediate")
|
||||
}
|
||||
emitLine("eori$s #$mask, ${regAddr(dstReg)}")
|
||||
invalidateD0CacheForSlot(dstReg)
|
||||
}
|
||||
|
||||
private fun AsmGen.xorMemory(dstReg: Int, sourceAddress: String, type: IRDataType) {
|
||||
val s = dtSuffix(type)
|
||||
emitLoadD0(dstReg, type)
|
||||
emitLine("eor$s d0, $sourceAddress")
|
||||
invalidateD0CacheForAddress(sourceAddress)
|
||||
}
|
||||
|
||||
// === Invert ===
|
||||
|
||||
private fun AsmGen.invertRegister(reg: Int, type: IRDataType) {
|
||||
val s = dtSuffix(type)
|
||||
emitLine("not$s ${regAddr(reg)}")
|
||||
invalidateD0CacheForSlot(reg)
|
||||
}
|
||||
|
||||
private fun AsmGen.invertMemory(target: String, type: IRDataType) {
|
||||
val s = dtSuffix(type)
|
||||
emitLine("not$s $target")
|
||||
invalidateD0CacheForAddress(target)
|
||||
}
|
||||
|
||||
// === Shift/rotate size helpers ===
|
||||
|
||||
private fun AsmGen.shiftOpcode(isLeft: Boolean, isArithmetic: Boolean, isRotate: Boolean = false): String {
|
||||
if (isRotate) {
|
||||
// Always use roxl/roxr (never rol/ror). X is managed as rotate-carry.
|
||||
return if (isLeft) "roxl" else "roxr"
|
||||
}
|
||||
return if (isLeft) {
|
||||
"lsl"
|
||||
} else {
|
||||
if (isArithmetic) "asr" else "lsr"
|
||||
}
|
||||
}
|
||||
|
||||
// === Shifts by 1 (constant count 1) ===
|
||||
|
||||
// Emit a .w count=1 memory-form shift/rotate. The absolute form (`op.w addr`)
|
||||
// is one instruction and optimal, but the QEMU 68020 emulation has a bug where
|
||||
// `asr.w` with absolute addressing zero-extends the 16-bit operand before the
|
||||
// shift instead of sign-extending it, producing a logical shift result for
|
||||
// negative values. The 68000 (vamos) and the `(a0)` / register forms are
|
||||
// unaffected. Workaround for the qemu68k target only: load the address into
|
||||
// a0 and use `(a0)` addressing. A0 is a scratch address register in the m68k
|
||||
// codegen, so this is safe.
|
||||
private fun AsmGen.emitMemoryWordShiftOrRotate(op: String, address: String) {
|
||||
if (target.name == Qemu68kTarget.NAME) {
|
||||
emitLine("lea $address, a0")
|
||||
emitLine("$op.w (a0)")
|
||||
} else {
|
||||
emitLine("$op.w $address")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.shiftRegister(reg: Int, count: Int, type: IRDataType, isArithmetic: Boolean, isLeft: Boolean) {
|
||||
val s = dtSuffix(type)
|
||||
val op = shiftOpcode(isLeft, isArithmetic)
|
||||
if (isLeft && type == IRDataType.LONG && count == 16) {
|
||||
// x << 16 for long: `swap; clr.w`. Left shift by 16 has the same
|
||||
// result for signed and unsigned long: the sign bit lives in the
|
||||
// top half, which is replaced by the bottom half; the bottom
|
||||
// half is always cleared.
|
||||
emitLoadD0(reg, IRDataType.LONG)
|
||||
emitLine("swap d0")
|
||||
emitLine("clr.w d0")
|
||||
emitStoreD0(reg, IRDataType.LONG)
|
||||
return
|
||||
}
|
||||
if (type == IRDataType.WORD && count == 1) {
|
||||
// m68k memory shift/rotate is supported for .w count=1; collapse the
|
||||
// d0 round-trip into a single memory form
|
||||
emitMemoryWordShiftOrRotate(op, regAddr(reg))
|
||||
invalidateD0CacheForSlot(reg)
|
||||
return
|
||||
}
|
||||
if (count in 1..8) {
|
||||
emitLoadD0(reg, type)
|
||||
emitLine("$op$s #$count, d0")
|
||||
emitStoreD0(reg, type)
|
||||
} else {
|
||||
// m68k immediate-shift range is 1..8; for larger counts the count
|
||||
// has to go into a data register first. `move.w` preserves the
|
||||
// count up to 32767 (Prog8 shift counts are 0..255, so this is
|
||||
// always safe).
|
||||
emitLine("move.w #$count, d1")
|
||||
emitLoadD0(reg, type)
|
||||
emitLine("$op$s d1, d0")
|
||||
emitStoreD0(reg, type)
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.memoryShiftRotate(target: String, count: Int, type: IRDataType, isArithmetic: Boolean, isLeft: Boolean, isRotate: Boolean, throughCarry: Boolean) {
|
||||
// For logical rotates (ROL/ROR), clear X before roxl/roxr
|
||||
// so the injected bit is 0 (logical rotate, not through-carry).
|
||||
if (isRotate && !throughCarry) {
|
||||
emitLine($$"andi #$ef, ccr")
|
||||
}
|
||||
// On 68k, shift/rotate instructions can operate directly on memory operands
|
||||
// (only for .w size, and shift/rotate count must be 1)
|
||||
if (type == IRDataType.WORD && count == 1) {
|
||||
val op = shiftOpcode(isLeft, isArithmetic, isRotate)
|
||||
emitMemoryWordShiftOrRotate(op, target)
|
||||
invalidateD0CacheForAddress(target)
|
||||
return
|
||||
}
|
||||
val s = dtSuffix(type)
|
||||
val op = shiftOpcode(isLeft, isArithmetic, isRotate)
|
||||
emitLoadD0FromAddress(target, type)
|
||||
emitLine("$op$s #$count, d0")
|
||||
emitStoreD0ToAddress(target, type)
|
||||
}
|
||||
|
||||
// === Variable-count shifts ===
|
||||
|
||||
private fun AsmGen.logicalShiftLeftVar(reg: Int, countReg: Int, type: IRDataType) {
|
||||
val s = dtSuffix(type)
|
||||
emitLine("move.b ${regAddr(countReg)}, d1")
|
||||
emitLoadD0(reg, type)
|
||||
emitLine("lsl$s d1, d0")
|
||||
emitStoreD0(reg, type)
|
||||
}
|
||||
|
||||
private fun AsmGen.logicalShiftRightVar(reg: Int, countReg: Int, type: IRDataType) {
|
||||
val s = dtSuffix(type)
|
||||
emitLine("move.b ${regAddr(countReg)}, d1")
|
||||
emitLoadD0(reg, type)
|
||||
emitLine("lsr$s d1, d0")
|
||||
emitStoreD0(reg, type)
|
||||
}
|
||||
|
||||
private fun AsmGen.arithmeticShiftRightVar(reg: Int, countReg: Int, type: IRDataType) {
|
||||
val s = dtSuffix(type)
|
||||
emitLine("move.b ${regAddr(countReg)}, d1")
|
||||
emitLoadD0(reg, type)
|
||||
emitLine("asr$s d1, d0")
|
||||
emitStoreD0(reg, type)
|
||||
}
|
||||
|
||||
// === Memory variable-count shifts ===
|
||||
|
||||
private fun AsmGen.shiftMemoryVar(target: String, countReg: Int, type: IRDataType, isArithmetic: Boolean) {
|
||||
val s = dtSuffix(type)
|
||||
val op = if (isArithmetic) "asr" else "lsr"
|
||||
emitLine("move.b ${regAddr(countReg)}, d1")
|
||||
emitLoadD0FromAddress(target, type)
|
||||
emitLine("$op$s d1, d0")
|
||||
emitStoreD0ToAddress(target, type)
|
||||
}
|
||||
|
||||
private fun AsmGen.shiftMemoryLeftVar(target: String, countReg: Int, type: IRDataType) {
|
||||
val s = dtSuffix(type)
|
||||
emitLine("move.b ${regAddr(countReg)}, d1")
|
||||
emitLoadD0FromAddress(target, type)
|
||||
emitLine("lsl$s d1, d0")
|
||||
emitStoreD0ToAddress(target, type)
|
||||
}
|
||||
|
||||
// === Rotates ===
|
||||
// On M68k, the X (extend/CCR bit 4) bit serves as the "carry for rotates",
|
||||
// while C (carry/CCR bit 0) is used for comparisons.
|
||||
// This maps naturally: ROL/ROR IR (logical rotate) = clear X + roxl/roxr,
|
||||
// ROXL/ROXR IR (rotate through carry) = roxl/roxr directly.
|
||||
// CLC/SEC manage both C+X to keep them in sync.
|
||||
|
||||
private fun AsmGen.rotateLeft(reg: Int, type: IRDataType) {
|
||||
// IR ROL: logical rotate left (inject 0 into LSB)
|
||||
if (type == IRDataType.WORD) {
|
||||
emitLine($$"andi #$ef, ccr") // clear X (and leave C alone)
|
||||
emitMemoryWordShiftOrRotate("roxl", regAddr(reg))
|
||||
invalidateD0CacheForSlot(reg)
|
||||
return
|
||||
}
|
||||
val s = dtSuffix(type)
|
||||
emitLoadD0(reg, type)
|
||||
emitLine($$"andi #$ef, ccr") // clear X (and leave C alone)
|
||||
emitLine("roxl$s #1, d0") // rotate through X: X=0 -> bit 0 gets 0
|
||||
emitStoreD0(reg, type)
|
||||
}
|
||||
|
||||
private fun AsmGen.rotateRight(reg: Int, type: IRDataType) {
|
||||
// IR ROR: logical rotate right (inject 0 into MSB)
|
||||
if (type == IRDataType.WORD) {
|
||||
emitLine($$"andi #$ef, ccr") // clear X (and leave C alone)
|
||||
emitMemoryWordShiftOrRotate("roxr", regAddr(reg))
|
||||
invalidateD0CacheForSlot(reg)
|
||||
return
|
||||
}
|
||||
val s = dtSuffix(type)
|
||||
emitLoadD0(reg, type)
|
||||
emitLine($$"andi #$ef, ccr") // clear X (and leave C alone)
|
||||
emitLine("roxr$s #1, d0") // rotate through X: X=0 -> MSB gets 0
|
||||
emitStoreD0(reg, type)
|
||||
}
|
||||
|
||||
// === Rotates through carry (extend) ===
|
||||
|
||||
private fun AsmGen.rotateLeftThroughCarry(reg: Int, type: IRDataType) {
|
||||
// IR ROXL: rotate left through carry (on M68k, X serves as rotate-carry)
|
||||
if (type == IRDataType.WORD) {
|
||||
emitMemoryWordShiftOrRotate("roxl", regAddr(reg))
|
||||
invalidateD0CacheForSlot(reg)
|
||||
return
|
||||
}
|
||||
val s = dtSuffix(type)
|
||||
emitLoadD0(reg, type)
|
||||
emitLine("roxl$s #1, d0")
|
||||
emitStoreD0(reg, type)
|
||||
}
|
||||
|
||||
private fun AsmGen.rotateRightThroughCarry(reg: Int, type: IRDataType) {
|
||||
// IR ROXR: rotate right through carry (on M68k, X serves as rotate-carry)
|
||||
if (type == IRDataType.WORD) {
|
||||
emitMemoryWordShiftOrRotate("roxr", regAddr(reg))
|
||||
invalidateD0CacheForSlot(reg)
|
||||
return
|
||||
}
|
||||
val s = dtSuffix(type)
|
||||
emitLoadD0(reg, type)
|
||||
emitLine("roxr$s #1, d0")
|
||||
emitStoreD0(reg, type)
|
||||
}
|
||||
|
||||
// === Bit manipulation ===
|
||||
|
||||
private fun AsmGen.bitOpMem(op: String, reg: Int, bit: Int, type: IRDataType) {
|
||||
// The register slot is big-endian in memory, so the target bit lives in
|
||||
// byte (size-1-bit/8) at bit position (bit % 8). A byte-sized bit op with
|
||||
// an immediate bit number operates on that byte directly, no d0 round-trip.
|
||||
val size = when(type) {
|
||||
IRDataType.BYTE -> 1
|
||||
IRDataType.WORD -> 2
|
||||
IRDataType.LONG -> 4
|
||||
else -> error("bit op on unsupported type $type")
|
||||
}
|
||||
val byteOffset = size - 1 - bit / 8
|
||||
val bitInByte = bit % 8
|
||||
emitLine("$op #$bitInByte, ${regAddrByte(reg, byteOffset)}")
|
||||
}
|
||||
|
||||
private fun AsmGen.bitTest(reg: Int, bit: Int, type: IRDataType) {
|
||||
bitOpMem("btst", reg, bit, type)
|
||||
}
|
||||
|
||||
private fun AsmGen.bitSet(reg: Int, bit: Int, type: IRDataType) {
|
||||
bitOpMem("bset", reg, bit, type)
|
||||
}
|
||||
|
||||
private fun AsmGen.bitClear(reg: Int, bit: Int, type: IRDataType) {
|
||||
bitOpMem("bclr", reg, bit, type)
|
||||
}
|
||||
|
||||
private fun AsmGen.bitToggle(reg: Int, bit: Int, type: IRDataType) {
|
||||
bitOpMem("bchg", reg, bit, type)
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Branch/compare IR instruction translations for the M68k code generator.
|
||||
*
|
||||
* Status bit branches (BSTCC/BSTCS/etc.) map directly to M68k conditional
|
||||
* branch instructions (BCC/BCS/BEQ/BNE/BMI/BPL/BVC/BVS).
|
||||
*
|
||||
* Comparison branches (BGT/BGE/BLT/BLE and signed variants) first emit
|
||||
* a CMP or CMPI between the operands, then branch using the appropriate
|
||||
* M68k condition code:
|
||||
*
|
||||
* Unsigned: BHI (C=0 & Z=0), BHS/BCC (C=0), BLO/BCS (C=1), BLS (C=1 | Z=1)
|
||||
* Signed: BGT (N=V & Z=0), BGE (N=V), BLT (N!=V), BLE (Z=1 | N!=V)
|
||||
*
|
||||
* M68k CMP/CMPI sets all flags correctly for .B, .W, and .L — no cascading
|
||||
* or overflow correction needed (unlike 6502).
|
||||
*/
|
||||
|
||||
package prog8.codegen.m68k
|
||||
|
||||
import prog8.intermediate.CodeReference
|
||||
import prog8.intermediate.IRDataType
|
||||
import prog8.intermediate.IRInstruction
|
||||
import prog8.intermediate.Opcode
|
||||
import prog8.intermediate.intNumber
|
||||
import prog8.intermediate.requireImmediateInt
|
||||
import prog8.intermediate.requireSrcA
|
||||
import prog8.intermediate.requireSrcB
|
||||
import prog8.intermediate.requireTarget
|
||||
|
||||
internal fun AsmGen.translateBranch(insn: IRInstruction) {
|
||||
val label: String = when (val target = insn.requireTarget()) {
|
||||
is CodeReference.Label -> fixNameSymbols(target.name)
|
||||
is CodeReference.Absolute -> target.address.toHex()
|
||||
is CodeReference.Indirect -> error("branch needs a static target")
|
||||
}
|
||||
|
||||
when (insn.opcode) {
|
||||
Opcode.BSTCC -> emitBranch("bcc", label)
|
||||
Opcode.BSTCS -> emitBranch("bcs", label)
|
||||
Opcode.BSTEQ -> emitBranch("beq", label)
|
||||
Opcode.BSTNE -> emitBranch("bne", label)
|
||||
Opcode.BSTNEG -> emitBranch("bmi", label)
|
||||
Opcode.BSTPOS -> emitBranch("bpl", label)
|
||||
Opcode.BSTVC -> emitBranch("bvc", label)
|
||||
Opcode.BSTVS -> emitBranch("bvs", label)
|
||||
|
||||
// Unsigned integer comparison branches
|
||||
Opcode.BGT -> cmpBranchUnsignedImm(insn, label, "bhi")
|
||||
Opcode.BGE -> cmpBranchUnsignedImm(insn, label, "bhs")
|
||||
Opcode.BLT -> cmpBranchUnsignedImm(insn, label, "blo")
|
||||
Opcode.BLE -> cmpBranchUnsignedImm(insn, label, "bls")
|
||||
|
||||
Opcode.BGTR -> cmpBranchUnsignedReg(insn, label, "bhi")
|
||||
Opcode.BGER -> cmpBranchUnsignedReg(insn, label, "bhs")
|
||||
// BLTR doesn't exist in IR — uses BGTR with swapped operands
|
||||
|
||||
// Signed integer comparison branches
|
||||
Opcode.BGTS -> cmpBranchSignedImm(insn, label, "bgt")
|
||||
Opcode.BGES -> cmpBranchSignedImm(insn, label, "bge")
|
||||
Opcode.BLTS -> cmpBranchSignedImm(insn, label, "blt")
|
||||
Opcode.BLES -> cmpBranchSignedImm(insn, label, "ble")
|
||||
|
||||
Opcode.BGTSR -> cmpBranchSignedReg(insn, label, "bgt")
|
||||
Opcode.BGESR -> cmpBranchSignedReg(insn, label, "bge")
|
||||
// BLTSR doesn't exist in IR — uses BGTSR with swapped operands
|
||||
|
||||
else -> error("Unknown branch opcode: ${insn.opcode}")
|
||||
}
|
||||
}
|
||||
|
||||
// === Unsigned comparisons: register vs immediate ===
|
||||
|
||||
private fun AsmGen.cmpBranchUnsignedImm(insn: IRInstruction, label: String, branchOp: String) {
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
val reg = insn.requireSrcA().intNumber
|
||||
val imm = insn.requireImmediateInt()
|
||||
val s = dtSuffix(type)
|
||||
// cmpi.x #imm, <ea> and tst.x <ea> accept memory operands, so the
|
||||
// register-file load into d0 is redundant
|
||||
if(imm==0)
|
||||
emitLine("tst$s ${regAddr(reg)}")
|
||||
else
|
||||
emitLine("cmpi$s #$imm, ${regAddr(reg)}")
|
||||
emitBranch(branchOp, label)
|
||||
}
|
||||
|
||||
// === Unsigned comparisons: register vs register ===
|
||||
|
||||
private fun AsmGen.cmpBranchUnsignedReg(insn: IRInstruction, label: String, branchOp: String) {
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
val left = insn.requireSrcA().intNumber
|
||||
val right = insn.requireSrcB().intNumber
|
||||
val s = dtSuffix(type)
|
||||
emitLoadD0(left, type)
|
||||
emitLine("cmp$s ${regAddr(right)}, d0")
|
||||
emitBranch(branchOp, label)
|
||||
}
|
||||
|
||||
// === Signed comparisons: register vs immediate ===
|
||||
|
||||
private fun AsmGen.cmpBranchSignedImm(insn: IRInstruction, label: String, branchOp: String) {
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
val reg = insn.requireSrcA().intNumber
|
||||
val imm = insn.requireImmediateInt()
|
||||
val s = dtSuffix(type)
|
||||
// cmpi.x #imm, <ea> and tst.x <ea> accept memory operands, so the
|
||||
// register-file load into d0 is redundant
|
||||
if(imm==0)
|
||||
emitLine("tst$s ${regAddr(reg)}")
|
||||
else
|
||||
emitLine("cmpi$s #$imm, ${regAddr(reg)}")
|
||||
emitBranch(branchOp, label)
|
||||
}
|
||||
|
||||
// === Signed comparisons: register vs register ===
|
||||
|
||||
private fun AsmGen.cmpBranchSignedReg(insn: IRInstruction, label: String, branchOp: String) {
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
val left = insn.requireSrcA().intNumber
|
||||
val right = insn.requireSrcB().intNumber
|
||||
val s = dtSuffix(type)
|
||||
emitLoadD0(left, type)
|
||||
emitLine("cmp$s ${regAddr(right)}, d0")
|
||||
emitBranch(branchOp, label)
|
||||
}
|
||||
|
||||
private fun AsmGen.emitBranch(branchOp: String, label: String) {
|
||||
invalidateD0Cache()
|
||||
emitLine("$branchOp $label")
|
||||
}
|
||||
@@ -0,0 +1,870 @@
|
||||
/*
|
||||
* Control flow IR instruction translations for the M68k code generator.
|
||||
*
|
||||
* Status flag returns (Pc/Pz/Pv/Pn) are handled by the IR's branch-based pattern
|
||||
* (bsteq/bstneg/bstvs). The backend skips status flag returns in translateCall/translateReturnValue
|
||||
* and trusts the IR to emit the appropriate branch instructions.
|
||||
*
|
||||
* PUSHST/POPST: save/restore CCR via the stack (Scc moves CCR to D0).
|
||||
*
|
||||
* Handles: JUMP, JUMPI, CALL, CALLI, CALLFAR, CALLFARVB, SYSCALL,
|
||||
* RETURN, RETURNR, RETURNI, PUSH, POP, PUSHST, POPST,
|
||||
* CLC, SEC, CLI, SEI, ALIGN,
|
||||
* byte/word extraction (LSIGB, LSIGW, MSIGB, MSIGW, BSIGB, MIDB),
|
||||
* sign extension (EXT, EXTS), and sign (SGN).
|
||||
*/
|
||||
|
||||
package prog8.codegen.m68k
|
||||
|
||||
import prog8.code.core.CpuType
|
||||
import prog8.code.core.toHex
|
||||
import prog8.code.target.Amiga500Target
|
||||
import prog8.intermediate.*
|
||||
|
||||
internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall: ImmediateCallOptimization? = null) {
|
||||
when (insn.opcode) {
|
||||
Opcode.JUMP -> {
|
||||
invalidateD0Cache()
|
||||
when (val t = insn.requireTarget()) {
|
||||
is CodeReference.Label -> emitLine("bra ${fixNameSymbols(t.name)}") // PC-relative branch; vasm picks the optimal size and falls back to jmp if out of range
|
||||
is CodeReference.Absolute -> emitLine("jmp ${t.address.value.toHex()}")
|
||||
is CodeReference.Indirect -> error("JUMP needs target")
|
||||
}
|
||||
}
|
||||
|
||||
Opcode.JUMPI -> {
|
||||
val ref = insn.requireTarget() as? CodeReference.Indirect ?: error("JUMPI needs an indirect target register")
|
||||
val reg = ref.pointer.intNumber
|
||||
invalidateD0Cache()
|
||||
if(program.options.compTarget.cpu >= CpuType.M68020) {
|
||||
// 68020+ supports memory-indirect addressing: fetch the target address from memory directly.
|
||||
emitLine("jmp ([${regAddr(reg)}])")
|
||||
} else {
|
||||
emitLine("movea.l ${regAddr(reg).removeSuffix("+0")}, a0")
|
||||
emitLine("jmp (a0)")
|
||||
}
|
||||
}
|
||||
|
||||
Opcode.CALL -> {
|
||||
val callSite = insn.requireCallSite()
|
||||
val fnLabel = insn.labelTarget?.let { fixNameSymbols(it) }
|
||||
?: (insn.codeTarget as? CodeReference.Absolute)?.address?.value?.toHex()
|
||||
?: error("CALL needs label or address")
|
||||
invalidateD0Cache()
|
||||
translateCall(fnLabel, callSite, forwardedImmediateCall)
|
||||
}
|
||||
|
||||
Opcode.CALLI -> {
|
||||
val ref = insn.codeTarget as? CodeReference.Indirect ?: error("CALLI needs an indirect target register")
|
||||
val reg = ref.pointer.intNumber
|
||||
invalidateD0Cache()
|
||||
if(program.options.compTarget.cpu >= CpuType.M68020) {
|
||||
// 68020+ supports memory-indirect addressing: fetch the target address from memory directly.
|
||||
emitLine("jsr ([${regAddr(reg)}])")
|
||||
} else {
|
||||
emitLine("movea.l ${regAddr(reg).removeSuffix("+0")}, a0")
|
||||
emitLine("jsr (a0)")
|
||||
}
|
||||
}
|
||||
|
||||
Opcode.CALLFAR -> {
|
||||
// On the amiga target, CALLFAR is repurposed to encode automatic library LVO calls:
|
||||
// library = library number (1=exec, 2=dos, 3=graphics, 4=intuition, ...)
|
||||
// lvo = negative LVO offset (the offset into the library's jump table)
|
||||
// name = symbolic function name, emitted as the displacement in jsr Symbol(a6)
|
||||
if(program.options.compTarget.name.contains("amiga")) {
|
||||
val callSite = insn.requireCallSite()
|
||||
val amigaTarget = callSite.target as? CallTarget.AmigaLibrary ?: error("CALLFAR needs an amiga library target")
|
||||
val offset = amigaTarget.lvo
|
||||
require(offset<0) { "amiga libcall offsets should all be <0" }
|
||||
val bank = amigaTarget.library
|
||||
if(bank==1) {
|
||||
// exec.library is always at 4.w
|
||||
emitLine("move.l 4.w,a6")
|
||||
} else {
|
||||
val library = Amiga500Target.LibraryNumbers.getValue(bank)
|
||||
val baseVar = "sys.${library}Base"
|
||||
emitLine("move.l $baseVar,a6")
|
||||
}
|
||||
|
||||
invalidateD0Cache()
|
||||
for(arg in callSite.arguments)
|
||||
translateArgument(arg)
|
||||
if(amigaTarget.name!=null) {
|
||||
emitLine("jsr ${amigaTarget.name}(a6)") // do the actual call (symbolic ref)
|
||||
} else {
|
||||
emitLine("jsr $offset(a6)") // do the actual call
|
||||
}
|
||||
// Skip status flag returns: handled by IR branch pattern.
|
||||
// In multi-assign context (returns.size > 1), also skip slot-based returns:
|
||||
// the IR generates LOADHR for them.
|
||||
// In single-return context, process slot returns normally.
|
||||
// LIMITATION: multiple status flag returns in one multi-assign are not supported
|
||||
// (codegen limitation: first flag extraction clobbers flags before second can be read).
|
||||
val isMultiReturn = callSite.results.size > 1
|
||||
for(ret in callSite.results) {
|
||||
if (ret.location is CallLocation.StatusFlag)
|
||||
continue
|
||||
if (isMultiReturn)
|
||||
continue
|
||||
translateReturnValue(ret)
|
||||
}
|
||||
}
|
||||
else error("CALLFAR not applicable on this M68k")
|
||||
}
|
||||
|
||||
Opcode.CALLFARVB -> {
|
||||
error("CALLFARVB not applicable on M68k")
|
||||
}
|
||||
|
||||
Opcode.SYSCALL -> {
|
||||
val callSite = insn.requireCallSite()
|
||||
val num = (callSite.target as CallTarget.SystemCall).number
|
||||
invalidateD0Cache()
|
||||
if (callSite.arguments.isNotEmpty())
|
||||
translateSyscall(num, callSite)
|
||||
else
|
||||
emitLine("; syscall #$num (no args)")
|
||||
}
|
||||
|
||||
Opcode.RETURN -> {
|
||||
invalidateD0Cache()
|
||||
emitLine("rts")
|
||||
}
|
||||
|
||||
Opcode.RETURNR -> {
|
||||
invalidateD0Cache()
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
if (type == IRDataType.FLOAT) {
|
||||
val fpReg = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(fpReg)}, $FP_ACC")
|
||||
} else {
|
||||
val reg = insn.requireIntSourceA().intNumber
|
||||
emitLoadD0(reg, type)
|
||||
}
|
||||
emitLine("rts")
|
||||
}
|
||||
|
||||
Opcode.RETURNI -> {
|
||||
invalidateD0Cache()
|
||||
val value = insn.immediate?.integerValue ?: 0
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
val s = dtSuffix(type)
|
||||
if (value in -128..127)
|
||||
emitLine("moveq #$value, d0")
|
||||
else
|
||||
emitLine("move$s #$value, d0")
|
||||
emitLine("rts")
|
||||
}
|
||||
|
||||
// === Stack operations ===
|
||||
|
||||
Opcode.PUSH -> {
|
||||
invalidateD0Cache()
|
||||
val reg = insn.requireIntSourceA().intNumber
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
val s = dtSuffix(type)
|
||||
emitLine("move$s ${regAddr(reg)}, -(sp)")
|
||||
}
|
||||
|
||||
Opcode.POP -> {
|
||||
invalidateD0Cache()
|
||||
val reg = insn.requireIntDest().intNumber
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
val s = dtSuffix(type)
|
||||
emitLine("move$s (sp)+, ${regAddr(reg)}")
|
||||
}
|
||||
|
||||
// === Status flag stack ops via Scc ===
|
||||
|
||||
Opcode.PUSHST -> {
|
||||
// Move CCR to D0 (low byte), then push as byte.
|
||||
if(program.options.compTarget.cpu == CpuType.M68000)
|
||||
error("the 68000 cpu cannot save/restore the status bits using a nonprivileged instruction. This is required to implement the 'PUSHST/POPST' IR opcodes. Compile for 68010 or higher cpu or change the code such that PUSHST/POPST are no longer used (sometimes complicated rol/ror operations need it)")
|
||||
invalidateD0Cache()
|
||||
emitLine("move ccr, d0")
|
||||
emitLine("move.b d0, -(sp)")
|
||||
}
|
||||
|
||||
Opcode.POPST -> {
|
||||
// Pop byte into D0, then restore CCR
|
||||
if(program.options.compTarget.cpu == CpuType.M68000)
|
||||
error("the 68000 cpu cannot save/restore the status bits using a nonprivileged instruction. This is required to implement the 'PUSHST/POPST' IR opcodes. Compile for 68010 or higher cpu or change the code such that PUSHST/POPST are no longer used (sometimes complicated rol/ror operations need it)")
|
||||
invalidateD0Cache()
|
||||
emitLine("move.b (sp)+, d0")
|
||||
emitLine("move d0, ccr")
|
||||
}
|
||||
|
||||
// === Flag manipulation ===
|
||||
|
||||
// On M68k, X (extend/CCR bit 4) serves as the rotate-carry,
|
||||
// while C (carry/CCR bit 0) is used for comparisons.
|
||||
// CLC/SEC must manage both to keep them in sync.
|
||||
Opcode.CLC -> emitLine($$"andi #$ee, ccr") // clear C and X
|
||||
Opcode.SEC -> emitLine($$"ori #$11, ccr") // set C and X
|
||||
Opcode.CLI -> emitLine($$"andi #$fb, ccr")
|
||||
Opcode.SEI -> emitLine($$"ori #$04, ccr")
|
||||
|
||||
Opcode.ALIGN -> {
|
||||
val alignment = insn.immediate?.integerValue ?: 2
|
||||
emitLine("ALIGN $alignment")
|
||||
}
|
||||
|
||||
// === Byte/word extraction (no Scc needed) ===
|
||||
|
||||
Opcode.LSIGB -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
val type = insn.type ?: IRDataType.WORD
|
||||
emitLoadD0(srcReg, type)
|
||||
emitStoreD0(dstReg, IRDataType.BYTE)
|
||||
}
|
||||
|
||||
Opcode.LSIGW -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
emitLoadD0(srcReg, IRDataType.LONG)
|
||||
emitStoreD0(dstReg, IRDataType.WORD)
|
||||
}
|
||||
|
||||
Opcode.MSIGB -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
// Big-endian: MSB is at byte offset 0 for both word and long.
|
||||
emitLoadD0FromAddress(regAddrByte(srcReg, 0), IRDataType.BYTE)
|
||||
emitStoreD0(dstReg, IRDataType.BYTE)
|
||||
}
|
||||
|
||||
Opcode.MSIGW -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
// Big-endian: word 0 (offset+0) is most significant. Extract directly.
|
||||
emitLoadD0FromAddress(regAddrByte(srcReg, 0), IRDataType.WORD)
|
||||
emitStoreD0(dstReg, IRDataType.WORD)
|
||||
}
|
||||
|
||||
Opcode.BSIGB -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
// Big-endian: bits 16-23 are at byte offset 1 of a long.
|
||||
emitLoadD0FromAddress(regAddrByte(srcReg, 1), IRDataType.BYTE)
|
||||
emitStoreD0(dstReg, IRDataType.BYTE)
|
||||
}
|
||||
|
||||
Opcode.MIDB -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
// Big-endian: bits 8-15 are at byte offset 2 of a long.
|
||||
emitLoadD0FromAddress(regAddrByte(srcReg, 2), IRDataType.BYTE)
|
||||
emitStoreD0(dstReg, IRDataType.BYTE)
|
||||
}
|
||||
|
||||
// === Sign/zero extension ===
|
||||
|
||||
Opcode.EXT -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
val type = insn.type ?: IRDataType.WORD
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
// zero-extend byte to word
|
||||
invalidateD0Cache()
|
||||
emitLine("moveq #0, d0")
|
||||
emitLoadD0(srcReg, IRDataType.BYTE)
|
||||
emitStoreD0(dstReg, IRDataType.WORD)
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
// zero-extend word to long
|
||||
invalidateD0Cache()
|
||||
emitLine("moveq #0, d0")
|
||||
emitLoadD0(srcReg, IRDataType.WORD)
|
||||
emitStoreD0(dstReg, IRDataType.LONG)
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
// no extension needed
|
||||
emitLoadD0(srcReg, IRDataType.LONG)
|
||||
emitStoreD0(dstReg, IRDataType.LONG)
|
||||
}
|
||||
else -> TODO("EXT for ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
Opcode.EXTS -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
val type = insn.type ?: IRDataType.WORD
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
// sign-extend byte to word
|
||||
emitLoadD0(srcReg, IRDataType.BYTE)
|
||||
emitLine("ext.w d0")
|
||||
emitStoreD0(dstReg, IRDataType.WORD)
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
// sign-extend word to long
|
||||
emitLoadD0(srcReg, IRDataType.WORD)
|
||||
emitLine("ext.l d0")
|
||||
emitStoreD0(dstReg, IRDataType.LONG)
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
// no extension needed
|
||||
emitLoadD0(srcReg, IRDataType.LONG)
|
||||
emitStoreD0(dstReg, IRDataType.LONG)
|
||||
}
|
||||
else -> TODO("EXTS for ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
Opcode.EXTL -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
// zero-extend byte to long: move.b only writes the low byte, so clear d0 first
|
||||
invalidateD0Cache()
|
||||
emitLine("moveq #0, d0")
|
||||
emitLoadD0(srcReg, IRDataType.BYTE)
|
||||
emitStoreD0(dstReg, IRDataType.LONG)
|
||||
}
|
||||
|
||||
Opcode.EXTLS -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
// sign-extend byte to long
|
||||
emitLoadD0(srcReg, IRDataType.BYTE)
|
||||
emitSignExtendByteToLong("d0")
|
||||
emitStoreD0(dstReg, IRDataType.LONG)
|
||||
}
|
||||
|
||||
// === Concatenation ===
|
||||
|
||||
Opcode.CONCAT -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val srcReg2 = insn.requireIntSourceA().intNumber
|
||||
val srcReg3 = insn.requireSrcB().intNumber
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
// r1 = WORD(r2 as MSB, r3 as LSB)
|
||||
emitLoadD0(srcReg2, IRDataType.BYTE)
|
||||
emitLine("lsl.w #8, d0")
|
||||
invalidateD0Cache()
|
||||
emitLine("or.b ${regAddr(srcReg3)}, d0")
|
||||
emitStoreD0(dstReg, IRDataType.WORD)
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
// r1 = LONG(r2 as MSW, r3 as LSW)
|
||||
emitLoadD0(srcReg2, IRDataType.WORD)
|
||||
emitLine("swap d0")
|
||||
invalidateD0Cache()
|
||||
emitLine("clr.w d0")
|
||||
emitLine("or.w ${regAddr(srcReg3)}, d0")
|
||||
emitStoreD0(dstReg, IRDataType.LONG)
|
||||
}
|
||||
else -> TODO("CONCAT for ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
// === Math ===
|
||||
|
||||
Opcode.SGN -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
if (insn.type == IRDataType.FLOAT) {
|
||||
val srcFp = insn.requireFloatSourceA().floatNumber
|
||||
invalidateD0Cache()
|
||||
emitLine("ftst.s ${floatRegFileAddr(srcFp)}")
|
||||
emitLine("fslt d0")
|
||||
emitLine("fsgt d1")
|
||||
emitLine("neg.b d1")
|
||||
emitLine("or.b d1, d0")
|
||||
emitStoreD0(dstReg, IRDataType.BYTE)
|
||||
} else {
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
val s = dtSuffix(type)
|
||||
val zeroLabel = makeLabel("sgn_zero")
|
||||
val doneLabel = makeLabel("sgn_done")
|
||||
emitLoadD0(srcReg, type)
|
||||
emitLine("tst$s d0")
|
||||
emitLine("beq $zeroLabel")
|
||||
emitLine("smi d1")
|
||||
emitLine("bmi $doneLabel")
|
||||
emitLine("moveq #1, d1")
|
||||
emitLine("bra $doneLabel")
|
||||
emitLabel(zeroLabel)
|
||||
emitLine("moveq #0, d1")
|
||||
emitLabel(doneLabel)
|
||||
emitLine("move.b d1, ${regAddr(dstReg)}")
|
||||
invalidateD0CacheForSlot(dstReg) // raw store bypasses emitStoreD0
|
||||
}
|
||||
}
|
||||
|
||||
// === Floating point operations via 68881/68882 FPU ===
|
||||
|
||||
Opcode.FFROMUB -> {
|
||||
val fpDst = insn.requireFloatDest().floatNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
emitLoadD0(srcReg, IRDataType.BYTE)
|
||||
emitLine($$"and.l #$ff, d0")
|
||||
invalidateD0Cache()
|
||||
emitLine("fmove.l d0, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(fpDst)}")
|
||||
}
|
||||
|
||||
Opcode.FFROMSB -> {
|
||||
val fpDst = insn.requireFloatDest().floatNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
emitLoadD0(srcReg, IRDataType.BYTE)
|
||||
emitSignExtendByteToLong("d0")
|
||||
emitLine("fmove.l d0, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(fpDst)}")
|
||||
}
|
||||
|
||||
Opcode.FFROMUW -> {
|
||||
val fpDst = insn.requireFloatDest().floatNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
emitLoadD0(srcReg, IRDataType.WORD)
|
||||
emitLine($$"and.l #$ffff, d0")
|
||||
invalidateD0Cache()
|
||||
emitLine("fmove.l d0, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(fpDst)}")
|
||||
}
|
||||
|
||||
Opcode.FFROMSW -> {
|
||||
val fpDst = insn.requireFloatDest().floatNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
emitLoadD0(srcReg, IRDataType.WORD)
|
||||
emitLine("ext.l d0")
|
||||
emitLine("fmove.l d0, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(fpDst)}")
|
||||
}
|
||||
|
||||
Opcode.FFROMSL -> {
|
||||
val fpDst = insn.requireFloatDest().floatNumber
|
||||
val srcReg = insn.requireIntSourceA().intNumber
|
||||
emitLoadD0(srcReg, IRDataType.LONG)
|
||||
emitLine("fmove.l d0, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(fpDst)}")
|
||||
}
|
||||
|
||||
Opcode.FTOUB -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val fpSrc = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(fpSrc)}, $FP_ACC")
|
||||
invalidateD0Cache()
|
||||
emitLine("fmove.b $FP_ACC, d0")
|
||||
emitLine($$"and.l #$ff, d0")
|
||||
emitStoreD0(dstReg, IRDataType.BYTE)
|
||||
}
|
||||
|
||||
Opcode.FTOSB -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val fpSrc = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(fpSrc)}, $FP_ACC")
|
||||
invalidateD0Cache()
|
||||
emitLine("fmove.b $FP_ACC, d0")
|
||||
emitStoreD0(dstReg, IRDataType.BYTE)
|
||||
}
|
||||
|
||||
Opcode.FTOUW -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val fpSrc = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(fpSrc)}, $FP_ACC")
|
||||
invalidateD0Cache()
|
||||
emitLine("fmove.w $FP_ACC, d0")
|
||||
emitStoreD0(dstReg, IRDataType.WORD)
|
||||
}
|
||||
|
||||
Opcode.FTOSW -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val fpSrc = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(fpSrc)}, $FP_ACC")
|
||||
invalidateD0Cache()
|
||||
emitLine("fmove.w $FP_ACC, d0")
|
||||
emitStoreD0(dstReg, IRDataType.WORD)
|
||||
}
|
||||
|
||||
Opcode.FTOSL -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val fpSrc = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(fpSrc)}, $FP_ACC")
|
||||
invalidateD0Cache()
|
||||
emitLine("fmove.l $FP_ACC, d0")
|
||||
emitStoreD0(dstReg, IRDataType.LONG)
|
||||
}
|
||||
|
||||
Opcode.FABS -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
val src = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
|
||||
emitLine("fabs $FP_ACC, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
|
||||
Opcode.FSIN -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
val src = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
|
||||
emitLine("fsin $FP_ACC, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
|
||||
Opcode.FCOS -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
val src = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
|
||||
emitLine("fcos $FP_ACC, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
|
||||
Opcode.FTAN -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
val src = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
|
||||
emitLine("ftan $FP_ACC, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
|
||||
Opcode.FATAN -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
val src = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
|
||||
emitLine("fatan $FP_ACC, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
|
||||
Opcode.FPOW -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
val src = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
|
||||
emitLine("fpow $FP_ACC, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
|
||||
Opcode.FLN -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
val src = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
|
||||
emitLine("flogn $FP_ACC, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
|
||||
Opcode.FLOG -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
val src = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
|
||||
emitLine("flog2 $FP_ACC, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
|
||||
Opcode.FROUND -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
val src = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
|
||||
emitLine("fround $FP_ACC, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
|
||||
Opcode.FFLOOR -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
val src = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
|
||||
emitLine("ffloor $FP_ACC, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
|
||||
Opcode.FCEIL -> {
|
||||
// 68881 has no fceil; implement as: if x == int(x) then x else x > 0 ? int(x)+1 : int(x)
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
val src = insn.requireFloatSourceA().floatNumber
|
||||
val isIntLabel = makeLabel("fceil_is_int")
|
||||
val doneLabel = makeLabel("fceil_done")
|
||||
val posLabel = makeLabel(".fceil_pos")
|
||||
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, $FP_SRC")
|
||||
emitLine("fintrz $FP_SRC, $FP_SRC") // truncate toward zero
|
||||
emitLine("fcmp $FP_ACC, $FP_SRC")
|
||||
emitLine("fbeq $isIntLabel") // if equal, already integer
|
||||
emitLine("ftst $FP_ACC")
|
||||
emitLine("fbgt $posLabel") // if >0, need to add 1
|
||||
emitLine("bra $doneLabel")
|
||||
emitLabel(posLabel)
|
||||
emitLine("fadd.s #1.0, $FP_SRC")
|
||||
emitLine("bra $doneLabel")
|
||||
emitLabel(isIntLabel)
|
||||
// dst already holds the integer (from fintrz)
|
||||
emitLabel(doneLabel)
|
||||
emitLine("fmove.s $FP_SRC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
|
||||
Opcode.FCOMP -> {
|
||||
val dstReg = insn.requireIntDest().intNumber
|
||||
val fr1 = insn.requireFloatSourceA().floatNumber
|
||||
val fr2 = insn.requireSrcB().floatNumber
|
||||
val eqLabel = makeLabel("fcomp_eq")
|
||||
val doneLabel = makeLabel("fcomp_done")
|
||||
val gtLabel = makeLabel("fcomp_gt")
|
||||
invalidateD0Cache()
|
||||
emitLine("fmove.s ${floatRegFileAddr(fr1)}, $FP_ACC")
|
||||
emitLine("fcmp.s ${floatRegFileAddr(fr2)}, $FP_ACC")
|
||||
emitLine("fbeq $eqLabel")
|
||||
emitLine("fbgt $gtLabel")
|
||||
emitLine("moveq #-1, d0")
|
||||
emitLine("bra $doneLabel")
|
||||
emitLabel(gtLabel)
|
||||
emitLine("moveq #1, d0")
|
||||
emitLine("bra $doneLabel")
|
||||
emitLabel(eqLabel)
|
||||
emitLine("moveq #0, d0")
|
||||
emitLabel(doneLabel)
|
||||
emitStoreD0(dstReg, IRDataType.BYTE)
|
||||
}
|
||||
|
||||
Opcode.LOADHFACZERO, Opcode.LOADHFACONE, Opcode.STOREHFACZERO, Opcode.STOREHFACONE ->
|
||||
error("${insn.opcode} should have been handled by translateLoadStore")
|
||||
|
||||
else -> error("Unknown control opcode: ${insn.opcode}")
|
||||
}
|
||||
}
|
||||
|
||||
// === CALL translation with argument handling ===
|
||||
|
||||
private fun AsmGen.translateCall(fnLabel: String, callSite: CallSite, forwardedImmediateCall: ImmediateCallOptimization? = null) {
|
||||
if (fnLabel == "sys.memcopy" && emitInlineMemcopyCall(callSite, forwardedImmediateCall))
|
||||
return
|
||||
|
||||
// Emit non-constant arguments first (from regfile), then constant immediates
|
||||
// just before the BSR/JSR to avoid clobbering the constant registers while
|
||||
// evaluating non-const arguments (e.g. address calculations that may use D0/A0 as scratch).
|
||||
val (forwarded, nonForwarded) = if (forwardedImmediateCall != null) {
|
||||
callSite.arguments.partition { it.source.register in forwardedImmediateCall.loads }
|
||||
} else {
|
||||
emptyList<CallArgument>() to callSite.arguments
|
||||
}
|
||||
for (arg in nonForwarded) {
|
||||
translateArgument(arg, fnLabel, forwardedImmediateCall)
|
||||
}
|
||||
for (arg in forwarded) {
|
||||
translateArgument(arg, fnLabel, forwardedImmediateCall)
|
||||
}
|
||||
|
||||
// Check if this call targets an inline asmsub — emit its body directly instead of jsr
|
||||
val inlineTarget = program.allAsmSubs().find { it.label == fnLabel && it.isInline }
|
||||
if (inlineTarget != null) {
|
||||
emitRaw(inlineTarget.asmChunk.assembly)
|
||||
} else {
|
||||
// PC-relative call; vasm picks the optimal size and falls back to jsr if out of range
|
||||
emitLine("bsr $fnLabel")
|
||||
}
|
||||
|
||||
// Move return values back to virtual registers.
|
||||
// Skip status flag returns: always handled by IR's bsteq/bstneg/bstvs branch pattern.
|
||||
// In multi-assign context (results.size > 1), also skip slot-based returns:
|
||||
// the IR generates LOADHR for them, and emitting here would clobber CPU flags
|
||||
// before the branch pattern can read them.
|
||||
// In single-return expression context, process all slot returns normally
|
||||
// (the IR doesn't generate LOADHR for single-return calls).
|
||||
// LIMITATION: multiple status flag returns in one multi-assign (e.g. -> bool @Pz, bool @Pc)
|
||||
// are not supported - codegen limitation: the first flag's extraction clobbers the state for subsequent flags.
|
||||
val isMultiReturn = callSite.results.size > 1
|
||||
for (ret in callSite.results) {
|
||||
if (ret.location is CallLocation.StatusFlag)
|
||||
continue
|
||||
if (isMultiReturn)
|
||||
continue
|
||||
translateReturnValue(ret)
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.emitInlineMemcopyCall(callSite: CallSite, forwardedImmediateCall: ImmediateCallOptimization?): Boolean {
|
||||
if (callSite.arguments.size != 3) return false
|
||||
val fwd = forwardedImmediateCall ?: return false
|
||||
val srcArg = callSite.arguments[0]
|
||||
val tgtArg = callSite.arguments[1]
|
||||
val countArg = callSite.arguments[2]
|
||||
// size must be a constant immediate forwarded to the call; non-constant -> always call
|
||||
val countLoad = fwd.loads[countArg.source.register] ?: return false
|
||||
val count = countLoad.immediate?.integerValue ?: return false
|
||||
if (count <= 0) return true
|
||||
|
||||
// Only use .w/.l if we can 100% prove both pointers are aligned.
|
||||
// At codegen we only know immediates; computed addresses (e.g. &arr[i]) are not immediate
|
||||
// so we conservatively fall back to byte copies. A future IR alignment analysis could prove more.
|
||||
val srcImm = fwd.loads[srcArg.source.register]?.immediate?.integerValue
|
||||
val tgtImm = fwd.loads[tgtArg.source.register]?.immediate?.integerValue
|
||||
val bothEven = srcImm != null && tgtImm != null && srcImm % 2 == 0 && tgtImm % 2 == 0
|
||||
val bothLongAligned = srcImm != null && tgtImm != null && srcImm % 4 == 0 && tgtImm % 4 == 0
|
||||
val useLong = bothLongAligned && count % 4 == 0
|
||||
val useWord = bothEven && count % 2 == 0
|
||||
// Heuristics: inline small copies; use a dbra loop for the medium "small" range
|
||||
// to keep code size reasonable. The call to memcopy/CopyMem wins for large copies.
|
||||
val threshold = when {
|
||||
useLong -> 64
|
||||
useWord -> 32
|
||||
else -> 16
|
||||
}
|
||||
if (count > threshold) return false
|
||||
// Load pointers into a0/a1 (benefit from forwarded immediates)
|
||||
translateArgument(srcArg, "sys.memcopy", fwd)
|
||||
translateArgument(tgtArg, "sys.memcopy", fwd)
|
||||
// count is constant, no need to load d0
|
||||
// For larger small counts, a dbra loop is smaller than unrolled moves.
|
||||
// Threshold: a dbra loop needs ~10 bytes setup (move.w #count,dN + label + dbra),
|
||||
// so it wins over more than ~5 unrolled moves of the same size.
|
||||
val unrolledLimit = 5
|
||||
when {
|
||||
useLong -> {
|
||||
val longs = count / 4
|
||||
emitRaw(" ; inline memcopy $count bytes as $longs longwords")
|
||||
if (longs <= unrolledLimit) {
|
||||
repeat(longs) { emitLine("move.l (a0)+,(a1)+") }
|
||||
} else {
|
||||
emitInlineCopyLoop(longs, 4)
|
||||
}
|
||||
}
|
||||
useWord -> {
|
||||
val words = count / 2
|
||||
emitRaw(" ; inline memcopy $count bytes as $words words")
|
||||
if (words <= unrolledLimit) {
|
||||
repeat(words) { emitLine("move.w (a0)+,(a1)+") }
|
||||
} else {
|
||||
emitInlineCopyLoop(words, 2)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
emitRaw(" ; inline memcopy $count bytes")
|
||||
if (count <= unrolledLimit) {
|
||||
repeat(count) { emitLine("move.b (a0)+,(a1)+") }
|
||||
} else {
|
||||
emitInlineCopyLoop(count, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun AsmGen.emitInlineCopyLoop(iterations: Int, size: Int) {
|
||||
val loopLabel = makeLabel("memcpy_inline")
|
||||
val moveInsn = when (size) {
|
||||
4 -> "move.l"
|
||||
2 -> "move.w"
|
||||
else -> "move.b"
|
||||
}
|
||||
emitLine("moveq #$iterations, d0")
|
||||
emitLabel(loopLabel)
|
||||
emitLine("$moveInsn (a0)+,(a1)+")
|
||||
emitLine("dbra d0,$loopLabel")
|
||||
}
|
||||
|
||||
private fun AsmGen.translateArgument(
|
||||
arg: CallArgument,
|
||||
fnLabel: String? = null,
|
||||
forwardedImmediateCall: ImmediateCallOptimization? = null
|
||||
) {
|
||||
val argReg = arg.source
|
||||
val forwarded = forwardedImmediateCall?.loads?.get(argReg.register)
|
||||
|
||||
// If the argument has a calling convention slot, load it into that hardware register
|
||||
val slot = (arg.location as? CallLocation.HardwareRegister)?.slot
|
||||
if (slot != null) {
|
||||
val hwReg = m68kSlotRegister(slot)
|
||||
if (argReg.isFloat) {
|
||||
if (forwarded != null) {
|
||||
emitFloadConstantTo(hwReg, forwarded.requireImmediateFloat())
|
||||
} else {
|
||||
emitLine("fmove.s ${floatRegFileAddr(argReg.floatNumber)}, $hwReg")
|
||||
}
|
||||
} else {
|
||||
if (forwarded != null) {
|
||||
val value = forwarded.requireImmediateInt()
|
||||
val s = dtSuffix(argReg.type)
|
||||
if (hwReg.startsWith("a")) {
|
||||
// address registers only accept movea/suba, not clr or moveq
|
||||
when (value) {
|
||||
0 -> emitLine("suba.l $hwReg, $hwReg")
|
||||
else -> emitLine("movea.l #$value, $hwReg")
|
||||
}
|
||||
} else {
|
||||
if (value == 0)
|
||||
emitLine("clr$s $hwReg")
|
||||
else {
|
||||
// use moveq (2 bytes, 4 cycles) when the value fits in its signed 8-bit range;
|
||||
// for byte args the value is unsigned 0-255, so map 128-255 to -128--1 (low byte is the same)
|
||||
val moveqValue = if (argReg.type == IRDataType.BYTE && value in 128..255) value - 256 else value
|
||||
if (moveqValue in -128..127)
|
||||
emitLine("moveq #$moveqValue, $hwReg")
|
||||
else
|
||||
emitLine("move$s #$value, $hwReg")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val source = regAddr(argReg.intNumber)
|
||||
if (hwReg.startsWith("a") && argReg.type in setOf(IRDataType.LONG, IRDataType.POINTER)) {
|
||||
// loading a pointer into an address register; movea.l is the proper form and skips the +0 offset
|
||||
emitLine("movea.l ${source.removeSuffix("+0")}, $hwReg")
|
||||
} else {
|
||||
val s = dtSuffix(argReg.type)
|
||||
emitLine("move$s $source, $hwReg")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Store to the callee's parameter variable (if this is a named param)
|
||||
val paramName = (arg.location as? CallLocation.ParameterMemory)?.name.orEmpty()
|
||||
if (paramName.isNotEmpty() && fnLabel != null) {
|
||||
val target = fixNameSymbols("$fnLabel.$paramName")
|
||||
if (argReg.isFloat) {
|
||||
emitLine("fmove.s ${floatRegFileAddr(argReg.floatNumber)}, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, $target")
|
||||
} else {
|
||||
val s = dtSuffix(argReg.type)
|
||||
emitLine("move$s ${regAddr(argReg.intNumber)}, $target")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.translateReturnValue(ret: CallResult) {
|
||||
// Status flag returns are handled by the IR's branch-based pattern (bsteq/bstneg/bstvs).
|
||||
// Do NOT emit Scc here - it would clobber the flags before the branch can read them.
|
||||
if (ret.location is CallLocation.StatusFlag)
|
||||
return
|
||||
|
||||
val destination = ret.destination ?: return
|
||||
|
||||
// Otherwise, return value is in a hardware register
|
||||
val slot = (ret.location as? CallLocation.HardwareRegister)?.slot
|
||||
if (slot != null) {
|
||||
val hwReg = m68kSlotRegister(slot)
|
||||
if (destination.isFloat) {
|
||||
emitLine("fmove.s $hwReg, ${floatRegFileAddr(destination.floatNumber)}")
|
||||
} else {
|
||||
val s = dtSuffix(destination.type)
|
||||
emitLine("move$s $hwReg, ${regAddr(destination.intNumber)}")
|
||||
}
|
||||
} else {
|
||||
// Default: return value in d0 (standard m68k calling convention)
|
||||
// All non-float returns go through d0-d7 (not a0-a6) because the calling convention
|
||||
// expects values in data registers. Using A0 for pointers would be inconsistent:
|
||||
// callers read return values from data regs unless the slot annotation says otherwise.
|
||||
// Slightly inefficient: m68k pointers ideally live in address registers, but returning
|
||||
// them in d0 is simpler and avoids ambiguity. Explicit @A0 can be used for hot paths.
|
||||
if (destination.isFloat) {
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(destination.floatNumber)}")
|
||||
} else {
|
||||
emitStoreD0(destination.intNumber, destination.type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Slot to M68k hardware register mapping ===
|
||||
|
||||
// Slots 0..7 are the 6502/cx16-style scalar registers (A, X, Y, AX, AY, XY,
|
||||
// FAC1, FAC2) used by target-independent builtins (e.g. divmod returns its
|
||||
// quotient in AY). Map them onto distinct M68k hardware registers so the
|
||||
// backend can emit code for them.
|
||||
fun m68kSlotRegister(slot: CallingConventionSlot): String = when (slot.value) {
|
||||
in 0..7 -> error("slots 0-7 should never be used on the M68K they are 6502 cpu registers")
|
||||
in 10..17 -> "d${slot.value - 10}" // M68k slots: D0-D7
|
||||
in 18..24 -> "a${slot.value - 18}" // M68k slots: A0-A6
|
||||
in 25..32 -> "fp${slot.value - 25}" // M68k slots: FP0-FP7
|
||||
else -> error("unknown calling convention slot: $slot")
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package prog8.codegen.m68k
|
||||
|
||||
import prog8.intermediate.IRDataType
|
||||
import prog8.intermediate.IRInstruction
|
||||
import prog8.intermediate.ImmediateOperand
|
||||
import prog8.intermediate.Opcode
|
||||
import prog8.intermediate.floatNumber
|
||||
import prog8.intermediate.intNumber
|
||||
import prog8.intermediate.requireFloatDest
|
||||
import prog8.intermediate.requireFloatSourceA
|
||||
import prog8.intermediate.requireHardwareSlot
|
||||
import prog8.intermediate.requireImmediate
|
||||
import prog8.intermediate.requireImmediateFloat
|
||||
import prog8.intermediate.requireImmediateInt
|
||||
import prog8.intermediate.requireIntDest
|
||||
import prog8.intermediate.requireIntSourceA
|
||||
import prog8.intermediate.requireMemory
|
||||
|
||||
internal fun AsmGen.translateLoadStore(insn: IRInstruction, suppressRegfileStore: Boolean = false) {
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
|
||||
if (type == IRDataType.FLOAT) {
|
||||
translateFloatLoadStore(insn, suppressRegfileStore)
|
||||
return
|
||||
}
|
||||
|
||||
val s = dtSuffix(type)
|
||||
|
||||
when (insn.opcode) {
|
||||
Opcode.LOAD -> {
|
||||
val dst = insn.requireIntDest().intNumber
|
||||
when (val value = insn.requireImmediate()) {
|
||||
is ImmediateOperand.Integer -> {
|
||||
if (suppressRegfileStore)
|
||||
return
|
||||
if(value.value == 0)
|
||||
emitLine("clr$s ${regAddr(dst)}")
|
||||
else
|
||||
emitLine("move$s #${value.value}, ${regAddr(dst)}")
|
||||
invalidateD0CacheForSlot(dst)
|
||||
}
|
||||
is ImmediateOperand.SymbolAddress -> {
|
||||
val resolved = resolveSymbolRef(value.symbol)
|
||||
val symOff = if (value.offset != 0) "$resolved+${value.offset}" else resolved
|
||||
emitLine("move.l #$symOff, ${regAddr(dst)}")
|
||||
invalidateD0CacheForSlot(dst)
|
||||
}
|
||||
is ImmediateOperand.FloatValue -> error("LOAD: float immediate for an integer register")
|
||||
}
|
||||
}
|
||||
|
||||
Opcode.LOADM -> {
|
||||
val dst = insn.requireIntDest().intNumber
|
||||
val target = resolveMemory(insn.requireMemory())
|
||||
emitLine("move$s $target, ${regAddr(dst)}")
|
||||
invalidateD0CacheForSlot(dst)
|
||||
}
|
||||
|
||||
Opcode.LOADR -> {
|
||||
val dst = insn.requireIntDest().intNumber
|
||||
val src = insn.requireIntSourceA().intNumber
|
||||
emitLine("move$s ${regAddr(src)}, ${regAddr(dst)}")
|
||||
invalidateD0CacheForSlot(dst)
|
||||
}
|
||||
|
||||
Opcode.LOADX, Opcode.LOADI -> {
|
||||
val dst = insn.requireIntDest().intNumber
|
||||
val source = resolveMemory(insn.requireMemory())
|
||||
emitLoadD0FromAddress(source, type)
|
||||
emitStoreD0(dst, type)
|
||||
}
|
||||
|
||||
Opcode.LOADHR -> {
|
||||
val dst = insn.requireIntDest().intNumber
|
||||
val hwReg = m68kSlotRegister(insn.requireHardwareSlot().slot)
|
||||
emitLine("move$s $hwReg, ${regAddr(dst)}")
|
||||
invalidateD0CacheForSlot(dst)
|
||||
}
|
||||
|
||||
Opcode.STOREM -> {
|
||||
val src = insn.requireIntSourceA().intNumber
|
||||
val target = resolveMemory(insn.requireMemory())
|
||||
emitLine("move$s ${regAddr(src)}, $target")
|
||||
invalidateD0CacheForAddress(target)
|
||||
}
|
||||
|
||||
Opcode.STOREIM -> {
|
||||
val value = insn.requireImmediateInt()
|
||||
val target = resolveMemory(insn.requireMemory())
|
||||
if(value == 0)
|
||||
emitLine("clr$s $target")
|
||||
else
|
||||
emitLine("move$s #$value, $target")
|
||||
invalidateD0CacheForAddress(target)
|
||||
}
|
||||
|
||||
Opcode.STOREX -> {
|
||||
val value = insn.requireIntSourceA().intNumber
|
||||
val target = resolveMemory(insn.requireMemory())
|
||||
// the indexed effective address uses a0/d0 as scratch, so stage the value in d1
|
||||
emitLine("move$s ${regAddr(value)}, d1")
|
||||
emitLine("move$s d1, $target")
|
||||
}
|
||||
|
||||
Opcode.STOREZM, Opcode.STOREZI, Opcode.STOREZX -> {
|
||||
val target = resolveMemory(insn.requireMemory())
|
||||
emitLine("clr$s $target")
|
||||
invalidateD0CacheForAddress(target)
|
||||
}
|
||||
|
||||
Opcode.STOREHR -> {
|
||||
val src = insn.requireIntSourceA().intNumber
|
||||
val hwReg = m68kSlotRegister(insn.requireHardwareSlot().slot)
|
||||
emitLine("move$s ${regAddr(src)}, $hwReg")
|
||||
}
|
||||
|
||||
Opcode.STOREI -> {
|
||||
val value = insn.requireIntSourceA().intNumber
|
||||
val target = resolveMemory(insn.requireMemory())
|
||||
emitLine("move$s ${regAddr(value)}, $target")
|
||||
}
|
||||
|
||||
Opcode.LOADP_INC -> {
|
||||
val dst = insn.requireIntDest().intNumber
|
||||
// r1 = *a; a += sizeof(type) where a is a pointer variable (LONG) in memory
|
||||
val target = resolveMemory(insn.requireMemory())
|
||||
emitLine("move.l $target, a0")
|
||||
emitLoadD0FromAddress("(a0)+", type)
|
||||
emitLine("move.l a0, $target")
|
||||
invalidateD0CacheForAddress(target)
|
||||
emitStoreD0(dst, type)
|
||||
}
|
||||
|
||||
Opcode.STOREP_INC -> {
|
||||
val value = insn.requireIntSourceA().intNumber
|
||||
// *a = r1; a += sizeof(type)
|
||||
val target = resolveMemory(insn.requireMemory())
|
||||
emitLine("move.l $target, a0")
|
||||
emitLine("move$s ${regAddr(value)}, (a0)+")
|
||||
emitLine("move.l a0, $target")
|
||||
invalidateD0CacheForAddress(target)
|
||||
}
|
||||
|
||||
else -> error("Unknown load/store opcode: ${insn.opcode}")
|
||||
}
|
||||
}
|
||||
|
||||
// === Float load/store via FPU (68881/68882) ===
|
||||
|
||||
private fun AsmGen.translateFloatLoadStore(insn: IRInstruction, suppressRegfileStore: Boolean = false) {
|
||||
when (insn.opcode) {
|
||||
Opcode.STOREZM, Opcode.STOREZI, Opcode.STOREZX -> {
|
||||
val target = resolveMemory(insn.requireMemory(), forFloat = true)
|
||||
emitLine($$"fmovecr #$0f, fp0")
|
||||
emitLine("fmove.s fp0, $target")
|
||||
invalidateD0CacheForAddress(target)
|
||||
}
|
||||
|
||||
Opcode.STOREIM -> {
|
||||
val target = resolveMemory(insn.requireMemory(), forFloat = true)
|
||||
emitFloadConstantTo("fp0", insn.requireImmediateFloat())
|
||||
emitLine("fmove.s fp0, $target")
|
||||
invalidateD0CacheForAddress(target)
|
||||
}
|
||||
|
||||
Opcode.LOAD -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
when (val value = insn.requireImmediate()) {
|
||||
is ImmediateOperand.FloatValue -> {
|
||||
if (suppressRegfileStore)
|
||||
return
|
||||
emitFloadConstantToAcc(value.value)
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
is ImmediateOperand.SymbolAddress -> {
|
||||
val resolved = resolveSymbolRef(value.symbol)
|
||||
val symOff = if (value.offset != 0) "$resolved+${value.offset}" else resolved
|
||||
emitLine("lea $symOff, a0")
|
||||
emitLine("fmove.s (a0), $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
is ImmediateOperand.Integer -> error("LOAD.f: integer immediate for a float register")
|
||||
}
|
||||
}
|
||||
|
||||
Opcode.LOADM, Opcode.LOADX, Opcode.LOADI -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
val source = resolveMemory(insn.requireMemory(), forFloat = true)
|
||||
emitLine("fmove.s $source, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
|
||||
Opcode.LOADR -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
val src = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
|
||||
Opcode.LOADHR -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
val hwReg = m68kSlotRegister(insn.requireHardwareSlot().slot)
|
||||
emitLine("fmove $hwReg, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
|
||||
Opcode.STOREM, Opcode.STOREX, Opcode.STOREI -> {
|
||||
val src = insn.requireFloatSourceA().floatNumber
|
||||
val target = resolveMemory(insn.requireMemory(), forFloat = true)
|
||||
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, $target")
|
||||
invalidateD0CacheForAddress(target)
|
||||
}
|
||||
|
||||
Opcode.STOREHR -> {
|
||||
val src = insn.requireFloatSourceA().floatNumber
|
||||
val hwReg = m68kSlotRegister(insn.requireHardwareSlot().slot)
|
||||
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
|
||||
emitLine("fmove $FP_ACC, $hwReg")
|
||||
}
|
||||
|
||||
// The 'FAC' hardware float registers only exist on the 6502 targets. On m68k they are
|
||||
// modelled with the two FPU scratch registers; loading them yields the 0.0 / 1.0 constants
|
||||
// that the 6502 targets keep in FAC1 / FAC2.
|
||||
Opcode.LOADHFACZERO -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
emitLine($$"fmovecr #$0f, $$FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
Opcode.LOADHFACONE -> {
|
||||
val dst = insn.requireFloatDest().floatNumber
|
||||
emitLine("fmovecr #$32, $FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
|
||||
}
|
||||
Opcode.STOREHFACZERO -> {
|
||||
val src = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
|
||||
}
|
||||
Opcode.STOREHFACONE -> {
|
||||
val src = insn.requireFloatSourceA().floatNumber
|
||||
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_SRC")
|
||||
}
|
||||
|
||||
else -> error("Unknown float load/store opcode: ${insn.opcode}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var floatConstCounter = 0
|
||||
internal fun AsmGen.makeFloatConstLabel(value: Double): String {
|
||||
floatConstCounter++
|
||||
val label = "p8c_fconst_$floatConstCounter"
|
||||
this.dataFloatConstants.add(Pair(label, value))
|
||||
return label
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* SYSCALL instruction translations for the M68k code generator.
|
||||
*
|
||||
* Handles inline expansion of intermediate representation syscall
|
||||
* instructions into M68k assembly code.
|
||||
*/
|
||||
|
||||
package prog8.codegen.m68k
|
||||
|
||||
import prog8.intermediate.CallSite
|
||||
import prog8.intermediate.IMSyscall
|
||||
import prog8.intermediate.IRDataType
|
||||
import prog8.intermediate.RegisterOperand
|
||||
import prog8.intermediate.intNumber
|
||||
|
||||
|
||||
// === SYSCALL dispatch ===
|
||||
|
||||
internal fun AsmGen.translateSyscall(num: Int, site: CallSite) {
|
||||
when (num) {
|
||||
IMSyscall.WORDARRAY_CONTAINS.number -> translateSyscallWordarrayContains(site)
|
||||
IMSyscall.COMPARE_STRINGS.number -> translateSyscallStringCompare(site)
|
||||
IMSyscall.CLAMP_BYTE.number -> translateSyscallClamp(site, IRDataType.BYTE, true)
|
||||
IMSyscall.CLAMP_UBYTE.number -> translateSyscallClamp(site, IRDataType.BYTE, false)
|
||||
IMSyscall.CLAMP_WORD.number -> translateSyscallClamp(site, IRDataType.WORD, true)
|
||||
IMSyscall.CLAMP_UWORD.number -> translateSyscallClamp(site, IRDataType.WORD, false)
|
||||
IMSyscall.CLAMP_LONG.number -> translateSyscallClamp(site, IRDataType.LONG, true)
|
||||
else -> TODO("syscall $num on m68k")
|
||||
}
|
||||
}
|
||||
|
||||
private fun CallSite.argument(index: Int): RegisterOperand =
|
||||
arguments.getOrNull(index)?.source ?: error("syscall is missing argument $index")
|
||||
|
||||
private fun CallSite.resultRegister(index: Int): RegisterOperand =
|
||||
results.getOrNull(index)?.destination ?: error("syscall is missing result $index")
|
||||
|
||||
// Compare two strings by delegating to the library routine prog8_lib.strcmp,
|
||||
// which already implements an efficient case-sensitive comparison that
|
||||
// correctly handles 32 bits pointers (the pointer type on M68k targets).
|
||||
private fun AsmGen.translateSyscallStringCompare(site: CallSite) {
|
||||
val arg1 = site.argument(0)
|
||||
val arg2 = site.argument(1)
|
||||
val resultReg = site.resultRegister(0).intNumber
|
||||
emitLoadD0(arg1.intNumber, arg1.type)
|
||||
loadStringArg(arg2.intNumber, arg2.type, "d1")
|
||||
invalidateD0Cache() // d0 is caller-saved across the subroutine call
|
||||
emitLine("bsr prog8_lib.strcmp")
|
||||
emitStoreD0(resultReg, IRDataType.BYTE)
|
||||
}
|
||||
|
||||
// Load a string pointer argument (16 or 32 bits) into the given data register,
|
||||
// matching the calling convention of strings.compare (full 32 bits address).
|
||||
private fun AsmGen.loadStringArg(reg: Int, dt: IRDataType, dreg: String) {
|
||||
if (dt == IRDataType.LONG) {
|
||||
emitLine("move.l ${regAddr(reg)}, $dreg")
|
||||
} else {
|
||||
emitLine("move.w ${regAddr(reg)}, $dreg")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.translateSyscallWordarrayContains(site: CallSite) {
|
||||
val regElem = site.argument(0).intNumber
|
||||
val regArr = site.argument(1).intNumber
|
||||
val regLen = site.argument(2).intNumber
|
||||
val resultReg = site.resultRegister(0).intNumber
|
||||
|
||||
val labelLoop = makeLabel(".wac_loop")
|
||||
val labelFound = makeLabel(".wac_found")
|
||||
val labelDone = makeLabel(".wac_done")
|
||||
|
||||
emitLoadD0(regElem, IRDataType.WORD)
|
||||
emitLine("move.l ${regAddr(regArr)}, a0")
|
||||
emitLine("moveq.l #0, d1")
|
||||
emitLine("move.b ${regAddrByte(regLen, 0)}, d1")
|
||||
emitLine("subq.w #1, d1")
|
||||
emitLine("bmi $labelDone ; length was 0 -> not found")
|
||||
emitRaw("$labelLoop:")
|
||||
emitLine("cmp.w (a0)+, d0")
|
||||
emitLine("beq $labelFound")
|
||||
emitLine("dbra d1, $labelLoop")
|
||||
emitLine("move.b #0, ${regAddrByte(resultReg, 0)}")
|
||||
emitLine("bra $labelDone")
|
||||
emitRaw("$labelFound:")
|
||||
emitLine("move.b #1, ${regAddrByte(resultReg, 0)}")
|
||||
emitRaw("$labelDone:")
|
||||
}
|
||||
|
||||
private fun AsmGen.translateSyscallClamp(site: CallSite, dt: IRDataType, signed: Boolean) {
|
||||
val valueReg = site.argument(0).intNumber
|
||||
val minReg = site.argument(1).intNumber
|
||||
val maxReg = site.argument(2).intNumber
|
||||
val resultReg = site.resultRegister(0).intNumber
|
||||
|
||||
val labelCheckMax = makeLabel(".clamp_max")
|
||||
val labelDone = makeLabel(".clamp_done")
|
||||
val bge = if (signed) "bge" else "bhs"
|
||||
val ble = if (signed) "ble" else "bls"
|
||||
|
||||
when (dt) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLoadD0(valueReg, IRDataType.BYTE)
|
||||
emitLine("cmp.b ${regAddr(minReg)}, d0")
|
||||
emitLine("$bge $labelCheckMax")
|
||||
emitLoadD0(minReg, IRDataType.BYTE)
|
||||
emitLine("bra $labelDone")
|
||||
emitRaw("$labelCheckMax:")
|
||||
invalidateD0Cache() // branch target: d0 still holds valueReg, not minReg
|
||||
emitLine("cmp.b ${regAddr(maxReg)}, d0")
|
||||
emitLine("$ble $labelDone")
|
||||
emitLoadD0(maxReg, IRDataType.BYTE)
|
||||
emitRaw("$labelDone:")
|
||||
invalidateD0Cache() // branch target: d0 differs per incoming path
|
||||
emitStoreD0(resultReg, IRDataType.BYTE)
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLoadD0(valueReg, IRDataType.WORD)
|
||||
emitLine("cmp.w ${regAddr(minReg)}, d0")
|
||||
emitLine("$bge $labelCheckMax")
|
||||
emitLoadD0(minReg, IRDataType.WORD)
|
||||
emitLine("bra $labelDone")
|
||||
emitRaw("$labelCheckMax:")
|
||||
invalidateD0Cache() // branch target: d0 still holds valueReg, not minReg
|
||||
emitLine("cmp.w ${regAddr(maxReg)}, d0")
|
||||
emitLine("$ble $labelDone")
|
||||
emitLoadD0(maxReg, IRDataType.WORD)
|
||||
emitRaw("$labelDone:")
|
||||
invalidateD0Cache() // branch target: d0 differs per incoming path
|
||||
emitStoreD0(resultReg, IRDataType.WORD)
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLoadD0(valueReg, IRDataType.LONG)
|
||||
emitLine("cmp.l ${regAddr(minReg)}, d0")
|
||||
emitLine("$bge $labelCheckMax")
|
||||
emitLoadD0(minReg, IRDataType.LONG)
|
||||
emitLine("bra $labelDone")
|
||||
emitRaw("$labelCheckMax:")
|
||||
invalidateD0Cache() // branch target: d0 still holds valueReg, not minReg
|
||||
emitLine("cmp.l ${regAddr(maxReg)}, d0")
|
||||
emitLine("$ble $labelDone")
|
||||
emitLoadD0(maxReg, IRDataType.LONG)
|
||||
emitRaw("$labelDone:")
|
||||
invalidateD0Cache() // branch target: d0 differs per incoming path
|
||||
emitStoreD0(resultReg, IRDataType.LONG)
|
||||
}
|
||||
else -> emitLine("; clamp: unsupported dt $dt")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package prog8.codegen.m68k
|
||||
|
||||
import prog8.code.ICodeGeneratorBackend
|
||||
import prog8.code.SymbolTable
|
||||
import prog8.code.assembly.IAssemblyProgram
|
||||
import prog8.code.ast.PtProgram
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.codegen.intermediate.IRCodeGen
|
||||
import prog8.intermediate.IRDataType
|
||||
import prog8.intermediate.IRFileWriter
|
||||
import prog8.intermediate.VirtualRegister
|
||||
import prog8.intermediate.dumpVariables
|
||||
|
||||
class M68kCodeGenerator(val retainSSA: Boolean): ICodeGeneratorBackend {
|
||||
override fun generate(
|
||||
program: PtProgram,
|
||||
symbolTable: SymbolTable,
|
||||
options: CompilationOptions,
|
||||
errors: IErrorReporter
|
||||
): IAssemblyProgram {
|
||||
|
||||
val irCodeGen = IRCodeGen(program, symbolTable, options, errors, retainSSA)
|
||||
val irProgram = irCodeGen.generate()
|
||||
val virtualRegisterTypes = irCodeGen.registerTypes().entries.associate { (num, type) ->
|
||||
val register: VirtualRegister = if (type == IRDataType.FLOAT) VirtualRegister.float(num.value) else VirtualRegister.int(num.value)
|
||||
register to type
|
||||
}
|
||||
irProgram.verifyRegisterTypes(virtualRegisterTypes)
|
||||
|
||||
if (options.dumpVariables)
|
||||
dumpVariables(irProgram)
|
||||
|
||||
IRFileWriter(irProgram, null).write()
|
||||
|
||||
val gen = AsmGen(irProgram, irProgram.options.compTarget)
|
||||
if (!gen.generate())
|
||||
throw RuntimeException("M68k assembly generation failed")
|
||||
|
||||
return AssemblyProgramM68k(irProgram.name, irProgram.options.outputDir)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package prog8.codegen.m68k
|
||||
|
||||
import com.github.ajalt.clikt.core.CliktCommand
|
||||
import com.github.ajalt.clikt.core.main
|
||||
import com.github.ajalt.clikt.parameters.arguments.argument
|
||||
import prog8.code.core.CpuType
|
||||
import prog8.intermediate.IRFileReader
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.readText
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val cli = M68kCli()
|
||||
cli.main(args)
|
||||
val inputFile = cli.inputFile
|
||||
|
||||
val reader = IRFileReader()
|
||||
val source = Path(inputFile).readText()
|
||||
val program = reader.read(source)
|
||||
val target = program.options.compTarget
|
||||
println("Loaded IR program: ${program.name}")
|
||||
println("Target system and CPU: ${target.name} / ${target.cpu}")
|
||||
|
||||
when(target.cpu) {
|
||||
CpuType.M68000, CpuType.M68020 -> {
|
||||
val gen = AsmGen(program, target)
|
||||
gen.generate()
|
||||
println("Generated assembly: ${program.name}.asm")
|
||||
}
|
||||
else -> {
|
||||
error("This code generator only works for M68000 or M68020 CPU.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class M68kCli : CliktCommand(name = "prog8-m68kgen") {
|
||||
val inputFile by argument(help = "path to .p8ir")
|
||||
|
||||
override fun run() = Unit
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,97 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
id("application")
|
||||
id("com.peterabeles.gversion") version "1.11.0"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":codeCore"))
|
||||
implementation(project(":simpleAst"))
|
||||
implementation(project(":intermediate"))
|
||||
implementation(project(":codeGenIntermediate"))
|
||||
implementation("com.michael-bull.kotlin-result:kotlin-result-jvm:2.3.1")
|
||||
|
||||
testImplementation(testFixtures(project(":codeCore")))
|
||||
testImplementation("io.kotest:kotest-runner-junit5")
|
||||
}
|
||||
|
||||
|
||||
val debugPort = 8000
|
||||
val debugArgs = "-agentlib:jdwp=transport=dt_socket,server=y,address=8000,suspend=n,quiet=y"
|
||||
|
||||
val serverMainClassName = "prog8.codegen.new6502.MainKt"
|
||||
val applicationName = "prog8-newgen"
|
||||
|
||||
application {
|
||||
mainClass.set(serverMainClassName)
|
||||
description = "New codegen based on IR files"
|
||||
applicationDistribution.into("bin") {
|
||||
filePermissions {
|
||||
user {
|
||||
read=true
|
||||
execute=true
|
||||
write=true
|
||||
}
|
||||
other.execute = true
|
||||
group.execute = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":codeCore"))
|
||||
implementation(project(":intermediate"))
|
||||
implementation("com.michael-bull.kotlin-result:kotlin-result-jvm:2.3.1")
|
||||
implementation("com.github.ajalt.clikt:clikt:5.1.0")
|
||||
|
||||
// Test dependencies - Kotest BOM is provided by root build.gradle.kts
|
||||
testImplementation("io.kotest:kotest-runner-junit5")
|
||||
}
|
||||
|
||||
configurations.forEach { config ->
|
||||
config.resolutionStrategy {
|
||||
preferProjectModules()
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDir("src")
|
||||
resources.srcDir("resources")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.startScripts {
|
||||
applicationName = "prog8-newgen"
|
||||
}
|
||||
|
||||
tasks.register<Exec>("fixFilePermissions") {
|
||||
description = "When running on macOS or Linux the start script needs executable permissions to run, we set those here"
|
||||
|
||||
onlyIf { !System.getProperty("os.name").lowercase().contains("windows") }
|
||||
commandLine("chmod", "+x", "${tasks.installDist.get().destinationDir}/bin/prog8-newgen")
|
||||
}
|
||||
|
||||
tasks.register<JavaExec>("debugRun") {
|
||||
description = "start with remote debugging"
|
||||
mainClass.set(serverMainClassName)
|
||||
classpath(sourceSets.main.get().runtimeClasspath)
|
||||
standardInput = System.`in`
|
||||
|
||||
jvmArgs(debugArgs)
|
||||
doLast {
|
||||
println("Using debug port $debugPort")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.installDist {
|
||||
finalizedBy("fixFilePermissions")
|
||||
}
|
||||
|
||||
tasks.build {
|
||||
finalizedBy("installDist")
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
New6502 Codegen Performance Optimizations
|
||||
==========================================
|
||||
|
||||
The new6502 codegen has NO CPU register allocation. IR virtual registers live
|
||||
in a memory "register file" (`p8_regfile`, in BSS), with variable-size slots
|
||||
per register type. Every instruction that touches a register does so through
|
||||
that memory slot using the A/X/Y registers as scratch. The file is sized
|
||||
dynamically from `registersUsed()`, so only the actually-used registers cost
|
||||
memory. Long (32-bit) operations in particular are expensive: each one is 4
|
||||
separate load/op/store round trips through memory.
|
||||
|
||||
This document lists improvements ordered roughly by impact and ease, in the
|
||||
same spirit as the m68k analysis (codeGenM68k/docs/m68k-optimizations.md).
|
||||
Items that fix outright correctness bugs are flagged [BUG].
|
||||
|
||||
What is already good
|
||||
---------------------
|
||||
|
||||
Some patterns are already tight and should be preserved:
|
||||
|
||||
- STOREZM / STOREIM zeroing uses `stz` on 65C02 (cx16) targets
|
||||
- WORD add/sub with immediate is emitted as a single carry-propagating
|
||||
`clc/adc`/`sec/sbc` chain on the slot, no extra temp
|
||||
- BYTE INC/DEC on 65C02 uses `ina`/`dea`
|
||||
- WORD INC/DEC use the branch-to-skip carry propagation pattern
|
||||
- LONG add/sub are unrolled (fast), not looped
|
||||
- signed word comparisons use the SEC/SBC/EOR #$80 technique
|
||||
- `lda slot; cmp #imm` for unsigned word compare/branch with the
|
||||
operand-swap trick for `>` and `<=` (no branch-over-jmp)
|
||||
- variable-count shifts use a single X-counter loop (compact)
|
||||
- constant-count shifts are unrolled (fast)
|
||||
- CALLI is turned into CALL by the IR peephole when possible
|
||||
- the regfile layout is packed by register type, sized by actual use
|
||||
- address computation for indirect access uses a single ZP scratch word
|
||||
|
||||
Suggested order of implementation
|
||||
-----------------------------------
|
||||
|
||||
1. A/X/Y register cache (biggest win, no real allocation)
|
||||
2. Peephole optimizer expansion on the emitted assembly
|
||||
3. Byte BGT/BLE via operand swap
|
||||
4. `addImmediate`/`subImmediate` value==1 to `inc`/`dec` memory
|
||||
5. 65C02-only instruction audit (FTOSL uses `stz`/`bra` on all targets) [BUG]
|
||||
6. TSB/TRB for BITSET/BITCLR on 65C02
|
||||
7. ZP placement of hot virtual registers
|
||||
8. Float FAC1 caching and DEC via FADD -1.0
|
||||
9. Inline asmsub handling [BUG]
|
||||
10. STOREZX LONG zeroing incomplete [BUG]
|
||||
11. JMP (abs) page-wrap on plain 6502 [BUG]
|
||||
12. Unroll remaining multi-byte loops for speed
|
||||
|
||||
|
||||
1. A/X/Y register cache (biggest win, still no full allocation)
|
||||
----------------------------------------------------------------
|
||||
|
||||
The scratch usage is extremely regular. BYTE ops funnel through A:
|
||||
`lda slot; <op>; sta slot`. After a `sta slot`, A still holds that value.
|
||||
The next IR instruction frequently reads that same slot again (e.g.
|
||||
`a = b + c` followed by `d = a + e`). Tracking "A currently holds virtual
|
||||
register rX" lets the codegen skip the redundant `lda slot`.
|
||||
|
||||
Track the same for the X register (used as an index / second data holder in
|
||||
`storeX`/`indirectStore`/STOREZX paths) if the A cache proves effective.
|
||||
|
||||
Invalidation rules:
|
||||
- any instruction that writes a register-file slot (the slot value in A/X
|
||||
becomes stale, and A itself is clobbered by most ops anyway)
|
||||
- any ALU instruction (clobbers A)
|
||||
- any branch target / basic-block boundary / chunk boundary
|
||||
- any CALL / JUMP / PUSH / POP (callee-clobbers A/X/Y)
|
||||
- after `lda #imm`, the cache holds an immediate, not a register slot
|
||||
- after `lda` from a non-register memory operand, cache is empty
|
||||
|
||||
The biggest wins appear in long chains of `LOADR` + ALU + `STORER`, and in
|
||||
the post-`sta` reload pattern described above.
|
||||
|
||||
Note: unlike m68k, flag-setting matters here. `cmp` sets flags for the
|
||||
branch that follows, so a cached load must never be skipped if the following
|
||||
instruction consumes the flags from the *load* itself (plain `lda` does not
|
||||
set flags, so this is only a concern around `ldx`/`ldy` used as data).
|
||||
Only skip pure loads, never reorder.
|
||||
|
||||
2. Peephole optimizer expansion
|
||||
--------------------------------
|
||||
|
||||
The current `optimization/PeepholeOptimizer.kt` only rewrites CALLI to CALL
|
||||
(42 lines). The old 6502 codegen has a rich assembly post-pass
|
||||
(codeGenCpu6502/AsmOptimizer.kt) with proven patterns that are still missing
|
||||
here:
|
||||
|
||||
- optimizeSameAssignments (redundant `sta slot; lda slot`)
|
||||
- optimizeStoreLoadSame
|
||||
- optimizeIncDec
|
||||
- optimizeJsrRtsAndOtherCombinations (jsr + rts -> jmp, branch-over-jmp)
|
||||
- optimizeUselessPushPopStack
|
||||
- optimizeTSBtoRegularOr
|
||||
- optimizeUnneededTempvarInAdd
|
||||
|
||||
Because AsmGen emits text directly to a StringBuilder, the natural fit is a
|
||||
post-pass over the emitted lines (same design as the old AsmOptimizer), or
|
||||
folding these patterns into the IR peephole that already runs.
|
||||
|
||||
3. Byte BGT/BLE via operand swap
|
||||
---------------------------------
|
||||
|
||||
Unsigned byte `a > imm` currently emits:
|
||||
```
|
||||
lda slot
|
||||
cmp #imm
|
||||
bcc skip
|
||||
beq skip
|
||||
jmp label
|
||||
skip:
|
||||
```
|
||||
(6 instructions, a label, and a branch-over-jmp). The WORD path already
|
||||
swaps operands to `lda #imm; cmp slot; bcc label`. The same swap works for
|
||||
bytes:
|
||||
```
|
||||
lda #imm
|
||||
cmp slot
|
||||
bcc label
|
||||
```
|
||||
3 instructions, no label. Same for `a <= imm` (`bcs` instead of `bcc`).
|
||||
This also removes the `jmp` that breaks the short-branch range.
|
||||
|
||||
4. `addImmediate`/`subImmediate` value==1 -> `inc`/`dec` memory
|
||||
----------------------------------------------------------------
|
||||
|
||||
On plain 6502 (c64/pet32/c128), BYTE ADDIM/SUBIM with value 1 emits:
|
||||
```
|
||||
lda slot
|
||||
clc
|
||||
adc #1
|
||||
sta slot
|
||||
```
|
||||
(4 instructions) even though the target is a memory slot. `inc slot` /
|
||||
`dec slot` do the whole thing in one instruction. The 65C02 path already
|
||||
handles value==1 with `ina`/`dea`; extend the plain-6502 path to use the
|
||||
memory `inc`/`dec` form (only for BYTE type, and only when the value is
|
||||
exactly 1, never for signed -1 which would need a different form).
|
||||
|
||||
5. 65C02-only instruction audit [BUG]
|
||||
---------------------------------------
|
||||
|
||||
`translateFloatToSignedLong` (InstrControl.kt, FTOSL) emits `stz` and `bra`
|
||||
unconditionally. Both are 65C02-only instructions. A program using floats on
|
||||
a c64/pet32/c128 target (plain 6502) will therefore fail to assemble or
|
||||
produce garbage. It must use `lda #0; sta` and `jmp` on non-65C02 targets.
|
||||
|
||||
Audit the whole codegen for other ungated 65C02 instructions (`stz`, `bra`,
|
||||
`tsb`, `trb`, `ina`, `dea`, `phx`, `phy`, `plx`, `ply`, `stx zp,y`, etc.)
|
||||
and gate them on `is65C02()`.
|
||||
|
||||
6. TSB/TRB for BITSET/BITCLR on 65C02
|
||||
--------------------------------------
|
||||
|
||||
BITSET/BITCLR/BITTOG currently emit:
|
||||
```
|
||||
lda slot
|
||||
ora/and/eor #mask
|
||||
sta slot
|
||||
```
|
||||
(3 instructions) on every target. On 65C02, TSB/TRB operate directly on
|
||||
memory (zeropage and absolute), collapsing BITSET and BITCLR to a single
|
||||
instruction. BITTOG has no TSB/TRB equivalent, keep it as-is. Apply only
|
||||
when `is65C02()`, keeping the current form on plain 6502.
|
||||
|
||||
7. ZP placement of hot virtual registers
|
||||
-----------------------------------------
|
||||
|
||||
Every register-file access uses absolute addressing (3 bytes). Moving the
|
||||
hottest virtual registers into zeropage makes those accesses 2-byte and
|
||||
faster. The full regfile can never live in ZP (ZP is only 256 bytes), but:
|
||||
|
||||
- reuse the existing ZeropageAllocator (it already scores variables by usage
|
||||
frequency and type) and extend the scoring to virtual registers, or
|
||||
- overlay a small set of "hot" virtual registers on dedicated ZP slots and
|
||||
have `regAddrLo()`/`regAddrHi()`/`regAddr()` return those ZP labels.
|
||||
|
||||
Combined with the IR-level RegisterPacker (currently disabled because it
|
||||
"doesn't work well", commit b0cf649c2), this is the closest step to real
|
||||
register allocation and the biggest structural win available.
|
||||
|
||||
8. Float FAC1 caching and DEC via FADD -1.0
|
||||
---------------------------------------------
|
||||
|
||||
Every float op loads the operand from the fp regfile into FAC1 and stores
|
||||
the result back. Add an FAC1 cache analogous to the A cache: track "FAC1
|
||||
currently holds fp register frX" and skip the redundant MOVFM.
|
||||
|
||||
DEC/DECM for floats is especially wasteful: it does MOVFM + pushFAC1 +
|
||||
MOVFM(1.0) + popFAC + FSUBT (because FSUB computes memory - FAC1). Since FADD
|
||||
computes FAC1 + memory, DEC can simply FADD the constant -1.0:
|
||||
```
|
||||
jsr floats.MOVFM
|
||||
lda #<-1.0 const
|
||||
ldy #>-1.0 const
|
||||
jsr floats.FADD
|
||||
jsr floats.MOVMF
|
||||
```
|
||||
dropping the push/pop and the FSUBT, and removing the register-clobbering
|
||||
stack round trip.
|
||||
|
||||
9. Inline asmsub handling [BUG]
|
||||
---------------------------------
|
||||
|
||||
The header of AsmGen.kt documents that `inline asmsub` bodies are emitted as
|
||||
regular subroutines, which is wrong: inline asmsubs must be inserted directly
|
||||
at the call site (no jsr, no .proc/.pend, no rts). Currently they are emitted
|
||||
as standalone routines lacking an rts, so calling them returns garbage.
|
||||
The IR does not preserve the `inline` flag, so either add an INLINE attribute
|
||||
to the ASMSUB IR instruction, or ensure the optimizer inlines them before IR
|
||||
generation.
|
||||
|
||||
10. STOREZX LONG zeroing incomplete [BUG]
|
||||
-------------------------------------------
|
||||
|
||||
`zeroMemoryIndexed` (InstrLoadStore.kt) handles BYTE and WORD, but the LONG
|
||||
case emits only the low byte with the comment "STOREZX LONG not fully
|
||||
implemented". Zeroing a long array element via STOREZX therefore leaves the
|
||||
upper 3 bytes untouched. The WORD pattern (reload X with the high byte and
|
||||
`sta base+1,x`) extends naturally to LONG by repeating for each byte.
|
||||
|
||||
11. JMP (abs) page-wrap on plain 6502 [BUG]
|
||||
---------------------------------------------
|
||||
|
||||
JUMPI and CALLI emit `jmp (p8_regfile+N)`. On the original 6502, `JMP (abs)`
|
||||
reads the high byte from the same page (the address wraps at $xxFF); 65C02
|
||||
fixed this. If a dispatch/indirect target stored in a register slot has a
|
||||
low byte of $FF, c64/pet32/c128 programs will jump to a wrong address.
|
||||
Mitigations: guarantee the regfile never holds a vector with low byte $FF
|
||||
(not under the compiler's control in general), or emit the classic
|
||||
workaround (push low, load high, push high, rts) on non-65C02 targets.
|
||||
|
||||
12. Unroll remaining multi-byte loops for speed
|
||||
-------------------------------------------------
|
||||
|
||||
LONG AND/OR/XOR use a `ldy #3` byte loop; LONG add/sub are unrolled. The
|
||||
loop is smaller, the unroll is faster. Pick one policy (unroll everything
|
||||
for speed, loop everything for size) rather than the current mix, or gate on
|
||||
an optimization-level switch. Also `copyRegister` for LONG could be unrolled.
|
||||
|
||||
Future register allocation
|
||||
----------------------------
|
||||
|
||||
The real fix for all of this is a proper register allocator that assigns
|
||||
frequently-used virtual registers to physical resources: A, X, Y, the
|
||||
s0-s5 calling-convention slots, and a set of ZP scratch words. The IR
|
||||
RegisterPacker (register-packing.md, graph-coloring packer, currently
|
||||
disabled) reduces the *number* of virtual registers and could be combined
|
||||
with ZP slot assignment so that the remaining hot registers fit in ZP. The
|
||||
A/X/Y cache in item 1 is a cheap stepping stone toward that.
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,936 @@
|
||||
/*
|
||||
* Bitwise and logical IR instruction translations for the new6502gen code generator.
|
||||
*
|
||||
* Handles: AND, OR, XOR (register/imm/memory), INV (bitwise not),
|
||||
* shifts (LSL, LSR, ASR) by constant or variable count,
|
||||
* rotates (ROL, ROR, ROXL, ROXR) and bit manipulation (BITTST, BITSET, BITCLR, BITTOG).
|
||||
*
|
||||
* Constant-count shifts are unrolled (for loop in the generator).
|
||||
* Variable-count shifts use a loop with X as the counter.
|
||||
* Arithmetic shifts use CMP #128 + ROR to preserve the sign bit.
|
||||
*
|
||||
* Note: ROL/ROR without carry prefix (clc) are rotate-through-carry,
|
||||
* while ROXL/ROXR explicitly use carry for multi-byte rotates.
|
||||
*/
|
||||
|
||||
package prog8.codegen.new6502
|
||||
|
||||
import prog8.intermediate.*
|
||||
|
||||
internal fun AsmGen.translateBitwise(insn: IRInstruction) {
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
|
||||
when (insn.opcode) {
|
||||
Opcode.ANDR -> {
|
||||
val dst = insn.requireIntDest().intNumber
|
||||
val src = insn.requireIntSourceA().intNumber
|
||||
andRegisters(dst, src, type)
|
||||
}
|
||||
Opcode.AND -> {
|
||||
val dst = insn.requireIntDest().intNumber
|
||||
val value = insn.requireImmediateInt()
|
||||
andImmediate(dst, value, type)
|
||||
}
|
||||
Opcode.ANDM -> {
|
||||
val src = insn.requireIntSourceA().intNumber
|
||||
val sourceAddress = resolveAddress(insn.requireMemory())
|
||||
andMemory(src, sourceAddress, type)
|
||||
}
|
||||
|
||||
Opcode.ORR -> {
|
||||
val dst = insn.requireIntDest().intNumber
|
||||
val src = insn.requireIntSourceA().intNumber
|
||||
orRegisters(dst, src, type)
|
||||
}
|
||||
Opcode.OR -> {
|
||||
val dst = insn.requireIntDest().intNumber
|
||||
val value = insn.requireImmediateInt()
|
||||
orImmediate(dst, value, type)
|
||||
}
|
||||
Opcode.ORM -> {
|
||||
val src = insn.requireIntSourceA().intNumber
|
||||
val sourceAddress = resolveAddress(insn.requireMemory())
|
||||
orMemory(src, sourceAddress, type)
|
||||
}
|
||||
|
||||
Opcode.XORR -> {
|
||||
val dst = insn.requireIntDest().intNumber
|
||||
val src = insn.requireIntSourceA().intNumber
|
||||
xorRegisters(dst, src, type)
|
||||
}
|
||||
Opcode.XOR -> {
|
||||
val dst = insn.requireIntDest().intNumber
|
||||
val value = insn.requireImmediateInt()
|
||||
xorImmediate(dst, value, type)
|
||||
}
|
||||
Opcode.XORM -> {
|
||||
val src = insn.requireIntSourceA().intNumber
|
||||
val sourceAddress = resolveAddress(insn.requireMemory())
|
||||
xorMemory(src, sourceAddress, type)
|
||||
}
|
||||
|
||||
Opcode.INV -> invertRegister(insn.requireIntDest().intNumber, type)
|
||||
Opcode.INVM -> invertMemory(resolveAddress(insn.requireMemory()), type)
|
||||
|
||||
// Shift by register count (ASRN/LSRN/LSLN: srcA = shift count register)
|
||||
Opcode.ASRN -> {
|
||||
val dst = insn.requireIntDest().intNumber
|
||||
val countReg = insn.requireIntSourceA().intNumber
|
||||
arithmeticShiftRightVar(dst, countReg, type)
|
||||
}
|
||||
Opcode.LSRN -> {
|
||||
val dst = insn.requireIntDest().intNumber
|
||||
val countReg = insn.requireIntSourceA().intNumber
|
||||
logicalShiftRightVar(dst, countReg, type)
|
||||
}
|
||||
Opcode.LSLN -> {
|
||||
val dst = insn.requireIntDest().intNumber
|
||||
val countReg = insn.requireIntSourceA().intNumber
|
||||
logicalShiftLeftVar(dst, countReg, type)
|
||||
}
|
||||
|
||||
// Shift memory by register count (ASRNM/LSRNM/LSLNM: srcA = count reg, memory = target)
|
||||
Opcode.ASRNM -> shiftMemoryVar(resolveAddress(insn.requireMemory()), insn.requireIntSourceA().intNumber, type, isArithmetic = true)
|
||||
Opcode.LSRNM -> shiftMemoryVar(resolveAddress(insn.requireMemory()), insn.requireIntSourceA().intNumber, type, isArithmetic = false)
|
||||
Opcode.LSLNM -> shiftMemoryLeftVar(resolveAddress(insn.requireMemory()), insn.requireIntSourceA().intNumber, type)
|
||||
|
||||
// Shift by 1 (ASR/LSR/LSL: single-bit shift, no count operand)
|
||||
Opcode.ASR -> arithmeticShiftRight(insn.requireIntDest().intNumber, 1, type)
|
||||
Opcode.ASRM -> arithmeticShiftRightMemory(resolveAddress(insn.requireMemory()), 1, type)
|
||||
Opcode.LSR -> logicalShiftRight(insn.requireIntDest().intNumber, 1, type)
|
||||
Opcode.LSRM -> logicalShiftRightMemory(resolveAddress(insn.requireMemory()), 1, type)
|
||||
Opcode.LSL -> logicalShiftLeft(insn.requireIntDest().intNumber, 1, type)
|
||||
Opcode.LSLM -> logicalShiftLeftMemory(resolveAddress(insn.requireMemory()), 1, type)
|
||||
|
||||
// Immediate-count shifts (LSLI/LSRI/ASRI): unroll the count into N 1-bit shifts
|
||||
Opcode.ASRI -> arithmeticShiftRight(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type)
|
||||
Opcode.LSRI -> logicalShiftRight(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type)
|
||||
Opcode.LSLI -> logicalShiftLeft(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type)
|
||||
|
||||
Opcode.ROR -> rotateRight(insn.requireIntDest().intNumber, type)
|
||||
Opcode.RORM -> rotateRightMemory(resolveAddress(insn.requireMemory()), type)
|
||||
Opcode.ROL -> rotateLeft(insn.requireIntDest().intNumber, type)
|
||||
Opcode.ROLM -> rotateLeftMemory(resolveAddress(insn.requireMemory()), type)
|
||||
Opcode.ROXR -> rotateRightThroughCarry(insn.requireIntDest().intNumber, type)
|
||||
Opcode.ROXRM -> rotateRightThroughCarryMemory(resolveAddress(insn.requireMemory()), type)
|
||||
Opcode.ROXL -> rotateLeftThroughCarry(insn.requireIntDest().intNumber, type)
|
||||
Opcode.ROXLM -> rotateLeftThroughCarryMemory(resolveAddress(insn.requireMemory()), type)
|
||||
|
||||
Opcode.BITTST -> bitTest(insn.requireIntSourceA().intNumber, insn.requireImmediateInt())
|
||||
Opcode.BITSET -> bitSet(insn.requireIntDest().intNumber, insn.requireImmediateInt())
|
||||
Opcode.BITCLR -> bitClear(insn.requireIntDest().intNumber, insn.requireImmediateInt())
|
||||
Opcode.BITTOG -> bitToggle(insn.requireIntDest().intNumber, insn.requireImmediateInt())
|
||||
|
||||
else -> error("Unknown bitwise opcode: ${insn.opcode}")
|
||||
}
|
||||
}
|
||||
|
||||
// === AND ===
|
||||
|
||||
private fun AsmGen.andRegisters(dstReg: Int, srcReg: Int, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("and ${regAddrLo(srcReg)}")
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
}
|
||||
IRDataType.WORD, IRDataType.POINTER -> {
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("and ${regAddrLo(srcReg)}")
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
emitLine("lda ${regAddrHi(dstReg)}")
|
||||
emitLine("and ${regAddrHi(srcReg)}")
|
||||
emitLine("sta ${regAddrHi(dstReg)}")
|
||||
}
|
||||
IRDataType.LONG -> byteLoop4(regAddrByte(dstReg, 0), regAddrByte(srcReg, 0), "and")
|
||||
IRDataType.FLOAT -> error("bitwise operations are not supported on floats")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.andImmediate(dstReg: Int, value: Int, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("and #${value and 0xff}")
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
}
|
||||
IRDataType.WORD, IRDataType.POINTER -> {
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("and #<${value and 0xffff}")
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
emitLine("lda ${regAddrHi(dstReg)}")
|
||||
emitLine("and #>${value and 0xffff}")
|
||||
emitLine("sta ${regAddrHi(dstReg)}")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("lda ${regAddrByte(dstReg, 0)}")
|
||||
emitLine("and #${value and 0xff}")
|
||||
emitLine("sta ${regAddrByte(dstReg, 0)}")
|
||||
emitLine("lda ${regAddrByte(dstReg, 1)}")
|
||||
emitLine("and #${(value shr 8) and 0xff}")
|
||||
emitLine("sta ${regAddrByte(dstReg, 1)}")
|
||||
emitLine("lda ${regAddrByte(dstReg, 2)}")
|
||||
emitLine("and #${(value shr 16) and 0xff}")
|
||||
emitLine("sta ${regAddrByte(dstReg, 2)}")
|
||||
emitLine("lda ${regAddrByte(dstReg, 3)}")
|
||||
emitLine("and #${(value shr 24) and 0xff}")
|
||||
emitLine("sta ${regAddrByte(dstReg, 3)}")
|
||||
}
|
||||
IRDataType.FLOAT -> error("bitwise operations are not supported on floats")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.andMemory(dstReg: Int, sourceAddress: String, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda $sourceAddress")
|
||||
emitLine("and ${regAddrLo(dstReg)}")
|
||||
emitLine("sta $sourceAddress")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("lda $sourceAddress")
|
||||
emitLine("and ${regAddrLo(dstReg)}")
|
||||
emitLine("sta $sourceAddress")
|
||||
emitLine("lda $sourceAddress+1")
|
||||
emitLine("and ${regAddrHi(dstReg)}")
|
||||
emitLine("sta $sourceAddress+1")
|
||||
}
|
||||
IRDataType.LONG -> byteLoop4(sourceAddress, regAddrByte(dstReg, 0), "and")
|
||||
else -> error("ANDM not supported on ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
// === OR ===
|
||||
|
||||
private fun AsmGen.orRegisters(dstReg: Int, srcReg: Int, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("ora ${regAddrLo(srcReg)}")
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("ora ${regAddrLo(srcReg)}")
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
emitLine("lda ${regAddrHi(dstReg)}")
|
||||
emitLine("ora ${regAddrHi(srcReg)}")
|
||||
emitLine("sta ${regAddrHi(dstReg)}")
|
||||
}
|
||||
IRDataType.LONG -> byteLoop4(regAddrByte(dstReg, 0), regAddrByte(srcReg, 0), "ora")
|
||||
else -> error("ORR not supported on ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.orImmediate(dstReg: Int, value: Int, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("ora #${value and 0xff}")
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("ora #<${value and 0xffff}")
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
emitLine("lda ${regAddrHi(dstReg)}")
|
||||
emitLine("ora #>${value and 0xffff}")
|
||||
emitLine("sta ${regAddrHi(dstReg)}")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("lda ${regAddrByte(dstReg, 0)}")
|
||||
emitLine("ora #${value and 0xff}")
|
||||
emitLine("sta ${regAddrByte(dstReg, 0)}")
|
||||
emitLine("lda ${regAddrByte(dstReg, 1)}")
|
||||
emitLine("ora #${(value shr 8) and 0xff}")
|
||||
emitLine("sta ${regAddrByte(dstReg, 1)}")
|
||||
emitLine("lda ${regAddrByte(dstReg, 2)}")
|
||||
emitLine("ora #${(value shr 16) and 0xff}")
|
||||
emitLine("sta ${regAddrByte(dstReg, 2)}")
|
||||
emitLine("lda ${regAddrByte(dstReg, 3)}")
|
||||
emitLine("ora #${(value shr 24) and 0xff}")
|
||||
emitLine("sta ${regAddrByte(dstReg, 3)}")
|
||||
}
|
||||
else -> error("OR not supported on ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.orMemory(dstReg: Int, sourceAddress: String, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda $sourceAddress")
|
||||
emitLine("ora ${regAddrLo(dstReg)}")
|
||||
emitLine("sta $sourceAddress")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("lda $sourceAddress")
|
||||
emitLine("ora ${regAddrLo(dstReg)}")
|
||||
emitLine("sta $sourceAddress")
|
||||
emitLine("lda $sourceAddress+1")
|
||||
emitLine("ora ${regAddrHi(dstReg)}")
|
||||
emitLine("sta $sourceAddress+1")
|
||||
}
|
||||
IRDataType.LONG -> byteLoop4(sourceAddress, regAddrByte(dstReg, 0), "ora")
|
||||
else -> error("ORM not supported on ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
// === XOR ===
|
||||
|
||||
private fun AsmGen.xorRegisters(dstReg: Int, srcReg: Int, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("eor ${regAddrLo(srcReg)}")
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("eor ${regAddrLo(srcReg)}")
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
emitLine("lda ${regAddrHi(dstReg)}")
|
||||
emitLine("eor ${regAddrHi(srcReg)}")
|
||||
emitLine("sta ${regAddrHi(dstReg)}")
|
||||
}
|
||||
IRDataType.LONG -> byteLoop4(regAddrByte(dstReg, 0), regAddrByte(srcReg, 0), "eor")
|
||||
else -> error("XORR not supported on ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.xorImmediate(dstReg: Int, value: Int, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("eor #${value and 0xff}")
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("eor #<${value and 0xffff}")
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
emitLine("lda ${regAddrHi(dstReg)}")
|
||||
emitLine("eor #>${value and 0xffff}")
|
||||
emitLine("sta ${regAddrHi(dstReg)}")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("lda ${regAddrByte(dstReg, 0)}")
|
||||
emitLine("eor #${value and 0xff}")
|
||||
emitLine("sta ${regAddrByte(dstReg, 0)}")
|
||||
emitLine("lda ${regAddrByte(dstReg, 1)}")
|
||||
emitLine("eor #${(value shr 8) and 0xff}")
|
||||
emitLine("sta ${regAddrByte(dstReg, 1)}")
|
||||
emitLine("lda ${regAddrByte(dstReg, 2)}")
|
||||
emitLine("eor #${(value shr 16) and 0xff}")
|
||||
emitLine("sta ${regAddrByte(dstReg, 2)}")
|
||||
emitLine("lda ${regAddrByte(dstReg, 3)}")
|
||||
emitLine("eor #${(value shr 24) and 0xff}")
|
||||
emitLine("sta ${regAddrByte(dstReg, 3)}")
|
||||
}
|
||||
else -> error("XOR not supported on ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.xorMemory(dstReg: Int, sourceAddress: String, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda $sourceAddress")
|
||||
emitLine("eor ${regAddrLo(dstReg)}")
|
||||
emitLine("sta $sourceAddress")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("lda $sourceAddress")
|
||||
emitLine("eor ${regAddrLo(dstReg)}")
|
||||
emitLine("sta $sourceAddress")
|
||||
emitLine("lda $sourceAddress+1")
|
||||
emitLine("eor ${regAddrHi(dstReg)}")
|
||||
emitLine("sta $sourceAddress+1")
|
||||
}
|
||||
IRDataType.LONG -> byteLoop4(sourceAddress, regAddrByte(dstReg, 0), "eor")
|
||||
else -> error("XORM not supported on ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
// === Invert ===
|
||||
|
||||
private fun AsmGen.invertRegister(reg: Int, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda ${regAddrLo(reg)}")
|
||||
emitLine("eor #255")
|
||||
emitLine("sta ${regAddrLo(reg)}")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("lda ${regAddrLo(reg)}")
|
||||
emitLine("eor #255")
|
||||
emitLine("sta ${regAddrLo(reg)}")
|
||||
emitLine("lda ${regAddrHi(reg)}")
|
||||
emitLine("eor #255")
|
||||
emitLine("sta ${regAddrHi(reg)}")
|
||||
}
|
||||
IRDataType.LONG -> byteLoop4Unary(regAddrByte(reg, 0), "eor #255")
|
||||
else -> error("INV not supported on ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.invertMemory(target: String, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda $target")
|
||||
emitLine("eor #255")
|
||||
emitLine("sta $target")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("lda $target")
|
||||
emitLine("eor #255")
|
||||
emitLine("sta $target")
|
||||
emitLine("lda $target+1")
|
||||
emitLine("eor #255")
|
||||
emitLine("sta $target+1")
|
||||
}
|
||||
IRDataType.LONG -> byteLoop4Unary(target, "eor #255")
|
||||
else -> error("INVM not supported on ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
// === Shifts ===
|
||||
|
||||
private fun AsmGen.logicalShiftLeft(reg: Int, count: Int, type: IRDataType) {
|
||||
repeat(count) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("asl ${regAddrLo(reg)}")
|
||||
}
|
||||
IRDataType.WORD, IRDataType.POINTER -> {
|
||||
emitLine("asl ${regAddrLo(reg)}")
|
||||
emitLine("rol ${regAddrHi(reg)}")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("asl ${regAddrLo(reg)}")
|
||||
emitLine("rol ${regAddrHi(reg)}")
|
||||
emitLine("rol ${regAddrByte(reg, 2)}")
|
||||
emitLine("rol ${regAddrByte(reg, 3)}")
|
||||
}
|
||||
IRDataType.FLOAT -> error("bitwise operations are not supported on floats")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.logicalShiftLeftMemory(target: String, count: Int, type: IRDataType) {
|
||||
repeat(count) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> emitLine("asl $target")
|
||||
IRDataType.WORD -> {
|
||||
emitLine("asl $target")
|
||||
emitLine("rol $target+1")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("asl $target")
|
||||
emitLine("rol $target+1")
|
||||
emitLine("rol $target+2")
|
||||
emitLine("rol $target+3")
|
||||
}
|
||||
else -> error("LSLM not supported on ${type.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.logicalShiftRight(reg: Int, count: Int, type: IRDataType) {
|
||||
repeat(count) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lsr ${regAddrLo(reg)}")
|
||||
}
|
||||
IRDataType.WORD, IRDataType.POINTER -> {
|
||||
emitLine("lsr ${regAddrHi(reg)}")
|
||||
emitLine("ror ${regAddrLo(reg)}")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("lsr ${regAddrByte(reg, 3)}")
|
||||
emitLine("ror ${regAddrByte(reg, 2)}")
|
||||
emitLine("ror ${regAddrHi(reg)}")
|
||||
emitLine("ror ${regAddrLo(reg)}")
|
||||
}
|
||||
IRDataType.FLOAT -> error("bitwise operations are not supported on floats")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.logicalShiftRightMemory(target: String, count: Int, type: IRDataType) {
|
||||
repeat(count) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> emitLine("lsr $target")
|
||||
IRDataType.WORD -> {
|
||||
emitLine("lsr $target+1")
|
||||
emitLine("ror $target")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("lsr $target+3")
|
||||
emitLine("ror $target+2")
|
||||
emitLine("ror $target+1")
|
||||
emitLine("ror $target")
|
||||
}
|
||||
else -> error("LSRM not supported on ${type.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.arithmeticShiftRight(reg: Int, count: Int, type: IRDataType) {
|
||||
repeat(count) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda ${regAddrLo(reg)}")
|
||||
emitLine("cmp #128")
|
||||
emitLine("ror ${regAddrLo(reg)}")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("lda ${regAddrHi(reg)}")
|
||||
emitLine("cmp #128")
|
||||
emitLine("ror ${regAddrHi(reg)}")
|
||||
emitLine("ror ${regAddrLo(reg)}")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("lda ${regAddrByte(reg, 3)}")
|
||||
emitLine("cmp #128")
|
||||
emitLine("ror ${regAddrByte(reg, 3)}")
|
||||
emitLine("ror ${regAddrByte(reg, 2)}")
|
||||
emitLine("ror ${regAddrHi(reg)}")
|
||||
emitLine("ror ${regAddrLo(reg)}")
|
||||
}
|
||||
else -> TODO("ASRN r$reg, $count ${type.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.arithmeticShiftRightMemory(target: String, count: Int, type: IRDataType) {
|
||||
repeat(count) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda $target")
|
||||
emitLine("cmp #128")
|
||||
emitLine("ror $target")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("lda $target+1")
|
||||
emitLine("cmp #128")
|
||||
emitLine("ror $target+1")
|
||||
emitLine("ror $target")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("lda $target+3")
|
||||
emitLine("cmp #128")
|
||||
emitLine("ror $target+3")
|
||||
emitLine("ror $target+2")
|
||||
emitLine("ror $target+1")
|
||||
emitLine("ror $target")
|
||||
}
|
||||
else -> TODO("ASRNM $target, $count ${type.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.logicalShiftLeftVar(reg: Int, countReg: Int, type: IRDataType) {
|
||||
val loopLabel = makeLabel("lsl_var_loop")
|
||||
emitLine("ldx ${regAddrLo(countReg)}")
|
||||
emitLine("beq +")
|
||||
emitLabel(loopLabel)
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("asl ${regAddrLo(reg)}")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("asl ${regAddrLo(reg)}")
|
||||
emitLine("rol ${regAddrHi(reg)}")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("asl ${regAddrLo(reg)}")
|
||||
emitLine("rol ${regAddrByte(reg, 1)}")
|
||||
emitLine("rol ${regAddrByte(reg, 2)}")
|
||||
emitLine("rol ${regAddrByte(reg, 3)}")
|
||||
}
|
||||
else -> TODO("LSL r$reg, r$countReg ${type.name}")
|
||||
}
|
||||
emitLine("dex")
|
||||
emitLine("bne $loopLabel")
|
||||
emitLabel("+")
|
||||
}
|
||||
|
||||
private fun AsmGen.logicalShiftRightVar(reg: Int, countReg: Int, type: IRDataType) {
|
||||
val loopLabel = makeLabel("lsr_var_loop")
|
||||
emitLine("ldx ${regAddrLo(countReg)}")
|
||||
emitLine("beq +")
|
||||
emitLabel(loopLabel)
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lsr ${regAddrLo(reg)}")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("lsr ${regAddrHi(reg)}")
|
||||
emitLine("ror ${regAddrLo(reg)}")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("lsr ${regAddrByte(reg, 3)}")
|
||||
emitLine("ror ${regAddrByte(reg, 2)}")
|
||||
emitLine("ror ${regAddrByte(reg, 1)}")
|
||||
emitLine("ror ${regAddrLo(reg)}")
|
||||
}
|
||||
else -> TODO("LSR r$reg, r$countReg ${type.name}")
|
||||
}
|
||||
emitLine("dex")
|
||||
emitLine("bne $loopLabel")
|
||||
emitLabel("+")
|
||||
}
|
||||
|
||||
private fun AsmGen.arithmeticShiftRightVar(reg: Int, countReg: Int, type: IRDataType) {
|
||||
val loopLabel = makeLabel("asr_var_loop") // TODO can be anonymous label ? (also at other places?)
|
||||
emitLine("ldx ${regAddrLo(countReg)}")
|
||||
emitLine("beq +")
|
||||
emitLabel(loopLabel)
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda ${regAddrLo(reg)}")
|
||||
emitLine("cmp #128")
|
||||
emitLine("ror ${regAddrLo(reg)}")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("lda ${regAddrHi(reg)}")
|
||||
emitLine("cmp #128")
|
||||
emitLine("ror ${regAddrHi(reg)}")
|
||||
emitLine("ror ${regAddrLo(reg)}")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("lda ${regAddrByte(reg, 3)}")
|
||||
emitLine("cmp #128")
|
||||
emitLine("ror ${regAddrByte(reg, 3)}")
|
||||
emitLine("ror ${regAddrByte(reg, 2)}")
|
||||
emitLine("ror ${regAddrByte(reg, 1)}")
|
||||
emitLine("ror ${regAddrLo(reg)}")
|
||||
}
|
||||
else -> TODO("ASR r$reg, r$countReg ${type.name}")
|
||||
}
|
||||
emitLine("dex")
|
||||
emitLine("bne $loopLabel")
|
||||
emitLabel("+")
|
||||
}
|
||||
|
||||
// === Memory variable-count shifts ===
|
||||
|
||||
private fun AsmGen.shiftMemoryVar(target: String, countReg: Int, type: IRDataType, isArithmetic: Boolean) {
|
||||
val loopLabel = makeLabel("shiftmem_loop")
|
||||
emitLine("ldx ${regAddrLo(countReg)}")
|
||||
emitLine("beq +")
|
||||
emitLabel(loopLabel)
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
if (isArithmetic) {
|
||||
emitLine("lda $target")
|
||||
emitLine("cmp #128")
|
||||
emitLine("ror $target")
|
||||
} else {
|
||||
emitLine("lsr $target")
|
||||
}
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
if (isArithmetic) {
|
||||
emitLine("lda $target+1")
|
||||
emitLine("cmp #128")
|
||||
emitLine("ror $target+1")
|
||||
emitLine("ror $target")
|
||||
} else {
|
||||
emitLine("lsr $target+1")
|
||||
emitLine("ror $target")
|
||||
}
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
if (isArithmetic) {
|
||||
emitLine("lda $target+3")
|
||||
emitLine("cmp #128")
|
||||
emitLine("ror $target+3")
|
||||
emitLine("ror $target+2")
|
||||
emitLine("ror $target+1")
|
||||
emitLine("ror $target")
|
||||
} else {
|
||||
emitLine("lsr $target+3")
|
||||
emitLine("ror $target+2")
|
||||
emitLine("ror $target+1")
|
||||
emitLine("ror $target")
|
||||
}
|
||||
}
|
||||
else -> TODO("shiftmem ${if(isArithmetic) "ASR" else "LSR"} $target ${type.name}")
|
||||
}
|
||||
emitLine("dex")
|
||||
emitLine("bne $loopLabel")
|
||||
emitLabel("+")
|
||||
}
|
||||
|
||||
private fun AsmGen.shiftMemoryLeftVar(target: String, countReg: Int, type: IRDataType) {
|
||||
val loopLabel = makeLabel("shiftmeml_loop")
|
||||
emitLine("ldx ${regAddrLo(countReg)}")
|
||||
emitLine("beq +")
|
||||
emitLabel(loopLabel)
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("asl $target")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("asl $target")
|
||||
emitLine("rol $target+1")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("asl $target")
|
||||
emitLine("rol $target+1")
|
||||
emitLine("rol $target+2")
|
||||
emitLine("rol $target+3")
|
||||
}
|
||||
else -> TODO("LSLNM $target ${type.name}")
|
||||
}
|
||||
emitLine("dex")
|
||||
emitLine("bne $loopLabel")
|
||||
emitLabel("+")
|
||||
}
|
||||
|
||||
// === Rotates ===
|
||||
|
||||
private fun AsmGen.rotateLeft(reg: Int, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("clc")
|
||||
emitLine("rol ${regAddrLo(reg)}")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("clc")
|
||||
emitLine("rol ${regAddrLo(reg)}")
|
||||
emitLine("rol ${regAddrHi(reg)}")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("clc")
|
||||
emitLine("rol ${regAddrLo(reg)}")
|
||||
emitLine("rol ${regAddrHi(reg)}")
|
||||
emitLine("rol ${regAddrByte(reg, 2)}")
|
||||
emitLine("rol ${regAddrByte(reg, 3)}")
|
||||
}
|
||||
else -> error("ROL not supported on ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.rotateLeftMemory(target: String, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("clc")
|
||||
emitLine("rol $target")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("clc")
|
||||
emitLine("rol $target")
|
||||
emitLine("rol $target+1")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("clc")
|
||||
emitLine("rol $target")
|
||||
emitLine("rol $target+1")
|
||||
emitLine("rol $target+2")
|
||||
emitLine("rol $target+3")
|
||||
}
|
||||
else -> error("ROLM not supported on ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.rotateRight(reg: Int, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("clc")
|
||||
emitLine("ror ${regAddrLo(reg)}")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("clc")
|
||||
emitLine("ror ${regAddrHi(reg)}")
|
||||
emitLine("ror ${regAddrLo(reg)}")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("clc")
|
||||
emitLine("ror ${regAddrByte(reg, 3)}")
|
||||
emitLine("ror ${regAddrByte(reg, 2)}")
|
||||
emitLine("ror ${regAddrHi(reg)}")
|
||||
emitLine("ror ${regAddrLo(reg)}")
|
||||
}
|
||||
else -> error("ROR not supported on ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.rotateRightMemory(target: String, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("clc")
|
||||
emitLine("ror $target")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("clc")
|
||||
emitLine("ror $target+1")
|
||||
emitLine("ror $target")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("clc")
|
||||
emitLine("ror $target+3")
|
||||
emitLine("ror $target+2")
|
||||
emitLine("ror $target+1")
|
||||
emitLine("ror $target")
|
||||
}
|
||||
else -> error("RORM not supported on ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.rotateLeftThroughCarry(reg: Int, type: IRDataType) {
|
||||
// rotate through carry (like the 6502 ROL instruction does)
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("rol ${regAddrLo(reg)}")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("rol ${regAddrLo(reg)}")
|
||||
emitLine("rol ${regAddrHi(reg)}")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("rol ${regAddrLo(reg)}")
|
||||
emitLine("rol ${regAddrHi(reg)}")
|
||||
emitLine("rol ${regAddrByte(reg, 2)}")
|
||||
emitLine("rol ${regAddrByte(reg, 3)}")
|
||||
}
|
||||
else -> error("ROXL not supported on ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.rotateRightThroughCarry(reg: Int, type: IRDataType) {
|
||||
// rotate through carry (like the 6502 ROR instruction does)
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("ror ${regAddrLo(reg)}")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("ror ${regAddrHi(reg)}")
|
||||
emitLine("ror ${regAddrLo(reg)}")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("ror ${regAddrByte(reg, 3)}")
|
||||
emitLine("ror ${regAddrByte(reg, 2)}")
|
||||
emitLine("ror ${regAddrHi(reg)}")
|
||||
emitLine("ror ${regAddrLo(reg)}")
|
||||
}
|
||||
else -> error("ROXR not supported on ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.rotateLeftThroughCarryMemory(target: String, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("rol $target")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("rol $target")
|
||||
emitLine("rol $target+1")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("rol $target")
|
||||
emitLine("rol $target+1")
|
||||
emitLine("rol $target+2")
|
||||
emitLine("rol $target+3")
|
||||
}
|
||||
else -> TODO("ROXLM $target ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.rotateRightThroughCarryMemory(target: String, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("ror $target")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("ror $target+1")
|
||||
emitLine("ror $target")
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
emitLine("ror $target+3")
|
||||
emitLine("ror $target+2")
|
||||
emitLine("ror $target+1")
|
||||
emitLine("ror $target")
|
||||
}
|
||||
else -> TODO("ROXRM $target ${type.name}")
|
||||
}
|
||||
}
|
||||
|
||||
// === Bit manipulation ===
|
||||
|
||||
private fun AsmGen.bitTest(reg: Int, bit: Int) {
|
||||
val mask = 1 shl bit
|
||||
emitLine("lda ${regAddrLo(reg)}")
|
||||
if (is65C02()) {
|
||||
emitLine("bit #$mask")
|
||||
} else {
|
||||
// 6502: bit only works with memory; use and to test
|
||||
emitLine("and #$mask")
|
||||
// Z flag set if result is zero (bit not set)
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.bitSet(reg: Int, bit: Int) {
|
||||
val mask = 1 shl bit
|
||||
if (mask <= 0xff) {
|
||||
emitLine("lda ${regAddrLo(reg)}")
|
||||
emitLine("ora #$mask")
|
||||
emitLine("sta ${regAddrLo(reg)}")
|
||||
} else {
|
||||
// high byte bit
|
||||
val hbit = bit - 8
|
||||
emitLine("lda ${regAddrHi(reg)}")
|
||||
emitLine("ora #${1 shl hbit}")
|
||||
emitLine("sta ${regAddrHi(reg)}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.bitClear(reg: Int, bit: Int) {
|
||||
val mask = 1 shl bit
|
||||
if (mask <= 0xff) {
|
||||
emitLine("lda ${regAddrLo(reg)}")
|
||||
emitLine("and #${0xff xor mask}")
|
||||
emitLine("sta ${regAddrLo(reg)}")
|
||||
} else {
|
||||
val hbit = bit - 8
|
||||
emitLine("lda ${regAddrHi(reg)}")
|
||||
emitLine("and #${0xff xor (1 shl hbit)}")
|
||||
emitLine("sta ${regAddrHi(reg)}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.bitToggle(reg: Int, bit: Int) {
|
||||
val mask = 1 shl bit
|
||||
if (mask <= 0xff) {
|
||||
emitLine("lda ${regAddrLo(reg)}")
|
||||
emitLine("eor #$mask")
|
||||
emitLine("sta ${regAddrLo(reg)}")
|
||||
} else {
|
||||
val hbit = bit - 8
|
||||
emitLine("lda ${regAddrHi(reg)}")
|
||||
emitLine("eor #${1 shl hbit}")
|
||||
emitLine("sta ${regAddrHi(reg)}")
|
||||
}
|
||||
}
|
||||
|
||||
// === LONG byte loop helpers ===
|
||||
// These operations have no carry dependency, so a simple Y-loop works.
|
||||
// Keep carry-dependent ops (shifts, rotates) unrolled.
|
||||
|
||||
private fun AsmGen.byteLoop4(targetBase: String, srcBase: String, op: String) {
|
||||
emitLine("ldy #3")
|
||||
emitLine("- lda $targetBase,y")
|
||||
emitLine("$op $srcBase,y")
|
||||
emitLine("sta $targetBase,y")
|
||||
emitLine("dey")
|
||||
emitLine("bpl -")
|
||||
}
|
||||
|
||||
private fun AsmGen.byteLoop4Unary(targetBase: String, op: String) {
|
||||
emitLine("ldy #3")
|
||||
emitLine("- lda $targetBase,y")
|
||||
emitLine(op)
|
||||
emitLine("sta $targetBase,y")
|
||||
emitLine("dey")
|
||||
emitLine("bpl -")
|
||||
}
|
||||
@@ -0,0 +1,633 @@
|
||||
/*
|
||||
* Branch/compare IR instruction translations for the new6502gen code generator.
|
||||
*
|
||||
* The comparison branch instructions in the IR (BGTR, BGER, BGE, BLT,
|
||||
* and their signed variants) are self-contained - they carry the operands to compare
|
||||
* (either two registers, or a register and an immediate). The codegen must emit a CMP
|
||||
* between these operands before the flag-checking branch pattern.
|
||||
*
|
||||
* Two categories of branches:
|
||||
*
|
||||
* 1. Simple status flag branches (BSTCC/BSTCS/BSTEQ/BSTNE/BSTNEG/BSTPOS/BSTVC/BSTVS):
|
||||
* These map directly to 6502 branch instructions (bcc/bcs/beq/bne/bmi/bpl/bvc/bvs)
|
||||
* and branch based on flags set by the most recent comparison or arithmetic operation.
|
||||
*
|
||||
* 2. Comparison branches (BGTR/BGER/BGE/BLT, BGTSR/BGTS/BGESR/BGES/BLTS/BLES):
|
||||
* These carry their own operands. The codegen emits a CMP between the operands first,
|
||||
* then checks the resulting flags.
|
||||
* Unsigned variants use simple bcc/bcs patterns.
|
||||
* Signed variants need to check both N and V flags.
|
||||
* Note: BGT and BLE (unsigned immediate) are unused - the AST normalizes
|
||||
* <= to < (value+1) and > to >= (value+1) before IR codegen.
|
||||
*
|
||||
* BGT, BLE need two branches (e.g. bne skip + bcs target) because the 6502
|
||||
* doesn't have a single "branch if greater than" instruction.
|
||||
*
|
||||
* Signed branch logic (N flag vs V flag):
|
||||
* - gt: Z=0 and (N==V)
|
||||
* - ge: N==V
|
||||
* - lt: N!=V
|
||||
* - le: Z=1 or (N!=V)
|
||||
*
|
||||
* The signed comparison technique uses overflow correction:
|
||||
* sec / sbc value / bvc + / eor #$80 / +
|
||||
* After this, N=0 means the result is >= 0 (left >= right for subtraction left-right).
|
||||
*/
|
||||
|
||||
package prog8.codegen.new6502
|
||||
|
||||
import prog8.intermediate.*
|
||||
import prog8.code.core.toHex
|
||||
|
||||
internal fun AsmGen.translateBranch(insn: IRInstruction) {
|
||||
val label = targetLabel(insn)
|
||||
|
||||
when (insn.opcode) {
|
||||
Opcode.BSTCC -> emitLine("bcc $label")
|
||||
Opcode.BSTCS -> emitLine("bcs $label")
|
||||
Opcode.BSTEQ -> emitLine("beq $label")
|
||||
Opcode.BSTNE -> emitLine("bne $label")
|
||||
Opcode.BSTNEG -> emitLine("bmi $label")
|
||||
Opcode.BSTPOS -> emitLine("bpl $label")
|
||||
Opcode.BSTVC -> emitLine("bvc $label")
|
||||
Opcode.BSTVS -> emitLine("bvs $label")
|
||||
|
||||
// Unsigned integer comparison branches: emit CMP then check flags.
|
||||
Opcode.BGT -> translateCmpBranchUnsigned(insn, label, greaterThan = true)
|
||||
Opcode.BGTR -> translateCmpBranchRegUnsigned(insn, label, greaterThan = true)
|
||||
Opcode.BGE -> translateCmpBranchUnsigned(insn, label, greaterOrEqual = true)
|
||||
Opcode.BGER -> translateCmpBranchRegUnsigned(insn, label, greaterOrEqual = true)
|
||||
Opcode.BLT -> translateCmpBranchUnsigned(insn, label, lessThan = true)
|
||||
Opcode.BLE -> translateCmpBranchUnsigned(insn, label, lessOrEqual = true)
|
||||
|
||||
// Signed integer comparison branches: emit CMP then check N/V flags with overflow correction.
|
||||
Opcode.BGTS -> translateCmpBranchSigned(insn, label, greaterThan = true)
|
||||
Opcode.BGTSR -> translateCmpBranchRegSigned(insn, label, greaterThan = true)
|
||||
Opcode.BGES -> translateCmpBranchSigned(insn, label, greaterOrEqual = true)
|
||||
Opcode.BGESR -> translateCmpBranchRegSigned(insn, label, greaterOrEqual = true)
|
||||
Opcode.BLTS -> translateCmpBranchSigned(insn, label, lessThan = true)
|
||||
Opcode.BLES -> translateCmpBranchSigned(insn, label, lessOrEqual = true)
|
||||
|
||||
else -> TODO("Unknown branch opcode: ${insn.opcode}")
|
||||
}
|
||||
}
|
||||
|
||||
/** the label (or fixed address) a branch instruction targets */
|
||||
internal fun AsmGen.targetLabel(insn: IRInstruction): String =
|
||||
when (val target = insn.requireTarget()) {
|
||||
is CodeReference.Label -> if (target.offset != 0) "${target.name}+${target.offset}" else target.name
|
||||
is CodeReference.Absolute -> target.address.value.toHex()
|
||||
is CodeReference.Indirect -> error("$insn: branch target cannot be indirect")
|
||||
}
|
||||
|
||||
private fun AsmGen.translateCmpBranchUnsigned(insn: IRInstruction, label: String,
|
||||
greaterThan: Boolean = false,
|
||||
greaterOrEqual: Boolean = false,
|
||||
lessThan: Boolean = false,
|
||||
lessOrEqual: Boolean = false) {
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
val reg = insn.requireIntSourceA().intNumber
|
||||
val imm = insn.requireImmediateInt()
|
||||
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda ${regAddrLo(reg)}")
|
||||
emitLine("cmp #${imm and 0xff}")
|
||||
when {
|
||||
greaterThan -> {
|
||||
// A > imm: use bcc (A < imm) + beq (A == imm) to skip
|
||||
val skip = makeLabel("bskip")
|
||||
emitLine("bcc $skip")
|
||||
emitLine("beq $skip")
|
||||
emitUnconditionalBranch(label)
|
||||
emitLabel(skip)
|
||||
}
|
||||
greaterOrEqual -> {
|
||||
// A >= imm: bcs label (branch if carry set, i.e., A >= imm unsigned)
|
||||
emitLine("bcs $label")
|
||||
}
|
||||
lessThan -> {
|
||||
// A < imm: bcc label (branch if C=0, i.e., A < imm)
|
||||
emitLine("bcc $label")
|
||||
}
|
||||
lessOrEqual -> {
|
||||
// A <= imm: bcc (A < imm) or beq (A == imm)
|
||||
emitLine("bcc $label")
|
||||
emitLine("beq $label")
|
||||
}
|
||||
}
|
||||
}
|
||||
IRDataType.WORD, IRDataType.POINTER -> {
|
||||
when {
|
||||
greaterThan -> {
|
||||
// a > imm (unsigned) -> imm < a: swap operands
|
||||
emitLine("lda #${imm and 0xff}")
|
||||
emitLine("cmp ${regAddrLo(reg)}")
|
||||
emitLine("lda #${(imm shr 8) and 0xff}")
|
||||
emitLine("sbc ${regAddrHi(reg)}")
|
||||
emitLine("bcc $label")
|
||||
}
|
||||
greaterOrEqual -> {
|
||||
emitLine("lda ${regAddrLo(reg)}")
|
||||
emitLine("cmp #${imm and 0xff}")
|
||||
emitLine("lda ${regAddrHi(reg)}")
|
||||
emitLine("sbc #${(imm shr 8) and 0xff}")
|
||||
emitLine("bcs $label")
|
||||
}
|
||||
lessThan -> {
|
||||
emitLine("lda ${regAddrLo(reg)}")
|
||||
emitLine("cmp #${imm and 0xff}")
|
||||
emitLine("lda ${regAddrHi(reg)}")
|
||||
emitLine("sbc #${(imm shr 8) and 0xff}")
|
||||
emitLine("bcc $label")
|
||||
}
|
||||
lessOrEqual -> {
|
||||
// a <= imm (unsigned) -> imm >= a: swap operands
|
||||
emitLine("lda #${imm and 0xff}")
|
||||
emitLine("cmp ${regAddrLo(reg)}")
|
||||
emitLine("lda #${(imm shr 8) and 0xff}")
|
||||
emitLine("sbc ${regAddrHi(reg)}")
|
||||
emitLine("bcs $label")
|
||||
}
|
||||
}
|
||||
}
|
||||
IRDataType.LONG -> translateCmpBranchLongUnsigned(insn, label, greaterThan, greaterOrEqual, lessThan, lessOrEqual)
|
||||
IRDataType.FLOAT -> error("float branch not supported")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.translateCmpBranchLongUnsigned(insn: IRInstruction, label: String,
|
||||
greaterThan: Boolean,
|
||||
greaterOrEqual: Boolean,
|
||||
lessThan: Boolean,
|
||||
lessOrEqual: Boolean) {
|
||||
val reg = insn.requireIntSourceA().intNumber
|
||||
val imm = insn.requireImmediateInt()
|
||||
when {
|
||||
greaterThan -> {
|
||||
// a > imm -> imm < a: swap operands
|
||||
for (byteIdx in 0..3) {
|
||||
val immPart = (imm shr (byteIdx * 8)) and 0xff
|
||||
if (byteIdx == 0) {
|
||||
emitLine("lda #$immPart")
|
||||
emitLine("cmp ${regAddrByte(reg, byteIdx)}")
|
||||
} else {
|
||||
emitLine("lda #$immPart")
|
||||
emitLine("sbc ${regAddrByte(reg, byteIdx)}")
|
||||
}
|
||||
}
|
||||
emitLine("bcc $label")
|
||||
}
|
||||
greaterOrEqual -> {
|
||||
for (byteIdx in 0..3) {
|
||||
val regPart = regAddrByte(reg, byteIdx)
|
||||
val immPart = (imm shr (byteIdx * 8)) and 0xff
|
||||
if (byteIdx == 0) {
|
||||
emitLine("lda $regPart")
|
||||
emitLine("cmp #$immPart")
|
||||
} else {
|
||||
emitLine("lda $regPart")
|
||||
emitLine("sbc #$immPart")
|
||||
}
|
||||
}
|
||||
emitLine("bcs $label")
|
||||
}
|
||||
lessThan -> {
|
||||
for (byteIdx in 0..3) {
|
||||
val regPart = regAddrByte(reg, byteIdx)
|
||||
val immPart = (imm shr (byteIdx * 8)) and 0xff
|
||||
if (byteIdx == 0) {
|
||||
emitLine("lda $regPart")
|
||||
emitLine("cmp #$immPart")
|
||||
} else {
|
||||
emitLine("lda $regPart")
|
||||
emitLine("sbc #$immPart")
|
||||
}
|
||||
}
|
||||
emitLine("bcc $label")
|
||||
}
|
||||
lessOrEqual -> {
|
||||
// a <= imm -> imm >= a: swap operands
|
||||
for (byteIdx in 0..3) {
|
||||
val immPart = (imm shr (byteIdx * 8)) and 0xff
|
||||
if (byteIdx == 0) {
|
||||
emitLine("lda #$immPart")
|
||||
emitLine("cmp ${regAddrByte(reg, byteIdx)}")
|
||||
} else {
|
||||
emitLine("lda #$immPart")
|
||||
emitLine("sbc ${regAddrByte(reg, byteIdx)}")
|
||||
}
|
||||
}
|
||||
emitLine("bcs $label")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.translateCmpBranchRegUnsigned(insn: IRInstruction, label: String,
|
||||
greaterThan: Boolean = false,
|
||||
greaterOrEqual: Boolean = false) {
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
val reg1 = insn.requireIntSourceA().intNumber
|
||||
val reg2 = insn.requireSrcB().intNumber
|
||||
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda ${regAddrLo(reg1)}")
|
||||
emitLine("cmp ${regAddrLo(reg2)}")
|
||||
if (greaterThan) {
|
||||
val skip = makeLabel("bskip")
|
||||
emitLine("bcc $skip")
|
||||
emitLine("beq $skip")
|
||||
emitUnconditionalBranch(label)
|
||||
emitLabel(skip)
|
||||
} else if (greaterOrEqual) {
|
||||
emitLine("bcs $label")
|
||||
}
|
||||
}
|
||||
IRDataType.WORD, IRDataType.POINTER -> {
|
||||
if (greaterThan) {
|
||||
// a > b -> b < a: swap operands
|
||||
emitLine("lda ${regAddrLo(reg2)}")
|
||||
emitLine("cmp ${regAddrLo(reg1)}")
|
||||
emitLine("lda ${regAddrHi(reg2)}")
|
||||
emitLine("sbc ${regAddrHi(reg1)}")
|
||||
emitLine("bcc $label")
|
||||
} else if (greaterOrEqual) {
|
||||
emitLine("lda ${regAddrLo(reg1)}")
|
||||
emitLine("cmp ${regAddrLo(reg2)}")
|
||||
emitLine("lda ${regAddrHi(reg1)}")
|
||||
emitLine("sbc ${regAddrHi(reg2)}")
|
||||
emitLine("bcs $label")
|
||||
}
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
if (greaterThan) {
|
||||
// a > b -> b < a: swap operands
|
||||
for (byteIdx in 0..3) {
|
||||
if (byteIdx == 0) {
|
||||
emitLine("lda ${regAddrByte(reg2, byteIdx)}")
|
||||
emitLine("cmp ${regAddrByte(reg1, byteIdx)}")
|
||||
} else {
|
||||
emitLine("lda ${regAddrByte(reg2, byteIdx)}")
|
||||
emitLine("sbc ${regAddrByte(reg1, byteIdx)}")
|
||||
}
|
||||
}
|
||||
emitLine("bcc $label")
|
||||
} else if (greaterOrEqual) {
|
||||
for (byteIdx in 0..3) {
|
||||
if (byteIdx == 0) {
|
||||
emitLine("lda ${regAddrByte(reg1, byteIdx)}")
|
||||
emitLine("cmp ${regAddrByte(reg2, byteIdx)}")
|
||||
} else {
|
||||
emitLine("lda ${regAddrByte(reg1, byteIdx)}")
|
||||
emitLine("sbc ${regAddrByte(reg2, byteIdx)}")
|
||||
}
|
||||
}
|
||||
emitLine("bcs $label")
|
||||
}
|
||||
}
|
||||
IRDataType.FLOAT -> error("float branch not supported")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signed comparison branch with immediate value.
|
||||
* Uses the overflow correction technique: sec / sbc / bvc+ / eor #$80 /
|
||||
* After correction, N=0 means left >= right (for subtraction left-right).
|
||||
*/
|
||||
private fun AsmGen.translateCmpBranchSigned(insn: IRInstruction, label: String,
|
||||
greaterThan: Boolean = false,
|
||||
greaterOrEqual: Boolean = false,
|
||||
lessThan: Boolean = false,
|
||||
lessOrEqual: Boolean = false) {
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
val reg = insn.requireIntSourceA().intNumber
|
||||
val imm = insn.requireImmediateInt()
|
||||
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda ${regAddrLo(reg)}")
|
||||
emitLine("sec")
|
||||
emitLine("sbc #${imm and 0xff}")
|
||||
when {
|
||||
greaterThan -> {
|
||||
// left > right (signed): Z=0 after subtraction, then N=0 after correction
|
||||
val skip = makeLabel("bskip")
|
||||
emitLine("beq $skip") // equal: not greater
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}") // overflow: correct N flag
|
||||
emitLine("+")
|
||||
emitLine("bpl $label") // N=0 after correction: left > right
|
||||
emitLabel(skip)
|
||||
}
|
||||
greaterOrEqual -> {
|
||||
// left >= right (signed): N=0 after correction
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
emitLine("bpl $label")
|
||||
}
|
||||
lessThan -> {
|
||||
// left < right (signed): N=1 after correction
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
emitLine("bmi $label")
|
||||
}
|
||||
lessOrEqual -> {
|
||||
// left <= right (signed): Z=1 or N=1 after correction
|
||||
val skip = makeLabel("bskip")
|
||||
emitLine("beq $label") // equal: is less-or-equal
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
emitLine("bmi $label") // negative: left < right
|
||||
emitLabel(skip)
|
||||
}
|
||||
}
|
||||
}
|
||||
IRDataType.WORD, IRDataType.POINTER -> {
|
||||
when {
|
||||
greaterThan -> {
|
||||
// a > imm -> imm < a: swap operands (subtract reg FROM imm)
|
||||
emitLine("lda #${imm and 0xff}")
|
||||
emitLine("cmp ${regAddrLo(reg)}")
|
||||
emitLine("lda #${(imm shr 8) and 0xff}")
|
||||
emitLine("sbc ${regAddrHi(reg)}")
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
emitLine("bmi $label")
|
||||
}
|
||||
greaterOrEqual -> {
|
||||
emitLine("lda ${regAddrLo(reg)}")
|
||||
emitLine("cmp #${imm and 0xff}")
|
||||
emitLine("lda ${regAddrHi(reg)}")
|
||||
emitLine("sbc #${(imm shr 8) and 0xff}")
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
emitLine("bpl $label")
|
||||
}
|
||||
lessThan -> {
|
||||
emitLine("lda ${regAddrLo(reg)}")
|
||||
emitLine("cmp #${imm and 0xff}")
|
||||
emitLine("lda ${regAddrHi(reg)}")
|
||||
emitLine("sbc #${(imm shr 8) and 0xff}")
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
emitLine("bmi $label")
|
||||
}
|
||||
lessOrEqual -> {
|
||||
// a <= imm -> imm >= a: swap operands
|
||||
emitLine("lda #${imm and 0xff}")
|
||||
emitLine("cmp ${regAddrLo(reg)}")
|
||||
emitLine("lda #${(imm shr 8) and 0xff}")
|
||||
emitLine("sbc ${regAddrHi(reg)}")
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
emitLine("bpl $label")
|
||||
}
|
||||
}
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
when {
|
||||
greaterThan -> {
|
||||
// a > imm -> imm < a: swap operands (subtract reg FROM imm)
|
||||
for (byteIdx in 0..3) {
|
||||
val immPart = (imm shr (byteIdx * 8)) and 0xff
|
||||
if (byteIdx == 0) {
|
||||
emitLine("lda #$immPart")
|
||||
emitLine("cmp ${regAddrByte(reg, byteIdx)}")
|
||||
} else if (byteIdx < 3) {
|
||||
emitLine("lda #$immPart")
|
||||
emitLine("sbc ${regAddrByte(reg, byteIdx)}")
|
||||
} else {
|
||||
emitLine("lda #$immPart")
|
||||
emitLine("sbc ${regAddrByte(reg, byteIdx)}")
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
}
|
||||
}
|
||||
emitLine("bmi $label")
|
||||
}
|
||||
greaterOrEqual -> {
|
||||
for (byteIdx in 0..3) {
|
||||
val immPart = (imm shr (byteIdx * 8)) and 0xff
|
||||
if (byteIdx == 0) {
|
||||
emitLine("lda ${regAddrByte(reg, byteIdx)}")
|
||||
emitLine("cmp #$immPart")
|
||||
} else if (byteIdx < 3) {
|
||||
emitLine("lda ${regAddrByte(reg, byteIdx)}")
|
||||
emitLine("sbc #$immPart")
|
||||
} else {
|
||||
emitLine("lda ${regAddrByte(reg, byteIdx)}")
|
||||
emitLine("sbc #$immPart")
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
}
|
||||
}
|
||||
emitLine("bpl $label")
|
||||
}
|
||||
lessThan -> {
|
||||
for (byteIdx in 0..3) {
|
||||
val immPart = (imm shr (byteIdx * 8)) and 0xff
|
||||
if (byteIdx == 0) {
|
||||
emitLine("lda ${regAddrByte(reg, byteIdx)}")
|
||||
emitLine("cmp #$immPart")
|
||||
} else if (byteIdx < 3) {
|
||||
emitLine("lda ${regAddrByte(reg, byteIdx)}")
|
||||
emitLine("sbc #$immPart")
|
||||
} else {
|
||||
emitLine("lda ${regAddrByte(reg, byteIdx)}")
|
||||
emitLine("sbc #$immPart")
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
}
|
||||
}
|
||||
emitLine("bmi $label")
|
||||
}
|
||||
lessOrEqual -> {
|
||||
// a <= imm -> imm >= a: swap operands
|
||||
for (byteIdx in 0..3) {
|
||||
val immPart = (imm shr (byteIdx * 8)) and 0xff
|
||||
if (byteIdx == 0) {
|
||||
emitLine("lda #$immPart")
|
||||
emitLine("cmp ${regAddrByte(reg, byteIdx)}")
|
||||
} else if (byteIdx < 3) {
|
||||
emitLine("lda #$immPart")
|
||||
emitLine("sbc ${regAddrByte(reg, byteIdx)}")
|
||||
} else {
|
||||
emitLine("lda #$immPart")
|
||||
emitLine("sbc ${regAddrByte(reg, byteIdx)}")
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
}
|
||||
}
|
||||
emitLine("bpl $label")
|
||||
}
|
||||
}
|
||||
}
|
||||
IRDataType.FLOAT -> error("float branch not supported")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signed comparison branch with register operand.
|
||||
* Uses the overflow correction technique on the subtracted result.
|
||||
*/
|
||||
private fun AsmGen.translateCmpBranchRegSigned(insn: IRInstruction, label: String,
|
||||
greaterThan: Boolean = false,
|
||||
greaterOrEqual: Boolean = false,
|
||||
lessThan: Boolean = false,
|
||||
lessOrEqual: Boolean = false) {
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
val reg1 = insn.requireIntSourceA().intNumber
|
||||
val reg2 = insn.requireSrcB().intNumber
|
||||
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda ${regAddrLo(reg1)}")
|
||||
emitLine("sec")
|
||||
emitLine("sbc ${regAddrLo(reg2)}")
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
if (greaterThan) {
|
||||
// left > right: Z=0 AND N=0 after correction
|
||||
val skip = makeLabel("bskip")
|
||||
emitLine("beq $skip")
|
||||
emitLine("bpl $label")
|
||||
emitLabel(skip)
|
||||
} else if (greaterOrEqual) {
|
||||
emitLine("bpl $label")
|
||||
} else if (lessThan) {
|
||||
emitLine("bmi $label")
|
||||
} else if (lessOrEqual) {
|
||||
emitLine("beq $label")
|
||||
emitLine("bmi $label")
|
||||
}
|
||||
}
|
||||
IRDataType.WORD, IRDataType.POINTER -> {
|
||||
if (greaterThan) {
|
||||
// a > b -> b < a: swap operands, check N=1 after correction
|
||||
emitLine("lda ${regAddrLo(reg2)}")
|
||||
emitLine("cmp ${regAddrLo(reg1)}")
|
||||
emitLine("lda ${regAddrHi(reg2)}")
|
||||
emitLine("sbc ${regAddrHi(reg1)}")
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
emitLine("bmi $label")
|
||||
} else if (greaterOrEqual) {
|
||||
emitLine("lda ${regAddrLo(reg1)}")
|
||||
emitLine("cmp ${regAddrLo(reg2)}")
|
||||
emitLine("lda ${regAddrHi(reg1)}")
|
||||
emitLine("sbc ${regAddrHi(reg2)}")
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
emitLine("bpl $label")
|
||||
} else if (lessThan) {
|
||||
emitLine("lda ${regAddrLo(reg1)}")
|
||||
emitLine("cmp ${regAddrLo(reg2)}")
|
||||
emitLine("lda ${regAddrHi(reg1)}")
|
||||
emitLine("sbc ${regAddrHi(reg2)}")
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
emitLine("bmi $label")
|
||||
} else if (lessOrEqual) {
|
||||
// a <= b -> b >= a: swap operands, check N=0 after correction
|
||||
emitLine("lda ${regAddrLo(reg2)}")
|
||||
emitLine("cmp ${regAddrLo(reg1)}")
|
||||
emitLine("lda ${regAddrHi(reg2)}")
|
||||
emitLine("sbc ${regAddrHi(reg1)}")
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
emitLine("bpl $label")
|
||||
}
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
if (greaterThan) {
|
||||
// a > b -> b < a: swap operands
|
||||
for (byteIdx in 0..3) {
|
||||
if (byteIdx == 0) {
|
||||
emitLine("lda ${regAddrByte(reg2, byteIdx)}")
|
||||
emitLine("cmp ${regAddrByte(reg1, byteIdx)}")
|
||||
} else if (byteIdx < 3) {
|
||||
emitLine("lda ${regAddrByte(reg2, byteIdx)}")
|
||||
emitLine("sbc ${regAddrByte(reg1, byteIdx)}")
|
||||
} else {
|
||||
emitLine("lda ${regAddrByte(reg2, byteIdx)}")
|
||||
emitLine("sbc ${regAddrByte(reg1, byteIdx)}")
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
}
|
||||
}
|
||||
emitLine("bmi $label")
|
||||
} else if (greaterOrEqual) {
|
||||
for (byteIdx in 0..3) {
|
||||
if (byteIdx == 0) {
|
||||
emitLine("lda ${regAddrByte(reg1, byteIdx)}")
|
||||
emitLine("cmp ${regAddrByte(reg2, byteIdx)}")
|
||||
} else if (byteIdx < 3) {
|
||||
emitLine("lda ${regAddrByte(reg1, byteIdx)}")
|
||||
emitLine("sbc ${regAddrByte(reg2, byteIdx)}")
|
||||
} else {
|
||||
emitLine("lda ${regAddrByte(reg1, byteIdx)}")
|
||||
emitLine("sbc ${regAddrByte(reg2, byteIdx)}")
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
}
|
||||
}
|
||||
emitLine("bpl $label")
|
||||
} else if (lessThan) {
|
||||
for (byteIdx in 0..3) {
|
||||
if (byteIdx == 0) {
|
||||
emitLine("lda ${regAddrByte(reg1, byteIdx)}")
|
||||
emitLine("cmp ${regAddrByte(reg2, byteIdx)}")
|
||||
} else if (byteIdx < 3) {
|
||||
emitLine("lda ${regAddrByte(reg1, byteIdx)}")
|
||||
emitLine("sbc ${regAddrByte(reg2, byteIdx)}")
|
||||
} else {
|
||||
emitLine("lda ${regAddrByte(reg1, byteIdx)}")
|
||||
emitLine("sbc ${regAddrByte(reg2, byteIdx)}")
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
}
|
||||
}
|
||||
emitLine("bmi $label")
|
||||
} else if (lessOrEqual) {
|
||||
// a <= b -> b >= a: swap operands
|
||||
for (byteIdx in 0..3) {
|
||||
if (byteIdx == 0) {
|
||||
emitLine("lda ${regAddrByte(reg2, byteIdx)}")
|
||||
emitLine("cmp ${regAddrByte(reg1, byteIdx)}")
|
||||
} else if (byteIdx < 3) {
|
||||
emitLine("lda ${regAddrByte(reg2, byteIdx)}")
|
||||
emitLine("sbc ${regAddrByte(reg1, byteIdx)}")
|
||||
} else {
|
||||
emitLine("lda ${regAddrByte(reg2, byteIdx)}")
|
||||
emitLine("sbc ${regAddrByte(reg1, byteIdx)}")
|
||||
emitLine("bvc +")
|
||||
emitLine("eor #${0x80}")
|
||||
emitLine("+")
|
||||
}
|
||||
}
|
||||
emitLine("bpl $label")
|
||||
}
|
||||
}
|
||||
IRDataType.FLOAT -> error("float branch not supported")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,54 @@
|
||||
package prog8.codegen.new6502
|
||||
|
||||
import com.github.ajalt.clikt.core.CliktCommand
|
||||
import com.github.ajalt.clikt.core.main
|
||||
import com.github.ajalt.clikt.parameters.arguments.argument
|
||||
import com.github.ajalt.clikt.parameters.options.flag
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import prog8.code.core.CpuType
|
||||
import prog8.intermediate.IRFileReader
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.readText
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val cli = New6502Cli()
|
||||
cli.main(args)
|
||||
val inputFile = cli.inputFile
|
||||
val asmListfile = cli.asmListfile
|
||||
|
||||
val reader = IRFileReader()
|
||||
val source = Path(inputFile).readText()
|
||||
val program = reader.read(source)
|
||||
if(asmListfile == true)
|
||||
program.options.asmListfile = true
|
||||
val target = program.options.compTarget
|
||||
|
||||
// program metrics, via the canonical IRProgram counters
|
||||
val numRegs = program.countUsedRegisters()
|
||||
val (numInstr, numChunks) = program.countCodeElements()
|
||||
System.err.println("Loaded IR program: ${program.name}")
|
||||
System.err.println("Target system and CPU: ${target.name} / ${target.cpu}")
|
||||
System.err.println("($numInstr instructions in $numChunks chunks, $numRegs registers)")
|
||||
|
||||
val gen = when(target.cpu) {
|
||||
CpuType.CPU6502, CpuType.CPU65C02 -> AsmGen(program, target, StderrErrorReporter())
|
||||
else -> {
|
||||
println("This code generator only works for 6502 and 65C02 CPUs.")
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
val ok = gen.generate()
|
||||
if(!ok) {
|
||||
System.err.println("Assembly failed.")
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
private class New6502Cli : CliktCommand(name = "prog8-newgen") {
|
||||
val inputFile by argument(help = "path to .p8ir")
|
||||
val asmListfile by option("-l", "-list", "--list", help = "produce assembler listing file (.list)").flag()
|
||||
|
||||
override fun run() = Unit
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package prog8.codegen.new6502
|
||||
|
||||
import prog8.code.ICodeGeneratorBackend
|
||||
import prog8.code.SymbolTable
|
||||
import prog8.code.assembly.AssemblyProgram6502
|
||||
import prog8.code.assembly.IAssemblyProgram
|
||||
import prog8.code.ast.PtProgram
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.codegen.intermediate.IRCodeGen
|
||||
import prog8.intermediate.IRDataType
|
||||
import prog8.intermediate.IRFileWriter
|
||||
import prog8.intermediate.VirtualRegister
|
||||
|
||||
class New6502CodeGenerator(val retainSSA: Boolean,
|
||||
private val preassignedCallSiteIds: Map<String, UByte> = emptyMap()
|
||||
): ICodeGeneratorBackend {
|
||||
override fun generate(
|
||||
program: PtProgram,
|
||||
symbolTable: SymbolTable,
|
||||
options: CompilationOptions,
|
||||
errors: IErrorReporter
|
||||
): IAssemblyProgram? {
|
||||
|
||||
// you could write a code generator directly on the PtProgram AST,
|
||||
// but you can also use the Intermediate Representation to build a codegen on:
|
||||
val irCodeGen = IRCodeGen(program, symbolTable, options, errors, retainSSA, preassignedCallSiteIds)
|
||||
val irProgram = irCodeGen.generate()
|
||||
if (!irCodeGen.wasPackingApplied) {
|
||||
val virtualRegisterTypes = irCodeGen.registerTypes().entries.associate { (num, type) ->
|
||||
val register: VirtualRegister = if (type == IRDataType.FLOAT) VirtualRegister.float(num.value) else VirtualRegister.int(num.value)
|
||||
register to type
|
||||
}
|
||||
irProgram.verifyRegisterTypes(virtualRegisterTypes)
|
||||
}
|
||||
|
||||
IRFileWriter(irProgram, null).write()
|
||||
|
||||
val gen = AsmGen(irProgram, irProgram.options.compTarget, errors)
|
||||
val success = gen.generate()
|
||||
if(!success)
|
||||
return null
|
||||
|
||||
return AssemblyProgram6502(irProgram.name, irProgram.options.outputDir, irProgram.options.compTarget)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package prog8.codegen.new6502
|
||||
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.code.core.Position
|
||||
|
||||
internal class StderrErrorReporter : IErrorReporter {
|
||||
override fun err(msg: String, position: Position) { System.err.println("ERROR: $msg") }
|
||||
override fun warn(msg: String, position: Position) { System.err.println("WARNING: $msg") }
|
||||
override fun info(msg: String, position: Position) {}
|
||||
override fun undefined(symbol: List<String>, suggestImport: Boolean, position: Position) {}
|
||||
override fun noErrors(): Boolean = true
|
||||
override fun report() {}
|
||||
override fun noErrorForLine(position: Position): Boolean = true
|
||||
override fun printSingleError(errormessage: String) { System.err.println(errormessage) }
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Zero-page variable allocator for the new6502gen code generator.
|
||||
*
|
||||
* Allocates variables to zero-page based on their ZeropageWish:
|
||||
* REQUIRE_ZEROPAGE - must be in ZP, error if not possible
|
||||
* PREFER_ZEROPAGE - best-effort, silently falls back to RAM
|
||||
* DONTCARE - best-effort, sorted by usage frequency
|
||||
* NOT_IN_ZEROPAGE - never in ZP
|
||||
*
|
||||
* Usage scoring counts variable references across IR instructions,
|
||||
* weighted by data type (word/pointer = 2, byte = 1).
|
||||
*/
|
||||
|
||||
package prog8.codegen.new6502
|
||||
|
||||
import prog8.code.core.*
|
||||
import prog8.intermediate.*
|
||||
|
||||
class ZeropageAllocator(
|
||||
private val program: IRProgram,
|
||||
target: ICompilationTarget
|
||||
) {
|
||||
private val zeropage = target.zeropage
|
||||
private val allVars by lazy { program.st.allVariables().toList() }
|
||||
private val usageScores by lazy { computeUsageScores() }
|
||||
|
||||
/**
|
||||
* Allocate variables into zero-page.
|
||||
* Returns the map of allocated variable names to their ZP addresses.
|
||||
*/
|
||||
fun allocate(): Map<String, MemoryAllocator.VarAllocation> {
|
||||
if (program.options.zeropage == ZeropageType.DONTUSE)
|
||||
return emptyMap()
|
||||
|
||||
val requireZp = allVars.filter { it.zpwish == ZeropageWish.REQUIRE_ZEROPAGE }
|
||||
val preferZp = allVars.filter { it.zpwish == ZeropageWish.PREFER_ZEROPAGE }
|
||||
val (dontcareNoAlign, _) = allVars
|
||||
.filter { it.zpwish == ZeropageWish.DONTCARE }
|
||||
.partition { it.align == 0u }
|
||||
|
||||
// 1st: REQUIRE_ZEROPAGE - fail if no room
|
||||
for (v in requireZp) {
|
||||
if (!allocateVar(v)) {
|
||||
System.err.println("WARNING: variable '${v.name}' requires zeropage but no space available")
|
||||
}
|
||||
}
|
||||
|
||||
// 2nd: PREFER_ZEROPAGE - best-effort, silently fall back
|
||||
for (v in preferZp) {
|
||||
allocateVar(v)
|
||||
}
|
||||
|
||||
// 3rd: DONTCARE (no alignment) - sorted by usage score, only integers/pointers
|
||||
val scored = dontcareNoAlign
|
||||
.filter { it.dt.isInteger || it.dt.isPointer }
|
||||
.sortedByDescending { usageScores[it.name] ?: 0 }
|
||||
for (v in scored) {
|
||||
if (!zeropage.hasByteAvailable()) break
|
||||
allocateVar(v)
|
||||
}
|
||||
|
||||
return zeropage.allocatedVariables
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a variable was allocated to zero-page.
|
||||
*/
|
||||
fun isZpVar(name: String): Boolean {
|
||||
if (name in zeropage.allocatedVariables)
|
||||
return true
|
||||
val v = program.st.lookup(name)
|
||||
return if (v is IRStMemVar) v.address <= 255u else false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ZP allocation for a variable, or null if not in ZP.
|
||||
*/
|
||||
fun getAllocation(name: String): MemoryAllocator.VarAllocation? {
|
||||
return zeropage.allocatedVariables[name]
|
||||
}
|
||||
|
||||
private fun allocateVar(v: IRStStaticVariable): Boolean {
|
||||
val result = zeropage.allocate(v.name, v.dt, v.length?.toInt(), null, SilentErrorReporter)
|
||||
return result.component1() != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Count variable references across IR instructions to determine usage frequency.
|
||||
* This is a simpler heuristic than the old codegen's AST walk with loop depth,
|
||||
* but still effectively identifies the hottest variables.
|
||||
*/
|
||||
private fun computeUsageScores(): Map<String, Int> {
|
||||
val scores = mutableMapOf<String, Int>()
|
||||
|
||||
// Build a set of variable names for quick lookup
|
||||
val varNames = allVars.associateBy { it.name }
|
||||
|
||||
fun scoreInstruction(insn: IRInstruction) {
|
||||
val label = insn.memory?.symbolName
|
||||
?: (insn.immediate as? ImmediateOperand.SymbolAddress)?.symbol
|
||||
?: return
|
||||
if (label !in varNames) return
|
||||
val weight = when {
|
||||
insn.type == IRDataType.WORD -> 2
|
||||
insn.type == IRDataType.LONG -> 3
|
||||
else -> 1
|
||||
}
|
||||
scores[label] = (scores[label] ?: 0) + weight
|
||||
}
|
||||
|
||||
// Walk all code chunks and count references
|
||||
program.foreachCodeChunk { chunk ->
|
||||
if (chunk is IRCodeChunk) {
|
||||
for (insn in chunk.instructions) {
|
||||
scoreInstruction(insn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scores
|
||||
}
|
||||
|
||||
/**
|
||||
* Error reporter that silently swallows errors during ZP allocation.
|
||||
* REQUIRE_ZEROPAGE failures are reported to stderr by the caller.
|
||||
*/
|
||||
private object SilentErrorReporter : IErrorReporter {
|
||||
private var errors = false
|
||||
override fun err(msg: String, position: Position) { errors = true }
|
||||
override fun warn(msg: String, position: Position) {}
|
||||
override fun info(msg: String, position: Position) {}
|
||||
override fun undefined(symbol: List<String>, suggestImport: Boolean, position: Position) {}
|
||||
override fun noErrors(): Boolean = !errors
|
||||
override fun report() {}
|
||||
override fun noErrorForLine(position: Position): Boolean = true
|
||||
override fun printSingleError(errormessage: String) {}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 556 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user