mirror of
https://github.com/irmen/prog8.git
synced 2026-10-24 03:02:20 +00:00
Compare commits
155
Commits
v14.0-BETA1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2520db872 | ||
|
|
ebe26d0cf9 | ||
|
|
63e2a45f09 | ||
|
|
6076b01f6d | ||
|
|
d0bf2c3dcb | ||
|
|
d9bf6f090e | ||
|
|
e08a995067 | ||
|
|
da2288db93 | ||
|
|
31b5cddb96 | ||
|
|
264b894909 | ||
|
|
09ae0c6322 | ||
|
|
9ca5db3436 | ||
|
|
375214b5e3 | ||
|
|
1da37e4119 | ||
|
|
16f7d588a7 | ||
|
|
f6c8a3f2ed | ||
|
|
5619898113 | ||
|
|
ae0414631e | ||
|
|
e3ca9ae4a8 | ||
|
|
38c60a2d75 | ||
|
|
5e83c44300 | ||
|
|
99ff5768c5 | ||
|
|
fba4bb5272 | ||
|
|
17322a08ec | ||
|
|
06d2f9ebbe | ||
|
|
3a5f2c33ab | ||
|
|
d523ecd1e1 | ||
|
|
159634a99f | ||
|
|
55dcb45230 | ||
|
|
de75342198 | ||
|
|
30d3b76787 | ||
|
|
8bbe5bd209 | ||
|
|
da7ea294ef | ||
|
|
05d8acc4bb | ||
|
|
6cac707fd1 | ||
|
|
dea43c99bb | ||
|
|
0b902f06b5 | ||
|
|
733a3fb752 | ||
|
|
460adb9e02 | ||
|
|
3c22dfc11a | ||
|
|
dfb3257c04 | ||
|
|
8bcd176746 | ||
|
|
fd3add76e4 | ||
|
|
4d0a7ff8bf | ||
|
|
e1f2faabe7 | ||
|
|
25bd60368a | ||
|
|
f61c1dc7ef | ||
|
|
eda21a6ebf | ||
|
|
c0267e37b0 | ||
|
|
78f9df203c | ||
|
|
d3e10b4974 | ||
|
|
c4679bd8af | ||
|
|
e7dfe79919 | ||
|
|
31dbbc3fe1 | ||
|
|
55b7239553 | ||
|
|
5702e5aa2a | ||
|
|
592637856f | ||
|
|
233ca21492 | ||
|
|
1a84616363 | ||
|
|
d32b70bb75 | ||
|
|
73241b0261 | ||
|
|
e515dee561 | ||
|
|
3d78415088 | ||
|
|
0189903a63 | ||
|
|
9c9552174e | ||
|
|
be61d37fd1 | ||
|
|
70bb199e12 | ||
|
|
09737187ad | ||
|
|
de39533fba | ||
|
|
8bc2b177a0 | ||
|
|
09eba995ae | ||
|
|
b739d7b663 | ||
|
|
5e709fca7e | ||
|
|
cc8df33418 | ||
|
|
125af552ff | ||
|
|
29254e892b | ||
|
|
a1a48405b0 | ||
|
|
acc8705ea8 | ||
|
|
bbff472457 | ||
|
|
353b0c84b7 | ||
|
|
329b3df4a3 | ||
|
|
fe90ca572d | ||
|
|
26bbd6c1fe | ||
|
|
822cfd4f35 | ||
|
|
0807d07ee5 | ||
|
|
2e3aa76a2f | ||
|
|
3beb565c84 | ||
|
|
b1642f4f7a | ||
|
|
1f8b42fae5 | ||
|
|
13828ea932 | ||
|
|
e550143ed0 | ||
|
|
47e6066a36 | ||
|
|
266f4ab380 | ||
|
|
e5954542dd | ||
|
|
8c2bc15b41 | ||
|
|
c9a7dc05d9 | ||
|
|
9d023c9568 | ||
|
|
a1ce45e409 | ||
|
|
09d469714b | ||
|
|
38db571f20 | ||
|
|
5f3b587af1 | ||
|
|
e74c34a5ab | ||
|
|
2b156ec652 | ||
|
|
e38f71fd2d | ||
|
|
6c107379df | ||
|
|
99f29cb9b9 | ||
|
|
d6ea0782e2 | ||
|
|
bdee93dc8f | ||
|
|
3d675a460e | ||
|
|
bd1747926c | ||
|
|
cff7732189 | ||
|
|
fa966086e9 | ||
|
|
ec4b47ac0d | ||
|
|
5222562493 | ||
|
|
c8cda7947a | ||
|
|
ffe855f295 | ||
|
|
8a66dabf6d | ||
|
|
b8c975704a | ||
|
|
2ef0ddd42c | ||
|
|
9a83b7455a | ||
|
|
9fce07565c | ||
|
|
d703d678b4 | ||
|
|
90a7c41586 | ||
|
|
b1a9ba929d | ||
|
|
75ab3ccfbd | ||
|
|
c031737da0 | ||
|
|
2f905ef6b6 | ||
|
|
4cbf93da85 | ||
|
|
976b64185d | ||
|
|
2050fcb4ee | ||
|
|
69525ae67f | ||
|
|
4eaba62f1e | ||
|
|
bf696bcf5e | ||
|
|
34bbb46514 | ||
|
|
f086918e6f | ||
|
|
521f7ffbe6 | ||
|
|
2d070aaec4 | ||
|
|
044a4a1789 | ||
|
|
6de6a81d4b | ||
|
|
815f4109a5 | ||
|
|
a49f080ca7 | ||
|
|
ac4ae0440f | ||
|
|
773f1f2292 | ||
|
|
efee8bde68 | ||
|
|
ddfdd9097d | ||
|
|
ef64e2f9a4 | ||
|
|
2f6ab002d8 | ||
|
|
ba76fbfe85 | ||
|
|
7f3b2e8aa6 | ||
|
|
f254fc2b62 | ||
|
|
457d55b392 | ||
|
|
b21a984dbb | ||
|
|
7427e6ddad | ||
|
|
192e0086f9 | ||
|
|
c8972c9561 |
No files matched your search
@@ -9,6 +9,10 @@ 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.
|
||||
|
||||
Reference documentation for the assembler is available locally in
|
||||
`~/Documents/prog8-dev-resources` (the 64tass manual, `64tass.md`). Search this
|
||||
folder first before looking online.
|
||||
|
||||
## Syntax
|
||||
|
||||
- Labels begin in column 1. Instructions are indented at least four spaces.
|
||||
@@ -125,6 +129,55 @@ 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.
|
||||
|
||||
## Reference
|
||||
|
||||
### Primary: 6502 Family CPU Reference
|
||||
|
||||
<https://www.pagetable.com/c64ref/6502/?cpu=65c02&tab=2> is the reference to reach for.
|
||||
It covers the whole 6502 family and lets you **select the CPU variant**, which matters
|
||||
because the parts genuinely differ. The variants are `6502`, `6502rorbug`, `65dtv02`,
|
||||
`65c02`, `r65c02` (Rockwell), `65c02s`, `65ce02` and `65c816`. Change the `cpu=` query
|
||||
parameter to switch; for Prog8, use `cpu=65c02` when working on cx16 and `cpu=6502` for
|
||||
c64, c128 and pet32.
|
||||
|
||||
Per instruction it gives the operation, the opcodes, the byte length, the cycle count
|
||||
and which status flags are affected, with tabs for opcodes, instructions, addressing
|
||||
modes and a full opcode table (switchable between 4-4 and 3-3-2 layout). It can also
|
||||
show the NMOS undocumented opcodes, and each part lists what it is based on, so a
|
||||
variant's inherited instructions are visible.
|
||||
|
||||
Cycle counts use a compact notation worth knowing: a trailing `t` means +1 if a branch
|
||||
is taken, `p` means +1 if a page is crossed, and `d` means +1 when the D (decimal) flag
|
||||
is set. So `d0 2+t+p` for `bne` is 2 cycles, 3 if taken, 4 if taken across a page.
|
||||
|
||||
Consult it whenever an exact figure is needed rather than recalled: the byte length or
|
||||
cycle cost of an instruction, which flags it clobbers, whether an addressing mode exists
|
||||
on the target CPU, or whether an instruction belongs to a different part at all. Typical
|
||||
reasons are costing a peephole or code-size tradeoff, checking a hand-written routine,
|
||||
and confirming a mnemonic is valid before using it.
|
||||
|
||||
Do not assume a timing from memory or from another part. Cycle counts are exactly the
|
||||
kind of detail that is misremembered, and the difference matters when it is the basis of
|
||||
an optimization claim. Read the number off the reference for the specific CPU being
|
||||
targeted.
|
||||
|
||||
### Secondary: WDC 65C02 reference
|
||||
|
||||
<https://cx16.dk/65c02/reference.html> is a 65C02-only reference. It is worth having
|
||||
because it states each instruction's effect in a single formula (for example
|
||||
`ADC: A,Z,C,N = A+M+C`), which makes the flag behaviour quick to check, and it lists
|
||||
every status bit per instruction including the ones not affected. Use it for flag
|
||||
semantics; use the primary reference for timings and for anything CPU-specific, since
|
||||
it covers the 65C02 only and will not answer a question about the NMOS 6502.
|
||||
|
||||
### Neither reference covers the assembler
|
||||
|
||||
Both document the CPU, not 64tass. For whether 64tass accepts a mnemonic or a given
|
||||
syntax, assemble it: an instruction outside the selected CPU set fails with
|
||||
`error: general syntax` rather than being silently ignored. Note also that 64tass has
|
||||
no `w65c02s`; its CPU selection is `--m65xx` (default), `--m65c02` and `--m65ce02`, and
|
||||
Prog8 emits `.cpu 'w65c02'`.
|
||||
|
||||
## Tools
|
||||
|
||||
Prog8 invokes 64tass with `--ascii --case-sensitive --long-branch -Wall` plus
|
||||
@@ -137,3 +190,15 @@ the target output option such as `--cbm-prg`. For manual assembly:
|
||||
|
||||
For debugging, add `--vice-labels --labels=labels.txt` and/or
|
||||
`--list=listing.txt`.
|
||||
|
||||
To check an instruction's encoding or size, assemble it in isolation and read the
|
||||
output bytes. The 2-byte load address comes first, so `stx $7b` / `ldy $7b` below
|
||||
shows as `86 7b a4 7b` - two instructions, four bytes:
|
||||
|
||||
```bash
|
||||
printf '.cpu %s\n* = $0801\nlbl\tstx\t$7b\n\tldy\t$7b\n' "'w65c02'" > sz.asm
|
||||
64tass --ascii --case-sensitive --cbm-prg -o sz.prg sz.asm && xxd sz.prg
|
||||
```
|
||||
|
||||
Note that a 64tass label takes no trailing colon; `lbl:` is a syntax error reported as
|
||||
`error: label required`.
|
||||
@@ -8,6 +8,10 @@ description: Writing or understanding Motorola 68000/68020 assembly in vasm mot
|
||||
Use Motorola syntax for `vasm`, not Devpac or other assemblers. Keep answers
|
||||
concise and practical.
|
||||
|
||||
Reference documentation for the assembler and linker is available locally in
|
||||
`~/Documents/prog8-dev-resources` (the vasm and vlink manuals). Search this
|
||||
folder first before looking online.
|
||||
|
||||
## Syntax
|
||||
|
||||
- Labels begin in column 1. Instructions are indented at least four spaces.
|
||||
@@ -63,6 +67,17 @@ when both operands are registers.
|
||||
is at offset `+3`.
|
||||
- Word and long accesses must be even-aligned on 68000. 68020 permits
|
||||
misalignment but it is slower.
|
||||
- **Byte `A7` auto-update steps by 2, not 1.** If A7 (== SP, the stack pointer) is the address
|
||||
register and the operand length for post-increment or post-decrement is one byte, A7 is
|
||||
decremented/incremented by 2 instead of 1, to maintain a word-aligned stack. This is the
|
||||
specified behaviour of all the 680x0, not a 68000 quirk, so it is identical everywhere and
|
||||
safe to rely on. Word and long operands step by 2 and 4.
|
||||
```asm
|
||||
move.b d0,-(sp) ; A7 steps by 2, so SP stays word-aligned
|
||||
...
|
||||
addq.l #2, sp ; and the matching pop is #2, not #1
|
||||
```
|
||||
Do not "fix" this to an explicit `subq.l #1,sp`: that leaves SP odd and cause crash.
|
||||
- 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`.
|
||||
@@ -82,8 +97,40 @@ when both operands are registers.
|
||||
pointer.
|
||||
- `D0-D1` and `A0-A1` are scratch registers. Preserve registers required by
|
||||
the surrounding calling convention, especially `D2-D7` and `A2-A6`.
|
||||
- Never clobber `A5`: on every M68K target it is the Prog8 stack frame pointer
|
||||
(locals at negative offsets, parameters at positive offsets). Overwriting it
|
||||
corrupts the frame of the running subroutine and breaks unwinding.
|
||||
- Never clobber `A6` on the Amiga targets (`amiga500`, `amiga1200`): it holds
|
||||
the AmigaOS library base pointer that library calls depend on. Prog8 itself
|
||||
leaves `A6` free for that reason.
|
||||
- `move.l d0,-(sp)` pushes and `move.l (sp)+,d0` pops.
|
||||
|
||||
### Prog8 M68K stack ABI
|
||||
|
||||
- Normal framed subroutines use `link a5,#-N` and `unlk a5`; locals are at
|
||||
negative `A5` offsets and incoming parameters are at positive offsets.
|
||||
- Arguments use word-packed slots of natural width: byte, bool, word and uword
|
||||
arguments take a 2-byte slot; long, pointer and float arguments take a 4-byte
|
||||
slot. The caller pushes arguments left-to-right (a byte sits at the base byte
|
||||
of its 2-byte slot, the second byte is garbage; word values fill their slot)
|
||||
and removes the complete argument area after the call. The area
|
||||
size is always even, so the stack stays word-aligned; slot bases accumulate from
|
||||
the rightmost argument at `8(a5)` according to each argument's width.
|
||||
- A normal call preserves the caller's frame. A `goto` or conditional branch
|
||||
leaving the current subroutine must execute `unlk a5` on the taken path before
|
||||
transferring control.
|
||||
- The built-in Prog8 `call(address)` is a zero-argument indirect call. It does
|
||||
not marshal stack arguments. `asmsub` and `extsub` keep their explicit
|
||||
register-annotated ABI, separate from normal framed calls.
|
||||
- Push narrow arguments into their own 2-byte slots, each with one instruction: a
|
||||
byte uses `move.b ..., -(sp)` (A7 byte predecrement steps by 2, reserving the
|
||||
slot; the byte lands at the slot's base byte), a word uses a plain
|
||||
`move.w ..., -(sp)`. Never push a narrow value where the callee expects a
|
||||
wide (4-byte) argument: the slot sizes must line up exactly.
|
||||
Wide arguments (`move.l ..., -(sp)` / `fmove.s fp0,-(sp)`) push in one step.
|
||||
Use `lea N(sp),sp` for the cleanup when the call returns a status flag
|
||||
(`addq` clobbers CCR).
|
||||
|
||||
## Prog8 Integration
|
||||
|
||||
Assembly can appear in `%asm {{ }}` blocks or `asmsub` routines. `asmsub`
|
||||
|
||||
@@ -9,18 +9,14 @@ 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.
|
||||
|
||||
For full language reference, link users to these project documents rather than
|
||||
repeating them here:
|
||||
Various reference documentation is available locally in
|
||||
`~/Documents/prog8-dev-resources`:
|
||||
|
||||
Prog8 reference documentation, emulator manuals such as VICE and the X16
|
||||
emulator, the 64tass/vasm/vlink assembler and linker docs, X16 hardware and register specs,
|
||||
the Amiga NDK (docs and includes), and other resources.
|
||||
Search in this folder first before looking online!
|
||||
|
||||
- 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`
|
||||
|
||||
## Essentials
|
||||
|
||||
@@ -34,13 +30,23 @@ repeating them here:
|
||||
- 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>`.
|
||||
- Options accept single-dash or double-dash (`-target` == `--target`). Use
|
||||
`-out <dir>` to place output in a directory, or `-o <file>` to name the exact
|
||||
final program artifact (all other output files then land beside it).
|
||||
|
||||
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()`.
|
||||
|
||||
Do not use `%option no_sysinit` on `amiga500` or `qemu68k` targets when the goal
|
||||
On any CBM machine with PETSCII (this includes the Commander X16), files
|
||||
on disk must only consist of 'safe' characters (`a-z`, `0-9`, `.-+!%`).
|
||||
Do NOT mix casing in filenames; in Prog8 source code, filenames should
|
||||
usually be written in *lowercase* letters. Other characters (such as
|
||||
`_` or uppercase mixed with lowercase) can make kernal file operations
|
||||
fail, depending on the drive/host layer.
|
||||
|
||||
Do not use `%option no_sysinit` on `amiga500`, `amiga1200`, 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
|
||||
@@ -57,9 +63,26 @@ libraries.
|
||||
- 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.
|
||||
- On 6502 and virtual targets, 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. Arrays are cleared once at startup here,
|
||||
not on every subroutine entry.
|
||||
- On the M68000 targets (`amiga500`, `amiga1200`, `qemu68k`) ordinary subroutine
|
||||
locals and parameters live in an activation record on the CPU stack, so true
|
||||
recursion and re-entrancy work. `link a5,#-N` / `unlk a5`, frame pointer `A5`,
|
||||
locals at negative offsets, parameters at positive offsets. Block-level
|
||||
variables, globals, `@shared`, memory slabs, and objects needing a permanent
|
||||
external address stay static. Clean locals are zeroed on every call
|
||||
including arrays, so the compiler warns for framed arrays over 8 bytes; use
|
||||
`@dirty` when you assign first, otherwise you read stack garbage.
|
||||
- Normal M68K calls use word-packed argument slots: byte/bool/word/uword arguments
|
||||
take a 2-byte slot, long/pointer/float take 4 bytes. The caller pushes arguments
|
||||
left-to-right and removes the complete argument area after the call. Slot bases
|
||||
accumulate right-to-left starting at positive `A5` offset 8, so a parameter's
|
||||
offset depends on the widths of the parameters to its right; locals use negative
|
||||
offsets. See `docs/source/technical.rst` for the full convention and static
|
||||
fallback rules.
|
||||
- `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()`
|
||||
@@ -80,8 +103,11 @@ registers survive calls. Long operations may clobber `R12-R15`.
|
||||
## Syntax Pitfalls
|
||||
|
||||
- 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.
|
||||
- There is no automatic type widening in expression results: `byte * byte`
|
||||
remains a byte. Cast explicitly when a wider result is needed. Assignments,
|
||||
call arguments, and mixed-type expressions do widen implicitly
|
||||
(`ubyte`/`byte` to `word`/`uword` to `long`); only same-width signedness
|
||||
reinterpretation (`uword` to `word` and back) needs an explicit `as` cast.
|
||||
- `&` 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
|
||||
@@ -100,16 +126,23 @@ 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.
|
||||
Replace a `for` loop with a `repeat` loop when the loop variable is not used in the loop body.
|
||||
Use `if_cs`, `if_cc`, `if_z`, and `if_nz` for direct CPU-flag branches.
|
||||
|
||||
## Subroutines and Assembly
|
||||
|
||||
- Subroutines can return zero, one, or multiple values:
|
||||
`a, b = routine()` or `void routine()`.
|
||||
- On M68K, ordinary calls preserve the caller's active frame. A `goto` or branch
|
||||
that leaves the current subroutine must tear that frame down before transferring
|
||||
control. The built-in `call(address)` is a zero-argument indirect call; it does
|
||||
not marshal stack arguments.
|
||||
- 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`.
|
||||
- On M68K targets never clobber `A5`, the stack frame pointer. On the Amiga
|
||||
targets never clobber `A6` either, it holds the AmigaOS library base.
|
||||
- `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
|
||||
@@ -133,8 +166,3 @@ Use `if_cs`, `if_cc`, `if_z`, and `if_nz` for direct CPU-flag branches.
|
||||
- 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.
|
||||
+4
-1
@@ -45,4 +45,7 @@ compiler/lib/
|
||||
.gradle
|
||||
sd*.img
|
||||
*.d64
|
||||
|
||||
AsyncIO/
|
||||
AsyncIO.info
|
||||
m68k-amigaos-ahi/
|
||||
Libs/
|
||||
Generated
-1
@@ -4,7 +4,6 @@
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="distributionType" value="LOCAL" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
|
||||
Generated
+2
-2
@@ -15,6 +15,6 @@
|
||||
</component>
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="externalSystemId" value="Gradle" />
|
||||
<option name="version" value="2.4.20" />
|
||||
<option name="version" value="2.4.21" />
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
@@ -13,17 +13,23 @@
|
||||
|
||||
## CHANGE / EDIT MODE
|
||||
|
||||
- Never implement features yourself when possible - use sub-agents!
|
||||
- Use a subagent for larger chunks of work
|
||||
- Identify changes from the plan that can be implemented in parallel, and use sub-agents to implement the features efficiently
|
||||
- When using sub-agents to implement features, act as a coordinator only
|
||||
- Use the best model for the task - premium models for complex tasks (like coding) and mid-tier models for simpler tasks, like documentation
|
||||
- After completing features (large or small), always run commands like lint, type check and next build to check code quality
|
||||
- After completing features (large or small), run the relevant quality commands (lint, type check, build) to check code quality.
|
||||
Only compiler/standard-library changes require a build: that means Kotlin sources (`.kt`), the grammar (`.g4`), or embedded
|
||||
standard library files (`compiler/res/prog8lib/**/*.p8` and `**/*.asm`). For changes limited to `examples/**`, `docs/**`, or
|
||||
other non-compiler files, verify with the target compiler directly instead (for example `prog8c -target <target> <file>`), and
|
||||
skip the build entirely
|
||||
(exception: if the change is only whitespace or code comments, a build or test run is not necessary)
|
||||
- `gradle build` already runs the full test suite; don't run a separate full `gradle test` before it, to avoid lengthy double test runs (targeted `--tests` filters to find failing tests are fine)
|
||||
|
||||
## Project Overview
|
||||
- This project is a compiler for the Prog8 programming language.
|
||||
- Prog8 is a programming language primarily targeting 8-bit retro systems with the 6502 CPU, such as the Commodore 64, Commodore 128, and Commander X16.
|
||||
- The compiler has a 6502 code generator backend, and an IR code generator.
|
||||
- The IR code is meant to be used in a new machine specific code generator backend (primarily 6502 but maybe 68000 as well later)
|
||||
- The IR code is used by machine-specific code generator backends, including 6502 and M68K.
|
||||
- The compiler includes a simple 'virtual machine' that can execute the IR code directly via interpretation.
|
||||
- Prog8 source files have .p8 extension – these are *not* LUA or PICO-8 source files in this case!
|
||||
- Prog8 source files are a "module" that can contain one or more "blocks". They can also import other modules, from internal library files or from source files on the filesystem.
|
||||
@@ -55,7 +61,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 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.
|
||||
- When writing or understanding M68K assembly code (amiga500, amiga1200, qemu68k targets), load the `m68k-coder` skill for vasm mot syntax and conventions.
|
||||
- M68K ordinary subroutine locals and parameters use `A5`-based stack frames. Normal calls pass word-packed argument slots on the stack (2-byte slots for byte/word args, 4-byte for wide args, byte values at the slot base), and the caller cleans up; see `docs/source/technical.rst` for the complete convention and frame eligibility rules.
|
||||
|
||||
|
||||
## DEBUGGING TIP: Use `-noopt` to isolate problems
|
||||
@@ -127,7 +134,19 @@ Steps when encountering a compiler crash:
|
||||
- **NO EMOJI in user documentation**. Do not use emoji or decorative unicode symbols in documentation files. Functional unicode symbols are acceptable when they serve a clear purpose (e.g., → for arrows, ± for plus-minus, × for multiplication, ° for degrees). Avoid decorative emoji like ❌ ✅ ⚠️ 🎉 etc.
|
||||
|
||||
### Code Style Guidelines
|
||||
**Minimal comments when making changes**: When modifying existing code, add only essential comments that explain *why* a change was made or document non-obvious behavior. **Do not add verbose comments** that restate what the code does; let the code speak for itself. Existing extensive comments should be preserved, but new changes should have minimal commentary.
|
||||
**Code comments**: Comments are the exception, not the norm. A comment earns its place only by recording
|
||||
something the code itself cannot say: the reason for a non-obvious choice, or a constraint that
|
||||
a well-meaning future edit would otherwise violate.
|
||||
|
||||
- Inline comment inside a function body: 1 line, never more.
|
||||
- KDoc on a declaration: 3 lines, unless a genuine non-obvious contract cannot be stated shorter.
|
||||
- Never restate the identifier, the type, or what the following line does.
|
||||
- Prefer a good name or a well-named function over a comment that explains it.
|
||||
|
||||
Before keeping any comment, delete it mentally and ask what the reader loses. If nothing, it goes.
|
||||
|
||||
Keep existing comments as they are; do not expand, reword, or reformat them while editing
|
||||
nearby lines - unless that would cause inconsistency.
|
||||
|
||||
**Kotlin string `$` handling**: The `$` character is special in Kotlin string literals (string interpolation). If you need a literal `$` character, use Kotlin's `$$"..."` interpolation prefix instead of backslash escaping (`\$`) or `${'$'}`. The `$$"..."` syntax is a fairly recent Kotlin feature that avoids nasty escaping. For example:
|
||||
```kotlin
|
||||
@@ -198,10 +217,12 @@ When the task involves writing or understanding `.p8` (Prog8 source) or `.p8ir`
|
||||
- This compiles AND installs, but still skips running tests (faster than `gradle build`)
|
||||
|
||||
### When to run full build with tests
|
||||
- **Before final verification**: Always run `gradle build` to ensure all tests pass
|
||||
- **Before final verification**: Run `gradle build` to ensure all tests pass - but only if the change actually touched the
|
||||
compiler or standard library (`.kt`, `.g4`, or `compiler/res/prog8lib/**`). For `examples/**` or `docs/**`-only changes,
|
||||
verify with `prog8c` directly instead and skip the build
|
||||
- **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 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.
|
||||
- **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 if the change touched compiler sources.
|
||||
|
||||
### Build command summary
|
||||
|
||||
@@ -218,13 +239,15 @@ When the task involves writing or understanding `.p8` (Prog8 source) or `.p8ir`
|
||||
- the prog8c compiler executable can be found in the compiler/build/install/prog8c/bin folder (this is already added to the shell's path)
|
||||
- **the `-check` switch performs a quick syntax/semantic check only; it will NOT produce any output files (no .prg, .asm, etc.)**. Use it only for fast error checking during development.
|
||||
- **the `-noopt` switch DISABLES all optimizations** - useful for debugging to determine if a problem is caused by the optimizer. **Optimizations are ENABLED by default** (no flag needed).
|
||||
- **prog8c uses single-dash command line options** (e.g., `-target`, `-noopt`, `-check`), NOT double-dash (`--target` is invalid).
|
||||
- **prog8c options accept single-dash or double-dash** (e.g., `-target` and `--target` are equivalent).
|
||||
- **the `-printast1` switch prints out the internal Compiler AST** after parsing and semantic analysis.
|
||||
- **the `-printast2` switch prints out the optimized Simple AST** just before it goes to the code generator. This is useful for debugging optimizer issues.
|
||||
- **the `-out outdir` switch sets an alternative output directory** for compiled files (.prg, .asm, .list, etc.). **By default, output files are written to the same directory as the source file**.
|
||||
- **the `-out outdir` switch sets an alternative output directory** for compiled files (.prg, .asm, .list, etc.). **By default, output files are written to the current working directory**.
|
||||
- **the `-o <file>` switch names the exact final program artifact** (also spelled `--output`); all other output files go in the same directory with the same stem. Cannot be combined with `-out` or with multiple source files.
|
||||
- **Other useful flags**: `-quiet` (suppress messages), `-warnimplicitcasts` (warn on implicit type widening), `-daemon` (keep a background compiler process alive — must be passed on every invocation)
|
||||
|
||||
### Compilation Output Files
|
||||
Names follow the source file name, or the stem of the `-o` file when that option is used.
|
||||
- `*.prg` - The final compiled program file for the target system (e.g., Commander X16)
|
||||
- `*.asm` - Generated assembly code from the Prog8 source
|
||||
- `*.list` - Generated full assembly listing file from the Prog8 source
|
||||
@@ -248,12 +271,21 @@ When the task involves writing or understanding `.p8` (Prog8 source) or `.p8ir`
|
||||
|
||||
### Automated Tests (gradle)
|
||||
Always use the `--console=plain` flag with these commands to avoid messing up the agent's text interface.
|
||||
- `gradle build --console=plain` - Full build including all tests (about 50s)
|
||||
- `gradle :compiler:test --console=plain` - Run only compiler tests (faster)
|
||||
- `gradle :compiler:test --tests "prog8tests.compiler.TestOptimization" --console=plain` - Run specific test class
|
||||
- `ionice -c 3 nice gradle build --console=plain` - Full build including all tests (about 50s)
|
||||
- `ionice -c 3 nice gradle :compiler:test --console=plain` - Run only compiler tests (faster)
|
||||
- `ionice -c 3 nice gradle :compiler:test --tests "prog8tests.compiler.TestOptimization" --console=plain` - Run specific test class
|
||||
|
||||
Note: By default, Gradle only shows failed tests. Passed and skipped tests are silent.
|
||||
|
||||
On Unix-like systems, prefix **every** gradle invocation with `nice` to lower its CPU priority,
|
||||
because gradle is CPU-heavy. This applies to all gradle commands (build, compile, test, install), not
|
||||
just `gradle build`. Use the same prefix for the quick compile checks and install commands in
|
||||
"Building and Installing the Compiler" above.
|
||||
|
||||
On **Linux** additionally put `ionice -c 3` in front (idle-class I/O scheduling), so the build doesn't
|
||||
compete for disk with whatever else is running: `ionice -c 3 nice gradle ...`. `ionice` is
|
||||
Linux-only (util-linux); on macOS/BSD just use `nice`.
|
||||
|
||||
**⚠️ CRITICAL: Test Filtering Patterns - Read This First!**
|
||||
|
||||
Gradle's `--tests` filter has **strict rules** that are easy to get wrong:
|
||||
@@ -287,6 +319,14 @@ gradle :compiler:compileTestKotlin --info 2>&1 | grep "^e:"
|
||||
- **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**
|
||||
|
||||
### TestCompilerOnExamples expected sizes
|
||||
`TestCompilerOnExamples` asserts exact output sizes per example. A code-generator change shifts them,
|
||||
so expect a handful of `expected:<N> but was:<M>` failures with no stack trace pointing at the cause.
|
||||
Attribute each one before editing: rebuild with the change reverted and compare the two generated
|
||||
`.asm` files, so the size delta is demonstrably yours and not a side effect. Then update the expected
|
||||
size. A delta of a few bytes from removed `ldy`/`lda` instructions is normal; a large or erratic delta
|
||||
means something else changed and needs investigating.
|
||||
|
||||
### Compiler Unit Test Snippets
|
||||
Prog8 code snippets embedded in the compiler's own Kotlin unit tests (e.g., in `TestAstChecks.kt`, `TestOptimization.kt`) follow different rules from standalone programs:
|
||||
- **Keep self-contained** — avoid `%import` directives (test setup may not have library search path configured)
|
||||
@@ -294,6 +334,11 @@ Prog8 code snippets embedded in the compiler's own Kotlin unit tests (e.g., in `
|
||||
- **Set `writeAssembly=false`** if the test only checks generated AST (much faster)
|
||||
- **Set `optimize=false`** by default for these snippets
|
||||
- **Do NOT use** the `%encoding iso` / `txt.iso()` / `sys.poweroff_system()` pattern — that's only for real CX16 emulator runs
|
||||
- **Do NOT** write compiler tests that depend on the `vasm` assembler: it is not always installed
|
||||
(CI only installs `64tass`). For m68k codegen tests (`amiga500`, `qemu68k` targets) pass
|
||||
`writeAssembly = true, assemble = false` to the `compileText`/`compileFile` helpers. This still
|
||||
generates the codegen AST to assert on but skips assembling, so no vasm is needed. If a test's
|
||||
assertions cannot be made from the AST alone, drop the test.
|
||||
|
||||
### Testing Assembly Output
|
||||
When testing generated assembly in Kotlin tests:
|
||||
@@ -310,6 +355,8 @@ When testing generated assembly in Kotlin tests:
|
||||
|
||||
**Testing tip**: When writing and testing Prog8 programs, **use the `virtual` target** (e.g., `prog8c -target virtual -emu input.p8` or `prog8c -vm input.p8ir`). This is the preferred way to test because the virtual target can easily write output to stdout, making it simple to verify program behavior and check results.
|
||||
|
||||
For the amiga targets, `-emu` launches FS-UAE (GUI) while `-emu2` launches Vamos (headless CLI). For automated/non-interactive testing, prefer `-emu2` (vamos) to avoid opening a GUI window.
|
||||
|
||||
**CX16 output verification**: Use `x16emu -echo iso -run -prg input.prg` to echo screen output to stdout **and auto-start the program**. The `-run` flag is **critical**: without it, the program loads but doesn't execute, so you'll see no output. Pipe through `strings` or `grep` to filter: `x16emu -echo iso -run -prg input.prg 2>&1 | grep -E "(PASS|FAIL)"`.
|
||||
**IMPORTANT**: Always add `%encoding iso` at the top of your source file and call `txt.iso()` in `start()`. This prevents PETSCII→ISO charset translation errors that garble uppercase/special characters and make output unreadable:
|
||||
```prog8
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
;
|
||||
; As the X16 computer is a more or less fixed system, it's not very useful
|
||||
; to benchmark the computer itself with.
|
||||
;
|
||||
; The individual scores are iteration counts, and they differ by more than an
|
||||
; order of magnitude between benchmarks (maze does ~3000, game of life ~180).
|
||||
; Summing the raw counts therefore let one or two benchmarks dominate the total
|
||||
; and hide regressions in all the others, so the total is a weighted sum - see
|
||||
; benchmark_weight below.
|
||||
|
||||
|
||||
%import textio
|
||||
@@ -26,7 +32,15 @@ main {
|
||||
|
||||
str[20] benchmark_names
|
||||
uword[20] benchmark_score
|
||||
str version = "12.1"
|
||||
; Weight per benchmark, in the same order as the benchmarks are run below. The total is the
|
||||
; weighted sum of the raw iteration counts.
|
||||
; The raw counts differ by more than an order of magnitude (maze ~3064, game of life ~180), so
|
||||
; in an unweighted sum the two heaviest - maze solver and text-elite - were 57% of the total
|
||||
; while game of life was 2%. A regression in a small benchmark was therefore nearly invisible.
|
||||
; Their weights are scaled down so that every benchmark contributes roughly comparably; the
|
||||
; other eight are left at 256 (i.e. unweighted).
|
||||
uword[10] benchmark_weight = [ 64, 256, 256, 256, 256, 256, 256, 64, 256, 256 ]
|
||||
str version = "14.0"
|
||||
|
||||
|
||||
sub start() {
|
||||
@@ -96,11 +110,14 @@ main {
|
||||
txt.print(")\n\n")
|
||||
|
||||
do {
|
||||
; Weighted contribution. Done in long arithmetic on purpose: score*weight overflows a
|
||||
; uword (3064*256 is 784384, which wraps to 63488 and would corrupt the total).
|
||||
uword weighted = ((benchmark_score[benchmark_number] as long) * benchmark_weight[benchmark_number] / 256) as uword
|
||||
txt.spc()
|
||||
txt.print_uw(benchmark_score[benchmark_number])
|
||||
txt.print_uw(weighted)
|
||||
txt.column(6)
|
||||
txt.print(benchmark_names[benchmark_number])
|
||||
total_score += benchmark_score[benchmark_number]
|
||||
total_score += weighted
|
||||
txt.nl()
|
||||
benchmark_number++
|
||||
} until benchmark_names[benchmark_number]==0
|
||||
|
||||
+5
-3
@@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.20"
|
||||
kotlin("jvm") version "2.4.21"
|
||||
}
|
||||
|
||||
allprojects {
|
||||
@@ -72,8 +72,10 @@ subprojects {
|
||||
// Enable JUnit 5 (required for Kotest)
|
||||
useJUnitPlatform()
|
||||
|
||||
val testConcurrency = project.findProperty("testConcurrency")?.toString()?.toIntOrNull()
|
||||
?: (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
|
||||
// Forked test JVMs each reserve up to -PtestMaxHeap, so keep the fork count
|
||||
// bounded even on many-core machines to avoid swapping during parallel builds.
|
||||
val testConcurrency = (project.findProperty("testConcurrency")?.toString()?.toIntOrNull()
|
||||
?: (Runtime.getRuntime().availableProcessors() / 2)).coerceIn(1, 4)
|
||||
project.findProperty("testMaxHeap")?.toString()?.let { maxHeapSize = it }
|
||||
|
||||
// Enable concurrent test execution for Kotest
|
||||
|
||||
@@ -40,12 +40,10 @@ object SymbolNames {
|
||||
* See https://github.com/irmen/prog8/issues/198
|
||||
*/
|
||||
fun stripPrefixes(scopedName: String): String {
|
||||
return scopedName.split('.')
|
||||
.map { part ->
|
||||
if(part.length > 4 && part.startsWith("p8") && part[2].isLetter() && part[3] == '_')
|
||||
part.drop(4)
|
||||
else part
|
||||
}
|
||||
.joinToString(".")
|
||||
return scopedName.split('.').joinToString(".") { part ->
|
||||
if (part.length > 4 && part.startsWith("p8") && part[2].isLetter() && part[3] == '_')
|
||||
part.drop(4)
|
||||
else part
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
package prog8.code.assembly
|
||||
|
||||
import prog8.code.GENERATED_LABEL_PREFIX
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.ICompilationTarget
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.code.core.OutputType
|
||||
import prog8.code.core.*
|
||||
import prog8.code.target.C128Target
|
||||
import prog8.code.target.C64Target
|
||||
import prog8.code.target.PETTarget
|
||||
@@ -13,25 +10,20 @@ import java.nio.file.Path
|
||||
|
||||
class AssemblyProgram6502(
|
||||
override val name: String,
|
||||
outputDir: Path,
|
||||
private val options: CompilationOptions,
|
||||
private val compTarget: ICompilationTarget) : IAssemblyProgram {
|
||||
|
||||
override val irInstructionCount: Int = 0
|
||||
override val irChunkCount: Int = 0
|
||||
override val irRegisterCount: Int = 0
|
||||
|
||||
private val assemblyFile = outputDir.resolve("$name.asm")
|
||||
private val prgFile = outputDir.resolve("$name.prg") // CBM prg executable program
|
||||
private val xexFile = outputDir.resolve("$name.xex") // Atari xex executable program
|
||||
private val binFile = outputDir.resolve("$name.bin")
|
||||
private val viceMonListFile = outputDir.resolve(C64Target.viceMonListName(name))
|
||||
private val listFile = outputDir.resolve("$name.list")
|
||||
|
||||
override fun assemble(options: CompilationOptions, errors: IErrorReporter): Boolean {
|
||||
|
||||
val assemblerCommand: List<String>
|
||||
val outs = OutputFiles.of(options, name)
|
||||
|
||||
fun addRemainingOptions(command: MutableList<String>, program: Path, assembly: Path): List<String> {
|
||||
// keep additionalAssemblerOptions BEFORE --output: a target config may contain its own --output
|
||||
if(options.compTarget.additionalAssemblerOptions.isNotEmpty())
|
||||
command.addAll(options.compTarget.additionalAssemblerOptions)
|
||||
|
||||
@@ -44,7 +36,7 @@ class AssemblyProgram6502(
|
||||
// CBM machines .prg generation.
|
||||
|
||||
val command = mutableListOf("64tass", "--cbm-prg", "--ascii", "--case-sensitive", "--long-branch",
|
||||
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=$viceMonListFile")
|
||||
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=${outs.viceMonList()}")
|
||||
|
||||
if(options.warnSymbolShadowing)
|
||||
command.add("-Wshadow")
|
||||
@@ -55,10 +47,10 @@ class AssemblyProgram6502(
|
||||
command.add("--quiet")
|
||||
|
||||
if(options.asmListfile) {
|
||||
command.add("--list=$listFile")
|
||||
command.add("--list=${outs.list()}")
|
||||
}
|
||||
|
||||
assemblerCommand = addRemainingOptions(command, prgFile, assemblyFile)
|
||||
assemblerCommand = addRemainingOptions(command, outs.programFile, outs.asm())
|
||||
if(!options.quiet)
|
||||
println("\nCreating prg for target ${compTarget.name}.")
|
||||
}
|
||||
@@ -66,7 +58,7 @@ class AssemblyProgram6502(
|
||||
// Atari800XL .xex generation.
|
||||
|
||||
val command = mutableListOf("64tass", "--atari-xex", "--case-sensitive", "--long-branch",
|
||||
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=$viceMonListFile")
|
||||
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=${outs.viceMonList()}")
|
||||
|
||||
if(options.warnSymbolShadowing)
|
||||
command.add("-Wshadow")
|
||||
@@ -77,16 +69,16 @@ class AssemblyProgram6502(
|
||||
command.add("--quiet")
|
||||
|
||||
if(options.asmListfile)
|
||||
command.add("--list=$listFile")
|
||||
command.add("--list=${outs.list()}")
|
||||
|
||||
assemblerCommand = addRemainingOptions(command,xexFile, assemblyFile)
|
||||
assemblerCommand = addRemainingOptions(command, outs.programFile, outs.asm())
|
||||
if(!options.quiet)
|
||||
println("\nCreating xex for target ${compTarget.name}.")
|
||||
}
|
||||
OutputType.RAW -> {
|
||||
// Neo6502/headerless raw program generation.
|
||||
val command = mutableListOf("64tass", "--nostart", "--case-sensitive", "--long-branch",
|
||||
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=$viceMonListFile")
|
||||
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=${outs.viceMonList()}")
|
||||
|
||||
if(options.warnSymbolShadowing)
|
||||
command.add("-Wshadow")
|
||||
@@ -97,9 +89,9 @@ class AssemblyProgram6502(
|
||||
command.add("--quiet")
|
||||
|
||||
if(options.asmListfile)
|
||||
command.add("--list=$listFile")
|
||||
command.add("--list=${outs.list()}")
|
||||
|
||||
assemblerCommand = addRemainingOptions(command, binFile, assemblyFile)
|
||||
assemblerCommand = addRemainingOptions(command, outs.programFile, outs.asm())
|
||||
if(!options.quiet)
|
||||
println("\nCreating raw binary for target ${compTarget.name}.")
|
||||
}
|
||||
@@ -107,7 +99,7 @@ class AssemblyProgram6502(
|
||||
// CBM machines library (.bin) generation (with or without 2 byte load address header depending on the compilation target machine)
|
||||
|
||||
val command = mutableListOf("64tass", "--ascii", "--case-sensitive", "--long-branch",
|
||||
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=$viceMonListFile")
|
||||
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=${outs.viceMonList()}")
|
||||
|
||||
if(options.warnSymbolShadowing)
|
||||
command.add("-Wshadow")
|
||||
@@ -118,7 +110,7 @@ class AssemblyProgram6502(
|
||||
command.add("--quiet")
|
||||
|
||||
if(options.asmListfile)
|
||||
command.add("--list=$listFile")
|
||||
command.add("--list=${outs.list()}")
|
||||
|
||||
if(compTarget.name in listOf(C64Target.NAME, C128Target.NAME, PETTarget.NAME)) {
|
||||
if(!options.quiet)
|
||||
@@ -130,7 +122,7 @@ class AssemblyProgram6502(
|
||||
command.add("--nostart") // should be headerless bin, because basic has problems doing a normal LOAD"lib",8,1 - need to use BLOAD
|
||||
}
|
||||
|
||||
assemblerCommand = addRemainingOptions(command, binFile, assemblyFile)
|
||||
assemblerCommand = addRemainingOptions(command, outs.programFile, outs.asm())
|
||||
}
|
||||
else -> error("Unsupported output type: ${compTarget.defaultOutputType}")
|
||||
}
|
||||
@@ -159,16 +151,16 @@ class AssemblyProgram6502(
|
||||
|
||||
val result = process.waitFor()
|
||||
if (result == 0) {
|
||||
removeGeneratedLabelsFromMonlist()
|
||||
generateBreakpointList()
|
||||
removeGeneratedLabelsFromMonlist(outs)
|
||||
generateBreakpointList(outs)
|
||||
}
|
||||
return result==0
|
||||
}
|
||||
|
||||
private fun removeGeneratedLabelsFromMonlist() {
|
||||
private fun removeGeneratedLabelsFromMonlist(outs: OutputFiles) {
|
||||
val pattern = Regex("""al (\w+) \S+$GENERATED_LABEL_PREFIX.+?""")
|
||||
val lines = viceMonListFile.toFile().readLines()
|
||||
viceMonListFile.toFile().outputStream().bufferedWriter().use {
|
||||
val lines = outs.viceMonList().toFile().readLines()
|
||||
outs.viceMonList().toFile().outputStream().bufferedWriter().use {
|
||||
for (line in lines) {
|
||||
if(pattern.matchEntire(line)==null)
|
||||
it.write(line+"\n")
|
||||
@@ -176,11 +168,11 @@ class AssemblyProgram6502(
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateBreakpointList() {
|
||||
private fun generateBreakpointList(outs: OutputFiles) {
|
||||
// builds list of breakpoints, appends to monitor list file
|
||||
val breakpoints = mutableListOf<String>()
|
||||
val pattern = Regex("""al (\w+) \S+_prog8_breakpoint_\d+.?""") // gather breakpoints by the source label that's generated for them
|
||||
for (line in viceMonListFile.toFile().readLines()) {
|
||||
for (line in outs.viceMonList().toFile().readLines()) {
|
||||
val match = pattern.matchEntire(line)
|
||||
if (match != null)
|
||||
breakpoints.add("break $" + match.groupValues[1])
|
||||
@@ -189,7 +181,7 @@ class AssemblyProgram6502(
|
||||
breakpoints.add(0, "; breakpoint list now follows")
|
||||
breakpoints.add(1, "; $num breakpoints have been defined")
|
||||
breakpoints.add(2, "del")
|
||||
viceMonListFile.toFile().appendText(breakpoints.joinToString("\n") + "\n")
|
||||
outs.viceMonList().toFile().appendText(breakpoints.joinToString("\n") + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, BaseDataType.ARRAY_POINTER)
|
||||
private val IterableDatatypes = arrayOf(BaseDataType.STR, BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW)
|
||||
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)
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@ class CompilationOptions(val output: OutputType,
|
||||
var quiet: Boolean = false,
|
||||
var profilingInstrumentation: Boolean = false,
|
||||
var symbolDefs: Map<String, String> = emptyMap(),
|
||||
var privateSymbols: Boolean = false
|
||||
var privateSymbols: Boolean = false,
|
||||
var outputFile: Path? = null
|
||||
) {
|
||||
init {
|
||||
compTarget.initializeMemoryAreas(this)
|
||||
@@ -77,6 +78,7 @@ class CompilationOptions(val output: OutputType,
|
||||
private var profilingInstrumentation: Boolean = false
|
||||
private var newCodegen: Boolean = false
|
||||
private var symbolDefs: Map<String, String> = emptyMap()
|
||||
private var outputFile: Path? = null
|
||||
|
||||
fun output(output: OutputType) = apply { this.output = output }
|
||||
fun launcher(launcher: CbmPrgLauncherType) = apply { this.launcher = launcher }
|
||||
@@ -107,6 +109,7 @@ class CompilationOptions(val output: OutputType,
|
||||
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 outputFile(outputFile: Path?) = apply { this.outputFile = outputFile }
|
||||
|
||||
fun build(): CompilationOptions {
|
||||
return CompilationOptions(
|
||||
@@ -115,7 +118,7 @@ class CompilationOptions(val output: OutputType,
|
||||
includeSourcelines, dumpVariables, dumpSymbols, newCodegen, varsHighBank, varsGolden,
|
||||
breakpointCpuInstruction, ignoreFootguns, outputDir, quiet,
|
||||
profilingInstrumentation, symbolDefs
|
||||
)
|
||||
).apply { this.outputFile = this@Builder.outputFile }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package prog8.code.core
|
||||
|
||||
import java.util.*
|
||||
import java.util.Objects
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
@@ -20,7 +22,6 @@ 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 or longs), subtype is whatever type each element points to
|
||||
UNDEFINED;
|
||||
|
||||
|
||||
@@ -91,11 +92,10 @@ val BaseDataType.isSigned get() = this in setOf(BaseDataType.BYTE, BaseDataType.
|
||||
val BaseDataType.isUnsigned get() = !this.isSigned
|
||||
val BaseDataType.isSignedInteger get() = this.isSigned && this.isInteger
|
||||
val BaseDataType.isUnsignedInteger get() = this.isUnsigned && this.isInteger
|
||||
val BaseDataType.isArray get() = this == BaseDataType.ARRAY || this == BaseDataType.ARRAY_SPLITW || this == BaseDataType.ARRAY_POINTER
|
||||
val BaseDataType.isArray get() = this == BaseDataType.ARRAY || this == BaseDataType.ARRAY_SPLITW
|
||||
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.isIterable get() = this in setOf(BaseDataType.STR, BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW, BaseDataType.ARRAY_POINTER)
|
||||
val BaseDataType.isIterable get() = this in setOf(BaseDataType.STR, BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW)
|
||||
val BaseDataType.isPassByRef get() = this.isIterable && !this.isPointer
|
||||
val BaseDataType.isPassByValue get() = !this.isIterable || this.isPointer
|
||||
|
||||
@@ -126,14 +126,15 @@ interface ISubType {
|
||||
class DataType private constructor(
|
||||
val base: BaseDataType,
|
||||
val sub: BaseDataType?,
|
||||
val pointeeSub: BaseDataType?,
|
||||
var subType: ISubType?,
|
||||
var subTypeFromAntlr: List<String>? = null
|
||||
) {
|
||||
|
||||
init {
|
||||
when {
|
||||
base.isPointerArray -> {
|
||||
require(sub!=null || subType!=null || subTypeFromAntlr!=null)
|
||||
base.isArray && sub == BaseDataType.POINTER -> {
|
||||
require(pointeeSub != null || subType != null || subTypeFromAntlr != null)
|
||||
}
|
||||
base.isArray -> {
|
||||
require(sub != null)
|
||||
@@ -155,7 +156,7 @@ class DataType private constructor(
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is DataType) return false
|
||||
if (base != other.base || sub != other.sub) return false
|
||||
if (base != other.base || sub != other.sub || pointeeSub != other.pointeeSub) return false
|
||||
val st1 = subType
|
||||
val st2 = other.subType
|
||||
if (st1 == st2) return true
|
||||
@@ -163,7 +164,7 @@ class DataType private constructor(
|
||||
return st1.sameas(st2)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = Objects.hash(base, sub, subType)
|
||||
override fun hashCode(): Int = Objects.hash(base, sub, pointeeSub, subType)
|
||||
|
||||
fun setActualSubType(actualSubType: ISubType) {
|
||||
subType = actualSubType
|
||||
@@ -176,73 +177,72 @@ class DataType private constructor(
|
||||
|
||||
companion object {
|
||||
|
||||
val UBYTE = DataType(BaseDataType.UBYTE, null, null)
|
||||
val BYTE = DataType(BaseDataType.BYTE, null, null)
|
||||
val UWORD = DataType(BaseDataType.UWORD, null, null)
|
||||
val WORD = DataType(BaseDataType.WORD, null, null)
|
||||
val LONG = DataType(BaseDataType.LONG, null, null)
|
||||
val FLOAT = DataType(BaseDataType.FLOAT, null, null)
|
||||
val BOOL = DataType(BaseDataType.BOOL, null, null)
|
||||
val STR = DataType(BaseDataType.STR, BaseDataType.UBYTE, null)
|
||||
val UNDEFINED = DataType(BaseDataType.UNDEFINED, null, null)
|
||||
val UBYTE = DataType(BaseDataType.UBYTE, null, null, null)
|
||||
val BYTE = DataType(BaseDataType.BYTE, null, null, null)
|
||||
val UWORD = DataType(BaseDataType.UWORD, null, null, null)
|
||||
val WORD = DataType(BaseDataType.WORD, null, null, null)
|
||||
val LONG = DataType(BaseDataType.LONG, null, null, null)
|
||||
val FLOAT = DataType(BaseDataType.FLOAT, null, null, null)
|
||||
val BOOL = DataType(BaseDataType.BOOL, null, null, null)
|
||||
val STR = DataType(BaseDataType.STR, BaseDataType.UBYTE, null, null)
|
||||
val UNDEFINED = DataType(BaseDataType.UNDEFINED, null, null, null)
|
||||
|
||||
private val simpletypes = mapOf(
|
||||
BaseDataType.UBYTE to DataType(BaseDataType.UBYTE, null, null),
|
||||
BaseDataType.BYTE to DataType(BaseDataType.BYTE, null, null),
|
||||
BaseDataType.UWORD to DataType(BaseDataType.UWORD, null, null),
|
||||
BaseDataType.WORD to DataType(BaseDataType.WORD, null, null),
|
||||
BaseDataType.LONG to DataType(BaseDataType.LONG, null, null),
|
||||
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)
|
||||
BaseDataType.UBYTE to DataType(BaseDataType.UBYTE, null, null, null),
|
||||
BaseDataType.BYTE to DataType(BaseDataType.BYTE, null, null, null),
|
||||
BaseDataType.UWORD to DataType(BaseDataType.UWORD, null, null, null),
|
||||
BaseDataType.WORD to DataType(BaseDataType.WORD, null, null, null),
|
||||
BaseDataType.LONG to DataType(BaseDataType.LONG, null, null, null),
|
||||
BaseDataType.FLOAT to DataType(BaseDataType.FLOAT, null, null, null),
|
||||
BaseDataType.BOOL to DataType(BaseDataType.BOOL, null, null, null),
|
||||
BaseDataType.STR to DataType(BaseDataType.STR, BaseDataType.UBYTE, null, null),
|
||||
BaseDataType.POINTER to DataType(BaseDataType.POINTER, null, null, null),
|
||||
BaseDataType.UNDEFINED to DataType(BaseDataType.UNDEFINED, null, null, null)
|
||||
)
|
||||
|
||||
fun forDt(dt: BaseDataType): DataType {
|
||||
if(dt.isStructInstance)
|
||||
return DataType(BaseDataType.STRUCT_INSTANCE, null, null)
|
||||
return DataType(BaseDataType.STRUCT_INSTANCE, null, null, null)
|
||||
return simpletypes.getValue(dt)
|
||||
}
|
||||
|
||||
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" }
|
||||
// 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).
|
||||
require(elementDt != BaseDataType.POINTER) { "use arrayOfPointersTo for arrays of pointers" }
|
||||
val actualElementDt = if(elementDt==BaseDataType.STR) {
|
||||
memsizer.pointerBaseType
|
||||
} else elementDt
|
||||
if(actualElementDt.isNumericOrBool)
|
||||
return DataType(BaseDataType.ARRAY, actualElementDt, null)
|
||||
return DataType(BaseDataType.ARRAY, actualElementDt, null, 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)
|
||||
return DataType(BaseDataType.ARRAY_SPLITW, elementDt, null, null)
|
||||
}
|
||||
|
||||
fun arrayOfPointersTo(sub: BaseDataType): DataType = DataType(BaseDataType.ARRAY_POINTER, sub, null)
|
||||
fun arrayOfPointersTo(structType: ISubType?): DataType = DataType(BaseDataType.ARRAY_POINTER, null, structType)
|
||||
fun arrayOfPointersTo(sub: BaseDataType): DataType =
|
||||
DataType(BaseDataType.ARRAY, BaseDataType.POINTER, sub, null)
|
||||
fun arrayOfPointersTo(structType: ISubType?): DataType =
|
||||
DataType(BaseDataType.ARRAY, BaseDataType.POINTER, BaseDataType.STRUCT_INSTANCE, structType)
|
||||
fun arrayOfPointersFromAntlrTo(sub: BaseDataType?, identifier: List<String>?): DataType =
|
||||
DataType(BaseDataType.ARRAY_POINTER, sub, null, identifier)
|
||||
DataType(BaseDataType.ARRAY, BaseDataType.POINTER, sub ?: BaseDataType.STRUCT_INSTANCE, 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 arrayOfStructs(structType: ISubType): DataType = DataType(BaseDataType.ARRAY, BaseDataType.STRUCT_INSTANCE, null, structType)
|
||||
fun arrayOfStructsFromAntlr(struct: List<String>): DataType =
|
||||
DataType(BaseDataType.ARRAY, BaseDataType.STRUCT_INSTANCE, null, subTypeFromAntlr = struct)
|
||||
DataType(BaseDataType.ARRAY, BaseDataType.STRUCT_INSTANCE, null, null, subTypeFromAntlr = struct)
|
||||
|
||||
fun pointer(base: BaseDataType): DataType = DataType(BaseDataType.POINTER, base, null)
|
||||
fun pointer(base: BaseDataType): DataType = DataType(BaseDataType.POINTER, base, null, null)
|
||||
fun pointer(dt: DataType): DataType = if(dt.isBasic)
|
||||
DataType(BaseDataType.POINTER, dt.base, null)
|
||||
DataType(BaseDataType.POINTER, dt.base, null, null)
|
||||
else
|
||||
DataType(BaseDataType.POINTER, null, dt.subType, dt.subTypeFromAntlr)
|
||||
fun pointer(structType: ISubType): DataType = DataType(BaseDataType.POINTER, null, structType)
|
||||
fun pointerFromAntlr(identifier: List<String>): DataType = DataType(BaseDataType.POINTER, null, null, identifier)
|
||||
fun structInstance(type: ISubType?): DataType = DataType(BaseDataType.STRUCT_INSTANCE, sub=null, type)
|
||||
fun structInstanceFromAntlr(struct: List<String>): DataType = DataType(BaseDataType.STRUCT_INSTANCE, null, null, subTypeFromAntlr = struct)
|
||||
DataType(BaseDataType.POINTER, null, null, dt.subType, dt.subTypeFromAntlr)
|
||||
fun pointer(structType: ISubType): DataType = DataType(BaseDataType.POINTER, null, null, structType)
|
||||
fun pointerFromAntlr(identifier: List<String>): DataType = DataType(BaseDataType.POINTER, null, null, null, identifier)
|
||||
fun structInstance(type: ISubType?): DataType = DataType(BaseDataType.STRUCT_INSTANCE, sub=null, pointeeSub=null, type)
|
||||
fun structInstanceFromAntlr(struct: List<String>): DataType = DataType(BaseDataType.STRUCT_INSTANCE, null, null, null, subTypeFromAntlr = struct)
|
||||
}
|
||||
|
||||
|
||||
@@ -267,29 +267,30 @@ class DataType private constructor(
|
||||
arrayOfStructsFromAntlr(subTypeFromAntlr!!)
|
||||
}
|
||||
|
||||
if(!target.cpu.is6502) {
|
||||
return if (base.isPointer)
|
||||
pointerArray()
|
||||
else
|
||||
arrayFor(base, target)
|
||||
}
|
||||
if(base.isPointer)
|
||||
return pointerArray()
|
||||
|
||||
if(!target.cpu.is6502)
|
||||
return 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)
|
||||
isPointerArray -> {
|
||||
if (subType != null) DataType(BaseDataType.POINTER, null, null, subType)
|
||||
else if (subTypeFromAntlr != null) DataType(BaseDataType.POINTER, null, null, null, subTypeFromAntlr)
|
||||
else DataType(BaseDataType.POINTER, pointeeSub!!, null, null)
|
||||
}
|
||||
base.isArray || base==BaseDataType.STR -> {
|
||||
if (sub == BaseDataType.STRUCT_INSTANCE && subType != null)
|
||||
return DataType(BaseDataType.STRUCT_INSTANCE, null, subType)
|
||||
return DataType(BaseDataType.STRUCT_INSTANCE, null, null, subType)
|
||||
val dt = forDt(sub!!)
|
||||
if (subType != null) DataType(BaseDataType.POINTER, null, subType) else dt
|
||||
if (subType != null) DataType(BaseDataType.POINTER, null, null, subType) else dt
|
||||
}
|
||||
else -> throw IllegalArgumentException("not an array")
|
||||
}
|
||||
@@ -307,7 +308,7 @@ class DataType private constructor(
|
||||
return untypedPointerType
|
||||
if (isArray) {
|
||||
val isSplit = base == BaseDataType.ARRAY_SPLITW ||
|
||||
(base == BaseDataType.ARRAY_POINTER && mem.memorySize(BaseDataType.POINTER) <= 2)
|
||||
(isPointerArray && mem.memorySize(BaseDataType.POINTER) <= 2)
|
||||
if (msb || isSplit)
|
||||
return pointer(BaseDataType.UBYTE)
|
||||
val elementDt = elementType()
|
||||
@@ -322,12 +323,12 @@ class DataType private constructor(
|
||||
}
|
||||
|
||||
fun dereference(): DataType {
|
||||
require(isPointer || isUnsignedWord) { "cannot dereference non-pointer type ${this}"}
|
||||
require(isPointer || isUnsignedWord) { "cannot dereference non-pointer type $this"}
|
||||
return when {
|
||||
isUnsignedWord -> forDt(BaseDataType.UBYTE)
|
||||
sub!=null -> forDt(sub)
|
||||
subType!=null -> DataType(BaseDataType.STRUCT_INSTANCE, null, subType)
|
||||
subTypeFromAntlr!=null -> DataType(BaseDataType.STRUCT_INSTANCE, null, null, subTypeFromAntlr)
|
||||
subType!=null -> DataType(BaseDataType.STRUCT_INSTANCE, null, null, subType)
|
||||
subTypeFromAntlr!=null -> DataType(BaseDataType.STRUCT_INSTANCE, null, null, null, subTypeFromAntlr)
|
||||
else -> throw IllegalArgumentException("cannot dereference this pointer type")
|
||||
}
|
||||
}
|
||||
@@ -342,6 +343,12 @@ class DataType private constructor(
|
||||
BaseDataType.UBYTE -> "ubyte[]"
|
||||
BaseDataType.UWORD -> "uword[]"
|
||||
BaseDataType.LONG -> "long[]"
|
||||
BaseDataType.POINTER -> {
|
||||
val pointee = if (subType != null) subType!!.scopedNameString
|
||||
else if (subTypeFromAntlr != null) "$subTypeFromAntlr"
|
||||
else pointeeSub!!.name.lowercase()
|
||||
"^^$pointee[]"
|
||||
}
|
||||
BaseDataType.STRUCT_INSTANCE -> if (subType != null) "${subType!!.scopedNameString}[]" else "$subTypeFromAntlr[]"
|
||||
else -> throw IllegalArgumentException("invalid sub type")
|
||||
}
|
||||
@@ -356,9 +363,6 @@ class DataType private constructor(
|
||||
BaseDataType.POINTER -> {
|
||||
if(sub!=null) "^^${sub.name.lowercase()}" else if(subType!=null) "^^${subType!!.scopedNameString}" else "^^${subTypeFromAntlr}"
|
||||
}
|
||||
BaseDataType.ARRAY_POINTER -> {
|
||||
if(sub!=null) "^^${sub.name.lowercase()}[] (split)" else if (subType!=null) "^^${subType!!.scopedNameString}[] (split)" else "^^${subTypeFromAntlr}[] (split)"
|
||||
}
|
||||
BaseDataType.STRUCT_INSTANCE -> {
|
||||
sub?.name?.lowercase() ?: if (subType!=null) subType!!.scopedNameString else "$subTypeFromAntlr"
|
||||
}
|
||||
@@ -390,14 +394,6 @@ class DataType private constructor(
|
||||
else -> "?????"
|
||||
}
|
||||
}
|
||||
BaseDataType.ARRAY_POINTER -> {
|
||||
when {
|
||||
sub!=null -> "^^${sub.name.lowercase()}["
|
||||
subType!=null -> "^^${subType!!.scopedNameString}["
|
||||
subTypeFromAntlr!=null -> "^^${subTypeFromAntlr!!.joinToString(".")}["
|
||||
else -> "????? ["
|
||||
}
|
||||
}
|
||||
BaseDataType.ARRAY -> {
|
||||
when(sub) {
|
||||
BaseDataType.UBYTE -> "ubyte["
|
||||
@@ -407,6 +403,12 @@ class DataType private constructor(
|
||||
BaseDataType.WORD -> "@nosplit word["
|
||||
BaseDataType.LONG -> "long["
|
||||
BaseDataType.FLOAT -> "float["
|
||||
BaseDataType.POINTER -> {
|
||||
val pointee = if (subType != null) "^^${subType!!.scopedNameString}["
|
||||
else if (subTypeFromAntlr != null) "^^${subTypeFromAntlr!!.joinToString(".")}["
|
||||
else "^^${pointeeSub!!.name.lowercase()}["
|
||||
pointee
|
||||
}
|
||||
BaseDataType.STRUCT_INSTANCE -> if (subType != null) "${subType!!.scopedNameString}[" else "$subTypeFromAntlr["
|
||||
else -> throw IllegalArgumentException("invalid sub type")
|
||||
}
|
||||
@@ -430,12 +432,20 @@ class DataType private constructor(
|
||||
BaseDataType.BOOL -> targetType.base == BaseDataType.BOOL
|
||||
BaseDataType.UBYTE -> targetType.base in setOf(BaseDataType.UBYTE, BaseDataType.WORD, BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.FLOAT)
|
||||
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.UWORD -> targetType.base in setOf(BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.FLOAT, BaseDataType.POINTER) || targetType.isPointerArray
|
||||
BaseDataType.WORD -> targetType.base in setOf(BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
|
||||
BaseDataType.LONG -> targetType.base in setOf(BaseDataType.LONG, BaseDataType.FLOAT, BaseDataType.POINTER, BaseDataType.ARRAY_POINTER)
|
||||
BaseDataType.LONG -> targetType.base in setOf(BaseDataType.LONG, BaseDataType.FLOAT, BaseDataType.POINTER) || targetType.isPointerArray
|
||||
BaseDataType.FLOAT -> targetType.base in arrayOf(BaseDataType.FLOAT)
|
||||
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.ARRAY, BaseDataType.ARRAY_SPLITW -> {
|
||||
if (isPointerArray) {
|
||||
targetType.isPointerArray && if (subType != null && targetType.subType != null) subType!!.sameas(targetType.subType!!)
|
||||
else if (subType == null && targetType.subType == null) pointeeSub == targetType.pointeeSub
|
||||
else false
|
||||
} else {
|
||||
targetType.base in setOf(BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW) && targetType.sub == sub
|
||||
}
|
||||
}
|
||||
BaseDataType.POINTER -> {
|
||||
when {
|
||||
targetType.base == BaseDataType.UWORD || targetType.base == BaseDataType.LONG -> true
|
||||
@@ -443,7 +453,6 @@ 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.UNDEFINED -> false
|
||||
}
|
||||
@@ -457,13 +466,12 @@ class DataType private constructor(
|
||||
* Note: for pointer types, size() doesn't return the size of the pointer itself
|
||||
* but the size of the thing it points to.
|
||||
*/
|
||||
fun size(memsizer: IMemSizer): Int = if(sub!=null) {
|
||||
memsizer.memorySize(sub)
|
||||
} else if(subType!=null) {
|
||||
subType!!.memsize(memsizer)
|
||||
} else {
|
||||
memsizer.memorySize(base)
|
||||
}
|
||||
fun size(memsizer: IMemSizer): Int = when {
|
||||
base == BaseDataType.ARRAY && sub == BaseDataType.POINTER -> memsizer.memorySize(BaseDataType.POINTER)
|
||||
sub!=null -> memsizer.memorySize(sub)
|
||||
subType!=null -> subType!!.memsize(memsizer)
|
||||
else -> memsizer.memorySize(base)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DataType Properties
|
||||
@@ -492,16 +500,16 @@ class DataType private constructor(
|
||||
val isPointerToByte = base.isPointer && sub?.isByteOrBool==true
|
||||
val isPointerToWord = base.isPointer && sub?.isWord==true
|
||||
val isStructInstance = base.isStructInstance
|
||||
val isPointerArray = base.isPointerArray
|
||||
val isBoolArray = base.isArray && !base.isPointerArray && sub == BaseDataType.BOOL
|
||||
val isByteArray = base.isArray && !base.isPointerArray && (sub == BaseDataType.UBYTE || sub == BaseDataType.BYTE)
|
||||
val isUnsignedByteArray = base.isArray && !base.isPointerArray && sub == BaseDataType.UBYTE
|
||||
val isSignedByteArray = base.isArray && !base.isPointerArray && sub == BaseDataType.BYTE
|
||||
val isWordArray = base.isArray && !base.isPointerArray && (sub == BaseDataType.UWORD || sub == BaseDataType.WORD)
|
||||
val isUnsignedWordArray = base.isArray && !base.isPointerArray && sub == BaseDataType.UWORD
|
||||
val isSignedWordArray = base.isArray && !base.isPointerArray && sub == BaseDataType.WORD
|
||||
val isLongArray = base.isArray && sub == BaseDataType.LONG
|
||||
val isFloatArray = base.isArray && !base.isPointerArray && sub == BaseDataType.FLOAT
|
||||
val isPointerArray = base == BaseDataType.ARRAY && sub == BaseDataType.POINTER
|
||||
val isBoolArray = base == BaseDataType.ARRAY && sub == BaseDataType.BOOL
|
||||
val isByteArray = base == BaseDataType.ARRAY && (sub == BaseDataType.UBYTE || sub == BaseDataType.BYTE)
|
||||
val isUnsignedByteArray = base == BaseDataType.ARRAY && sub == BaseDataType.UBYTE
|
||||
val isSignedByteArray = base == BaseDataType.ARRAY && sub == BaseDataType.BYTE
|
||||
val isWordArray = (base == BaseDataType.ARRAY || base == BaseDataType.ARRAY_SPLITW) && (sub == BaseDataType.UWORD || sub == BaseDataType.WORD)
|
||||
val isUnsignedWordArray = (base == BaseDataType.ARRAY || base == BaseDataType.ARRAY_SPLITW) && sub == BaseDataType.UWORD
|
||||
val isSignedWordArray = (base == BaseDataType.ARRAY || base == BaseDataType.ARRAY_SPLITW) && sub == BaseDataType.WORD
|
||||
val isLongArray = base == BaseDataType.ARRAY && sub == BaseDataType.LONG
|
||||
val isFloatArray = base == BaseDataType.ARRAY && sub == BaseDataType.FLOAT
|
||||
val isString = base == BaseDataType.STR
|
||||
val isBool = base == BaseDataType.BOOL
|
||||
val isFloat = base == BaseDataType.FLOAT
|
||||
@@ -512,11 +520,11 @@ class DataType private constructor(
|
||||
(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
|
||||
return when (base) {
|
||||
BaseDataType.ARRAY_SPLITW -> true
|
||||
BaseDataType.ARRAY if sub == BaseDataType.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
|
||||
|
||||
@@ -87,13 +87,25 @@ interface ICompilationTarget: IStringEncoding, IMemSizer {
|
||||
val defaultLauncherType: CbmPrgLauncherType
|
||||
val supportsBankedCalls: Boolean
|
||||
|
||||
/**
|
||||
* True when the code generator allocates subroutine locals and per-activation intermediate values
|
||||
* in a stack frame instead of static memory, so a subroutine on a call-graph cycle can have more
|
||||
* than one live activation. False means two live activations would share the same storage.
|
||||
*/
|
||||
val supportsStackFrames: Boolean
|
||||
|
||||
fun initializeMemoryAreas(compilerOptions: CompilationOptions)
|
||||
fun getFloatAsmBytes(num: Number): String
|
||||
|
||||
fun convertFloatToBytes(num: Double): List<UByte>
|
||||
fun convertBytesToFloat(bytes: List<UByte>): Double
|
||||
|
||||
fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean)
|
||||
/**
|
||||
* Launches an emulator to run the compiled program artifact.
|
||||
* [programFile] is the exact artifact path including its extension; launchers must use it
|
||||
* verbatim, without stripping or appending one.
|
||||
*/
|
||||
fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean)
|
||||
fun isIOAddress(address: UInt): Boolean
|
||||
|
||||
override fun encodeString(str: String, encoding: Encoding): List<UByte>
|
||||
|
||||
@@ -66,12 +66,9 @@ abstract class Zeropage(options: CompilationOptions): MemoryAllocator(options) {
|
||||
}
|
||||
|
||||
fun availableBytes() = if(options.zeropage== ZeropageType.DONTUSE) 0 else free.size
|
||||
fun hasByteAvailable() = if(options.zeropage== ZeropageType.DONTUSE) false else free.isNotEmpty()
|
||||
fun hasByteAvailable() = options.zeropage != ZeropageType.DONTUSE && free.isNotEmpty()
|
||||
fun hasWordAvailable(): Boolean {
|
||||
if(options.zeropage== ZeropageType.DONTUSE)
|
||||
return false
|
||||
|
||||
return free.windowed(2).any { it[0] == it[1] - 1u }
|
||||
return options.zeropage != ZeropageType.DONTUSE && free.windowed(2).any { it[0] == it[1] - 1u }
|
||||
}
|
||||
|
||||
override fun allocate(name: String,
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package prog8.code.core
|
||||
|
||||
import prog8.code.target.VMTarget
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.extension
|
||||
import kotlin.io.path.name
|
||||
|
||||
|
||||
/** VICE/box16 monitor (symbol) list file name for an artifact stem. */
|
||||
fun viceMonListName(stem: String) = "$stem.vice-mon-list"
|
||||
|
||||
|
||||
/**
|
||||
* Artifact stem of a user-supplied `-o` output file name.
|
||||
* A dot at index 0 is not an extension separator: ".pgz" keeps its whole name.
|
||||
* Only applied to `-o`; the legacy program stem (source name without extension) is used verbatim.
|
||||
*/
|
||||
fun outputStemOf(name: Path): String {
|
||||
val filename = name.name
|
||||
val dotIndex = filename.lastIndexOf('.')
|
||||
return if (dotIndex <= 0) filename else filename.substring(0, dotIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* The final program artifact path: [CompilationOptions.outputFile] verbatim when set,
|
||||
* otherwise [defaultName] plus the extension for [CompilationOptions.output], in [CompilationOptions.outputDir].
|
||||
* RAW and LIBRARY both use ".bin"; AMIGAHUNK has no extension.
|
||||
* On the virtual target the artifact is the IR file, so ".p8ir" is always enforced on -o.
|
||||
*/
|
||||
fun CompilationOptions.programFile(defaultName: String): Path {
|
||||
// VMTarget's defaultOutputType = PRG is misleading: its artifact is the .p8ir file, never PRG.
|
||||
// The VM identifies the IR by its extension, so -o is not verbatim here: ".p8ir" is enforced.
|
||||
// Only the exact lowercase extension is recognized (same rule as -vm in CompilerMain).
|
||||
if(compTarget is VMTarget) {
|
||||
val ir = outputFile ?: return outputDir.resolve("$defaultName.p8ir")
|
||||
return if (ir.extension=="p8ir") ir else ir.resolveSibling("${ir.name}.p8ir")
|
||||
}
|
||||
outputFile?.let { return it }
|
||||
val filename = when(output) {
|
||||
OutputType.PRG -> "$defaultName.prg"
|
||||
OutputType.XEX -> "$defaultName.xex"
|
||||
OutputType.RAW -> "$defaultName.bin"
|
||||
OutputType.LIBRARY -> "$defaultName.bin"
|
||||
OutputType.ELF -> "$defaultName.elf"
|
||||
OutputType.AMIGAHUNK -> defaultName
|
||||
}
|
||||
return outputDir.resolve(filename)
|
||||
}
|
||||
|
||||
/**
|
||||
* Output artifact naming.
|
||||
* Invariant: [asm] must resolve into the same directory as `CompilationOptions.outputDir`,
|
||||
* because 64tass resolves `.binary` includes relative to the generated `.asm`.
|
||||
*/
|
||||
class OutputFiles(val programFile: Path, val stem: String, val dir: Path) {
|
||||
fun asm() = dir.resolve("$stem.asm")
|
||||
fun list() = dir.resolve("$stem.list")
|
||||
fun viceMonList() = dir.resolve(viceMonListName(stem))
|
||||
fun binFile() = dir.resolve("$stem.bin")
|
||||
fun ir() = dir.resolve("$stem.p8ir")
|
||||
fun bankedCalls() = dir.resolve("$stem.bankedcalls")
|
||||
fun obj() = dir.resolve("$stem.o")
|
||||
fun linkScript() = dir.resolve("$stem.link.ld")
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Builds the output file set. Without `-o` the stem is [defaultName] verbatim
|
||||
* (outputStemOf is only applied to the `-o` name). With `-o` the stem is derived
|
||||
* from the resolved artifact name, so it also covers the virtual target appending
|
||||
* ".p8ir". [dir] is the program file's parent, or [CompilationOptions.outputDir]
|
||||
* for a bare -o filename.
|
||||
*/
|
||||
fun of(options: CompilationOptions, defaultName: String): OutputFiles {
|
||||
val programFile = options.programFile(defaultName)
|
||||
val stem = options.outputFile?.let { outputStemOf(programFile) } ?: defaultName
|
||||
val dir = programFile.parent ?: options.outputDir
|
||||
return OutputFiles(programFile, stem, dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package prog8.code.cpu6502
|
||||
|
||||
// Horner ("shift-add") expansion of a multiply by a small compile-time constant, for the 6502 and
|
||||
// 65C02 backends. The runtime routines prog8_math.multiply_words / multiply_bytes cost the same
|
||||
// whatever the constant is (~442 and ~130 cycles), while this expansion costs O(bit length), so
|
||||
// for a small constant inlining wins. It generalises the hand-written prog8_math.mul_word_N /
|
||||
// mul_byte_N routines, which are themselves unrolled Horner chains (mul_word_3 is "AY = AY*2 + AY",
|
||||
// one Horner step for 11).
|
||||
//
|
||||
// The sequences live here rather than in either backend because the register bookkeeping is easy to
|
||||
// get subtly wrong and the two backends must not drift apart.
|
||||
//
|
||||
// The gate is on generated size, not on the constant's magnitude: cycles alone would argue for
|
||||
// expanding nearly every multiply and bloating every program. Budgets are the max instruction count
|
||||
// the expansion may take; the call sequence each replaces is 6 instructions.
|
||||
|
||||
private const val WORD_BUDGET = 32
|
||||
private const val BYTE_BUDGET = 24
|
||||
|
||||
// --- byte ---
|
||||
// Contract: A holds the multiplicand. Result in A. Only A and one scratch byte are live, so there
|
||||
// is no second register to keep in sync (unlike the word case, where a bare doubling leaves the
|
||||
// high byte stale in Y).
|
||||
|
||||
fun byteShiftAddExpansion(value: Int): List<String>? {
|
||||
if (value !in 3..255)
|
||||
return null
|
||||
val bitlen = 31 - Integer.numberOfLeadingZeros(value)
|
||||
val popcount = Integer.bitCount(value)
|
||||
if (1 + (bitlen-1) + 2*(popcount-1) > BYTE_BUDGET)
|
||||
return null
|
||||
val result = ArrayList<String>(1 + 3*bitlen)
|
||||
result.add("sta P8ZP_SCRATCH_REG") // keep x for the conditional adds
|
||||
for (bit in (bitlen-1) downTo 0) {
|
||||
result.add("asl a") // r = r*2
|
||||
if (value and (1 shl bit) != 0) { // r += x
|
||||
result.add("clc")
|
||||
result.add("adc P8ZP_SCRATCH_REG")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// --- word ---
|
||||
// Contract on entry: A = r.lo, Y = r.hi, P8ZP_SCRATCH_W1 = r, P8ZP_SCRATCH_W2 = x (all 16-bit,
|
||||
// little-endian: byte 0 is the low byte). On exit: A = result.lo, Y = result.hi.
|
||||
//
|
||||
// Two things this sequence must get right, both learned the hard way:
|
||||
// * the add step must store the new high byte back to W1+1 as well as to Y. Writing it only to Y
|
||||
// leaves W1+1 stale, and the next step's 'rol P8ZP_SCRATCH_W1+1' would then double the wrong
|
||||
// high byte.
|
||||
// * a trailing bare doubling (any even constant) updates A and W1+1 but not Y, so Y needs an
|
||||
// explicit reload at the end. It cannot be 'tay', because at that point A holds the low byte.
|
||||
|
||||
fun wordShiftAddExpansion(value: Int): List<String>? {
|
||||
if (value < 3)
|
||||
return null
|
||||
val bitlen = 31 - Integer.numberOfLeadingZeros(value)
|
||||
val popcount = Integer.bitCount(value)
|
||||
if (1 + 2*(bitlen-1) + 7*(popcount-1) > WORD_BUDGET)
|
||||
return null
|
||||
val result = ArrayList<String>(2 + 9*bitlen)
|
||||
for (bit in (bitlen-1) downTo 0) {
|
||||
result.add("asl a") // r = r*2
|
||||
result.add("rol P8ZP_SCRATCH_W1+1")
|
||||
if (value and (1 shl bit) != 0) { // r += x
|
||||
result.add("clc")
|
||||
result.add("adc P8ZP_SCRATCH_W2")
|
||||
result.add("sta P8ZP_SCRATCH_W1")
|
||||
result.add("lda P8ZP_SCRATCH_W1+1")
|
||||
result.add("adc P8ZP_SCRATCH_W2+1")
|
||||
result.add("sta P8ZP_SCRATCH_W1+1")
|
||||
result.add("tay")
|
||||
result.add("lda P8ZP_SCRATCH_W1")
|
||||
}
|
||||
}
|
||||
if (value and 1 == 0)
|
||||
result.add("ldy P8ZP_SCRATCH_W1+1")
|
||||
return result
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package prog8.code.target
|
||||
|
||||
import prog8.code.core.*
|
||||
import prog8.code.target.encodings.Encoder
|
||||
import prog8.code.target.zp.M68kZeropage
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
@@ -11,6 +12,7 @@ class Amiga1200Target: ICompilationTarget,
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = true
|
||||
override val supportsStackFrames = true
|
||||
override val defaultEncoding = Encoding.ISO
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
@@ -54,31 +56,22 @@ class Amiga1200Target: ICompilationTarget,
|
||||
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).")
|
||||
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
|
||||
if (selectedEmulator == 1) {
|
||||
CopperlineEmulator.launch(programFile, quiet, "A1200", "68020", listOf("--fast", "2M", "--fpu"))
|
||||
return
|
||||
}
|
||||
val cpuStr = when(this.cpu) {
|
||||
if (selectedEmulator != 2) {
|
||||
System.err.println("The amiga1200 target supports FS-UAE (-emu) and Amitools's vamos (-emu2).")
|
||||
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")
|
||||
}
|
||||
VamosEmulator.launch(programFile, quiet, cpuStr)
|
||||
return
|
||||
}
|
||||
|
||||
override fun isIOAddress(address: UInt): Boolean =
|
||||
@@ -88,20 +81,6 @@ class Amiga1200Target: ICompilationTarget,
|
||||
address in 0xe80000u..0xefffffu // Zorro II autoconfig space
|
||||
|
||||
override fun initializeMemoryAreas(compilerOptions: CompilationOptions) {
|
||||
zeropage = Amiga1200Zeropage(compilerOptions)
|
||||
zeropage = M68kZeropage(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")
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package prog8.code.target
|
||||
|
||||
import prog8.code.core.*
|
||||
import prog8.code.target.encodings.Encoder
|
||||
import prog8.code.target.zp.M68kZeropage
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
@@ -11,6 +12,7 @@ class Amiga500Target: ICompilationTarget,
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = true
|
||||
override val supportsStackFrames = true
|
||||
override val defaultEncoding = Encoding.ISO
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
@@ -58,7 +60,10 @@ class Amiga500Target: ICompilationTarget,
|
||||
33 to "AmigaGuide",
|
||||
34 to "ARexx",
|
||||
35 to "BattClock",
|
||||
36 to "BattMem"
|
||||
36 to "BattMem",
|
||||
// from here, non-standard libraries are listed
|
||||
100 to "AsyncIO",
|
||||
101 to "AHI", // ahi.device base (amiga1200 target only, 68020+, used by the low-level ahi.* LVOs)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -91,31 +96,22 @@ class Amiga500Target: ICompilationTarget,
|
||||
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).")
|
||||
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
|
||||
if (selectedEmulator == 1) {
|
||||
CopperlineEmulator.launch(programFile, quiet, "A500", "68000", listOf("--slow", "512K"))
|
||||
return
|
||||
}
|
||||
val cpuStr = when(this.cpu) {
|
||||
if (selectedEmulator != 2) {
|
||||
System.err.println("The amiga500 target supports FS-UAE (-emu) and Amitools's vamos (-emu2).")
|
||||
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")
|
||||
}
|
||||
VamosEmulator.launch(programFile, quiet, cpuStr)
|
||||
return
|
||||
}
|
||||
|
||||
override fun isIOAddress(address: UInt): Boolean =
|
||||
@@ -125,20 +121,6 @@ class Amiga500Target: ICompilationTarget,
|
||||
address in 0xe80000u..0xefffffu // Zorro II autoconfig space
|
||||
|
||||
override fun initializeMemoryAreas(compilerOptions: CompilationOptions) {
|
||||
zeropage = AmigaZeropage(compilerOptions)
|
||||
zeropage = M68kZeropage(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")
|
||||
}
|
||||
@@ -12,6 +12,7 @@ class C128Target: ICompilationTarget,
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = true
|
||||
override val supportsStackFrames = false
|
||||
override val defaultEncoding = Encoding.PETSCII
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
@@ -28,7 +29,7 @@ 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 FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE
|
||||
override val POINTER_MEM_SIZE = 2u
|
||||
override val ARRAY_SIZE_LIMIT = 256u
|
||||
override val PROGRAM_LOAD_ADDRESS = 0x1c01u
|
||||
@@ -54,7 +55,7 @@ class C128Target: ICompilationTarget,
|
||||
return m5.toDouble()
|
||||
}
|
||||
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
|
||||
if(selectedEmulator!=1) {
|
||||
System.err.println("The c128 target only supports the main emulator (Vice).")
|
||||
return
|
||||
@@ -63,9 +64,9 @@ class C128Target: ICompilationTarget,
|
||||
if(!quiet)
|
||||
println("\nStarting C-128 emulator x128...")
|
||||
|
||||
val viceMonlist = C64Target.viceMonListName(programNameWithPath.toString())
|
||||
val cmdline = listOf("x128", "-silent", "-moncommands", viceMonlist,
|
||||
"-autostartprgmode", "1", "-autostart-warp", "-autostart", "${programNameWithPath}.prg")
|
||||
val viceMonlist = programFile.resolveSibling(viceMonListName(outputStemOf(programFile)))
|
||||
val cmdline = listOf("x128", "-silent", "-moncommands", viceMonlist.toString(),
|
||||
"-autostartprgmode", "1", "-autostart-warp", "-autostart", programFile.toString())
|
||||
val processb = ProcessBuilder(cmdline)
|
||||
if(!quiet)
|
||||
processb.inheritIO()
|
||||
|
||||
@@ -13,6 +13,7 @@ class C64Target: ICompilationTarget,
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = true
|
||||
override val supportsStackFrames = false
|
||||
override val defaultEncoding = Encoding.PETSCII
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
@@ -22,8 +23,6 @@ class C64Target: ICompilationTarget,
|
||||
|
||||
companion object {
|
||||
const val NAME = "c64"
|
||||
|
||||
fun viceMonListName(baseFilename: String) = "$baseFilename.vice-mon-list"
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +57,7 @@ class C64Target: ICompilationTarget,
|
||||
return m5.toDouble()
|
||||
}
|
||||
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
|
||||
if(selectedEmulator!=1) {
|
||||
System.err.println("The c64 target only supports the main emulator (Vice).")
|
||||
return
|
||||
@@ -68,9 +67,9 @@ class C64Target: ICompilationTarget,
|
||||
if(!quiet)
|
||||
println("\nStarting C-64 emulator $emulator...")
|
||||
|
||||
val viceMonlist = viceMonListName(programNameWithPath.toString())
|
||||
val cmdline = listOf(emulator, "-silent", "-moncommands", viceMonlist,
|
||||
"-autostartprgmode", "1", "-autostart-warp", "-autostart", "${programNameWithPath}.prg")
|
||||
val viceMonlist = programFile.resolveSibling(viceMonListName(outputStemOf(programFile)))
|
||||
val cmdline = listOf(emulator, "-silent", "-moncommands", viceMonlist.toString(),
|
||||
"-autostartprgmode", "1", "-autostart-warp", "-autostart", programFile.toString())
|
||||
val processb = ProcessBuilder(cmdline)
|
||||
if(!quiet)
|
||||
processb.inheritIO()
|
||||
|
||||
@@ -4,6 +4,7 @@ import prog8.code.core.*
|
||||
import prog8.code.source.ImportFileSystem.expandTilde
|
||||
import prog8.code.target.encodings.Encoder
|
||||
import prog8.code.target.zp.ConfigurableZeropage
|
||||
import prog8.code.target.zp.M68kZeropage
|
||||
import java.io.IOException
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
@@ -18,6 +19,7 @@ class ConfigFileTarget(
|
||||
override val supportsBankedCalls: Boolean,
|
||||
override val defaultEncoding: Encoding,
|
||||
override val cpu: CpuType,
|
||||
override val supportsStackFrames: Boolean,
|
||||
override val PROGRAM_LOAD_ADDRESS: UInt,
|
||||
override val PROGRAM_MEMTOP_ADDRESS: UInt,
|
||||
override val BSSHIGHRAM_START: UInt,
|
||||
@@ -109,9 +111,12 @@ class ConfigFileTarget(
|
||||
CpuType.valueOf("CPU$cpuString")
|
||||
}
|
||||
val ioAddresses = parseAddressRanges("io_regions", props)
|
||||
val zpFullsafe = parseAddressRanges("zp_fullsafe", props)
|
||||
val zpKernalsafe = parseAddressRanges("zp_kernalsafe", props)
|
||||
val zpBasicsafe = parseAddressRanges("zp_basicsafe", props)
|
||||
// The m68k has no zero page, so the zeropage settings below don't apply to an m68k
|
||||
// target and are not required in its config file.
|
||||
fun zpSetting(name: String) = if(cpuType.is68k) 0u else props.getInteger(name)
|
||||
val zpFullsafe = if(cpuType.is68k) emptyList() else parseAddressRanges("zp_fullsafe", props)
|
||||
val zpKernalsafe = if(cpuType.is68k) emptyList() else parseAddressRanges("zp_kernalsafe", props)
|
||||
val zpBasicsafe = if(cpuType.is68k) emptyList() else parseAddressRanges("zp_basicsafe", props)
|
||||
|
||||
val libraryPath = expandTilde(Path(props.getString("library")))
|
||||
if(!libraryPath.isDirectory())
|
||||
@@ -140,6 +145,7 @@ class ConfigFileTarget(
|
||||
props.getProperty("supports_banked_calls", "false").toBoolean(),
|
||||
Encoding.entries.first { it.prefix==props.getString("encoding") },
|
||||
cpuType,
|
||||
props.getProperty("supports_stack_frames", cpuType.is68k.toString()).toBoolean(),
|
||||
props.getInteger("load_address"),
|
||||
props.getInteger("memtop"),
|
||||
props.getInteger("bss_highram_start"),
|
||||
@@ -153,12 +159,12 @@ class ConfigFileTarget(
|
||||
if(assemblerOptionsStr=="") emptyList() else assemblerOptionsStr.split(" "),
|
||||
varsAddress,
|
||||
ioAddresses,
|
||||
props.getInteger("zp_scratch_b1"),
|
||||
props.getInteger("zp_scratch_reg"),
|
||||
props.getInteger("zp_scratch_w1"),
|
||||
props.getInteger("zp_scratch_w2"),
|
||||
props.getInteger("zp_scratch_ptr"),
|
||||
props.getInteger("virtual_registers"),
|
||||
zpSetting("zp_scratch_b1"),
|
||||
zpSetting("zp_scratch_reg"),
|
||||
zpSetting("zp_scratch_w1"),
|
||||
zpSetting("zp_scratch_w2"),
|
||||
zpSetting("zp_scratch_ptr"),
|
||||
zpSetting("virtual_registers"),
|
||||
zpFullsafe,
|
||||
zpKernalsafe,
|
||||
zpBasicsafe,
|
||||
@@ -178,14 +184,15 @@ class ConfigFileTarget(
|
||||
override fun getFloatAsmBytes(num: Number) = TODO("floats")
|
||||
override fun convertFloatToBytes(num: Double): List<UByte> = TODO("floats")
|
||||
override fun convertBytesToFloat(bytes: List<UByte>): Double = TODO("floats")
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
|
||||
throw IllegalArgumentException("Custom compiler target cannot automatically launch an emulator. Do this manually.")
|
||||
}
|
||||
|
||||
override fun isIOAddress(address: UInt): Boolean = ioAddresses.any { address in it }
|
||||
|
||||
override fun initializeMemoryAreas(compilerOptions: CompilationOptions) {
|
||||
zeropage = ConfigurableZeropage(
|
||||
// the m68k has no zero page, so the zp scratch addresses in the config file don't apply there
|
||||
zeropage = if(cpu.is68k) M68kZeropage(compilerOptions) else ConfigurableZeropage(
|
||||
zpScratchB1, zpScratchReg, zpScratchW1, zpScratchW2, zpScratchPtr,
|
||||
virtualregistersStart,
|
||||
zpBasicsafe,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package prog8.code.target
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.copyTo
|
||||
import kotlin.io.path.createDirectories
|
||||
|
||||
object CopperlineEmulator {
|
||||
fun launch(programFile: Path, quiet: Boolean, model: String, cpu: String, memoryArgs: List<String>) {
|
||||
val home = System.getProperty("user.home")
|
||||
val sourceHdName = if (model == "A500") "Harddisk500" else "Harddisk1200"
|
||||
val cmdDir = if (model == "A500") "c" else "C"
|
||||
val libsDir = if (model == "A500") "libs" else "Libs"
|
||||
val libs = listOf("diskfont.library", "iffparse.library", "lowlevel.library", "rexxsyslib.library", "asyncio.library", "icon.library")
|
||||
|
||||
val tempDir = Paths.get(System.getProperty("java.io.tmpdir"), "prog8-copperline-$model")
|
||||
tempDir.createDirectories()
|
||||
val progName = programFile.fileName.toString()
|
||||
val stagedProg = tempDir.resolve(progName)
|
||||
programFile.copyTo(stagedProg, overwrite = true)
|
||||
|
||||
val srcHd = File("$home/Documents/FS-UAE/Hard Drives/$sourceHdName")
|
||||
if (!srcHd.isDirectory) System.err.println("Warning: source hard disk folder not found: ${srcHd}; skipping file staging.")
|
||||
for (lib in libs) {
|
||||
val src = File(srcHd, "$libsDir/$lib")
|
||||
if (src.exists()) src.toPath().copyTo(tempDir.resolve(lib), overwrite = true)
|
||||
}
|
||||
val commands = if (model == "A500") listOf("Dir","List","Makedir","Assign","SetPatch","Mount","CPU","Echo","Avail") else listOf("Dir","List","MakeDir","Assign","SetPatch","Mount","CPU","Echo","Avail")
|
||||
for (cmd in commands) {
|
||||
val src = File(srcHd, "$cmdDir/$cmd")
|
||||
if (src.exists()) src.toPath().copyTo(tempDir.resolve(cmd), overwrite = true)
|
||||
}
|
||||
val ramHandlerSrc = File(srcHd, "l/Ram-Handler")
|
||||
if (model == "A500" && ramHandlerSrc.exists()) ramHandlerSrc.toPath().copyTo(tempDir.resolve("Ram-Handler"), overwrite = true)
|
||||
val sysCfgSrc = File(srcHd, "Devs/system-configuration")
|
||||
if (sysCfgSrc.exists()) sysCfgSrc.toPath().copyTo(tempDir.resolve("system-configuration"), overwrite = true)
|
||||
|
||||
val ksDirs = listOf(
|
||||
File("${System.getProperty("user.home")}/Documents/FS-UAE/Kickstarts"),
|
||||
File("${System.getProperty("user.home")}/Documents/Kickstarts")
|
||||
)
|
||||
val ksDir = ksDirs.firstOrNull { it.isDirectory } ?: ksDirs.first()
|
||||
val candidates = ksDirs.filter { it.isDirectory }.flatMap { d -> d.listFiles()?.filter { it.name.endsWith(".rom", ignoreCase = true) && it.name.contains(model, ignoreCase = true) } ?: emptyList() }
|
||||
val preferredVersion = if (model == "A500") "v1.3" else "v3.1"
|
||||
val rom = candidates.firstOrNull { it.name.contains(preferredVersion, ignoreCase = true) && it.name.contains("[!", ignoreCase = true) }
|
||||
?: candidates.firstOrNull { it.name.contains(preferredVersion, ignoreCase = true) && !it.name.contains("[b", ignoreCase = true) && !it.name.contains("[h", ignoreCase = true) }
|
||||
?: candidates.firstOrNull { it.name.contains(preferredVersion, ignoreCase = true) }
|
||||
if (rom == null) {
|
||||
System.err.println("Warning: no Kickstart ROM for $model found in ${ksDir}; using bundled AROS.")
|
||||
}
|
||||
val cmd = mutableListOf(
|
||||
"copperline",
|
||||
"--factory",
|
||||
"--model", model,
|
||||
"--cpu", cpu
|
||||
)
|
||||
cmd.addAll(memoryArgs)
|
||||
if (rom != null) cmd.add(rom.absolutePath)
|
||||
cmd.addAll(listOf(
|
||||
"--run", stagedProg.toString()
|
||||
))
|
||||
if (!quiet) println("Launching Copperline...")
|
||||
val pb = ProcessBuilder(cmd).inheritIO()
|
||||
try {
|
||||
pb.start().waitFor()
|
||||
} catch (_: Exception) {
|
||||
System.err.println("Cannot find 'copperline'. Install it from https://copperline.dev/")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ class Cx16Target: ICompilationTarget,
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = true
|
||||
override val supportsStackFrames = false
|
||||
override val defaultEncoding = Encoding.PETSCII
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
@@ -28,7 +29,7 @@ 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 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
|
||||
@@ -54,7 +55,7 @@ class Cx16Target: ICompilationTarget,
|
||||
return m5.toDouble()
|
||||
}
|
||||
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
|
||||
val emulator: String
|
||||
val extraArgs: List<String>
|
||||
|
||||
@@ -65,7 +66,7 @@ class Cx16Target: ICompilationTarget,
|
||||
}
|
||||
2 -> {
|
||||
emulator = "box16"
|
||||
extraArgs = listOf("-sym", C64Target.viceMonListName(programNameWithPath.toString()))
|
||||
extraArgs = listOf("-sym", programFile.resolveSibling(viceMonListName(outputStemOf(programFile))).toString())
|
||||
}
|
||||
else -> {
|
||||
System.err.println("Cx16 target only supports x16emu and box16 emulators.")
|
||||
@@ -76,7 +77,7 @@ class Cx16Target: ICompilationTarget,
|
||||
if(!quiet)
|
||||
println("\nStarting Commander X16 emulator $emulator...")
|
||||
|
||||
val cmdline = listOf(emulator, "-scale", "2", "-rtc", "-run", "-prg", "${programNameWithPath}.prg") + extraArgs
|
||||
val cmdline = listOf(emulator, "-scale", "2", "-rtc", "-run", "-prg", programFile.toString()) + extraArgs
|
||||
val processb = ProcessBuilder(cmdline)
|
||||
if(!quiet)
|
||||
processb.inheritIO()
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package prog8.code.target
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.copyTo
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.writeText
|
||||
|
||||
object FsUaeEmulator {
|
||||
fun launch(
|
||||
programFile: Path,
|
||||
quiet: Boolean,
|
||||
model: String,
|
||||
preferredVersion: String,
|
||||
romHint: String,
|
||||
sourceHdName: String,
|
||||
cmdDir: String,
|
||||
libsDir: String,
|
||||
commands: List<String>,
|
||||
libs: List<String>,
|
||||
extraFiles: List<Pair<String, String>>,
|
||||
startupSequence: String
|
||||
) {
|
||||
val home = System.getProperty("user.home")
|
||||
val ksDir = File("$home/Documents/FS-UAE/Kickstarts")
|
||||
val candidates = ksDir.listFiles()?.filter { it.name.endsWith(".rom", ignoreCase = true) && it.name.contains(model, ignoreCase = true) } ?: emptyList()
|
||||
val rom = candidates.firstOrNull { it.name.contains(preferredVersion, ignoreCase = true) && it.name.contains("[!", ignoreCase = true) }
|
||||
?: candidates.firstOrNull { it.name.contains(preferredVersion, ignoreCase = true) && !it.name.contains("[b", ignoreCase = true) && !it.name.contains("[h", ignoreCase = true) }
|
||||
?: candidates.firstOrNull { it.name.contains(preferredVersion, ignoreCase = true) }
|
||||
?: candidates.firstOrNull { !it.name.contains("[b", ignoreCase = true) && !it.name.contains("[h", ignoreCase = true) }
|
||||
?: candidates.firstOrNull()
|
||||
?: ksDir.listFiles()?.firstOrNull { it.name.contains(romHint, ignoreCase = true) && it.name.endsWith(".rom", ignoreCase = true) }
|
||||
if (rom == null) {
|
||||
System.err.println("Cannot find a Kickstart ROM for $model in ${ksDir}")
|
||||
return
|
||||
}
|
||||
val tempDir = Paths.get(System.getProperty("java.io.tmpdir"), "prog8-fsuae-$model")
|
||||
tempDir.createDirectories()
|
||||
val hdDir = tempDir.resolve("hd")
|
||||
hdDir.resolve("S").createDirectories()
|
||||
hdDir.resolve("C").createDirectories()
|
||||
hdDir.resolve("L").createDirectories()
|
||||
hdDir.resolve("Libs").createDirectories()
|
||||
hdDir.resolve("Devs").createDirectories()
|
||||
val progName = programFile.fileName.toString()
|
||||
programFile.copyTo(hdDir.resolve(progName), overwrite = true)
|
||||
val srcHd = File("$home/Documents/FS-UAE/Hard Drives/$sourceHdName")
|
||||
for (cmd in commands) {
|
||||
val src = File(srcHd, "$cmdDir/$cmd")
|
||||
if (src.exists()) src.toPath().copyTo(hdDir.resolve("C/$cmd"), overwrite = true)
|
||||
}
|
||||
for (lib in libs) {
|
||||
val src = File(srcHd, "$libsDir/$lib")
|
||||
if (src.exists()) src.toPath().copyTo(hdDir.resolve("Libs/$lib"), overwrite = true)
|
||||
}
|
||||
for ((srcRel, dstRel) in extraFiles) {
|
||||
val src = File(srcHd, srcRel)
|
||||
if (src.exists()) {
|
||||
val dst = hdDir.resolve(dstRel)
|
||||
dst.parent.createDirectories()
|
||||
src.toPath().copyTo(dst, overwrite = true)
|
||||
}
|
||||
}
|
||||
hdDir.resolve("S/Startup-Sequence").writeText(startupSequence.replace("\$progName", progName))
|
||||
val memoryArgs = if (model == "A500") listOf("--slow_memory=512") else listOf("--fast_memory=2048")
|
||||
val cmd = listOf(
|
||||
"fs-uae",
|
||||
"--amiga_model=$model",
|
||||
"--kickstart_file=${rom.absolutePath}",
|
||||
"--hard_drive_0=${hdDir}",
|
||||
"--automatic_input_grab=0",
|
||||
"--window_width=1280",
|
||||
"--window_height=1024",
|
||||
"--floppy_drive_volume=8",
|
||||
"--floppy_drive_volume_empty=8"
|
||||
) + memoryArgs
|
||||
if (!quiet) println("Launching FS-UAE...")
|
||||
val pb = ProcessBuilder(cmd).inheritIO()
|
||||
try {
|
||||
pb.start().waitFor()
|
||||
} catch (_: Exception) {
|
||||
System.err.println("Cannot find 'fs-uae'. Install it from https://fs-uae.net/")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ class PETTarget: ICompilationTarget,
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = false
|
||||
override val supportsStackFrames = false
|
||||
override val defaultEncoding = Encoding.PETSCII
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
@@ -27,7 +28,7 @@ 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 FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE
|
||||
override val POINTER_MEM_SIZE = 2u
|
||||
override val ARRAY_SIZE_LIMIT = 256u
|
||||
override val PROGRAM_LOAD_ADDRESS = 0x0401u
|
||||
@@ -53,7 +54,7 @@ class PETTarget: ICompilationTarget,
|
||||
return m5.toDouble()
|
||||
}
|
||||
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
|
||||
if(selectedEmulator!=1) {
|
||||
System.err.println("The pet target only supports the main emulator (Vice).")
|
||||
return
|
||||
@@ -62,9 +63,9 @@ class PETTarget: ICompilationTarget,
|
||||
if(!quiet)
|
||||
println("\nStarting PET emulator...")
|
||||
|
||||
val viceMonlist = C64Target.viceMonListName(programNameWithPath.toString())
|
||||
val cmdline = listOf("xpet", "-model", "4032", "-ramsize", "32", "-videosize", "40", "-silent", "-moncommands", viceMonlist,
|
||||
"-autostartprgmode", "1", "-autostart-warp", "-autostart", "${programNameWithPath}.prg")
|
||||
val viceMonlist = programFile.resolveSibling(viceMonListName(outputStemOf(programFile)))
|
||||
val cmdline = listOf("xpet", "-model", "4032", "-ramsize", "32", "-videosize", "40", "-silent", "-moncommands", viceMonlist.toString(),
|
||||
"-autostartprgmode", "1", "-autostart-warp", "-autostart", programFile.toString())
|
||||
val processb = ProcessBuilder(cmdline)
|
||||
if(!quiet)
|
||||
processb.inheritIO()
|
||||
|
||||
@@ -2,6 +2,7 @@ package prog8.code.target
|
||||
|
||||
import prog8.code.core.*
|
||||
import prog8.code.target.encodings.Encoder
|
||||
import prog8.code.target.zp.M68kZeropage
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
@@ -15,6 +16,7 @@ class Qemu68kTarget: ICompilationTarget,
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = false
|
||||
override val supportsStackFrames = true
|
||||
override val defaultEncoding = Encoding.ISO
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
@@ -55,42 +57,27 @@ class Qemu68kTarget: ICompilationTarget,
|
||||
TODO("convert bytes to float")
|
||||
}
|
||||
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
override fun launchEmulator(selectedEmulator: Int, programFile: 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}")
|
||||
if(!programFile.toFile().exists()) {
|
||||
System.err.println("No program file found: $programFile")
|
||||
return
|
||||
}
|
||||
val cpuStr = this.cpu.toString().lowercase()
|
||||
|
||||
val cmd = listOf(
|
||||
"qemu-system-m68k",
|
||||
"-M", "virt",
|
||||
"-cpu", cpuStr,
|
||||
"-m", "1M",
|
||||
"-kernel", programFile.toString(),
|
||||
"-nographic"
|
||||
)
|
||||
if(!quiet) {
|
||||
val launchMsg = if (isElf) "ELF" else "raw binary"
|
||||
println("Launching QEMU (press Ctrl-A X to exit)... (from $launchMsg)")
|
||||
println("Launching QEMU (press Ctrl-A X to exit)...")
|
||||
}
|
||||
val pb = ProcessBuilder(cmd).inheritIO()
|
||||
try {
|
||||
@@ -110,16 +97,3 @@ class Qemu68kTarget: ICompilationTarget,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -3,9 +3,7 @@ package prog8.code.target
|
||||
import prog8.code.core.*
|
||||
import prog8.code.target.encodings.Encoder
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.extension
|
||||
import kotlin.io.path.isReadable
|
||||
import kotlin.io.path.name
|
||||
import kotlin.io.path.readText
|
||||
|
||||
class VMTarget: ICompilationTarget,
|
||||
@@ -14,6 +12,7 @@ class VMTarget: ICompilationTarget,
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = false
|
||||
override val supportsStackFrames = false
|
||||
override val defaultEncoding = Encoding.ISO
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
@@ -71,21 +70,24 @@ class VMTarget: ICompilationTarget,
|
||||
return Double.fromBits(b0 or b1 or b2 or b3 or b4 or b5 or b6 or b7)
|
||||
}
|
||||
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
launchEmulatorWithTrace(programNameWithPath, quiet, traceEnabled = false)
|
||||
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
|
||||
launchEmulatorWithTrace(programFile, quiet, traceEnabled = false)
|
||||
}
|
||||
|
||||
fun launchEmulatorWithTrace(programNameWithPath: Path, quiet: Boolean, traceEnabled: Boolean) {
|
||||
/**
|
||||
* Runs the given .p8ir file in the embedded virtual machine.
|
||||
* The caller must pass the exact path; no fallback probing happens here, the -vm CLI flow resolves a bare name itself.
|
||||
*/
|
||||
fun launchEmulatorWithTrace(programFile: Path, quiet: Boolean, traceEnabled: Boolean) {
|
||||
if(!quiet)
|
||||
println("\nStarting Virtual Machine...")
|
||||
|
||||
// to not have external module dependencies in our own module, we launch the virtual machine via reflection
|
||||
val vm = Class.forName("prog8.vm.VmRunner").getDeclaredConstructor().newInstance() as IVirtualMachineRunner
|
||||
val withExt = if(programNameWithPath.extension=="p8ir") programNameWithPath else programNameWithPath.resolveSibling("${programNameWithPath.name}.p8ir")
|
||||
if(withExt.isReadable())
|
||||
vm.runProgram(withExt.readText(), quiet, traceEnabled)
|
||||
if(programFile.isReadable())
|
||||
vm.runProgram(programFile.readText(), quiet, traceEnabled)
|
||||
else
|
||||
throw java.nio.file.NoSuchFileException(withExt.name, null, "not a .p8ir file")
|
||||
throw java.nio.file.NoSuchFileException(programFile.toString(), null, "not a .p8ir file")
|
||||
}
|
||||
|
||||
override fun isIOAddress(address: UInt): Boolean = false
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package prog8.code.target
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
object VamosEmulator {
|
||||
fun launch(programFile: Path, quiet: Boolean, cpu: String) {
|
||||
val cmd = listOf("vamos", "--cpu", cpu, programFile.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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package prog8.code.target.zp
|
||||
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.Zeropage
|
||||
|
||||
/**
|
||||
* The m68k has no zero page, so there are no scratch locations to hand out.
|
||||
* Any attempt to use one is a bug in the code generator.
|
||||
*/
|
||||
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")
|
||||
}
|
||||
@@ -21,6 +21,8 @@ object DummyMemsizer : IMemSizer {
|
||||
BaseDataType.UWORD, BaseDataType.WORD -> numElements * 2
|
||||
BaseDataType.LONG -> numElements * 4
|
||||
BaseDataType.FLOAT -> numElements * 5
|
||||
BaseDataType.POINTER -> numElements * 2
|
||||
BaseDataType.STRUCT_INSTANCE -> numElements * (dt.subType?.memsize(this) ?: throw IllegalArgumentException("struct instance array has unresolved subtype"))
|
||||
else -> throw IllegalArgumentException("invalid sub type")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ class TestDataType: FunSpec({
|
||||
BaseDataType.UWORD, BaseDataType.WORD -> numElements * 2
|
||||
BaseDataType.LONG -> numElements * 4
|
||||
BaseDataType.FLOAT -> numElements * 5
|
||||
BaseDataType.POINTER -> numElements * 2
|
||||
BaseDataType.STRUCT_INSTANCE -> numElements * (dt.subType?.memsize(this) ?: throw IllegalArgumentException("struct instance array has unresolved subtype"))
|
||||
else -> throw IllegalArgumentException("invalid sub type")
|
||||
}
|
||||
}
|
||||
@@ -93,10 +95,15 @@ class TestDataType: FunSpec({
|
||||
test("isArray extension") {
|
||||
BaseDataType.ARRAY.isArray shouldBe true
|
||||
BaseDataType.ARRAY_SPLITW.isArray shouldBe true
|
||||
BaseDataType.ARRAY_POINTER.isArray shouldBe true
|
||||
BaseDataType.UBYTE.isArray shouldBe false
|
||||
}
|
||||
|
||||
test("isPointerArray property") {
|
||||
DataType.arrayOfPointersTo(BaseDataType.UBYTE).isPointerArray shouldBe true
|
||||
DataType.arrayOfPointersTo(BaseDataType.WORD).isPointerArray shouldBe true
|
||||
DataType.arrayFor(BaseDataType.UBYTE, tgt).isPointerArray shouldBe false
|
||||
}
|
||||
|
||||
test("isPointer extension") {
|
||||
BaseDataType.POINTER.isPointer shouldBe true
|
||||
BaseDataType.UBYTE.isPointer shouldBe false
|
||||
|
||||
@@ -19,7 +19,7 @@ import prog8.code.target.zp.PETZeropage
|
||||
|
||||
|
||||
/**
|
||||
* Comprehensive tests for Zeropage free list configurations across all targets.
|
||||
* Tests for Zeropage free list configurations across all targets.
|
||||
*
|
||||
* Tests verify that for each target and ZeropageType combination:
|
||||
* - The exact list of free locations is correct
|
||||
|
||||
@@ -6,6 +6,8 @@ import prog8.code.assembly.AssemblyProgram6502
|
||||
import prog8.code.assembly.IAssemblyProgram
|
||||
import prog8.code.ast.*
|
||||
import prog8.code.core.*
|
||||
import prog8.code.cpu6502.byteShiftAddExpansion
|
||||
import prog8.code.cpu6502.wordShiftAddExpansion
|
||||
import prog8.code.source.ImportFileSystem
|
||||
import prog8.code.source.SourceCode
|
||||
import prog8.code.target.Cx16Target
|
||||
@@ -45,8 +47,36 @@ class AsmGen6502Internal (
|
||||
preassignedCallSiteIds: Map<PtAsmSub, UByte> = emptyMap()
|
||||
) {
|
||||
|
||||
internal val optimizedByteMultiplications = setOf(3,5,6,7,9,10,11,12,13,14,15,20,25,40,50,80,100)
|
||||
internal val optimizedWordMultiplications = setOf(3,5,6,7,9,10,12,15,20,25,40,50,80,100,320,640)
|
||||
// 40, 50, 80 and 100 are deliberately absent: prog8_math's mul_byte_40/50/80/100 are wrong.
|
||||
// They mask the multiplicand with `and #7` or `and #3` and index a small table, which only
|
||||
// works if x*k mod 256 has that period. The true period is 256/gcd(k,256), i.e. 32, 128, 16
|
||||
// and 64 entries respectively, so the tables are far too small (e.g. mul_byte_100 returns
|
||||
// (x&3)*100, giving 0 for x=4 where the answer is 144). The generic expansion below computes
|
||||
// them correctly in 8-11 instructions.
|
||||
internal val optimizedByteMultiplications = setOf(3,5,6,7,9,10,11,12,13,14,15,20,25)
|
||||
// 320 and 640 are deliberately absent: mul_word_320/640 compute (low byte of x)*320/*640 and
|
||||
// ignore the multiplicand's high byte, so they are wrong for any x >= 256 (the comment
|
||||
// "msb in Y doesn't matter" is false). The generic expansion handles them in 27-29 instructions.
|
||||
internal val optimizedWordMultiplications = setOf(3,5,6,7,9,10,12,15,20,25,40,50,80,100)
|
||||
|
||||
// Thin wrappers over the shared expansion in codeCore, so this backend and codeGenNew6502 emit
|
||||
// an identical sequence. Works on the raw bit pattern, exactly like the hand-written
|
||||
// prog8_math.mul_word_N / mul_byte_N routines, so signed and unsigned agree because both
|
||||
// truncate mod 2^n and no sign extension is applied afterwards.
|
||||
internal fun emitWordShiftAdd(value: Int): Boolean {
|
||||
val expansion = wordShiftAddExpansion(value) ?: return false
|
||||
out(" sta P8ZP_SCRATCH_W2 | sty P8ZP_SCRATCH_W2+1") // x
|
||||
out(" sta P8ZP_SCRATCH_W1 | sty P8ZP_SCRATCH_W1+1") // r = x
|
||||
expansion.forEach { out(" $it") }
|
||||
return true
|
||||
}
|
||||
|
||||
internal fun emitByteShiftAdd(value: Int): Boolean {
|
||||
val expansion = byteShiftAddExpansion(value) ?: return false
|
||||
expansion.forEach { out(" $it") }
|
||||
return true
|
||||
}
|
||||
|
||||
internal val loopEndLabels = ArrayDeque<String>()
|
||||
private val zeropage = options.compTarget.zeropage
|
||||
private val allocator = VariableAllocator(symbolTable, options, errors, program)
|
||||
@@ -80,7 +110,7 @@ class AsmGen6502Internal (
|
||||
programGen.generate()
|
||||
|
||||
if(errors.noErrors()) {
|
||||
val output = options.outputDir.resolve("${program.name}.asm")
|
||||
val output = OutputFiles.of(options, program.name).asm()
|
||||
val asmLines = assembly.toMutableList()
|
||||
if(options.compTarget.name==Cx16Target.NAME) {
|
||||
scanInvalid65816instructions(asmLines)
|
||||
@@ -100,7 +130,7 @@ class AsmGen6502Internal (
|
||||
if(options.dumpVariables)
|
||||
dumpVariables()
|
||||
|
||||
return AssemblyProgram6502(program.name, options.outputDir, options.compTarget)
|
||||
return AssemblyProgram6502(program.name, options, options.compTarget)
|
||||
} else {
|
||||
errors.report()
|
||||
return null
|
||||
@@ -287,12 +317,12 @@ class AsmGen6502Internal (
|
||||
return Pair("#<$label", "#>$label")
|
||||
} else if (e.value != null) {
|
||||
val value = e.value!!.toInt()
|
||||
return Pair("#<${(value and 0xff).toHex()}", "#>${((value shr 8) and 0xff).toHex()}")
|
||||
return Pair("#<${value.toHex()}", "#>${value.toHex()}")
|
||||
}
|
||||
}
|
||||
if (e is PtNumber) {
|
||||
val value = e.number.toInt()
|
||||
return Pair("#<${(value and 0xff).toHex()}", "#>${((value shr 8) and 0xff).toHex()}")
|
||||
return Pair("#<${value.toHex()}", "#>${value.toHex()}")
|
||||
}
|
||||
if (e is PtAddressOf && e.identifier != null && !e.isFromArrayElement && e.dereference == null) {
|
||||
var symbol = asmVariableName(e.identifier!!)
|
||||
@@ -707,7 +737,7 @@ class AsmGen6502Internal (
|
||||
Statusflag.Pc -> out(" lda #0 | rol a | sta $tempReg")
|
||||
else -> throw AssemblyError("unsupported statusflag ${fromReg.statusflag}")
|
||||
}
|
||||
else -> throw AssemblyError("weird CPU register ${fromReg}")
|
||||
else -> throw AssemblyError("weird CPU register $fromReg")
|
||||
}
|
||||
|
||||
// Store from temp to destination register
|
||||
@@ -720,7 +750,7 @@ class AsmGen6502Internal (
|
||||
Statusflag.Pc -> out(" lda $tempReg | asl a")
|
||||
else -> throw AssemblyError("unsupported statusflag ${toReg.statusflag}")
|
||||
}
|
||||
else -> throw AssemblyError("weird CPU register ${toReg}")
|
||||
else -> throw AssemblyError("weird CPU register $toReg")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1409,23 +1439,35 @@ $repeatLabel""")
|
||||
pla
|
||||
sta ($var2ZpPtrVar),y""")
|
||||
} else {
|
||||
if (v1.address is PtNumber) {
|
||||
out(" lda ${v1.address.asConstInteger()!!.toHex()} | pha")
|
||||
} else if (v1.address is PtIdentifier) {
|
||||
var1ZpPtrVar = loadByteFromPointerIntoA(v1.address as PtIdentifier)
|
||||
out(" pha")
|
||||
} else {
|
||||
TODO("swap bytes not supported for this expression. Use a simpler expression, or even just a temporary variable and assignments for now. ${v1.position}")
|
||||
when (v1.address) {
|
||||
is PtNumber -> {
|
||||
out(" lda ${v1.address.asConstInteger()!!.toHex()} | pha")
|
||||
}
|
||||
|
||||
is PtIdentifier -> {
|
||||
var1ZpPtrVar = loadByteFromPointerIntoA(v1.address as PtIdentifier)
|
||||
out(" pha")
|
||||
}
|
||||
|
||||
else -> {
|
||||
TODO("swap bytes not supported for this expression. Use a simpler expression, or even just a temporary variable and assignments for now. ${v1.position}")
|
||||
}
|
||||
}
|
||||
if (v2.address is PtNumber) {
|
||||
out(" lda ${v2.address.asConstInteger()!!.toHex()}")
|
||||
} else if (v2.address is PtIdentifier) {
|
||||
var2ZpPtrVar = loadByteFromPointerIntoA(
|
||||
v2.address as PtIdentifier,
|
||||
tempZpPtrVar = "P8ZP_SCRATCH_W1"
|
||||
)
|
||||
} else {
|
||||
TODO("swap bytes not supported for this expression. Use a simpler expression, or even just a temporary variable and assignments for now. ${v2.position}")
|
||||
when (v2.address) {
|
||||
is PtNumber -> {
|
||||
out(" lda ${v2.address.asConstInteger()!!.toHex()}")
|
||||
}
|
||||
|
||||
is PtIdentifier -> {
|
||||
var2ZpPtrVar = loadByteFromPointerIntoA(
|
||||
v2.address as PtIdentifier,
|
||||
tempZpPtrVar = "P8ZP_SCRATCH_W1"
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
TODO("swap bytes not supported for this expression. Use a simpler expression, or even just a temporary variable and assignments for now. ${v2.position}")
|
||||
}
|
||||
}
|
||||
|
||||
if (v1.address is PtNumber) {
|
||||
@@ -1954,8 +1996,7 @@ $repeatLabel""")
|
||||
?: throw AssemblyError("expected function call for multi-value return ${ret.position}")
|
||||
|
||||
// Get the return register specs for the called function
|
||||
val calledSub = symbolTable.lookup(fcall.name)
|
||||
val calledReturnRegs = when(calledSub) {
|
||||
val calledReturnRegs = when(val calledSub = symbolTable.lookup(fcall.name)) {
|
||||
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,6 +2023,16 @@ $repeatLabel""")
|
||||
return
|
||||
}
|
||||
|
||||
if(sub.signature.returns.isEmpty()) {
|
||||
// a void subroutine may return the result of a void function call (e.g. "return voidfunc(args)"):
|
||||
// perform the call for its side effects, then return
|
||||
val fcall = ret.children.single() as? PtFunctionCall
|
||||
?: throw AssemblyError("void subroutine can only return the result of a void function call ${ret.position}")
|
||||
functioncallAsmGen.translateFunctionCall(fcall)
|
||||
out(" rts")
|
||||
return
|
||||
}
|
||||
|
||||
val returnDt = sub.signature.returns.single()
|
||||
if (returnDt.isNumericOrBool || returnDt.isPointer) {
|
||||
assignExpressionToRegister(returnvalue, returnRegs.single().first.registerOrPair!!, returnDt.isSigned)
|
||||
@@ -2016,14 +2067,19 @@ $repeatLabel""")
|
||||
val tgt = AsmAssignTarget(TargetStorageKind.REGISTER, this, targetDt, null, it.first.position, register = it.second.first.registerOrPair!!)
|
||||
assignExpressionTo(it.first as PtExpression, tgt)
|
||||
}
|
||||
// if the float is not the first value, assign it now: the float load (MOVFM) clobbers A/Y,
|
||||
// and the first non-float value is passed in AY, so it must be loaded after the float
|
||||
if(floatIdx > 0 && floatAssign != null) {
|
||||
assignExpressionToRegister(floatAssign.first as PtExpression, floatAssign.second.first.registerOrPair!!, false)
|
||||
}
|
||||
if(nonFloatAssigns.isNotEmpty()) {
|
||||
nonFloatAssigns.first().also {
|
||||
assignExpressionToRegister(it.first as PtExpression, it.second.first.registerOrPair!!, (it.first as PtExpression).type.isSigned)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Assign float return LAST (so FAC1 has the correct value when we RTS)
|
||||
if(floatAssign != null) {
|
||||
if(floatIdx == 0 && floatAssign != null) {
|
||||
assignExpressionToRegister(floatAssign.first as PtExpression, floatAssign.second.first.registerOrPair!!, false)
|
||||
}
|
||||
}
|
||||
@@ -2048,11 +2104,17 @@ $repeatLabel""")
|
||||
throw AssemblyError("%asmbinary inside non-library/non-filesystem module not yet supported")
|
||||
val sourcePath = Path(incbin.definingBlock()!!.source.origin)
|
||||
val includedPath = sourcePath.resolveSibling(incbin.file)
|
||||
val pathForAssembler = options.outputDir // #54: 64tass needs the path *relative to the .asm file*
|
||||
.sanitize()
|
||||
.relativize(includedPath.sanitize())
|
||||
.normalize() // avoid assembler warnings (-Wportable; only some, not all)
|
||||
.toString().replace('\\', '/')
|
||||
val pathForAssembler = try {
|
||||
options.outputDir // #54: 64tass needs the path *relative to the .asm file*
|
||||
.sanitize()
|
||||
.relativize(includedPath.sanitize())
|
||||
.normalize() // avoid assembler warnings (-Wportable; only some, not all)
|
||||
.toString().replace('\\', '/')
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// Path.relativize throws when the two paths have different roots (different drive/volume)
|
||||
errors.err("cannot determine relative path for %asmbinary file '${incbin.file}' (at ${includedPath}): it must be on the same drive/volume as the generated assembly in ${options.outputDir}", incbin.position)
|
||||
return
|
||||
}
|
||||
out(" .binary \"$pathForAssembler\" $offset $length")
|
||||
}
|
||||
|
||||
@@ -2229,9 +2291,7 @@ $repeatLabel""")
|
||||
// optimize pointer,indexregister if possible
|
||||
|
||||
fun evalBytevalueWillClobberA(expr: PtExpression): Boolean {
|
||||
if(!expr.type.isByte)
|
||||
return true
|
||||
return when(expr) {
|
||||
return !expr.type.isByte || when(expr) {
|
||||
is PtIdentifier -> false
|
||||
is PtNumber -> false
|
||||
is PtBool -> false
|
||||
@@ -2640,8 +2700,7 @@ $repeatLabel""")
|
||||
out(" $compare P8ZP_SCRATCH_REG")
|
||||
}
|
||||
|
||||
val e = unwrapCasts(value)
|
||||
when(e) {
|
||||
when(val e = unwrapCasts(value)) {
|
||||
is PtArrayIndexer -> {
|
||||
val constIndex = e.index.asConstInteger()
|
||||
if(constIndex!=null) {
|
||||
|
||||
@@ -49,7 +49,7 @@ internal fun optimizeAssembly(lines: MutableList<String>, machine: ICompilationT
|
||||
|
||||
var linesByFourteen = getLinesBy(pretrimmed, lines, 14)
|
||||
linesByFourteen = runPass(optimizeSameAssignments(linesByFourteen, machine, symbolTable), 14, linesByFourteen)
|
||||
linesByFourteen = runPass(optimizeSamePointerIndexingAndUselessBeq(linesByFourteen), 14, linesByFourteen)
|
||||
linesByFourteen = runPass(optimizeSamePointerIndexingAndUselessBeq(linesByFourteen, machine, symbolTable), 14, linesByFourteen)
|
||||
linesByFourteen = runPass(optimizeAddWordToSameVariableOrExtraRegisterLoadInWordStore(linesByFourteen), 14, linesByFourteen)
|
||||
|
||||
if(!modified)
|
||||
@@ -94,7 +94,7 @@ private fun apply(modifications: List<Modification>, lines: MutableList<String>,
|
||||
lines.removeAt(modification.lineIndex)
|
||||
else {
|
||||
val pretrim = pretrimmed.getOrNull(modification.lineIndex)
|
||||
if (pretrim == null || pretrim.isBlank() || pretrim[0] == ';')
|
||||
if (pretrim.isNullOrBlank() || pretrim[0] == ';')
|
||||
lines.removeAt(modification.lineIndex)
|
||||
else if (haslabelPretrimmed(pretrim)) {
|
||||
val label = keeplabelPretrimmed(pretrim)
|
||||
@@ -412,7 +412,95 @@ private fun optimizeSameAssignments(
|
||||
return mods
|
||||
}
|
||||
|
||||
private fun optimizeSamePointerIndexingAndUselessBeq(linesByFourteen: Sequence<List<TrimmedLine>>): List<Modification> {
|
||||
private const val NO_MEMORY_WRITE = ""
|
||||
private const val UNKNOWN_WRITE_TARGET = "\u0000"
|
||||
|
||||
/** Mnemonics that can change memory (direct stores plus read-modify-write). */
|
||||
private val memoryWritingMnemonics = setOf(
|
||||
"sta", "stx", "sty", "stz", "inc", "dec", "asl", "lsr", "rol", "ror",
|
||||
"and", "ora", "eor", "adc", "sbc", "bit"
|
||||
)
|
||||
|
||||
/** Control transfers that can reach the code after them with a different Y, or change it. */
|
||||
private val flowMnemonics = setOf(
|
||||
"jsr", "rts", "rti", "jmp", "bra", "brk",
|
||||
"bcc", "bcs", "beq", "bmi", "bne", "bpl", "bvc", "bvs", "bbs", "bbr"
|
||||
)
|
||||
|
||||
private fun TrimmedLine.mnemonic(): String =
|
||||
instruction.trimStart().takeWhile { !it.isWhitespace() }.lowercase()
|
||||
|
||||
/** True if the line carries a label, with or without an instruction following it. */
|
||||
private fun TrimmedLine.hasLabel(): Boolean {
|
||||
return haslabelPretrimmed(trimmed) || trimmed.isNotEmpty() && (trimmed[0] == '+' || trimmed[0] == '-')
|
||||
}
|
||||
|
||||
/** The memory operand this instruction writes, with any +offset and ,index suffix stripped.
|
||||
* Returns [NO_MEMORY_WRITE] for immediate and implied forms, and [UNKNOWN_WRITE_TARGET] for
|
||||
* indirect forms, where the destination cannot be determined statically. */
|
||||
private fun TrimmedLine.memoryWriteBase(): String {
|
||||
val op = instruction.extractOperandTrimmed()
|
||||
if (op.isEmpty() || op.startsWith("#") || op == "a") return NO_MEMORY_WRITE
|
||||
if (op.startsWith("(")) return UNKNOWN_WRITE_TARGET
|
||||
return op.removeSuffix(",y").removeSuffix(",x").substringBefore('+')
|
||||
}
|
||||
|
||||
/** True if this line may write to [base]. Conservative: an indirect store is assumed to. */
|
||||
private fun TrimmedLine.mayWriteBase(base: String): Boolean {
|
||||
if (mnemonic() !in memoryWritingMnemonics) return false
|
||||
val target = memoryWriteBase()
|
||||
if (target == NO_MEMORY_WRITE) return false
|
||||
if (target == UNKNOWN_WRITE_TARGET) return true
|
||||
return target.equals(base, ignoreCase = true)
|
||||
}
|
||||
|
||||
/** True if operand is safe to read more than once: an immediate, or memory that is not an
|
||||
* IO location (an IO read can return a different value without the CPU having written it). */
|
||||
private fun isNonVolatileIndexOperand(indexLine: TrimmedLine, machine: ICompilationTarget, symbolTable: SymbolTable): Boolean {
|
||||
val operand = indexLine.instruction.extractOperandTrimmed()
|
||||
if (operand.isEmpty() || operand.startsWith("#")) return true
|
||||
val addr = getAddressArg(indexLine.instruction, symbolTable)
|
||||
return if (addr == null) !looksLikeIOAddress(operand, machine)
|
||||
else !machine.isIOAddress(addr)
|
||||
}
|
||||
|
||||
/** Decides whether the index reload at [reloadIdx] inside [window] can be deleted.
|
||||
*
|
||||
* The reload is redundant only if Y still holds the value it would have loaded. That requires
|
||||
* the reload to load the very same operand as the original index load, and none of the
|
||||
* following in between:
|
||||
* - modifies Y
|
||||
* - writes the index operand itself (so `ldy i / lda arr,y / inc i / ldy i / sta arr,y` keeps
|
||||
* its reload: without it the store lands one element earlier)
|
||||
* - is a call or control transfer, since a callee can clobber Y and another path can arrive
|
||||
* with a different Y
|
||||
* - carries a label, for the same reason
|
||||
* The index operand must also not be a volatile IO location.
|
||||
*/
|
||||
private fun canDropIndexReload(
|
||||
window: List<TrimmedLine>,
|
||||
reloadIdx: Int,
|
||||
indexLine: TrimmedLine,
|
||||
machine: ICompilationTarget,
|
||||
symbolTable: SymbolTable
|
||||
): Boolean {
|
||||
if (reloadIdx <= 1) return false
|
||||
val operand = indexLine.instruction.extractOperandTrimmed()
|
||||
// the reload has to load the same index, or Y holds a different one and is not redundant
|
||||
if (window[reloadIdx].instruction.extractOperandTrimmed() != operand) return false
|
||||
val base = if (operand.startsWith("#") || operand.isEmpty()) NO_MEMORY_WRITE
|
||||
else operand.removeSuffix(",y").removeSuffix(",x").substringBefore('+')
|
||||
for (idx in 1 until reloadIdx) {
|
||||
val line = window[idx]
|
||||
if (line.instruction.modifiesYRegister()) return false
|
||||
if (line.mnemonic() in flowMnemonics) return false
|
||||
if (line.hasLabel()) return false
|
||||
if (base != NO_MEMORY_WRITE && line.mayWriteBase(base)) return false
|
||||
}
|
||||
return isNonVolatileIndexOperand(indexLine, machine, symbolTable)
|
||||
}
|
||||
|
||||
private fun optimizeSamePointerIndexingAndUselessBeq(linesByFourteen: Sequence<List<TrimmedLine>>, machine: ICompilationTarget, symbolTable: SymbolTable): List<Modification> {
|
||||
|
||||
// Optimize same pointer indexing where for instance we load and store to the same ptr index in Y
|
||||
// if Y isn't modified in between we can omit the second LDY:
|
||||
@@ -426,30 +514,48 @@ private fun optimizeSamePointerIndexingAndUselessBeq(linesByFourteen: Sequence<L
|
||||
for (lines in linesByFourteen) {
|
||||
val f1 = lines[0].instruction
|
||||
val f2 = lines[1].instruction
|
||||
val f3 = lines[2].instruction
|
||||
//val f3 = lines[2].instruction
|
||||
val f4 = lines[3].instruction
|
||||
val f5 = lines[4].instruction
|
||||
val f6 = lines[5].instruction
|
||||
|
||||
if(f1.startsWith("ldy ") && f2.startsWith("lda ") && f4.startsWith("ldy ") && f5.startsWith("sta ")) {
|
||||
val firstvalue = f1.extractOperandTrimmed()
|
||||
//val firstvalue = f1.extractOperandTrimmed()
|
||||
val secondvalue = f2.extractOperandTrimmed()
|
||||
val fourthvalue = f4.extractOperandTrimmed()
|
||||
//val fourthvalue = f4.extractOperandTrimmed()
|
||||
val fifthvalue = f5.extractOperandTrimmed()
|
||||
if(!f3.modifiesYRegister() && firstvalue==fourthvalue && secondvalue==fifthvalue && secondvalue.endsWith(",y") && fifthvalue.endsWith(",y")) {
|
||||
if(secondvalue==fifthvalue && secondvalue.endsWith(",y") && fifthvalue.endsWith(",y") &&
|
||||
canDropIndexReload(lines, 3, lines[0], machine, symbolTable)) {
|
||||
mods.add(Modification(lines[3].index, true, null))
|
||||
}
|
||||
}
|
||||
if(f1.startsWith("ldy ") && f2.startsWith("lda ") && f5.startsWith("ldy ") && f6.startsWith("sta ")) {
|
||||
val firstvalue = f1.extractOperandTrimmed()
|
||||
val secondvalue = f2.extractOperandTrimmed()
|
||||
val fifthvalue = f5.extractOperandTrimmed()
|
||||
val sixthvalue = f6.extractOperandTrimmed()
|
||||
if(!f3.modifiesYRegister() && !f4.modifiesYRegister() && firstvalue==fifthvalue && secondvalue==sixthvalue && secondvalue.endsWith(",y") && sixthvalue.endsWith(",y")) {
|
||||
if(secondvalue==sixthvalue && secondvalue.endsWith(",y") && sixthvalue.endsWith(",y") &&
|
||||
canDropIndexReload(lines, 4, lines[0], machine, symbolTable)) {
|
||||
mods.add(Modification(lines[4].index, true, null))
|
||||
}
|
||||
}
|
||||
|
||||
// Relaxed variant: two consecutive indexed accesses, not necessarily a matched load/store
|
||||
// pair. Only the index reload has to be the same, and nothing in between may disturb it.
|
||||
// ldy V
|
||||
// lda A,y
|
||||
// ... <-- does not modify Y, does not write V, no call or label
|
||||
// ldy V <-- can be removed
|
||||
// lda B,y
|
||||
if(f1.startsWith("ldy ") && f2.startsWith("lda ") && f4.startsWith("ldy ") && f5.startsWith("lda ") &&
|
||||
f2.endsWith(",y") && f5.endsWith(",y") &&
|
||||
canDropIndexReload(lines, 3, lines[0], machine, symbolTable)) {
|
||||
mods.add(Modification(lines[3].index, true, null))
|
||||
}
|
||||
if(f1.startsWith("ldy ") && f2.startsWith("lda ") && f5.startsWith("ldy ") && f6.startsWith("lda ") &&
|
||||
f2.endsWith(",y") && f6.endsWith(",y") &&
|
||||
canDropIndexReload(lines, 4, lines[0], machine, symbolTable)) {
|
||||
mods.add(Modification(lines[4].index, true, null))
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
beq +
|
||||
@@ -637,11 +743,9 @@ private fun looksLikeIOAddress(operand: String, machine: ICompilationTarget): Bo
|
||||
}
|
||||
// Try to parse decimal address (e.g. "53280" = $d000) and delegate to machine
|
||||
val decVal = operand.toUIntOrNull()
|
||||
if(decVal != null)
|
||||
return machine.isIOAddress(decVal)
|
||||
// For unresolved symbolic names we can't determine - assume NOT IO
|
||||
// (the symbol table lookup already failed, so we have no better info)
|
||||
return false
|
||||
return decVal != null && machine.isIOAddress(decVal)
|
||||
}
|
||||
|
||||
private fun optimizeIncDec(linesByFour: Sequence<List<TrimmedLine>>): List<Modification> {
|
||||
@@ -913,8 +1017,7 @@ private fun scaleConstant(constStr: String, scale: Int): String {
|
||||
}
|
||||
|
||||
private fun isArrayByteSizedAndSmall(name: String, symbolTable: SymbolTable): Boolean {
|
||||
val symbol = symbolTable.flat[name]
|
||||
return when (symbol) {
|
||||
return when (val symbol = symbolTable.flat[name]) {
|
||||
is StStaticVariable -> {
|
||||
val len = symbol.length
|
||||
(symbol.dt.isByteArray || symbol.dt.isString) && len != null && len <= 256u
|
||||
@@ -928,8 +1031,7 @@ private fun isArrayByteSizedAndSmall(name: String, symbolTable: SymbolTable): Bo
|
||||
}
|
||||
|
||||
private fun isArrayWordSizedAndSmall(name: String, symbolTable: SymbolTable): Boolean {
|
||||
val symbol = symbolTable.flat[name]
|
||||
return when (symbol) {
|
||||
return when (val symbol = symbolTable.flat[name]) {
|
||||
is StStaticVariable -> {
|
||||
val len = symbol.length
|
||||
symbol.dt.isWordArray && len != null && len <= 256u
|
||||
|
||||
@@ -20,6 +20,25 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
translateFunctioncall(fcall, null, discardResult = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Several builtins can only produce their result in a CPU register, and which one that is
|
||||
* depends on the width of the result: the byte-returning ones (lsb/msb) take a single register,
|
||||
* the word-returning ones (lsw/msw) and popw take a register pair, and the builtin picks the
|
||||
* specific register. [hardwareReg] is the register to fall back on when the caller has no
|
||||
* preference, or when it asked for something the builtin cannot deliver to directly.
|
||||
*
|
||||
* The caller's requested register is honoured when it is a real CPU register. A CX16 virtual
|
||||
* register (for example an asmsub parameter declared as @R0) is not: those are memory locations,
|
||||
* so the builtin produces its result in [hardwareReg] and the caller's assignment then stores it
|
||||
* into the virtual register. Passing the virtual register through would make the builtin reject
|
||||
* it as an invalid result register.
|
||||
*
|
||||
* (funcPeekW and funcMkword already handle virtual registers themselves.)
|
||||
*/
|
||||
private fun withResultRegister(requested: RegisterOrPair?, hardwareReg: RegisterOrPair, compute: (RegisterOrPair) -> Array<RegisterOrPair>): Array<RegisterOrPair> =
|
||||
if (requested != null && requested !in Cx16VirtualRegisters) compute(requested)
|
||||
else compute(hardwareReg)
|
||||
|
||||
private fun translateFunctioncall(fcall: PtFunctionCall, firstReturnRegister: RegisterOrPair?, discardResult: Boolean): Array<RegisterOrPair> {
|
||||
|
||||
// Note: returns the actual register(s) that the return value is in, so the caller should still make sure to place it into the actual correct destination.
|
||||
@@ -31,12 +50,12 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
val sscope = fcall.definingISub()
|
||||
|
||||
return when (fcall.name) {
|
||||
"msw" -> funcMsw(fcall, firstReturnRegister ?: RegisterOrPair.AY)
|
||||
"lsw" -> funcLsw(fcall, firstReturnRegister ?: RegisterOrPair.AY)
|
||||
"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)
|
||||
"msw" -> withResultRegister(firstReturnRegister, RegisterOrPair.AY) { funcMsw(fcall, it) }
|
||||
"lsw" -> withResultRegister(firstReturnRegister, RegisterOrPair.AY) { funcLsw(fcall, it) }
|
||||
"msb" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcMsb(fcall, false, it) }
|
||||
"msb__long" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcMsb(fcall, true, it) }
|
||||
"lsb" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcLsb(fcall, false, it) }
|
||||
"lsb__long" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcLsb(fcall, true, it) }
|
||||
"mkword" -> funcMkword(fcall, firstReturnRegister ?: RegisterOrPair.AY)
|
||||
"mklong", "mklong2" -> funcMklong(fcall, firstReturnRegister ?: RegisterOrPair.R14R15)
|
||||
"clamp__byte", "clamp__ubyte" -> funcClamp(fcall)
|
||||
@@ -98,8 +117,8 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
"pushw" -> funcPushW(fcall)
|
||||
"pushl" -> funcPushL(fcall)
|
||||
"pushf" -> funcPushF(fcall)
|
||||
"pop" -> funcPop(firstReturnRegister ?: RegisterOrPair.A)
|
||||
"popw" -> funcPopW(firstReturnRegister ?: RegisterOrPair.AY)
|
||||
"pop" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcPop(it) }
|
||||
"popw" -> withResultRegister(firstReturnRegister, RegisterOrPair.AY) { funcPopW(it) }
|
||||
"popl" -> funcPopL()
|
||||
"popf" -> funcPopF()
|
||||
else -> throw AssemblyError("sizeof must have been replaced with a constant ${fcall.position}")
|
||||
@@ -247,11 +266,15 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
}
|
||||
BaseDataType.UWORD -> {
|
||||
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.AY)
|
||||
// prog8_math.square does its own range check and falls back to the general
|
||||
// multiply_words for values outside -255..255, so the fast path stays valid.
|
||||
asmgen.out(" jsr prog8_math.square") // result is in AY
|
||||
return arrayOf(RegisterOrPair.AY)
|
||||
}
|
||||
BaseDataType.LONG -> {
|
||||
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.R14R15, true)
|
||||
// prog8_math.square_long does its own range check and falls back to the general
|
||||
// multiply_longs when the long doesn't fit in a signed 16-bit word.
|
||||
asmgen.out(" jsr prog8_math.square_long") // result is in R14:R15
|
||||
return arrayOf(RegisterOrPair.R14R15)
|
||||
}
|
||||
@@ -409,8 +432,7 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
}
|
||||
|
||||
private fun funcCallFar(fcall: PtFunctionCall): Array<RegisterOrPair> {
|
||||
val targetName = asmgen.options.compTarget.name
|
||||
val jsrfar = when(targetName) {
|
||||
val jsrfar = when(val targetName = asmgen.options.compTarget.name) {
|
||||
"cx16" -> "cx16.JSRFAR"
|
||||
"c64" -> "c64.x16jsrfar"
|
||||
"c128" -> "c128.x16jsrfar"
|
||||
@@ -426,7 +448,7 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
.byte $constBank""")
|
||||
} else {
|
||||
if(asmgen.options.romable)
|
||||
TODO("non-const callfar (jsrfar) yet that's usable in ROM ${fcall.position}")
|
||||
asmgen.errors.err("non-const callfar (jsrfar) has no ROMable code generation", fcall.position)
|
||||
// self-modifying code: set jsrfar arguments
|
||||
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A) // bank
|
||||
asmgen.out(" sta (++)+0")
|
||||
@@ -483,7 +505,7 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
.byte $constBank""")
|
||||
} else {
|
||||
if(asmgen.options.romable)
|
||||
TODO("non-const callfar2 (jsrfar) yet that's usable in ROM ${fcall.position}")
|
||||
asmgen.errors.err("non-const callfar2 (jsrfar) has no ROMable code generation", fcall.position)
|
||||
// self-modifying code: set jsrfar arguments
|
||||
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A) // bank
|
||||
asmgen.out(" sta (++)+0")
|
||||
@@ -2065,6 +2087,154 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
}
|
||||
}
|
||||
|
||||
private fun funcClampWordConst(minExpr: PtExpression, maxExpr: PtExpression, minOp: Pair<String, String>, maxOp: Pair<String, String>, signed: Boolean) {
|
||||
// value is already in AY on entry, result in AY
|
||||
val minVal = (asmgen.unwrapCasts(minExpr) as? PtNumber)?.number?.toInt()
|
||||
val maxVal = (asmgen.unwrapCasts(maxExpr) as? PtNumber)?.number?.toInt()
|
||||
val typeMin = if (signed) -32768 else 0
|
||||
val typeMax = if (signed) 32767 else 65535
|
||||
val oneSided = minVal != null && minVal == typeMin || maxVal != null && maxVal == typeMax
|
||||
val fullRange = minVal != null && maxVal != null && minVal == typeMin && maxVal == typeMax
|
||||
when {
|
||||
fullRange -> {
|
||||
// no-op: the value in AY is already the clamped result
|
||||
}
|
||||
signed && minVal == typeMin -> {
|
||||
// upper clamp only
|
||||
emitSignedWordClampProlog()
|
||||
emitSignedWordUpperCompare(maxOp)
|
||||
emitWordLoadValueFromW1()
|
||||
}
|
||||
signed && maxVal == typeMax -> {
|
||||
// lower clamp only
|
||||
emitSignedWordClampProlog()
|
||||
emitSignedWordLowerCompare(minOp)
|
||||
}
|
||||
signed -> {
|
||||
emitSignedWordClampProlog()
|
||||
emitSignedWordUpperCompare(maxOp)
|
||||
emitSignedWordLowerCompare(minOp)
|
||||
}
|
||||
!oneSided -> {
|
||||
// uword, two-sided clamp
|
||||
emitWordUpperClamp(maxOp)
|
||||
emitWordLowerClamp(minOp)
|
||||
}
|
||||
minVal == typeMin -> {
|
||||
// uword, upper clamp only
|
||||
emitWordUpperClamp(maxOp)
|
||||
}
|
||||
else -> {
|
||||
// uword, lower clamp only
|
||||
emitWordLowerClamp(minOp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitSignedWordClampProlog() {
|
||||
asmgen.out(listOf(
|
||||
" sta P8ZP_SCRATCH_W1",
|
||||
" sty P8ZP_SCRATCH_W1+1"
|
||||
).joinToString("\n"))
|
||||
}
|
||||
|
||||
private fun emitWordLoadValueFromW1() {
|
||||
asmgen.out(listOf(
|
||||
" lda P8ZP_SCRATCH_W1",
|
||||
" ldy P8ZP_SCRATCH_W1+1"
|
||||
).joinToString("\n"))
|
||||
}
|
||||
|
||||
// signed compare of max/min (16-bit immediates) against the running value in P8ZP_SCRATCH_W1.
|
||||
// The result of the two-sided compare is the high byte of (bound - value), with the
|
||||
// overflow-compensated sign bit set, mirroring the func_clamp_word sequence.
|
||||
|
||||
private fun emitSignedWordUpperCompare(maxOp: Pair<String, String>) {
|
||||
// if value > max: running value becomes max
|
||||
val keep = asmgen.makeLabel("clampkeep")
|
||||
val done = asmgen.makeLabel("clampdone")
|
||||
asmgen.out(listOf(
|
||||
" ldy ${maxOp.second}",
|
||||
" lda ${maxOp.first}",
|
||||
" cmp P8ZP_SCRATCH_W1",
|
||||
" tya",
|
||||
" sbc P8ZP_SCRATCH_W1+1",
|
||||
" bvc $keep",
|
||||
" eor #$80",
|
||||
keep,
|
||||
" bpl $done",
|
||||
" lda ${maxOp.first}",
|
||||
" ldy ${maxOp.second}",
|
||||
" sta P8ZP_SCRATCH_W1",
|
||||
" sty P8ZP_SCRATCH_W1+1",
|
||||
done
|
||||
).joinToString("\n"))
|
||||
}
|
||||
|
||||
private fun emitSignedWordLowerCompare(minOp: Pair<String, String>) {
|
||||
// if value < min: result becomes min, otherwise the running value (in W1)
|
||||
val keep = asmgen.makeLabel("clampkeep")
|
||||
val set = asmgen.makeLabel("clampset")
|
||||
val done = asmgen.makeLabel("clampdone")
|
||||
asmgen.out(listOf(
|
||||
" ldy ${minOp.second}",
|
||||
" lda ${minOp.first}",
|
||||
" cmp P8ZP_SCRATCH_W1",
|
||||
" tya",
|
||||
" sbc P8ZP_SCRATCH_W1+1",
|
||||
" bvc $keep",
|
||||
" eor #$80",
|
||||
keep,
|
||||
" bpl $set",
|
||||
" lda P8ZP_SCRATCH_W1",
|
||||
" ldy P8ZP_SCRATCH_W1+1",
|
||||
" jmp $done",
|
||||
set,
|
||||
" lda ${minOp.first}",
|
||||
" ldy ${minOp.second}",
|
||||
done
|
||||
).joinToString("\n"))
|
||||
}
|
||||
|
||||
private fun emitWordUpperClamp(maxOp: Pair<String, String>) {
|
||||
// uword: clamp the value in AY down to maxOp (result = min(value, max))
|
||||
val keep = asmgen.makeLabel("clampkeep")
|
||||
val set = asmgen.makeLabel("clampset")
|
||||
asmgen.out(listOf(
|
||||
" cpy ${maxOp.second}",
|
||||
" bcc $keep",
|
||||
" bne $set",
|
||||
" cmp ${maxOp.first}",
|
||||
" bcc $keep",
|
||||
" beq $keep",
|
||||
set,
|
||||
" lda ${maxOp.first}",
|
||||
" ldy ${maxOp.second}",
|
||||
keep
|
||||
).joinToString("\n"))
|
||||
}
|
||||
|
||||
private fun emitWordLowerClamp(minOp: Pair<String, String>) {
|
||||
// uword: clamp the value in AY up to minOp (result = max(value, min))
|
||||
val keep = asmgen.makeLabel("clampkeep")
|
||||
val set = asmgen.makeLabel("clampset")
|
||||
val done = asmgen.makeLabel("clampdone")
|
||||
asmgen.out(listOf(
|
||||
" cpy ${minOp.second}",
|
||||
" bcc $set",
|
||||
" bne $keep",
|
||||
" cmp ${minOp.first}",
|
||||
" bcc $set",
|
||||
" beq $keep",
|
||||
keep,
|
||||
" jmp $done",
|
||||
set,
|
||||
" lda ${minOp.first}",
|
||||
" ldy ${minOp.second}",
|
||||
done
|
||||
).joinToString("\n"))
|
||||
}
|
||||
|
||||
private fun funcClamp(fcall: PtFunctionCall): Array<RegisterOrPair> {
|
||||
val signed = fcall.type.isSigned
|
||||
val zpCheck = { name: String -> asmgen.isZpVar(name) }
|
||||
@@ -2099,6 +2269,17 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
return arrayOf(RegisterOrPair.A)
|
||||
}
|
||||
fcall.type.isWord -> {
|
||||
val minOp = asmgen.getStaticAddressLowHigh(fcall.args[1])
|
||||
val maxOp = asmgen.getStaticAddressLowHigh(fcall.args[2])
|
||||
val minIsConst = asmgen.unwrapCasts(fcall.args[1]) !is PtIdentifier
|
||||
val maxIsConst = asmgen.unwrapCasts(fcall.args[2]) !is PtIdentifier
|
||||
if (minOp != null && maxOp != null && minIsConst && maxIsConst) {
|
||||
// both bounds are compile-time constants (numeric literals or fixed label addresses):
|
||||
// emit a small inline clamp instead of evaluating bounds into scratch + jsr func_clamp_word/uword
|
||||
assignAsmGen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.AY, signed) // value
|
||||
funcClampWordConst(fcall.args[1], fcall.args[2], minOp, maxOp, signed)
|
||||
return arrayOf(RegisterOrPair.AY)
|
||||
}
|
||||
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck) && fcall.args[2].isSimple(zpCheck)) {
|
||||
assignAsmGen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W1", fcall.args[1].type) // minimum
|
||||
assignAsmGen.assignExpressionToVariable(fcall.args[2], "P8ZP_SCRATCH_W2", fcall.args[2].type) // maximum
|
||||
@@ -2393,9 +2574,7 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
|
||||
internal fun funcMklong(fcall: PtFunctionCall, resultReg: RegisterOrPair, targetVarName: String? = null): Array<RegisterOrPair> {
|
||||
fun isArgRegister(expression: PtExpression, reg: RegisterOrPair): Boolean {
|
||||
if(expression !is PtIdentifier)
|
||||
return false
|
||||
return expression.name.startsWith("cx16.${reg.name.lowercase()}")
|
||||
return expression is PtIdentifier && expression.name.startsWith("cx16.${reg.name.lowercase()}")
|
||||
}
|
||||
|
||||
val (r1, r2) = when(resultReg) {
|
||||
|
||||
@@ -313,9 +313,7 @@ internal class IfElseAsmGen(private val program: PtProgram,
|
||||
|
||||
val left = condition.left
|
||||
val right = condition.right
|
||||
val operator = condition.operator
|
||||
|
||||
when (operator) {
|
||||
when (val operator = condition.operator) {
|
||||
"==" -> {
|
||||
// if X==value
|
||||
asmgen.assignExpressionToRegister(left, RegisterOrPair.A, signed)
|
||||
@@ -595,9 +593,7 @@ internal class IfElseAsmGen(private val program: PtProgram,
|
||||
|
||||
val left = condition.left
|
||||
val right = condition.right
|
||||
val operator = condition.operator
|
||||
|
||||
return when (operator) {
|
||||
return when (val operator = condition.operator) {
|
||||
"==" -> longEqualsValue(left, right, false, jumpAfterIf, stmt)
|
||||
"!=" -> longEqualsValue(left, right, true, jumpAfterIf, stmt)
|
||||
"<", "<=", ">", ">=" -> compareLongValues(left, operator, right, jumpAfterIf, stmt)
|
||||
@@ -932,8 +928,7 @@ internal class IfElseAsmGen(private val program: PtProgram,
|
||||
asmgen.out(" lda $varname+$offset")
|
||||
return
|
||||
}
|
||||
val e = asmgen.unwrapCasts(expr)
|
||||
when (e) {
|
||||
when (val e = asmgen.unwrapCasts(expr)) {
|
||||
is PtArrayIndexer -> {
|
||||
if (e.variable == null)
|
||||
throw AssemblyError("support for ptr indexing ${e.position}")
|
||||
@@ -1830,24 +1825,30 @@ internal class IfElseAsmGen(private val program: PtProgram,
|
||||
block("#<${constValue}", "#>${constValue}")
|
||||
} else {
|
||||
val e = asmgen.unwrapCasts(expr)
|
||||
if (e is PtIdentifier) {
|
||||
val varname = asmgen.asmVariableName(e)
|
||||
block(varname, "$varname+1")
|
||||
} else if (e is PtAddressOf && !e.isFromArrayElement && e.identifier != null) {
|
||||
val identifier = e.identifier!!
|
||||
val varname = if (identifier.type.isSplitWordArray(asmgen.options.compTarget)) {
|
||||
if (e.isMsbForSplitArray) identifier.name + "_msb" else identifier.name + "_lsb"
|
||||
} else {
|
||||
identifier.name
|
||||
when (e) {
|
||||
is PtIdentifier -> {
|
||||
val varname = asmgen.asmVariableName(e)
|
||||
block(varname, "$varname+1")
|
||||
}
|
||||
|
||||
is PtAddressOf if !e.isFromArrayElement && e.identifier != null -> {
|
||||
val identifier = e.identifier!!
|
||||
val varname = if (identifier.type.isSplitWordArray(asmgen.options.compTarget)) {
|
||||
if (e.isMsbForSplitArray) identifier.name + "_msb" else identifier.name + "_lsb"
|
||||
} else {
|
||||
identifier.name
|
||||
}
|
||||
block("#<$varname", "#>$varname")
|
||||
}
|
||||
|
||||
else -> {
|
||||
asmgen.saveRegisterStack(CpuRegister.A, false)
|
||||
asmgen.saveRegisterStack(CpuRegister.Y, false)
|
||||
asmgen.assignExpressionToVariable(expr, "P8ZP_SCRATCH_W1", expr.type)
|
||||
asmgen.restoreRegisterStack(CpuRegister.Y, false)
|
||||
asmgen.restoreRegisterStack(CpuRegister.A, false)
|
||||
block("P8ZP_SCRATCH_W1", "P8ZP_SCRATCH_W1+1")
|
||||
}
|
||||
block("#<$varname", "#>$varname")
|
||||
} else {
|
||||
asmgen.saveRegisterStack(CpuRegister.A, false)
|
||||
asmgen.saveRegisterStack(CpuRegister.Y, false)
|
||||
asmgen.assignExpressionToVariable(expr, "P8ZP_SCRATCH_W1", expr.type)
|
||||
asmgen.restoreRegisterStack(CpuRegister.Y, false)
|
||||
asmgen.restoreRegisterStack(CpuRegister.A, false)
|
||||
block("P8ZP_SCRATCH_W1", "P8ZP_SCRATCH_W1+1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,8 +104,7 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
|
||||
}
|
||||
|
||||
private fun evalIfExpressionConditonAndBranchWhenFalse(stmtCondition: PtExpression, falseLabel: String) {
|
||||
val condition = asmgen.unwrapCasts(stmtCondition)
|
||||
when (condition) {
|
||||
when (val condition = asmgen.unwrapCasts(stmtCondition)) {
|
||||
is PtBinaryExpression -> {
|
||||
val rightDt = condition.right.type
|
||||
return when {
|
||||
@@ -802,8 +801,7 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
|
||||
translateWordExprIsZero(expr, falseLabel)
|
||||
return
|
||||
}
|
||||
val e = asmgen.unwrapCasts(expr)
|
||||
when (e) {
|
||||
when (val e = asmgen.unwrapCasts(expr)) {
|
||||
is PtIdentifier -> {
|
||||
val varname = asmgen.asmVariableName(e)
|
||||
asmgen.out("""
|
||||
@@ -853,8 +851,7 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
|
||||
translateWordExprIsNotZero(expr, falseLabel)
|
||||
return
|
||||
}
|
||||
val e = asmgen.unwrapCasts(expr)
|
||||
when (e) {
|
||||
when (val e = asmgen.unwrapCasts(expr)) {
|
||||
is PtIdentifier -> {
|
||||
val varname = asmgen.asmVariableName(e)
|
||||
asmgen.out("""
|
||||
@@ -1092,8 +1089,7 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
|
||||
asmgen.out(" lda $varname+$offset")
|
||||
return
|
||||
}
|
||||
val e = asmgen.unwrapCasts(expr)
|
||||
when (e) {
|
||||
when (val e = asmgen.unwrapCasts(expr)) {
|
||||
is PtArrayIndexer -> {
|
||||
if (e.variable == null)
|
||||
throw AssemblyError("support for ptr indexing ${e.position}")
|
||||
@@ -1149,23 +1145,29 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
|
||||
block("#<${constValue}", "#>${constValue}")
|
||||
} else {
|
||||
val e = asmgen.unwrapCasts(expr)
|
||||
if (e is PtIdentifier) {
|
||||
val varname = asmgen.asmVariableName(e)
|
||||
block(varname, "$varname+1")
|
||||
} else if (e is PtAddressOf && !e.isFromArrayElement) {
|
||||
val identifier = e.identifier!!
|
||||
var varname = asmgen.asmVariableName(identifier)
|
||||
if (identifier.type.isSplitWordArray(asmgen.options.compTarget)) {
|
||||
varname += if (e.isMsbForSplitArray) "_msb" else "_lsb"
|
||||
when (e) {
|
||||
is PtIdentifier -> {
|
||||
val varname = asmgen.asmVariableName(e)
|
||||
block(varname, "$varname+1")
|
||||
}
|
||||
|
||||
is PtAddressOf if !e.isFromArrayElement -> {
|
||||
val identifier = e.identifier!!
|
||||
var varname = asmgen.asmVariableName(identifier)
|
||||
if (identifier.type.isSplitWordArray(asmgen.options.compTarget)) {
|
||||
varname += if (e.isMsbForSplitArray) "_msb" else "_lsb"
|
||||
}
|
||||
block("#<$varname", "#>$varname")
|
||||
}
|
||||
|
||||
else -> {
|
||||
asmgen.saveRegisterStack(CpuRegister.A, false)
|
||||
asmgen.saveRegisterStack(CpuRegister.Y, false)
|
||||
asmgen.assignExpressionToVariable(expr, "P8ZP_SCRATCH_W1", expr.type)
|
||||
asmgen.restoreRegisterStack(CpuRegister.Y, false)
|
||||
asmgen.restoreRegisterStack(CpuRegister.A, false)
|
||||
block("P8ZP_SCRATCH_W1", "P8ZP_SCRATCH_W1+1")
|
||||
}
|
||||
block("#<$varname", "#>$varname")
|
||||
} else {
|
||||
asmgen.saveRegisterStack(CpuRegister.A, false)
|
||||
asmgen.saveRegisterStack(CpuRegister.Y, false)
|
||||
asmgen.assignExpressionToVariable(expr, "P8ZP_SCRATCH_W1", expr.type)
|
||||
asmgen.restoreRegisterStack(CpuRegister.Y, false)
|
||||
asmgen.restoreRegisterStack(CpuRegister.A, false)
|
||||
block("P8ZP_SCRATCH_W1", "P8ZP_SCRATCH_W1+1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ internal class ProgramAndVarsGen(
|
||||
|
||||
asmgen.out("; $cpu assembly code for '${program.name}'")
|
||||
asmgen.out("; generated by $ourName on ${LocalDateTime.now().withNano(0)}")
|
||||
asmgen.out("; prog8 compiler version: ${options.compilerVersion}")
|
||||
asmgen.out("; assembler syntax is for the 64tasm cross-assembler")
|
||||
asmgen.out("; output options: output=${options.output} launcher=${options.launcher} zp=${options.zeropage}")
|
||||
asmgen.out("")
|
||||
@@ -396,24 +397,27 @@ internal class ProgramAndVarsGen(
|
||||
|
||||
asmgen.out("; struct types")
|
||||
symboltable.allStructTypes().distinctBy { it.name }.forEach { structtype ->
|
||||
val paramFields = structtype.fields.filter { !it.isArray }
|
||||
val structargs = paramFields.indices.joinToString(",") { "f$it" }
|
||||
asmgen.out("${structtype.scopedNameString} .struct $structargs\n")
|
||||
var paramIdx = 0
|
||||
for(field in structtype.fields) {
|
||||
val type = asmTypeString(field.type)
|
||||
if(field.isArray) {
|
||||
val anonymous = List(field.arraySize!!) { "?" }.joinToString(",")
|
||||
asmgen.out("p8v_${field.name} $type $anonymous")
|
||||
} else {
|
||||
asmgen.out("p8v_${field.name} $type \\f$paramIdx")
|
||||
paramIdx++
|
||||
}
|
||||
if(structtype.isUnion) {
|
||||
// NB: .union takes no constructor parameters, unlike .struct
|
||||
asmgen.out("${structtype.scopedNameString} .union\n")
|
||||
emitStructDefFields(structtype) { "?" }
|
||||
asmgen.out(" .endunion\n")
|
||||
asmgen.out("${structtype.scopedNameString}_size=${structtype.size}\n")
|
||||
} else {
|
||||
val paramFields = structtype.fields.filter { !it.isArray }
|
||||
val structargs = paramFields.indices.joinToString(",") { "f$it" }
|
||||
asmgen.out("${structtype.scopedNameString} .struct $structargs\n")
|
||||
var paramIdx = 0
|
||||
emitStructDefFields(structtype) { "\\f${paramIdx++}" }
|
||||
asmgen.out(" .endstruct\n")
|
||||
asmgen.out("${structtype.scopedNameString}_size=${structtype.size}\n")
|
||||
}
|
||||
asmgen.out(" .endstruct\n")
|
||||
}
|
||||
|
||||
val (instancesNoInit, instances) = symboltable.allStructInstances().partition { it.initialValues.isEmpty() }
|
||||
val (instancesNoInit, instances) = symboltable.allStructInstances().partition {
|
||||
val structtype = symboltable.lookup(it.structName) as StStruct
|
||||
it.initialValues.isEmpty() || structtype.isUnion
|
||||
}
|
||||
asmgen.out("; struct instances without initialization values, as BSS zeroed at startup\n")
|
||||
asmgen.out(" .section BSS\n")
|
||||
asmgen.out("${StStructInstanceBlockName}_bss .block\n")
|
||||
@@ -454,6 +458,27 @@ internal class ProgramAndVarsGen(
|
||||
asmgen.out(" .send STRUCTINSTANCES\n")
|
||||
}
|
||||
|
||||
private fun emitStructDefFields(structtype: StStruct, scalarValue: () -> String) {
|
||||
for(field in structtype.fields) {
|
||||
val effectiveDt = if(field.isArray) field.type.elementType() else field.type
|
||||
if(effectiveDt.isFloat) {
|
||||
val floatSize = compTarget.FLOAT_MEM_SIZE.toInt()
|
||||
if(field.isArray)
|
||||
asmgen.out("p8v_${field.name} .fill ${floatSize*field.arraySize!!}, ?")
|
||||
else
|
||||
asmgen.out("p8v_${field.name} .fill $floatSize, ${scalarValue()}")
|
||||
} else {
|
||||
val type = asmTypeString(field.type)
|
||||
if(field.isArray) {
|
||||
val anonymous = List(field.arraySize!!) { "?" }.joinToString(",")
|
||||
asmgen.out("p8v_${field.name} $type $anonymous")
|
||||
} else {
|
||||
asmgen.out("p8v_${field.name} $type ${scalarValue()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun translateAsmSubroutine(sub: PtAsmSub) {
|
||||
if(sub.inline) {
|
||||
return // subroutine gets inlined at call site.
|
||||
@@ -654,7 +679,6 @@ internal class ProgramAndVarsGen(
|
||||
|
||||
private class ZpStringWithInitial(
|
||||
val name: String,
|
||||
val alloc: MemoryAllocator.VarAllocation,
|
||||
val value: Pair<String, Encoding>
|
||||
)
|
||||
|
||||
@@ -671,7 +695,7 @@ internal class ProgramAndVarsGen(
|
||||
val scopedName = variable.key
|
||||
val svar = symboltable.lookup(scopedName) as? StStaticVariable
|
||||
if(svar?.initializationStringValue!=null)
|
||||
result.add(ZpStringWithInitial(scopedName, variable.value, svar.initializationStringValue!!))
|
||||
result.add(ZpStringWithInitial(scopedName, svar.initializationStringValue!!))
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -799,9 +823,6 @@ internal class ProgramAndVarsGen(
|
||||
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 -> {
|
||||
TODO("pointers are not supported yet for uninitialized array ${variable.astNode?.position}")
|
||||
}
|
||||
else -> {
|
||||
throw AssemblyError("weird dt")
|
||||
}
|
||||
@@ -991,14 +1012,14 @@ internal class ProgramAndVarsGen(
|
||||
consts.sortedBy { it.name }.forEach {
|
||||
val nameWithoutColons = it.name.replace("::", "_")
|
||||
if(it.dt.isFloat)
|
||||
asmgen.out(" ${nameWithoutColons} = ${it.value}")
|
||||
asmgen.out(" $nameWithoutColons = ${it.value}")
|
||||
else {
|
||||
if(it.value!=null)
|
||||
asmgen.out(" ${nameWithoutColons} = ${it.value!!.toHex()}")
|
||||
asmgen.out(" $nameWithoutColons = ${it.value!!.toHex()}")
|
||||
else if(it.memorySlab!=null)
|
||||
asmgen.out(" ${nameWithoutColons} = ${StMemorySlabBlockName}.${it.memorySlab!!.name}")
|
||||
asmgen.out(" $nameWithoutColons = ${StMemorySlabBlockName}.${it.memorySlab!!.name}")
|
||||
else
|
||||
TODO("asm gen for ${nameWithoutColons} = memory alloc?")
|
||||
TODO("asm gen for $nameWithoutColons = memory alloc?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ internal class VariableAllocator(private val symboltable: SymbolTable,
|
||||
return true
|
||||
|
||||
val v = symboltable.lookup(scopedName)
|
||||
return if(v is StMemVar) v.address <= 255u else false
|
||||
return v is StMemVar && v.address <= 255u
|
||||
}
|
||||
|
||||
internal fun getFloatAsmConst(number: Double): String {
|
||||
@@ -123,11 +123,11 @@ internal class VariableAllocator(private val symboltable: SymbolTable,
|
||||
private fun collectAllVariables(st: SymbolTable): Collection<StStaticVariable> {
|
||||
val vars = mutableListOf<StStaticVariable>()
|
||||
fun collect(node: StNode) {
|
||||
for(child in node.children) {
|
||||
if(child.value.type == StNodeType.STATICVAR)
|
||||
vars.add(child.value as StStaticVariable)
|
||||
for((_, value) in node.children) {
|
||||
if(value.type == StNodeType.STATICVAR)
|
||||
vars.add(value as StStaticVariable)
|
||||
else
|
||||
collect(child.value)
|
||||
collect(value)
|
||||
}
|
||||
}
|
||||
collect(st)
|
||||
@@ -136,6 +136,12 @@ internal class VariableAllocator(private val symboltable: SymbolTable,
|
||||
|
||||
private fun computeUsageScores(vars: Collection<StStaticVariable>): Map<String, Int> {
|
||||
val scores = mutableMapOf<String, Int>()
|
||||
// Note: by the time codegen runs, the simple-AST identifier names are already fully scoped
|
||||
// AND symbol-prefixed ("p8b_maze.p8s_solve.p8v_cx"), so a plain lookup by PtIdentifier.name
|
||||
// correctly matches the scopedNameString keys used here. This looks like a name/scope
|
||||
// mismatch bug but is not: resolving identifiers to their enclosing scope instead (or
|
||||
// keying this map by the bare variable name) silently makes every score zero, which
|
||||
// degenerates the sort in allocateZeropageVariables() into a stable no-op.
|
||||
val varInfo = vars.associateBy { it.scopedNameString }
|
||||
var loopDepth = 0
|
||||
|
||||
|
||||
@@ -68,10 +68,15 @@ internal class AssignmentAsmGen(
|
||||
|
||||
// Check if we need to save A register before extracting status flags
|
||||
// (flag extraction functions like assignCarryFlagResult overwrite A)
|
||||
val hasByteInA = registersResults.any { (ret, _) ->
|
||||
// A holds the low byte of a byte/bool return in @A, and of a word return in @AX/@AY
|
||||
val byteInA = registersResults.any { (ret, _) ->
|
||||
ret.type.isByteOrBool && ret.register.registerOrPair == RegisterOrPair.A
|
||||
}
|
||||
if(hasByteInA && statusFlagResults.isNotEmpty()) {
|
||||
val wordLowByteInA = registersResults.any { (ret, _) ->
|
||||
ret.type.isWord && ret.register.registerOrPair in setOf(RegisterOrPair.AX, RegisterOrPair.AY)
|
||||
}
|
||||
val hasAInResult = byteInA || wordLowByteInA
|
||||
if(hasAInResult && statusFlagResults.isNotEmpty()) {
|
||||
asmgen.out(" pha") // Save A (contains return value) before flag extraction
|
||||
}
|
||||
|
||||
@@ -101,7 +106,7 @@ internal class AssignmentAsmGen(
|
||||
}
|
||||
|
||||
// Restore A if we saved it for flag extraction
|
||||
if(hasByteInA && statusFlagResults.isNotEmpty()) {
|
||||
if(hasAInResult && statusFlagResults.isNotEmpty()) {
|
||||
asmgen.out(" pla")
|
||||
}
|
||||
|
||||
|
||||
+116
-3
@@ -2,6 +2,8 @@ package prog8.codegen.cpu6502.assignment
|
||||
|
||||
import prog8.code.ast.*
|
||||
import prog8.code.core.*
|
||||
import prog8.code.cpu6502.byteShiftAddExpansion
|
||||
import prog8.code.cpu6502.wordShiftAddExpansion
|
||||
import prog8.codegen.cpu6502.AsmGen6502Internal
|
||||
import prog8.codegen.cpu6502.VariableAllocator
|
||||
import prog8.codegen.cpu6502.toLongHex
|
||||
@@ -119,6 +121,96 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
|
||||
}
|
||||
}
|
||||
|
||||
private fun inplaceModificationPointerField(target: AsmAssignTarget, operator: String, value: AsmAssignSource) {
|
||||
// the pointer to the target field is materialized in the shared scratch pointer
|
||||
// P8ZP_SCRATCH_PTR because the pointer variable isn't in the zeropage (or the
|
||||
// deref chain is too complex). Evaluating the source value may clobber that
|
||||
// scratch pointer, so we operate on a temporary variable and preserve the
|
||||
// address on the hardware stack, mirroring inplaceModificationIndexedPointerField.
|
||||
val elementDt = target.datatype
|
||||
if(!elementDt.isByteOrBool && !elementDt.isWord && !elementDt.isLong)
|
||||
throw AssemblyError("inplace modification of pointer field for type $elementDt not supported ${target.position}")
|
||||
val ptrTarget = PtrTarget(target)
|
||||
val (zpPtrVar, offset) = ptrgen.deref(ptrTarget.pointer)
|
||||
require(zpPtrVar=="P8ZP_SCRATCH_PTR")
|
||||
|
||||
val elementTemp = asmgen.createTempVarReused(elementDt.base, false, target.origAstTarget ?: target.pointer!!)
|
||||
if(elementDt.isByteOrBool) {
|
||||
asmgen.out("""
|
||||
ldy #$offset
|
||||
lda ($zpPtrVar),y
|
||||
sta $elementTemp""")
|
||||
} else if(elementDt.isWord) {
|
||||
asmgen.out("""
|
||||
ldy #$offset
|
||||
lda ($zpPtrVar),y
|
||||
sta $elementTemp
|
||||
iny
|
||||
lda ($zpPtrVar),y
|
||||
sta $elementTemp+1""")
|
||||
} else {
|
||||
asmgen.out("""
|
||||
ldy #$offset
|
||||
lda ($zpPtrVar),y
|
||||
sta $elementTemp
|
||||
iny
|
||||
lda ($zpPtrVar),y
|
||||
sta $elementTemp+1
|
||||
iny
|
||||
lda ($zpPtrVar),y
|
||||
sta $elementTemp+2
|
||||
iny
|
||||
lda ($zpPtrVar),y
|
||||
sta $elementTemp+3""")
|
||||
}
|
||||
|
||||
// save the computed address on the hardware stack so later code can't clobber it
|
||||
asmgen.out("""
|
||||
lda $zpPtrVar
|
||||
pha
|
||||
lda $zpPtrVar+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 $zpPtrVar+1
|
||||
pla
|
||||
sta $zpPtrVar""")
|
||||
if(elementDt.isByteOrBool) {
|
||||
asmgen.out("""
|
||||
ldy #$offset
|
||||
lda $elementTemp
|
||||
sta ($zpPtrVar),y""")
|
||||
} else if(elementDt.isWord) {
|
||||
asmgen.out("""
|
||||
ldy #$offset
|
||||
lda $elementTemp
|
||||
sta ($zpPtrVar),y
|
||||
iny
|
||||
lda $elementTemp+1
|
||||
sta ($zpPtrVar),y""")
|
||||
} else {
|
||||
asmgen.out("""
|
||||
ldy #$offset
|
||||
lda $elementTemp
|
||||
sta ($zpPtrVar),y
|
||||
iny
|
||||
lda $elementTemp+1
|
||||
sta ($zpPtrVar),y
|
||||
iny
|
||||
lda $elementTemp+2
|
||||
sta ($zpPtrVar),y
|
||||
iny
|
||||
lda $elementTemp+3
|
||||
sta ($zpPtrVar),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.
|
||||
@@ -611,7 +703,19 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
|
||||
}
|
||||
}
|
||||
}
|
||||
TargetStorageKind.POINTER -> ptrgen.inplaceModification(PtrTarget(target), operator, value)
|
||||
TargetStorageKind.POINTER -> {
|
||||
// if the effective target address ends up in the shared scratch pointer
|
||||
// P8ZP_SCRATCH_PTR, evaluating the source value could clobber it. In that
|
||||
// case operate on a protected temporary value instead.
|
||||
val usesOffsetPointer = target.datatype.isFloat || operator in setOf("*", "/", "%")
|
||||
val needsProtectedPtr = ptrgen.derefUsesScratchPointer(target.pointer!!, usesOffsetPointer)
|
||||
if (value.kind in setOf(SourceStorageKind.EXPRESSION, SourceStorageKind.REGISTER)
|
||||
&& needsProtectedPtr
|
||||
&& (target.datatype.isByteOrBool || target.datatype.isWord || target.datatype.isLong))
|
||||
inplaceModificationPointerField(target, operator, value)
|
||||
else
|
||||
ptrgen.inplaceModification(PtrTarget(target), operator, value)
|
||||
}
|
||||
TargetStorageKind.REGISTER -> throw AssemblyError("no asm gen for reg in-place modification")
|
||||
TargetStorageKind.VOID -> { /* do nothing */ }
|
||||
}
|
||||
@@ -2143,6 +2247,11 @@ $shortcutLabel:""")
|
||||
"*" -> {
|
||||
if(value in asmgen.optimizedByteMultiplications)
|
||||
asmgen.out(" lda $name | jsr prog8_math.mul_byte_$value | sta $name")
|
||||
else if(byteShiftAddExpansion(value)!=null) {
|
||||
asmgen.out(" lda $name")
|
||||
asmgen.emitByteShiftAdd(value)
|
||||
asmgen.out(" sta $name")
|
||||
}
|
||||
else
|
||||
asmgen.out(" lda $name | ldy #$value | jsr prog8_math.multiply_bytes | sta $name")
|
||||
}
|
||||
@@ -2454,8 +2563,7 @@ $shortcutLabel:""")
|
||||
if(value in asmgen.optimizedWordMultiplications) {
|
||||
asmgen.out(" lda $lsb | ldy $msb | jsr prog8_math.mul_word_$value | sta $lsb | sty $msb")
|
||||
} else {
|
||||
if(block?.options?.veraFxMuls==true) {
|
||||
// cx16 verafx hardware mul
|
||||
if(block?.options?.veraFxMuls==true) { // cx16 verafx hardware mul
|
||||
asmgen.out("""
|
||||
lda $lsb
|
||||
ldy $msb
|
||||
@@ -2469,6 +2577,11 @@ $shortcutLabel:""")
|
||||
sta $lsb
|
||||
sty $msb""")
|
||||
}
|
||||
else if(wordShiftAddExpansion(value)!=null) {
|
||||
asmgen.out(" lda $lsb | ldy $msb")
|
||||
asmgen.emitWordShiftAdd(value)
|
||||
asmgen.out(" sta $lsb | sty $msb")
|
||||
}
|
||||
else
|
||||
asmgen.out("""
|
||||
lda $lsb
|
||||
|
||||
@@ -3,6 +3,8 @@ package prog8.codegen.cpu6502.assignment
|
||||
import prog8.code.StExtSub
|
||||
import prog8.code.ast.*
|
||||
import prog8.code.core.*
|
||||
import prog8.code.cpu6502.byteShiftAddExpansion
|
||||
import prog8.code.cpu6502.wordShiftAddExpansion
|
||||
import prog8.codegen.cpu6502.AsmGen6502Internal
|
||||
import prog8.codegen.cpu6502.toLongHex
|
||||
import kotlin.math.log2
|
||||
@@ -28,10 +30,7 @@ internal class BinaryOpAssignmentsGen(
|
||||
else -> false
|
||||
}
|
||||
|
||||
return if(translatedOk)
|
||||
true
|
||||
else
|
||||
anyExprGen.assignAnyExpressionUsingStack(expr, assign)
|
||||
return translatedOk || anyExprGen.assignAnyExpressionUsingStack(expr, assign)
|
||||
}
|
||||
|
||||
|
||||
@@ -272,7 +271,10 @@ internal class BinaryOpAssignmentsGen(
|
||||
is PtIdentifier -> true
|
||||
is PtIrRegister -> true
|
||||
is PtNumber -> true
|
||||
is PtFunctionCall -> expr.builtin && expr.name in arrayOf("lsb", "msb")
|
||||
// msb/lsb only load straight into Y without touching A for a plain identifier argument;
|
||||
// any other argument is evaluated via A first, which would clobber A (the left operand).
|
||||
is PtFunctionCall -> expr.builtin && expr.name in arrayOf("lsb", "msb") &&
|
||||
expr.args.singleOrNull() is PtIdentifier
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -412,6 +414,26 @@ internal class BinaryOpAssignmentsGen(
|
||||
asmgen.out(" jsr verafx.muls16")
|
||||
assignmentAsmGen.assignRegisterpairWord(target, RegisterOrPair.AY)
|
||||
return true
|
||||
} else if (asmgen.unwrapCasts(expr.left).type.isUnsignedByte || asmgen.unwrapCasts(expr.right).type.isUnsignedByte) {
|
||||
// One operand is an unsigned byte (possibly under an implicit widening cast,
|
||||
// hence unwrapCasts). The general path would zero-extend it into a full word
|
||||
// and run 16 shift-add iterations even though only 8 bits are significant, so
|
||||
// use the 16x8 routine instead. isUnsignedByte matters: a signed byte is
|
||||
// sign-extended into A/Y and multiply_ubyte_word deliberately ignores the high
|
||||
// byte, so it would compute the unsigned product instead.
|
||||
//
|
||||
// multiply_ubyte_word wants the 8-bit value in A and the word in the variable.
|
||||
// assignWordOperandsToAYAndVar(left, right, var) puts `left` in AY and `right`
|
||||
// in the variable, so the byte has to be passed as the `left` argument. Only
|
||||
// the low 16 bits of the product are wanted and that is commutative mod 2^16,
|
||||
// so swapping the operands is safe. (Prog8 rejects mixed signedness, so the
|
||||
// other operand here is always an unsigned word.)
|
||||
if (asmgen.unwrapCasts(expr.left).type.isUnsignedByte)
|
||||
asmgen.assignWordOperandsToAYAndVar(expr.left, expr.right, "prog8_math.multiply_ubyte_word.multiplier")
|
||||
else
|
||||
asmgen.assignWordOperandsToAYAndVar(expr.right, expr.left, "prog8_math.multiply_ubyte_word.multiplier")
|
||||
asmgen.out(" jsr prog8_math.multiply_ubyte_word")
|
||||
assignmentAsmGen.assignRegisterpairWord(target, RegisterOrPair.AY)
|
||||
} else {
|
||||
asmgen.assignWordOperandsToAYAndVar(expr.right, expr.left, "prog8_math.multiply_words.multiplier")
|
||||
asmgen.out(" jsr prog8_math.multiply_words")
|
||||
@@ -449,6 +471,8 @@ internal class BinaryOpAssignmentsGen(
|
||||
repeat(shifts) { asmgen.out(" asl a") }
|
||||
}
|
||||
}
|
||||
else if(byteShiftAddExpansion(value)!=null)
|
||||
asmgen.emitByteShiftAdd(value)
|
||||
else
|
||||
asmgen.out(" ldy #$value | jsr prog8_math.multiply_bytes")
|
||||
assignmentAsmGen.assignRegisterByte(target, CpuRegister.A, false, true)
|
||||
@@ -485,6 +509,9 @@ internal class BinaryOpAssignmentsGen(
|
||||
sta cx16.r0
|
||||
sty cx16.r0+1
|
||||
jsr verafx.muls16""")
|
||||
} else if(wordShiftAddExpansion(value)!=null) {
|
||||
assignmentAsmGen.assignExpressionToRegister(expr.left, RegisterOrPair.AY, expr.type.isSigned)
|
||||
asmgen.emitWordShiftAdd(value)
|
||||
} else {
|
||||
asmgen.assignWordOperandsToAYAndVar(expr.right, expr.left, "prog8_math.multiply_words.multiplier")
|
||||
asmgen.out(" jsr prog8_math.multiply_words")
|
||||
@@ -701,13 +728,13 @@ internal class BinaryOpAssignmentsGen(
|
||||
if(target.kind==TargetStorageKind.VARIABLE) {
|
||||
asmgen.assignExpressionTo(expr.left, target)
|
||||
require(expr.right.type.isByte)
|
||||
asmgen.out(" lda #<${target.asmVarname} | ldy #>${target.asmVarname}")
|
||||
asmgen.out(" ldx #$shifts")
|
||||
if (expr.operator == "<<") {
|
||||
asmgen.out(" jsr prog8_lib.long_shiftleftX_inplace")
|
||||
} else {
|
||||
asmgen.out(" jsr prog8_lib.long_shiftrightX_inplace")
|
||||
}
|
||||
// The shift count is a compile-time constant here and the value is already in the
|
||||
// target variable, so shift it in place directly. prog8_lib's long_shift*X_inplace
|
||||
// takes a pointer and shifts through (zp),y inside a loop, which costs ~67 cycles
|
||||
// per bit; inplacemodificationLongWithLiteralval already unrolls the small counts
|
||||
// with plain asl/rol on the 4 bytes, and its '>>' keeps the same arithmetic
|
||||
// (sign-preserving) semantics as the library routine.
|
||||
assignmentAsmGen.augmentableAsmGen.inplacemodificationLongWithLiteralval(target.asmVarname, expr.operator, shifts)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1209,8 +1236,7 @@ internal class BinaryOpAssignmentsGen(
|
||||
return true
|
||||
}
|
||||
val left = expr.left
|
||||
val right = expr.right
|
||||
when(right) {
|
||||
when(val right = expr.right) {
|
||||
is PtNumber -> {
|
||||
asmgen.assignExpressionTo(left, target)
|
||||
val hex = right.number.toLongHex()
|
||||
@@ -1291,78 +1317,87 @@ internal class BinaryOpAssignmentsGen(
|
||||
}
|
||||
}
|
||||
is PtIdentifier -> {
|
||||
if(target.kind == TargetStorageKind.VARIABLE) {
|
||||
asmgen.assignExpressionTo(left, target)
|
||||
val rightsym = asmgen.asmVariableName(right)
|
||||
asmgen.out("""
|
||||
lda #<$rightsym
|
||||
ldy #>$rightsym
|
||||
sta P8ZP_SCRATCH_W1
|
||||
sty P8ZP_SCRATCH_W1+1
|
||||
lda #<${target.asmVarname}
|
||||
ldy #>${target.asmVarname}""")
|
||||
if (expr.operator == "+") {
|
||||
asmgen.out(" jsr prog8_lib.long_add_inplace")
|
||||
} else {
|
||||
asmgen.out(" jsr prog8_lib.long_sub_inplace")
|
||||
}
|
||||
return true
|
||||
} else if(target.kind == TargetStorageKind.REGISTER) {
|
||||
val startreg = target.register!!.startregname()
|
||||
asmgen.assignExpressionTo(left, target)
|
||||
val rightsym = asmgen.asmVariableName(right)
|
||||
asmgen.out(
|
||||
"""
|
||||
lda #<$rightsym
|
||||
ldy #>$rightsym
|
||||
sta P8ZP_SCRATCH_W1
|
||||
sty P8ZP_SCRATCH_W1+1
|
||||
lda #<cx16.$startreg
|
||||
ldy #>cx16.$startreg"""
|
||||
)
|
||||
if (expr.operator == "+") {
|
||||
asmgen.out(" jsr prog8_lib.long_add_inplace")
|
||||
} else {
|
||||
asmgen.out(" jsr prog8_lib.long_sub_inplace")
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
// isn't an expression, so no need to preserve R12-R15
|
||||
asmgen.assignExpressionToRegister(expr.left, RegisterOrPair.R12R13, left.type.isSigned)
|
||||
asmgen.assignExpressionToRegister(expr.right, RegisterOrPair.R14R15, left.type.isSigned)
|
||||
if(expr.operator=="+") {
|
||||
when (target.kind) {
|
||||
TargetStorageKind.VARIABLE -> {
|
||||
asmgen.assignExpressionTo(left, target)
|
||||
val rightsym = asmgen.asmVariableName(right)
|
||||
asmgen.out("""
|
||||
clc
|
||||
lda cx16.r12
|
||||
adc cx16.r14
|
||||
sta cx16.r12
|
||||
lda cx16.r12+1
|
||||
adc cx16.r14+1
|
||||
sta cx16.r12+1
|
||||
lda cx16.r12+2
|
||||
adc cx16.r14+2
|
||||
sta cx16.r12+2
|
||||
lda cx16.r12+3
|
||||
adc cx16.r14+3
|
||||
sta cx16.r12+3""")
|
||||
} else {
|
||||
asmgen.out("""
|
||||
sec
|
||||
lda cx16.r12
|
||||
sbc cx16.r14
|
||||
sta cx16.r12
|
||||
lda cx16.r12+1
|
||||
sbc cx16.r14+1
|
||||
sta cx16.r12+1
|
||||
lda cx16.r12+2
|
||||
sbc cx16.r14+2
|
||||
sta cx16.r12+2
|
||||
lda cx16.r12+3
|
||||
sbc cx16.r14+3
|
||||
sta cx16.r12+3""")
|
||||
lda #<$rightsym
|
||||
ldy #>$rightsym
|
||||
sta P8ZP_SCRATCH_W1
|
||||
sty P8ZP_SCRATCH_W1+1
|
||||
lda #<${target.asmVarname}
|
||||
ldy #>${target.asmVarname}"""
|
||||
)
|
||||
if (expr.operator == "+") {
|
||||
asmgen.out(" jsr prog8_lib.long_add_inplace")
|
||||
} else {
|
||||
asmgen.out(" jsr prog8_lib.long_sub_inplace")
|
||||
}
|
||||
return true
|
||||
}
|
||||
TargetStorageKind.REGISTER -> {
|
||||
val startreg = target.register!!.startregname()
|
||||
asmgen.assignExpressionTo(left, target)
|
||||
val rightsym = asmgen.asmVariableName(right)
|
||||
asmgen.out(
|
||||
"""
|
||||
lda #<$rightsym
|
||||
ldy #>$rightsym
|
||||
sta P8ZP_SCRATCH_W1
|
||||
sty P8ZP_SCRATCH_W1+1
|
||||
lda #<cx16.$startreg
|
||||
ldy #>cx16.$startreg"""
|
||||
)
|
||||
if (expr.operator == "+") {
|
||||
asmgen.out(" jsr prog8_lib.long_add_inplace")
|
||||
} else {
|
||||
asmgen.out(" jsr prog8_lib.long_sub_inplace")
|
||||
}
|
||||
return true
|
||||
}
|
||||
else -> {
|
||||
// isn't an expression, so no need to preserve R12-R15
|
||||
asmgen.assignExpressionToRegister(expr.left, RegisterOrPair.R12R13, left.type.isSigned)
|
||||
asmgen.assignExpressionToRegister(expr.right, RegisterOrPair.R14R15, left.type.isSigned)
|
||||
if (expr.operator == "+") {
|
||||
asmgen.out(
|
||||
"""
|
||||
clc
|
||||
lda cx16.r12
|
||||
adc cx16.r14
|
||||
sta cx16.r12
|
||||
lda cx16.r12+1
|
||||
adc cx16.r14+1
|
||||
sta cx16.r12+1
|
||||
lda cx16.r12+2
|
||||
adc cx16.r14+2
|
||||
sta cx16.r12+2
|
||||
lda cx16.r12+3
|
||||
adc cx16.r14+3
|
||||
sta cx16.r12+3"""
|
||||
)
|
||||
} else {
|
||||
asmgen.out(
|
||||
"""
|
||||
sec
|
||||
lda cx16.r12
|
||||
sbc cx16.r14
|
||||
sta cx16.r12
|
||||
lda cx16.r12+1
|
||||
sbc cx16.r14+1
|
||||
sta cx16.r12+1
|
||||
lda cx16.r12+2
|
||||
sbc cx16.r14+2
|
||||
sta cx16.r12+2
|
||||
lda cx16.r12+3
|
||||
sbc cx16.r14+3
|
||||
sta cx16.r12+3"""
|
||||
)
|
||||
}
|
||||
asmgen.assignRegister(RegisterOrPair.R12R13, target)
|
||||
return true
|
||||
}
|
||||
asmgen.assignRegister(RegisterOrPair.R12R13, target)
|
||||
return true
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
@@ -1582,10 +1617,7 @@ internal class BinaryOpAssignmentsGen(
|
||||
internal fun optimizedLogicalExpr(expr: PtBinaryExpression, target: AsmAssignTarget): Boolean {
|
||||
|
||||
fun swapOperands(): Boolean =
|
||||
if(expr.right is PtIdentifier || expr.right is PtMemoryByte)
|
||||
false
|
||||
else
|
||||
expr.left is PtIdentifier || expr.left is PtMemoryByte
|
||||
!(expr.right is PtIdentifier || expr.right is PtMemoryByte) && (expr.left is PtIdentifier || expr.left is PtMemoryByte)
|
||||
|
||||
fun assignResultIntoA(left: PtExpression, operator: String, right: PtExpression) {
|
||||
// non short-circuit evaluation it is *likely* shorter and faster because of the simple operands.
|
||||
@@ -1611,26 +1643,26 @@ internal class BinaryOpAssignmentsGen(
|
||||
when(right) {
|
||||
is PtBool -> {
|
||||
// Handle boolean literals that weren't optimized away (e.g., with -noopt)
|
||||
when {
|
||||
operator == "and" && right.value -> {
|
||||
when (operator) {
|
||||
"and" if right.value -> {
|
||||
// left AND true = left (already in A, do nothing)
|
||||
}
|
||||
operator == "and" && !right.value -> {
|
||||
"and" if !right.value -> {
|
||||
// left AND false = false
|
||||
asmgen.out(" lda #0")
|
||||
}
|
||||
operator == "or" && right.value -> {
|
||||
"or" if right.value -> {
|
||||
// left OR true = true
|
||||
asmgen.out(" lda #1")
|
||||
}
|
||||
operator == "or" && !right.value -> {
|
||||
"or" if !right.value -> {
|
||||
// left OR false = left (already in A, do nothing)
|
||||
}
|
||||
operator == "xor" && right.value -> {
|
||||
"xor" if right.value -> {
|
||||
// left XOR true = NOT left (invert low bit)
|
||||
asmgen.out(" eor #1")
|
||||
}
|
||||
operator == "xor" && !right.value -> {
|
||||
"xor" if !right.value -> {
|
||||
// left XOR false = left (already in A, do nothing)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
sta ($zpPtrVar),y""")
|
||||
}
|
||||
|
||||
internal fun inplaceByteNegate(target: PtrTarget, scope: IPtSubroutine?) {
|
||||
internal fun inplaceByteNegate(target: PtrTarget) {
|
||||
val (zpPtrVar, offset) = deref(target.pointer)
|
||||
if(asmgen.isTargetCpu(CpuType.CPU65C02)) {
|
||||
asmgen.out("""
|
||||
@@ -180,7 +180,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
}
|
||||
}
|
||||
|
||||
internal fun inplaceWordNegate(target: PtrTarget, scope: IPtSubroutine?) {
|
||||
internal fun inplaceWordNegate(target: PtrTarget) {
|
||||
val (zpPtrVar, offset) = deref(target.pointer)
|
||||
asmgen.out("""
|
||||
ldy #$offset
|
||||
@@ -194,7 +194,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
sta ($zpPtrVar),y""")
|
||||
}
|
||||
|
||||
internal fun inplaceLongNegate(target: PtrTarget, scope: IPtSubroutine?) {
|
||||
internal fun inplaceLongNegate(target: PtrTarget) {
|
||||
val (zpPtrVar, offset) = deref(target.pointer)
|
||||
asmgen.out("""
|
||||
ldy #$offset
|
||||
@@ -216,7 +216,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
sta ($zpPtrVar),y""")
|
||||
}
|
||||
|
||||
internal fun inplaceFloatNegate(target: PtrTarget, scope: IPtSubroutine?) {
|
||||
internal fun inplaceFloatNegate(target: PtrTarget) {
|
||||
val (zpPtrVar, offset) = deref(target.pointer)
|
||||
// flip sign bit in second byte of 5-byte float
|
||||
asmgen.out(
|
||||
@@ -230,11 +230,19 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Walk the pointer deref chain. Always returns (zpVar, offset) such that one indirect
|
||||
* access through zpVar at byte offset reaches the final thing:
|
||||
* derefLast=true -> (zpVar),offset is the address of the value itself
|
||||
* derefLast=false -> (zpVar),offset is the location of the field content
|
||||
* This contract holds for every chain length: the final pointer (if any) is already
|
||||
* followed inside deref(), so callers never need a second dereference.
|
||||
* zpVar is P8ZP_SCRATCH_PTR for non-empty chains or non-zeropage pointers; it may be
|
||||
* the original (zeropage) pointer variable only for an empty chain with derefLast=false.
|
||||
* loadPointerFieldValue: for addOffsetToPointer=true, also load the value if the final
|
||||
* field is a pointer-typed field (rather than returning its address).
|
||||
*/
|
||||
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)
|
||||
@@ -287,20 +295,22 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
if(pointer.derefLast) {
|
||||
asmgen.assignExpressionToVariable(pointer.startpointer, "P8ZP_SCRATCH_PTR", DataType.UWORD)
|
||||
addFieldOffsetToScratchPointer(field.second.toUInt())
|
||||
updateScratchPointer()
|
||||
return "P8ZP_SCRATCH_PTR" to 0u
|
||||
}
|
||||
else
|
||||
return pointer.startpointer.name to field.second
|
||||
return pointer.startpointer.name to field.second.toUByte()
|
||||
}
|
||||
else {
|
||||
// have to copy it to temp zp var
|
||||
asmgen.assignExpressionToVariable(pointer.startpointer, "P8ZP_SCRATCH_PTR", DataType.UWORD)
|
||||
if(pointer.derefLast) {
|
||||
addFieldOffsetToScratchPointer(field.second.toUInt())
|
||||
updateScratchPointer()
|
||||
return "P8ZP_SCRATCH_PTR" to 0u
|
||||
}
|
||||
else
|
||||
return "P8ZP_SCRATCH_PTR" to field.second
|
||||
return "P8ZP_SCRATCH_PTR" to field.second.toUByte()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -333,10 +343,24 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
updateScratchPointer()
|
||||
return "P8ZP_SCRATCH_PTR" to 0u
|
||||
} else
|
||||
return "P8ZP_SCRATCH_PTR" to fieldinfo.second
|
||||
return "P8ZP_SCRATCH_PTR" to fieldinfo.second.toUByte()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun derefUsesScratchPointer(pointer: PtPointerDeref, addOffsetToPointer: Boolean=false): Boolean {
|
||||
// determine if deref() would need to keep the effective address in the shared
|
||||
// scratch pointer P8ZP_SCRATCH_PTR, which a subsequent evaluation of a source
|
||||
// expression could then clobber. Only if it doesn't, may the pointer itself (if
|
||||
// located in the zeropage) keep being used for the read-modify-write.
|
||||
if(pointer.chain.isEmpty())
|
||||
return !allocator.isZpVar(pointer.startpointer.name)
|
||||
if(pointer.chain.size > 1 || addOffsetToPointer)
|
||||
return true
|
||||
// chain==1 derefLast=true now fully follows into P8ZP_SCRATCH_PTR (see deref()),
|
||||
// so it always uses the scratch pointer regardless of isZpVar.
|
||||
return !(allocator.isZpVar(pointer.startpointer.name) && !pointer.derefLast)
|
||||
}
|
||||
|
||||
internal fun indexedEffectiveAddress(target: IndexedPtrTarget): String {
|
||||
val eltSize = asmgen.program.memsizer.memorySize(target.elementDt, null)
|
||||
// generic: evaluate index as word, scale by element size, add field-corrected base pointer
|
||||
@@ -383,15 +407,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
}
|
||||
|
||||
internal fun assignPointerDerefExpression(target: AsmAssignTarget, value: PtPointerDeref) {
|
||||
val (zpPtrVar, offset1) = deref(value)
|
||||
val offset: UByte
|
||||
if(value.derefLast) {
|
||||
asmgen.loadIndirectWordAY(zpPtrVar, offset1)
|
||||
asmgen.out(" sta $zpPtrVar | sty $zpPtrVar+1")
|
||||
offset = 0.toUByte()
|
||||
} else {
|
||||
offset = offset1
|
||||
}
|
||||
val (zpPtrVar, offset) = deref(value)
|
||||
if(value.type.isByteOrBool) {
|
||||
asmgen.loadIndirectByte(zpPtrVar, offset)
|
||||
asmgen.assignRegister(RegisterOrPair.A, target)
|
||||
@@ -946,7 +962,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
val right = binExpr.right as? PtIdentifier
|
||||
require(binExpr.operator=="." && left!=null && right!=null) {"invalid dereference expression ${binExpr.position}"}
|
||||
|
||||
val field: Pair<DataType, UByte>
|
||||
val field: Pair<DataType, Int>
|
||||
var extraFieldOffset = 0
|
||||
|
||||
if(left.type.isStructInstance) {
|
||||
@@ -977,7 +993,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
field = struct.getField(right.name, asmgen.program.memsizer)
|
||||
}
|
||||
|
||||
val offset = extraFieldOffset + field.second.toInt()
|
||||
val offset = extraFieldOffset + field.second
|
||||
// TODO avoid always using a zp scratch reg below if the only thing we dereferenced above is a pointer variable that is already in zeropage itself
|
||||
if(offset>=256) {
|
||||
// add field offset to pointer if it doesn't fit in the index register
|
||||
|
||||
@@ -2293,7 +2293,7 @@ internal class PrimitiveAssignmentsGen(
|
||||
}
|
||||
TargetStorageKind.MEMORY -> throw AssemblyError("memory is ubyte, can't negate that")
|
||||
TargetStorageKind.ARRAY -> assignmentAsmGen.assignPrefixedExpressionToArrayElt(makePrefixedExprFromArrayExprAssign("-", assign), scope)
|
||||
TargetStorageKind.POINTER -> pointergen.inplaceByteNegate(PtrTarget(target), scope)
|
||||
TargetStorageKind.POINTER -> pointergen.inplaceByteNegate(PtrTarget(target))
|
||||
TargetStorageKind.VOID -> { /* do nothing */ }
|
||||
}
|
||||
}
|
||||
@@ -2354,7 +2354,7 @@ internal class PrimitiveAssignmentsGen(
|
||||
}
|
||||
TargetStorageKind.MEMORY -> throw AssemblyError("memory is ubyte, can't negate that")
|
||||
TargetStorageKind.ARRAY -> assignmentAsmGen.assignPrefixedExpressionToArrayElt(makePrefixedExprFromArrayExprAssign("-", assign), scope)
|
||||
TargetStorageKind.POINTER -> pointergen.inplaceWordNegate(PtrTarget(target), scope)
|
||||
TargetStorageKind.POINTER -> pointergen.inplaceWordNegate(PtrTarget(target))
|
||||
TargetStorageKind.VOID -> { /* do nothing */ }
|
||||
}
|
||||
}
|
||||
@@ -2385,7 +2385,7 @@ internal class PrimitiveAssignmentsGen(
|
||||
ldy #>cx16.$regstart
|
||||
jsr prog8_lib.long_negate_inplace""")
|
||||
}
|
||||
TargetStorageKind.POINTER -> pointergen.inplaceLongNegate(PtrTarget(target), scope)
|
||||
TargetStorageKind.POINTER -> pointergen.inplaceLongNegate(PtrTarget(target))
|
||||
TargetStorageKind.VOID -> { /* do nothing */ }
|
||||
}
|
||||
}
|
||||
@@ -2406,7 +2406,7 @@ internal class PrimitiveAssignmentsGen(
|
||||
sta ${target.asmVarname}+1""")
|
||||
}
|
||||
TargetStorageKind.ARRAY -> assignmentAsmGen.assignPrefixedExpressionToArrayElt(makePrefixedExprFromArrayExprAssign("-", assign), scope)
|
||||
TargetStorageKind.POINTER -> pointergen.inplaceFloatNegate(PtrTarget(target), scope)
|
||||
TargetStorageKind.POINTER -> pointergen.inplaceFloatNegate(PtrTarget(target))
|
||||
else -> throw AssemblyError("weird target for in-place float negation")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,15 @@ 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 prog8.codegen.cpu6502.hasInstr
|
||||
import prog8.codegen.cpu6502.haslabel
|
||||
import prog8.codegen.cpu6502.isBranch
|
||||
import prog8.codegen.cpu6502.isLoadReg
|
||||
import prog8.codegen.cpu6502.isStoreReg
|
||||
import prog8.codegen.cpu6502.isStoreRegOrZero
|
||||
import prog8.codegen.cpu6502.keeplabel
|
||||
import prog8.codegen.cpu6502.modifiesYRegister
|
||||
import prog8.codegen.cpu6502.optimizeAssembly
|
||||
|
||||
|
||||
class TestAsmOptimizer: FunSpec({
|
||||
@@ -262,6 +270,106 @@ class TestAsmOptimizer: FunSpec({
|
||||
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop")
|
||||
}
|
||||
|
||||
// --- optimizeSamePointerIndexing: guards on the index reload ---
|
||||
|
||||
fun padded(vararg body: String): MutableList<String> =
|
||||
(body.toList() + List(14 - body.size) { " nop" }).toMutableList()
|
||||
|
||||
/** Same, but one line shorter, for cases where the optimizer is expected to drop a line. */
|
||||
fun paddedAfterDrop(vararg body: String): MutableList<String> =
|
||||
(body.toList() + List(13 - body.size) { " nop" }).toMutableList()
|
||||
|
||||
test("samePointerIndexing: KEEPS reload when the index variable is written in between") {
|
||||
val lines = padded(" ldy i", " lda arr,y", " inc i", " ldy i", " sta arr,y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy i", " lda arr,y", " inc i", " ldy i", " sta arr,y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: KEEPS reload when the index variable is decremented in between") {
|
||||
val lines = padded(" ldy i", " lda arr,y", " dec i", " ldy i", " sta arr,y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy i", " lda arr,y", " dec i", " ldy i", " sta arr,y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: KEEPS reload across a jsr") {
|
||||
val lines = padded(" ldy #0", " lda (ptr),y", " jsr someSub", " ldy #0", " sta (ptr),y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy #0", " lda (ptr),y", " jsr someSub", " ldy #0", " sta (ptr),y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: KEEPS reload across a label") {
|
||||
val lines = padded(" ldy #0", " lda (ptr),y", "someLabel:", " ldy #0", " sta (ptr),y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy #0", " lda (ptr),y", "someLabel:", " ldy #0", " sta (ptr),y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: KEEPS reload when an indirect store may alias the index") {
|
||||
val lines = padded(" ldy i", " lda arr,y", " sta (vptr),y", " ldy i", " lda arr2,y")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy i", " lda arr,y", " sta (vptr),y", " ldy i", " lda arr2,y")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: KEEPS reload when an intervening instruction modifies Y") {
|
||||
val lines = padded(" ldy #0", " lda (ptr),y", " tay", " ldy #0", " sta (ptr),y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy #0", " lda (ptr),y", " tay", " ldy #0", " sta (ptr),y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: KEEPS reload for an IO index operand") {
|
||||
val lines = padded($$" ldy c64.$d020", " lda arr,y", " lda arr2,y", $$" ldy c64.$d020", " lda arr3,y")
|
||||
optimize(lines)
|
||||
lines shouldBe padded($$" ldy c64.$d020", " lda arr,y", " lda arr2,y", $$" ldy c64.$d020", " lda arr3,y")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: still drops reload when a store to an unrelated var intervenes") {
|
||||
val lines = padded(" ldy i", " lda arr,y", " sta other", " ldy i", " sta arr,y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe paddedAfterDrop(" ldy i", " lda arr,y", " sta other", " sta arr,y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: KEEPS reload when the two ldy load DIFFERENT immediates") {
|
||||
// regression: seed[1] then seed[0] - the reload selects a different element
|
||||
val lines = padded(" ldy #1", " lda seed_msb,y", " sta x", " ldy #0", " lda seed_msb,y", " sta y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy #1", " lda seed_msb,y", " sta x", " ldy #0", " lda seed_msb,y", " sta y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: KEEPS reload when the two ldy load different variables") {
|
||||
val lines = padded(" ldy i", " lda arr,y", " sta x", " ldy j", " lda arr,y", " sta y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy i", " lda arr,y", " sta x", " ldy j", " lda arr,y", " sta y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: KEEPS reload when the two ldy differ by an offset") {
|
||||
val lines = padded(" ldy w+0", " lda arr,y", " sta x", " ldy w+1", " lda arr,y", " sta y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy w+0", " lda arr,y", " sta x", " ldy w+1", " lda arr,y", " sta y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: matched load/store arm also requires the same index") {
|
||||
val lines = padded(" ldy i", " lda arr,y", " clc", " ldy j", " sta arr,y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy i", " lda arr,y", " clc", " ldy j", " sta arr,y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: relaxed arm drops reload for two indexed loads") {
|
||||
val lines = padded(" ldy sp", " lda cx,y", " sta cxv", " ldy sp", " lda cy,y", " sta cyv", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe paddedAfterDrop(" ldy sp", " lda cx,y", " sta cxv", " lda cy,y", " sta cyv", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: relaxed arm does not fire when Y is modified") {
|
||||
val lines = padded(" ldy sp", " lda cx,y", " dey", " ldy sp", " lda cy,y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy sp", " lda cx,y", " dey", " ldy sp", " lda cy,y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: relaxed arm keeps reload when index is written in between") {
|
||||
val lines = padded(" ldy i", " lda arr,y", " inc i", " ldy i", " lda arr2,y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy i", " lda arr,y", " inc i", " ldy i", " lda arr2,y", " rts")
|
||||
}
|
||||
|
||||
// --- optimizeAddWordToSameVariableOrExtraRegisterLoadInWordStore ---
|
||||
|
||||
test("optimizeAddWordToSameVariable: optimizes P8ZP_SCRATCH_PTR += AY") {
|
||||
|
||||
@@ -33,9 +33,10 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
if (extsub.returns.size == assignmentTargets.size) {
|
||||
// Targets and values match. Assign all the things. Skip 'void' targets.
|
||||
// We need to handle both FP registers (FAC1/FAC2) and regular CPU registers
|
||||
// Status flag returns MUST be processed first, because their branch-based
|
||||
// code needs to read CPU flags immediately after the call, before any
|
||||
// LOADHR/STOREM instructions clobber them.
|
||||
// Status flag returns must be read before anything that clobbers the CPU status
|
||||
// bits. When the call also has non-flag register returns, those are processed
|
||||
// first (they may clobber the scratch register used by the flag extraction),
|
||||
// protected by an outer PUSHST/POPST so the flags stay intact for the extraction.
|
||||
val fpRegs = funcCall.multipleResultFpRegs.toMutableList()
|
||||
val cpuRegs = funcCall.multipleResultRegs.toMutableList()
|
||||
|
||||
@@ -59,8 +60,40 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
}
|
||||
}
|
||||
|
||||
flagPairs.forEach(::processPair)
|
||||
otherPairs.forEach(::processPair)
|
||||
fun isVoidTarget(pair: Pair<Pair<StExtSubParameter, PtNode>, Int>) =
|
||||
(pair.first.second as PtAssignTarget).void
|
||||
|
||||
// When multiple status flags are returned in one call, the branch-based
|
||||
// extraction of one flag clobbers the CPU status bits that a subsequent
|
||||
// flag still needs to read. Wrap every flag pair's handling in
|
||||
// PUSHST/POPST so each one sees the original status right after the call.
|
||||
val wrapStatusFlags = flagPairs.count { !isVoidTarget(it) } > 1
|
||||
fun processFlagPairs() {
|
||||
flagPairs.forEach { pair ->
|
||||
if (wrapStatusFlags && !isVoidTarget(pair)) {
|
||||
result += IRCodeChunk(null, null).also { it += IRInstructions.simple(Opcode.PUSHST) }
|
||||
processPair(pair)
|
||||
result += IRCodeChunk(null, null).also { it += IRInstructions.simple(Opcode.POPST) }
|
||||
} else {
|
||||
processPair(pair)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val hasStatusFlags = flagPairs.any { !isVoidTarget(it) }
|
||||
val hasOtherResults = otherPairs.any { !isVoidTarget(it) }
|
||||
if (hasStatusFlags && hasOtherResults) {
|
||||
// The non-flag register returns are read before the flag extraction,
|
||||
// so the extraction's scratch register cannot clobber them. The flag
|
||||
// bits are preserved across the register loads/stores with PUSHST/POPST.
|
||||
result += IRCodeChunk(null, null).also { it += IRInstructions.simple(Opcode.PUSHST) }
|
||||
otherPairs.forEach(::processPair)
|
||||
result += IRCodeChunk(null, null).also { it += IRInstructions.simple(Opcode.POPST) }
|
||||
processFlagPairs()
|
||||
} else {
|
||||
processFlagPairs()
|
||||
otherPairs.forEach(::processPair)
|
||||
}
|
||||
} else {
|
||||
throw AssemblyError("number of values and targets don't match")
|
||||
}
|
||||
@@ -222,7 +255,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
val (addressReg, fieldOffset) = codeGen.evaluatePointerAddressIntoReg(inplaceInstrs, pointerDeref)
|
||||
val oldvalueReg = codeGen.registers.next(targetDt)
|
||||
|
||||
if((augAssign.operator=="+=" || augAssign.operator=="-=") && value.asConstInteger()==1 || value.asConstInteger()==2) {
|
||||
if((augAssign.operator=="+=" || augAssign.operator=="-=") && (value.asConstInteger()==1 || value.asConstInteger()==2)) {
|
||||
// INC/DEC optimization instead of ADD/SUB
|
||||
|
||||
loadfield(inplaceInstrs, addressReg, fieldOffset, targetDt, oldvalueReg)
|
||||
@@ -346,20 +379,20 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
private fun loadfield(
|
||||
inplaceInstrs: MutableList<IRCodeChunkBase>,
|
||||
addressReg: Int,
|
||||
fieldOffset: UByte,
|
||||
fieldOffset: Int,
|
||||
targetDt: IRDataType,
|
||||
oldvalueReg: Int
|
||||
) {
|
||||
if (targetDt == IRDataType.FLOAT) {
|
||||
addInstr(
|
||||
inplaceInstrs,
|
||||
IRInstructions.loadMemory(Opcode.LOADI, targetDt, oldvalueReg, IRMemory.indirect(addressReg, fieldOffset.toInt())),
|
||||
IRInstructions.loadMemory(Opcode.LOADI, targetDt, oldvalueReg, IRMemory.indirect(addressReg, fieldOffset)),
|
||||
null
|
||||
)
|
||||
} else {
|
||||
addInstr(
|
||||
inplaceInstrs,
|
||||
IRInstructions.loadMemory(Opcode.LOADI, targetDt, oldvalueReg, IRMemory.indirect(addressReg, fieldOffset.toInt())),
|
||||
IRInstructions.loadMemory(Opcode.LOADI, targetDt, oldvalueReg, IRMemory.indirect(addressReg, fieldOffset)),
|
||||
null
|
||||
)
|
||||
}
|
||||
@@ -622,10 +655,10 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
else -> throw AssemblyError("assignment value and target dt mismatch: $valueDt -> $targetDt")
|
||||
}
|
||||
}
|
||||
fun extOpcode(src: IRDataType, dest: IRDataType, signed: Boolean): Opcode = when {
|
||||
src==IRDataType.BYTE && dest==IRDataType.WORD -> if(signed) Opcode.EXTS else Opcode.EXT
|
||||
src==IRDataType.WORD && dest==IRDataType.LONG -> if(signed) Opcode.EXTS else Opcode.EXT
|
||||
src==IRDataType.BYTE && dest==IRDataType.LONG -> if(signed) Opcode.EXTLS else Opcode.EXTL
|
||||
fun extOpcode(src: IRDataType, dest: IRDataType, signed: Boolean): Opcode = when (src) {
|
||||
IRDataType.BYTE if dest==IRDataType.WORD -> if(signed) Opcode.EXTS else Opcode.EXT
|
||||
IRDataType.WORD if dest==IRDataType.LONG -> if(signed) Opcode.EXTS else Opcode.EXT
|
||||
IRDataType.BYTE if dest==IRDataType.LONG -> if(signed) Opcode.EXTLS else Opcode.EXTL
|
||||
else -> throw AssemblyError("unsupported extension $src -> $dest")
|
||||
}
|
||||
if (assignment.value is PtIrRegister) {
|
||||
@@ -785,26 +818,26 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
val offset = eltSize * constIndex
|
||||
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, offset), null)
|
||||
} else {
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, eltSize, true, targetArray.splitWords)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, true)
|
||||
result += code
|
||||
if(eltSize!=1)
|
||||
result += codeGen.multiplyByConst(DataType.UWORD, indexReg, eltSize)
|
||||
addInstr(result, IRInstructions.binary(Opcode.ADDR, IRDataType.POINTER, pointerReg, indexReg), null)
|
||||
}
|
||||
codeGen.storeValueAtPointersLocation(result, pointerReg, 0u, targetIdent.type.dereference(), true, -1)
|
||||
codeGen.storeValueAtPointersLocation(result, pointerReg, 0, targetIdent.type.dereference(), true, -1)
|
||||
} else {
|
||||
if(constIndex!=null) {
|
||||
val offset = eltSize * constIndex
|
||||
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, offset), null)
|
||||
} else {
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, eltSize, true, targetArray.splitWords)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, true)
|
||||
result += code
|
||||
if(eltSize!=1)
|
||||
result += codeGen.multiplyByConst(DataType.UWORD, indexReg, eltSize)
|
||||
addInstr(result, IRInstructions.binary(Opcode.ADDR, IRDataType.POINTER, pointerReg, indexReg), null)
|
||||
}
|
||||
val realValueReg = if(targetDt == IRDataType.FLOAT) valueFpRegister else valueRegister
|
||||
codeGen.storeValueAtPointersLocation(result, pointerReg, 0u, targetIdent.type.dereference(), false, realValueReg)
|
||||
codeGen.storeValueAtPointersLocation(result, pointerReg, 0, targetIdent.type.dereference(), false, realValueReg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -819,7 +852,6 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
valueFpRegister: Int
|
||||
) {
|
||||
val fixedIndex = targetArray.index.asConstInteger()
|
||||
val arrayLength = codeGen.symbolTable.getLength(variable)
|
||||
if(zero) {
|
||||
if(fixedIndex!=null) {
|
||||
val chunk = IRCodeChunk(null, null).also {
|
||||
@@ -832,7 +864,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
}
|
||||
result += chunk
|
||||
} else {
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, eltSize, codeGen.wordArrayIndex, targetArray.splitWords)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, codeGen.wordArrayIndex)
|
||||
result += code
|
||||
val scaleZX = if(targetArray.splitWords) 1 else eltSize
|
||||
result += IRCodeChunk(null, null).also {
|
||||
@@ -853,7 +885,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
}
|
||||
result += chunk
|
||||
} else {
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, eltSize, codeGen.wordArrayIndex, targetArray.splitWords)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, codeGen.wordArrayIndex)
|
||||
result += code
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstructions.storeMemory(Opcode.STOREX, targetDt, valueFpRegister, IRMemory.indexed(variable, indexReg, codeGen.options.compTarget.indexRegType, scale=eltSize))
|
||||
@@ -874,7 +906,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
}
|
||||
result += chunk
|
||||
} else {
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, eltSize, codeGen.wordArrayIndex, targetArray.splitWords)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, codeGen.wordArrayIndex)
|
||||
result += code
|
||||
result += IRCodeChunk(null, null).also {
|
||||
if(targetArray.splitWords) {
|
||||
@@ -922,7 +954,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
}
|
||||
} else {
|
||||
// index is an expression
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, eltSize, true, targetArray.splitWords)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, true)
|
||||
result += code
|
||||
if(eltSize!=1)
|
||||
result += codeGen.multiplyByConst(DataType.UWORD, indexReg, eltSize)
|
||||
@@ -1505,10 +1537,10 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
if(valueTr!=null)
|
||||
addToResult(result, valueTr, -1, valueTr.resultFpReg)
|
||||
result += IRCodeChunk(null, null).also {
|
||||
if(constFloat!=null)
|
||||
it += IRInstructions.binaryImmediateFloat(divROpcode, loadReg, constFloat)
|
||||
it += if(constFloat!=null)
|
||||
IRInstructions.binaryImmediateFloat(divROpcode, loadReg, constFloat)
|
||||
else
|
||||
it += IRInstructions.binary(divROpcode, IRDataType.FLOAT, loadReg, valueTr!!.resultFpReg)
|
||||
IRInstructions.binary(divROpcode, IRDataType.FLOAT, loadReg, valueTr!!.resultFpReg)
|
||||
it += IRInstructions.storeMemory(Opcode.STOREX, IRDataType.FLOAT, loadReg, IRMemory.indexed(arrayVariableName, indexReg, codeGen.options.compTarget.indexRegType))
|
||||
}
|
||||
return result
|
||||
@@ -3030,18 +3062,18 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
|
||||
private fun tryFoldStructArrayDirectWrite(addressExpr: PtExpression, zero: Boolean, valueReg: Int, targetDt: IRDataType, result: MutableList<IRCodeChunkBase>): Boolean {
|
||||
val info = extractStructArrayIndexInfo(addressExpr, codeGen) ?: return false
|
||||
val (idxCode, indexReg) = codeGen.loadIndexReg(info.idxExpr, info.structSize, codeGen.wordArrayIndex, false)
|
||||
val (idxCode, indexReg) = codeGen.loadIndexReg(info.idxExpr, codeGen.wordArrayIndex)
|
||||
result += idxCode
|
||||
if(zero) {
|
||||
result += IRCodeChunk(null, null).also {
|
||||
if(info.fieldOffset==0) it += IRInstructions.storeZero(Opcode.STOREZX, targetDt, IRMemory.indexed(info.arrayName, indexReg, codeGen.options.compTarget.indexRegType, scale=info.structSize))
|
||||
else it += IRInstructions.storeZero(Opcode.STOREZX, targetDt, IRMemory.indexed(info.arrayName, indexReg, codeGen.options.compTarget.indexRegType, scale=info.structSize, displacement=info.fieldOffset))
|
||||
it += if(info.fieldOffset==0) IRInstructions.storeZero(Opcode.STOREZX, targetDt, IRMemory.indexed(info.arrayName, indexReg, codeGen.options.compTarget.indexRegType, scale=info.structSize))
|
||||
else IRInstructions.storeZero(Opcode.STOREZX, targetDt, IRMemory.indexed(info.arrayName, indexReg, codeGen.options.compTarget.indexRegType, scale=info.structSize, displacement=info.fieldOffset))
|
||||
}
|
||||
} else {
|
||||
if(valueReg<0) return false
|
||||
result += IRCodeChunk(null, null).also {
|
||||
if(info.fieldOffset==0) it += IRInstructions.storeMemory(Opcode.STOREX, targetDt, valueReg, IRMemory.indexed(info.arrayName, indexReg, codeGen.options.compTarget.indexRegType, scale=info.structSize))
|
||||
else it += IRInstructions.storeMemory(Opcode.STOREX, targetDt, valueReg, IRMemory.indexed(info.arrayName, indexReg, codeGen.options.compTarget.indexRegType, scale=info.structSize, displacement=info.fieldOffset))
|
||||
it += if(info.fieldOffset==0) IRInstructions.storeMemory(Opcode.STOREX, targetDt, valueReg, IRMemory.indexed(info.arrayName, indexReg, codeGen.options.compTarget.indexRegType, scale=info.structSize))
|
||||
else IRInstructions.storeMemory(Opcode.STOREX, targetDt, valueReg, IRMemory.indexed(info.arrayName, indexReg, codeGen.options.compTarget.indexRegType, scale=info.structSize, displacement=info.fieldOffset))
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -120,21 +120,21 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val resultRegister = codeGen.registers.next(IRDataType.WORD)
|
||||
addInstr(result, IRInstructions.pop(IRDataType.WORD, resultRegister), null)
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, resultRegister, -1)
|
||||
return ExpressionCodeResult(result, IRDataType.WORD, resultRegister, -1)
|
||||
}
|
||||
|
||||
private fun funcPopL(): ExpressionCodeResult {
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val resultRegister = codeGen.registers.next(IRDataType.LONG)
|
||||
addInstr(result, IRInstructions.pop(IRDataType.LONG, resultRegister), null)
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, resultRegister, -1)
|
||||
return ExpressionCodeResult(result, IRDataType.LONG, resultRegister, -1)
|
||||
}
|
||||
|
||||
private fun funcPopF(): ExpressionCodeResult {
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val resultRegister = codeGen.registers.next(IRDataType.FLOAT)
|
||||
addInstr(result, IRInstructions.pop(IRDataType.FLOAT, resultRegister), null)
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, -1, resultRegister)
|
||||
return ExpressionCodeResult(result, IRDataType.FLOAT, -1, resultRegister)
|
||||
}
|
||||
|
||||
private fun funcCopyFromPointer1ToPointer2(call: PtFunctionCall, type: IRDataType): ExpressionCodeResult {
|
||||
@@ -261,31 +261,39 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
// 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))
|
||||
when (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))
|
||||
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 -> 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))
|
||||
when (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))
|
||||
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")
|
||||
else -> throw AssemblyError("invalid type for DIVMOD")
|
||||
}
|
||||
}
|
||||
return ExpressionCodeResult(result, type, -1, -1)
|
||||
}
|
||||
@@ -876,7 +884,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
if(offset > 0)
|
||||
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerTr.resultReg, offset), null)
|
||||
} else {
|
||||
val (code, indexWordReg) = codeGen.loadIndexReg(target.index, eltSize, true, false)
|
||||
val (code, indexWordReg) = codeGen.loadIndexReg(target.index, true)
|
||||
result += code
|
||||
if(eltSize!=1)
|
||||
result += codeGen.multiplyByConst(DataType.UWORD, indexWordReg, eltSize)
|
||||
@@ -946,7 +954,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
it += IRInstructions.storeZero(Opcode.STOREZX, IRDataType.BYTE, IRMemory.indexed(targetVariable.name, offsetReg, codeGen.options.compTarget.indexRegType))
|
||||
}
|
||||
} else {
|
||||
val (code, indexReg) = codeGen.loadIndexReg(target.index, eltSize, codeGen.wordArrayIndex, false)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(target.index, codeGen.wordArrayIndex)
|
||||
result += code
|
||||
result += IRCodeChunk(null, null).also {
|
||||
val offset = byteOffset(eltSize)
|
||||
@@ -964,7 +972,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
it += IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, valueTr.resultReg, IRMemory.indexed(targetVariable.name, offsetReg, codeGen.options.compTarget.indexRegType))
|
||||
}
|
||||
} else {
|
||||
val (code, indexReg) = codeGen.loadIndexReg(target.index, eltSize, codeGen.wordArrayIndex, false)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(target.index, codeGen.wordArrayIndex)
|
||||
result += code
|
||||
result += IRCodeChunk(null, null).also {
|
||||
val offset = byteOffset(eltSize)
|
||||
@@ -985,7 +993,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
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)
|
||||
val (idxCode, indexReg) = codeGen.loadIndexReg(info.idxExpr, codeGen.wordArrayIndex)
|
||||
result += idxCode
|
||||
return if(isZero) {
|
||||
result += IRCodeChunk(null, null).also {
|
||||
@@ -996,10 +1004,10 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
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))
|
||||
it += if(dt==IRDataType.FLOAT) {
|
||||
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))
|
||||
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)
|
||||
|
||||
@@ -22,6 +22,21 @@ internal class ExpressionCodeResult(val chunks: IRCodeChunks, val dt: IRDataType
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the explicit CMPI #0 that would test a translated result value against zero can be
|
||||
* skipped, because the last emitted instruction of that translation already sets the status
|
||||
* bits. This is only allowed on targets that honor the "multi-byte ops set status bits based
|
||||
* on the full value" contract (see CpuType.statusBitsOnMultiByteOps): on 8-bit targets,
|
||||
* multi-byte ops may only set Z from the last byte, so the explicit CMPI is required there.
|
||||
*
|
||||
* Note: when the CMPI is skipped, a following branch tests Z meaning "the compared value is
|
||||
* zero". That is only meaningful when the last instruction was itself a CMP/CMPI/SGN/BITTST
|
||||
* (what OpcodesThatSetStatusbits guarantees). A future target that honors the contract while
|
||||
* setting status bits with other instructions must re-check this assumption.
|
||||
*/
|
||||
internal fun CpuType.canSkipExplicitCmpi(lastInstr: IRInstruction?): Boolean =
|
||||
statusBitsOnMultiByteOps && lastInstr != null && lastInstr.opcode in OpcodesThatSetStatusbits
|
||||
|
||||
internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
|
||||
fun translateExpression(expr: PtExpression): ExpressionCodeResult {
|
||||
@@ -62,19 +77,19 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
val vmDt = codeGen.irType(expr.type)
|
||||
if(vmDt==IRDataType.FLOAT) {
|
||||
val resultFpRegister = codeGen.registers.next(IRDataType.FLOAT)
|
||||
if (isConstant) {
|
||||
code += IRInstructions.loadAddress(IRDataType.FLOAT, resultFpRegister, expr.name)
|
||||
code += if (isConstant) {
|
||||
IRInstructions.loadAddress(IRDataType.FLOAT, resultFpRegister, expr.name)
|
||||
} else {
|
||||
code += IRInstructions.loadMemory(Opcode.LOADM, IRDataType.FLOAT, resultFpRegister, IRMemory.direct(expr.name))
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.FLOAT, resultFpRegister, IRMemory.direct(expr.name))
|
||||
}
|
||||
ExpressionCodeResult(code, vmDt, -1, resultFpRegister)
|
||||
}
|
||||
else {
|
||||
val resultRegister = codeGen.registers.next(vmDt)
|
||||
if (isConstant) {
|
||||
code += IRInstructions.loadAddress(vmDt, resultRegister, expr.name)
|
||||
code += if (isConstant) {
|
||||
IRInstructions.loadAddress(vmDt, resultRegister, expr.name)
|
||||
} else {
|
||||
code += IRInstructions.loadMemory(Opcode.LOADM, vmDt, resultRegister, IRMemory.direct(expr.name))
|
||||
IRInstructions.loadMemory(Opcode.LOADM, vmDt, resultRegister, IRMemory.direct(expr.name))
|
||||
}
|
||||
ExpressionCodeResult(code, vmDt, resultRegister, -1)
|
||||
}
|
||||
@@ -186,7 +201,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
}
|
||||
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, firstField.second.toInt())
|
||||
it += IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, firstField.second)
|
||||
if (firstField.first.isPointer) {
|
||||
// get the address stored in the pointer and use that for the rest of the chain
|
||||
// LOADI has an exception to allo reg1 and reg2 to be the same, so we can avoid using extra temporary registers and LOADs
|
||||
@@ -214,19 +229,19 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
|
||||
// For inline array fields (not pointer fields), return the address (ptr+offset) instead of loading from it
|
||||
if(deref.type.isArray && !deref.derefLast) {
|
||||
if(offset > 0u)
|
||||
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, offset.toInt()), null)
|
||||
if(offset > 0)
|
||||
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, offset), null)
|
||||
return ExpressionCodeResult(result, IRDataType.POINTER, pointerReg, -1)
|
||||
}
|
||||
|
||||
return if(deref.type.isFloat) {
|
||||
val resultReg = codeGen.registers.next(IRDataType.FLOAT)
|
||||
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, IRDataType.FLOAT, resultReg, IRMemory.indirect(pointerReg, offset.toInt())), null)
|
||||
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, IRDataType.FLOAT, resultReg, IRMemory.indirect(pointerReg, offset)), null)
|
||||
ExpressionCodeResult(result, IRDataType.FLOAT, -1, resultReg)
|
||||
} else {
|
||||
val irdt = codeGen.irType(deref.type)
|
||||
val resultReg = codeGen.registers.next(irdt)
|
||||
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, irdt, resultReg, IRMemory.indirect(pointerReg, offset.toInt())), null)
|
||||
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, irdt, resultReg, IRMemory.indirect(pointerReg, offset)), null)
|
||||
ExpressionCodeResult(result, irdt, resultReg, -1)
|
||||
}
|
||||
}
|
||||
@@ -269,14 +284,8 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
// Use status flags from condition expression when possible, avoiding redundant CMPI #0
|
||||
val condTr = translateExpression(ifExpr.condition)
|
||||
addToResult(result, condTr, condTr.resultReg, -1)
|
||||
// Only skip the CMPI if the target honors the "multi-byte ops set Z based on full
|
||||
// value" contract. On 8-bit targets (6502/65C02) this is false, so we always emit
|
||||
// an explicit CMPI - a multi-byte op may have set Z based on only the last byte.
|
||||
// See CpuType.statusBitsOnMultiByteOps for the rationale.
|
||||
val lastInstr = condTr.chunks.lastOrNull()?.instructions?.lastOrNull()
|
||||
val skipCmpi = codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps
|
||||
&& lastInstr != null
|
||||
&& lastInstr.opcode in OpcodesThatSetZeroFlagOnM68k
|
||||
val skipCmpi = codeGen.options.compTarget.cpu.canSkipExplicitCmpi(lastInstr)
|
||||
if (!skipCmpi) {
|
||||
addInstr(result, IRInstructions.compareImmediate(IRDataType.BYTE, condTr.resultReg, 0), null)
|
||||
}
|
||||
@@ -395,7 +404,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
if(ptr.value != null)
|
||||
IRInstructions.load(vmDt, resultRegister, ptr.value!!.toInt())
|
||||
else
|
||||
TODO("LOAD memory() address? ${ptr}")
|
||||
TODO("LOAD memory() address? $ptr")
|
||||
}
|
||||
else
|
||||
IRInstructions.loadMemory(Opcode.LOADM, vmDt, resultRegister, IRMemory.direct(identifier.name))
|
||||
@@ -436,7 +445,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
result += pointerTr.chunks
|
||||
val (instructions, offset) = traverseRestOfDerefChainToCalculateFinalAddress(expr.dereference!!, pointerTr.resultReg)
|
||||
result += instructions
|
||||
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerTr.resultReg, offset.toInt()), null)
|
||||
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerTr.resultReg, offset), null)
|
||||
return ExpressionCodeResult(result, vmDt, pointerTr.resultReg, -1)
|
||||
}
|
||||
}
|
||||
@@ -528,11 +537,11 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
addToResult(result, elementTr, elementTr.resultReg, -1)
|
||||
val iterableTr = translateExpression(haystackVar)
|
||||
addToResult(result, iterableTr, iterableTr.resultReg, -1)
|
||||
val lengthReg = codeGen.registers.next(IRDataType.BYTE)
|
||||
val lengthReg = codeGen.registers.next(IRDataType.WORD)
|
||||
val iterableLength = codeGen.symbolTable.getLength(haystackVar.name)
|
||||
addInstr(result, IRInstructions.load(IRDataType.BYTE, lengthReg, iterableLength!!), null)
|
||||
addInstr(result, IRInstructions.load(IRDataType.WORD, lengthReg, iterableLength!!), null)
|
||||
val resultReg = codeGen.registers.next(IRDataType.BYTE)
|
||||
result += codeGen.makeSyscall(IMSyscall.BYTEARRAY_CONTAINS, listOf(IRDataType.BYTE to elementTr.resultReg, addressDt to iterableTr.resultReg, IRDataType.BYTE to lengthReg), IRDataType.BYTE to resultReg)
|
||||
result += codeGen.makeSyscall(IMSyscall.BYTEARRAY_CONTAINS, listOf(IRDataType.BYTE to elementTr.resultReg, addressDt to iterableTr.resultReg, IRDataType.WORD to lengthReg), IRDataType.BYTE to resultReg)
|
||||
addInstr(result, IRInstructions.compareImmediate(IRDataType.BYTE, resultReg, 0), null)
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
|
||||
}
|
||||
@@ -541,12 +550,12 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
addToResult(result, elementTr, elementTr.resultReg, -1)
|
||||
val iterableTr = translateExpression(haystackVar)
|
||||
addToResult(result, iterableTr, iterableTr.resultReg, -1)
|
||||
val lengthReg = codeGen.registers.next(IRDataType.BYTE)
|
||||
val lengthReg = codeGen.registers.next(IRDataType.WORD)
|
||||
val iterableLength = codeGen.symbolTable.getLength(haystackVar.name)
|
||||
addInstr(result, IRInstructions.load(IRDataType.BYTE, lengthReg, iterableLength!!), null)
|
||||
addInstr(result, IRInstructions.load(IRDataType.WORD, lengthReg, iterableLength!!), null)
|
||||
val resultReg = codeGen.registers.next(IRDataType.BYTE)
|
||||
val syscall = if(haystackVar.type.isSplitWordArray(codeGen.options.compTarget)) IMSyscall.SPLIT_WORDARRAY_CONTAINS else IMSyscall.WORDARRAY_CONTAINS
|
||||
result += codeGen.makeSyscall(syscall, listOf(IRDataType.WORD to elementTr.resultReg, addressDt to iterableTr.resultReg, IRDataType.BYTE to lengthReg), IRDataType.BYTE to resultReg)
|
||||
result += codeGen.makeSyscall(syscall, listOf(IRDataType.WORD to elementTr.resultReg, addressDt to iterableTr.resultReg, IRDataType.WORD to lengthReg), IRDataType.BYTE to resultReg)
|
||||
addInstr(result, IRInstructions.compareImmediate(IRDataType.BYTE, resultReg, 0), null)
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
|
||||
}
|
||||
@@ -555,11 +564,11 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
addToResult(result, elementTr, elementTr.resultReg, -1)
|
||||
val iterableTr = translateExpression(haystackVar)
|
||||
addToResult(result, iterableTr, iterableTr.resultReg, -1)
|
||||
val lengthReg = codeGen.registers.next(IRDataType.BYTE)
|
||||
val lengthReg = codeGen.registers.next(IRDataType.WORD)
|
||||
val resultReg = codeGen.registers.next(IRDataType.BYTE)
|
||||
val iterableLength = codeGen.symbolTable.getLength(haystackVar.name)
|
||||
addInstr(result, IRInstructions.load(IRDataType.BYTE, lengthReg, iterableLength!!), null)
|
||||
result += codeGen.makeSyscall(IMSyscall.LONGARRAY_CONTAINS, listOf(IRDataType.LONG to elementTr.resultReg, addressDt to iterableTr.resultReg, IRDataType.BYTE to lengthReg), IRDataType.BYTE to resultReg)
|
||||
addInstr(result, IRInstructions.load(IRDataType.WORD, lengthReg, iterableLength!!), null)
|
||||
result += codeGen.makeSyscall(IMSyscall.LONGARRAY_CONTAINS, listOf(IRDataType.LONG to elementTr.resultReg, addressDt to iterableTr.resultReg, IRDataType.WORD to lengthReg), IRDataType.BYTE to resultReg)
|
||||
addInstr(result, IRInstructions.compareImmediate(IRDataType.BYTE, resultReg, 0), null)
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
|
||||
}
|
||||
@@ -568,11 +577,11 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
addToResult(result, elementTr, -1, elementTr.resultFpReg)
|
||||
val iterableTr = translateExpression(haystackVar)
|
||||
addToResult(result, iterableTr, iterableTr.resultReg, -1)
|
||||
val lengthReg = codeGen.registers.next(IRDataType.BYTE)
|
||||
val lengthReg = codeGen.registers.next(IRDataType.WORD)
|
||||
val resultReg = codeGen.registers.next(IRDataType.BYTE)
|
||||
val iterableLength = codeGen.symbolTable.getLength(haystackVar.name)
|
||||
addInstr(result, IRInstructions.load(IRDataType.BYTE, lengthReg, iterableLength!!), null)
|
||||
result += codeGen.makeSyscall(IMSyscall.FLOATARRAY_CONTAINS, listOf(IRDataType.FLOAT to elementTr.resultFpReg, addressDt to iterableTr.resultReg, IRDataType.BYTE to lengthReg), IRDataType.BYTE to resultReg)
|
||||
addInstr(result, IRInstructions.load(IRDataType.WORD, lengthReg, iterableLength!!), null)
|
||||
result += codeGen.makeSyscall(IMSyscall.FLOATARRAY_CONTAINS, listOf(IRDataType.FLOAT to elementTr.resultFpReg, addressDt to iterableTr.resultReg, IRDataType.WORD to lengthReg), IRDataType.BYTE to resultReg)
|
||||
addInstr(result, IRInstructions.compareImmediate(IRDataType.BYTE, resultReg, 0), null)
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
|
||||
}
|
||||
@@ -645,7 +654,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
}
|
||||
|
||||
fun indexByExpression() {
|
||||
val (code, indexReg) = codeGen.loadIndexReg(arrayIx.index, eltSize, codeGen.wordArrayIndex, arrayIx.splitWords)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(arrayIx.index, codeGen.wordArrayIndex)
|
||||
result += code
|
||||
val scale = if(arrayIx.splitWords) 1 else eltSize
|
||||
if(vmDt==IRDataType.FLOAT) {
|
||||
@@ -681,7 +690,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, memOffset), null)
|
||||
}
|
||||
else {
|
||||
val (code, indexWordReg) = codeGen.loadIndexReg(index, eltSize, true, false)
|
||||
val (code, indexWordReg) = codeGen.loadIndexReg(index, true)
|
||||
result += code
|
||||
if(eltSize!=1)
|
||||
result += codeGen.multiplyByConst(DataType.UWORD, indexWordReg, eltSize)
|
||||
@@ -931,9 +940,6 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
}
|
||||
}
|
||||
}
|
||||
BaseDataType.ARRAY_POINTER -> {
|
||||
TODO("typecast to array of pointers $valueDt -> ${cast.type} ${cast.position}")
|
||||
}
|
||||
else -> throw AssemblyError("weird cast value type ${cast.position}")
|
||||
}
|
||||
|
||||
@@ -1393,14 +1399,8 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
val leftTr = translateExpression(binExpr.left)
|
||||
addToResult(result, leftTr, leftTr.resultReg, -1)
|
||||
val lastInstr = leftTr.chunks.lastOrNull()?.instructions?.lastOrNull()
|
||||
// Only skip the CMPI if the target honors the "multi-byte ops set Z
|
||||
// based on full value" contract. On 8-bit targets a previous op like
|
||||
// DEC only set Z from the last byte, so the explicit CMPI is required.
|
||||
// See CpuType.statusBitsOnMultiByteOps for the rationale.
|
||||
val skipCmpi = rightConst.toInt() == 0
|
||||
&& codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps
|
||||
&& lastInstr != null
|
||||
&& lastInstr.opcode in OpcodesThatSetZeroFlagOnM68k
|
||||
&& codeGen.options.compTarget.cpu.canSkipExplicitCmpi(lastInstr)
|
||||
if (!skipCmpi) {
|
||||
addInstr(result, IRInstructions.compareImmediate(leftTr.dt, leftTr.resultReg, rightConst.toInt()), null)
|
||||
}
|
||||
@@ -1564,6 +1564,25 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the branch that decides if the short-circuit evaluation of a boolean `or`/`and` has to
|
||||
* skip its right hand side: [branchWhenZeroTrue] selects if the branch is taken when the left
|
||||
* hand side value in the register is zero (false) instead of when it is non-zero (true).
|
||||
* The status bits have to be set from that register first, because only CMP/CMPI/SGN/BITTST do
|
||||
* that on targets that don't honor CpuType.statusBitsOnMultiByteOps, and the value can also come
|
||||
* straight from a load or a register move. [canSkipExplicitCmpi] decides if setting them can be
|
||||
* skipped on this target.
|
||||
*/
|
||||
private fun addShortCircuitBranch(result: MutableList<IRCodeChunkBase>, leftTr: ExpressionCodeResult, shortcutLabel: String, branchWhenZeroTrue: Boolean) {
|
||||
val lastInstr = leftTr.chunks.lastOrNull()?.instructions?.lastOrNull()
|
||||
val skipCmpi = codeGen.options.compTarget.cpu.canSkipExplicitCmpi(lastInstr)
|
||||
if (!skipCmpi) {
|
||||
addInstr(result, IRInstructions.compareImmediate(IRDataType.BYTE, leftTr.resultReg, 0), null)
|
||||
}
|
||||
val branchOpcode = if (branchWhenZeroTrue) Opcode.BSTEQ else Opcode.BSTNE
|
||||
addInstr(result, IRInstructions.branch(branchOpcode, codeLabel(shortcutLabel)), null)
|
||||
}
|
||||
|
||||
private fun operatorAnd(binExpr: PtBinaryExpression, vmDt: IRDataType, bitwise: Boolean): ExpressionCodeResult {
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
if(!bitwise && !binExpr.right.isSimple()) {
|
||||
@@ -1571,7 +1590,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
val leftTr = translateExpression(binExpr.left)
|
||||
addToResult(result, leftTr, leftTr.resultReg, -1)
|
||||
val shortcutLabel = codeGen.createLabelName()
|
||||
addInstr(result, IRInstructions.branch(Opcode.BSTEQ, codeLabel(shortcutLabel)), null)
|
||||
addShortCircuitBranch(result, leftTr, shortcutLabel, branchWhenZeroTrue = true)
|
||||
val rightTr = translateExpression(binExpr.right)
|
||||
addToResult(result, rightTr, leftTr.resultReg, -1)
|
||||
result += IRCodeChunk(shortcutLabel, null)
|
||||
@@ -1605,7 +1624,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
val leftTr = translateExpression(binExpr.left)
|
||||
addToResult(result, leftTr, leftTr.resultReg, -1)
|
||||
val shortcutLabel = codeGen.createLabelName()
|
||||
addInstr(result, IRInstructions.branch(Opcode.BSTNE, codeLabel(shortcutLabel)), null)
|
||||
addShortCircuitBranch(result, leftTr, shortcutLabel, branchWhenZeroTrue = false)
|
||||
val rightTr = translateExpression(binExpr.right)
|
||||
addToResult(result, rightTr, leftTr.resultReg, -1)
|
||||
result += IRCodeChunk(shortcutLabel, null)
|
||||
@@ -1876,7 +1895,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
val right = binExpr.right as? PtIdentifier
|
||||
require(binExpr.operator=="." && left!=null && right!=null) {"invalid dereference expression ${binExpr.position}"}
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val field: Pair<DataType, UByte>
|
||||
val field: Pair<DataType, Int>
|
||||
val pointerReg: Int
|
||||
var extraFieldOffset = 0
|
||||
|
||||
@@ -1886,7 +1905,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
val struct = left.type.subType!! as StStruct
|
||||
field = struct.getField(right.name, codeGen.program.memsizer)
|
||||
val fieldVmDt = codeGen.irType(field.first)
|
||||
val fieldOffset = field.second.toInt()
|
||||
val fieldOffset = field.second
|
||||
if(left.variable!=null) {
|
||||
val constindex = left.index as? PtNumber
|
||||
if(constindex!=null) {
|
||||
@@ -1898,7 +1917,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
} else if(left.variable!!.type.isArray) {
|
||||
// variable index: fold to single LOADX with scale=structSize, disp=fieldOffset
|
||||
// only for known array symbols (not generic pointer derefs)
|
||||
val (chunks, indexReg) = codeGen.loadIndexReg(left.index, struct.size.toInt(), codeGen.wordArrayIndex, false)
|
||||
val (chunks, indexReg) = codeGen.loadIndexReg(left.index, codeGen.wordArrayIndex)
|
||||
result += chunks
|
||||
if (fieldVmDt == IRDataType.FLOAT) {
|
||||
val fpReg = codeGen.registers.next(IRDataType.FLOAT)
|
||||
@@ -1914,7 +1933,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
val pointerTr = translateExpression(left.variable!!)
|
||||
result += pointerTr.chunks
|
||||
pointerReg = pointerTr.resultReg
|
||||
val (chunks, indexReg) = codeGen.loadIndexReg(left.index, struct.size.toInt(), true, false)
|
||||
val (chunks, indexReg) = codeGen.loadIndexReg(left.index, true)
|
||||
result += chunks
|
||||
if(struct.size.toInt()!=1)
|
||||
result += codeGen.multiplyByConst(DataType.UWORD, indexReg, struct.size.toInt())
|
||||
@@ -1940,7 +1959,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
|
||||
// add field offset to pointer and load the value into the result register
|
||||
val fieldVmDt = codeGen.irType(field.first)
|
||||
val fieldOffset = field.second.toInt() + extraFieldOffset
|
||||
val fieldOffset = field.second + extraFieldOffset
|
||||
var resultFpReg = -1
|
||||
var resultReg = -1
|
||||
if (fieldVmDt == IRDataType.FLOAT)
|
||||
@@ -1955,14 +1974,14 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
return ExpressionCodeResult(result, fieldVmDt, resultReg, resultFpReg)
|
||||
}
|
||||
|
||||
internal fun traverseRestOfDerefChainToCalculateFinalAddress(targetPointerDeref: PtPointerDeref, pointerReg: Int): Pair<IRCodeChunks, UByte> {
|
||||
internal fun traverseRestOfDerefChainToCalculateFinalAddress(targetPointerDeref: PtPointerDeref, pointerReg: Int): Pair<IRCodeChunks, Int> {
|
||||
// returns instructions to calculate the pointer address, and the offset into the struct it points to
|
||||
// so that LOADI and STOREFIELD opcodes can be used instead of having an explicit extra ADD
|
||||
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
|
||||
if(targetPointerDeref.chain.isEmpty())
|
||||
return result to 0u // nothing to do; there's no deref chain
|
||||
return result to 0 // nothing to do; there's no deref chain
|
||||
|
||||
var struct: StStruct? = null
|
||||
if(targetPointerDeref.startpointer.type.subType!=null)
|
||||
@@ -1978,11 +1997,11 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
|
||||
val field = targetPointerDeref.chain.last()
|
||||
val fieldinfo = struct!!.getField(field, codeGen.program.memsizer)
|
||||
if(fieldinfo.second>0u) {
|
||||
if(fieldinfo.second>0) {
|
||||
if(targetPointerDeref.derefLast) {
|
||||
require(fieldinfo.first.isPointer)
|
||||
result += updatePointerFromField(fieldinfo, pointerReg)
|
||||
return result to 0u
|
||||
return result to 0
|
||||
} else {
|
||||
return result to fieldinfo.second
|
||||
}
|
||||
@@ -1992,13 +2011,13 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
// LOADI has an exception to allow reg1 and reg2 to be the same, so we can avoid using extra temporary registers and LOADs
|
||||
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, IRDataType.POINTER, pointerReg, IRMemory.indirect(pointerReg, 0)), null)
|
||||
}
|
||||
return result to 0u
|
||||
return result to 0
|
||||
}
|
||||
|
||||
private fun updatePointerFromField(fieldinfo: Pair<DataType, UByte>, pointerReg: Int): IRCodeChunk {
|
||||
private fun updatePointerFromField(fieldinfo: Pair<DataType, Int>, pointerReg: Int): IRCodeChunk {
|
||||
return IRCodeChunk(null, null).also {
|
||||
// LOADI has an exception to allow reg1 and reg2 to be the same, so we can avoid using extra temporary registers and LOADs
|
||||
it += IRInstructions.loadMemory(Opcode.LOADI, IRDataType.POINTER, pointerReg, IRMemory.indirect(pointerReg, fieldinfo.second.toInt()))
|
||||
it += IRInstructions.loadMemory(Opcode.LOADI, IRDataType.POINTER, pointerReg, IRMemory.indirect(pointerReg, fieldinfo.second))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ class IRCodeGen(
|
||||
replacements += IRStStaticVariable(
|
||||
variable.name, variable.dt, null, variable.length,
|
||||
variable.zpwish, variable.align, variable.dirty,
|
||||
variable.inBss, variable.readonly
|
||||
variable.inBss, variable.readonly, variable.shared
|
||||
)
|
||||
}
|
||||
is IRVariableInitializer.Str, is IRVariableInitializer.Array -> {
|
||||
@@ -274,7 +274,8 @@ class IRCodeGen(
|
||||
variable.align,
|
||||
variable.dirty,
|
||||
variable.inBss,
|
||||
variable.readonly
|
||||
variable.readonly,
|
||||
variable.shared
|
||||
)
|
||||
irProg.st.add(clearedVar)
|
||||
|
||||
@@ -957,9 +958,9 @@ class IRCodeGen(
|
||||
addToResult(result, toTr, toTr.resultReg, -1)
|
||||
val stepTr = expressionEval.translateExpression(iterable.step)
|
||||
addToResult(result, stepTr, stepTr.resultReg, -1)
|
||||
val stepReg = when {
|
||||
stepTr.dt == loopvarDtIr -> stepTr.resultReg
|
||||
stepTr.dt == IRDataType.WORD && loopvarDtIr == IRDataType.LONG && !iterable.step.type.isSigned -> {
|
||||
val stepReg = when (stepTr.dt) {
|
||||
loopvarDtIr -> stepTr.resultReg
|
||||
IRDataType.WORD if loopvarDtIr == IRDataType.LONG && !iterable.step.type.isSigned -> {
|
||||
val widenedReg = registers.next(IRDataType.LONG)
|
||||
addInstr(result, IRInstructions.binary(Opcode.EXT, IRDataType.WORD, widenedReg, stepTr.resultReg), null)
|
||||
widenedReg
|
||||
@@ -1193,11 +1194,10 @@ class IRCodeGen(
|
||||
code += IRInstructions.memoryOp(Opcode.SUBM, dt, mem(), valueReg)
|
||||
}
|
||||
} else {
|
||||
if(value>0) {
|
||||
code += IRInstructions.memoryOpImmediate(Opcode.ADDIM, dt, mem(), value)
|
||||
} else {
|
||||
code += IRInstructions.memoryOpImmediate(Opcode.SUBIM, dt, mem(), -value)
|
||||
}
|
||||
code += if(value>0)
|
||||
IRInstructions.memoryOpImmediate(Opcode.ADDIM, dt, mem(), value)
|
||||
else
|
||||
IRInstructions.memoryOpImmediate(Opcode.SUBIM, dt, mem(), -value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1236,15 +1236,14 @@ class IRCodeGen(
|
||||
if(factor==1)
|
||||
return code
|
||||
val pow2 = powersOfTwoInt.indexOf(factor)
|
||||
if(pow2==1) {
|
||||
code += if(pow2==1) {
|
||||
// just shift 1 bit
|
||||
code += IRInstructions.unary(Opcode.LSL, irdt, reg)
|
||||
}
|
||||
else if(pow2>=1) {
|
||||
IRInstructions.unary(Opcode.LSL, irdt, reg)
|
||||
} else if(pow2>=1) {
|
||||
// just shift multiple bits
|
||||
code += IRInstructions.binaryImmediate(Opcode.LSLI, irdt, reg, pow2)
|
||||
IRInstructions.binaryImmediate(Opcode.LSLI, irdt, reg, pow2)
|
||||
} else {
|
||||
code += if (factor == 0) {
|
||||
if (factor == 0) {
|
||||
IRInstructions.load(irdt, reg, 0)
|
||||
} else {
|
||||
val opcode = if(dt.isSigned) Opcode.MULS else Opcode.MUL
|
||||
@@ -1317,13 +1316,13 @@ class IRCodeGen(
|
||||
val pow2 = powersOfTwoInt.indexOf(factor)
|
||||
if(pow2>=0 && !signed) {
|
||||
// unsigned division by a power of two: logical shift right (correct)
|
||||
if(pow2==1) {
|
||||
code += IRInstructions.unary(Opcode.LSR, dt, reg)
|
||||
code += if(pow2==1) {
|
||||
IRInstructions.unary(Opcode.LSR, dt, reg)
|
||||
} else if(dt == IRDataType.LONG && pow2 == 16) {
|
||||
// x / 65536 for unsigned long == x >> 16 == MSIGW(x)
|
||||
code += IRInstructions.binary(Opcode.MSIGW, dt, reg, reg)
|
||||
IRInstructions.binary(Opcode.MSIGW, dt, reg, reg)
|
||||
} else {
|
||||
code += IRInstructions.binaryImmediate(Opcode.LSRI, dt, reg, pow2)
|
||||
IRInstructions.binaryImmediate(Opcode.LSRI, dt, reg, pow2)
|
||||
}
|
||||
return code
|
||||
}
|
||||
@@ -1516,17 +1515,8 @@ class IRCodeGen(
|
||||
// Fallback: materialize expression and branch on result
|
||||
val tr = expressionEval.translateExpression(condition)
|
||||
result += tr.chunks
|
||||
// Only skip the CMPI #0 if BOTH:
|
||||
// 1) The previous instruction is documented to set status bits (Z/N), AND
|
||||
// 2) The target CPU honors the "multi-byte ops set Z based on full value" contract
|
||||
// (see CpuType.statusBitsOnMultiByteOps). For 8-bit targets this contract
|
||||
// is false: e.g. a 16-bit DEC only sets Z from the low byte's dec, not the
|
||||
// full 16-bit value, so we MUST emit an explicit CMPI here.
|
||||
val lastInstr = tr.chunks.lastOrNull()?.instructions?.lastOrNull()
|
||||
val targetHonorsContract = options.compTarget.cpu.statusBitsOnMultiByteOps
|
||||
val skipCmpi = targetHonorsContract
|
||||
&& lastInstr != null
|
||||
&& lastInstr.opcode in OpcodesThatSetZeroFlagOnM68k
|
||||
val skipCmpi = options.compTarget.cpu.canSkipExplicitCmpi(lastInstr)
|
||||
if (!skipCmpi) {
|
||||
addInstr(result, IRInstructions.compareImmediate(tr.dt, tr.resultReg, 0), null)
|
||||
}
|
||||
@@ -1569,15 +1559,8 @@ class IRCodeGen(
|
||||
if (number != null) {
|
||||
val isComparingWithZero = number == 0
|
||||
val lastInstr = leftTr.chunks.lastOrNull()?.instructions?.lastOrNull()
|
||||
// Skip the CMPI only if the target honors the "multi-byte ops set Z correctly"
|
||||
// contract. On 8-bit targets a 16/32-bit op like DEC only sets Z from the last
|
||||
// byte, so we cannot rely on the previous instruction's status bits - the
|
||||
// CMPI is required to test the full multi-byte value.
|
||||
// See CpuType.statusBitsOnMultiByteOps for details.
|
||||
val canSkipCmpi = isComparingWithZero
|
||||
&& options.compTarget.cpu.statusBitsOnMultiByteOps
|
||||
&& lastInstr != null
|
||||
&& lastInstr.opcode in OpcodesThatSetZeroFlagOnM68k
|
||||
&& options.compTarget.cpu.canSkipExplicitCmpi(lastInstr)
|
||||
|
||||
if ((onTrueLabel != null || onTrueAddress != null) && (onFalseLabel == null && onFalseAddress == null)) {
|
||||
var (opcode, useCmpi) = getIntegerComparisonBranch(condition.operator, false, signed)
|
||||
@@ -1828,8 +1811,7 @@ class IRCodeGen(
|
||||
result += callResult.chunks
|
||||
|
||||
// Get the return register specs for the called function
|
||||
val calledSub = symbolTable.lookup(fcall.name)
|
||||
val calledReturnRegs = when(calledSub) {
|
||||
val calledReturnRegs = when(val calledSub = symbolTable.lookup(fcall.name)) {
|
||||
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}")
|
||||
@@ -1847,12 +1829,12 @@ class IRCodeGen(
|
||||
when(fromReg.registerOrPair) {
|
||||
RegisterOrPair.FAC1 -> addInstr(result, IRInstructions.unary(Opcode.LOADHFACZERO, IRDataType.FLOAT, tempFpReg), null)
|
||||
RegisterOrPair.FAC2 -> addInstr(result, IRInstructions.unary(Opcode.LOADHFACONE, IRDataType.FLOAT, tempFpReg), null)
|
||||
else -> throw AssemblyError("unexpected FP return register ${fromReg}")
|
||||
else -> throw AssemblyError("unexpected FP return register $fromReg")
|
||||
}
|
||||
when(toReg.registerOrPair) {
|
||||
RegisterOrPair.FAC1 -> addInstr(result, storeHwFac(Opcode.STOREHFACZERO, tempFpReg), null)
|
||||
RegisterOrPair.FAC2 -> addInstr(result, storeHwFac(Opcode.STOREHFACONE, tempFpReg), null)
|
||||
else -> throw AssemblyError("unexpected FP return register ${toReg}")
|
||||
else -> throw AssemblyError("unexpected FP return register $toReg")
|
||||
}
|
||||
} else {
|
||||
// For non-float returns, load from source to temp IR register, then store to destination
|
||||
@@ -1895,7 +1877,14 @@ class IRCodeGen(
|
||||
}
|
||||
|
||||
val value = ret.children.singleOrNull()
|
||||
if(value==null) {
|
||||
if(ret.numReturnValues()==0) {
|
||||
// a void subroutine may return the result of a void function call (e.g. "return voidfunc(args)"):
|
||||
// translate the call for its side effects, then return void
|
||||
val callValue = value as? PtExpression
|
||||
if(callValue!=null)
|
||||
result += expressionEval.translateExpression(callValue).chunks
|
||||
addInstr(result, IRInstructions.returnVoid(), null)
|
||||
} else if(value==null) {
|
||||
addInstr(result, IRInstructions.returnVoid(), null)
|
||||
} else {
|
||||
value as PtExpression
|
||||
@@ -1942,7 +1931,7 @@ class IRCodeGen(
|
||||
irBlock += chunk
|
||||
}
|
||||
is PtSub -> {
|
||||
val sub = IRSubroutine(child.name, translateParameters(child.signature.children), child.signature.returns, child.position)
|
||||
val sub = IRSubroutine(child.name, translateParameters(child.signature.children), child.signature.returns, child.position, noframe = child.noframe)
|
||||
for (subchild in child.children) {
|
||||
translateNode(subchild).forEach { sub += it }
|
||||
}
|
||||
@@ -2169,12 +2158,12 @@ class IRCodeGen(
|
||||
}
|
||||
else -> throw AssemblyError("unsupported statusflag ${registerOrFlag.statusflag}")
|
||||
}
|
||||
else -> throw AssemblyError("weird CPU register ${registerOrFlag}")
|
||||
else -> throw AssemblyError("weird CPU register $registerOrFlag")
|
||||
}
|
||||
return chunk
|
||||
}
|
||||
|
||||
internal fun evaluatePointerAddressIntoReg(result: MutableList<IRCodeChunkBase>, deref: PtPointerDeref): Pair<Int, UByte> {
|
||||
internal fun evaluatePointerAddressIntoReg(result: MutableList<IRCodeChunkBase>, deref: PtPointerDeref): Pair<Int, Int> {
|
||||
// calculates the pointer address and returns the register it's in + remaining offset into the struct (so that LOADI/STOREFIELD instructions can be used)
|
||||
val pointerTr = expressionEval.translateExpression(deref.startpointer)
|
||||
result += pointerTr.chunks
|
||||
@@ -2183,8 +2172,8 @@ class IRCodeGen(
|
||||
return pointerTr.resultReg to offset
|
||||
}
|
||||
|
||||
internal fun storeValueAtPointersLocation(result: MutableList<IRCodeChunkBase>, addressReg: Int, offset: UByte, type: DataType, valueIsZero: Boolean, existingValueRegister: Int) {
|
||||
if(offset<=0u) {
|
||||
internal fun storeValueAtPointersLocation(result: MutableList<IRCodeChunkBase>, addressReg: Int, offset: Int, type: DataType, valueIsZero: Boolean, existingValueRegister: Int) {
|
||||
if(offset<=0) {
|
||||
val irdt = irType(type)
|
||||
val instr = if(type.isFloat) {
|
||||
if (valueIsZero) IRInstructions.storeZero(Opcode.STOREZI, IRDataType.FLOAT, IRMemory.indirect(addressReg, 0))
|
||||
@@ -2201,17 +2190,17 @@ class IRCodeGen(
|
||||
val valueRegister = existingValueRegister
|
||||
val irdt = irType(type)
|
||||
if(valueIsZero && valueRegister<0) {
|
||||
addInstr(result, IRInstructions.storeZero(Opcode.STOREZI, irdt, IRMemory.indirect(addressReg, offset.toInt())), null)
|
||||
addInstr(result, IRInstructions.storeZero(Opcode.STOREZI, irdt, IRMemory.indirect(addressReg, offset)), null)
|
||||
} else {
|
||||
val instr = if (type.isFloat)
|
||||
IRInstructions.storeMemory(Opcode.STOREI, IRDataType.FLOAT, valueRegister, IRMemory.indirect(addressReg, offset.toInt()))
|
||||
IRInstructions.storeMemory(Opcode.STOREI, IRDataType.FLOAT, valueRegister, IRMemory.indirect(addressReg, offset))
|
||||
else
|
||||
IRInstructions.storeMemory(Opcode.STOREI, irdt, valueRegister, IRMemory.indirect(addressReg, offset.toInt()))
|
||||
IRInstructions.storeMemory(Opcode.STOREI, irdt, valueRegister, IRMemory.indirect(addressReg, offset))
|
||||
addInstr(result, instr, null)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun loadIndexReg(index: PtExpression, itemsize: Int, wordIndex: Boolean, arrayIsSplitWords: Boolean): Pair<IRCodeChunks, Int> {
|
||||
internal fun loadIndexReg(index: PtExpression, wordIndex: Boolean): Pair<IRCodeChunks, Int> {
|
||||
// returns the code to load the Index into the register, which is also returned.
|
||||
// The returned register is always canonicalized to the expected index width
|
||||
// (WORD on 32-bit targets, BYTE on 8-bit) so that LOADX/STOREX/STOREZX never
|
||||
@@ -2250,15 +2239,19 @@ class IRCodeGen(
|
||||
val indexDt = byteIndexTr.dt
|
||||
if(indexDt != indexRegType) {
|
||||
val newReg = registers.next(indexRegType)
|
||||
when {
|
||||
indexRegType == IRDataType.WORD && indexDt == IRDataType.BYTE ->
|
||||
when (indexRegType) {
|
||||
IRDataType.WORD if indexDt == IRDataType.BYTE ->
|
||||
addInstr(result, IRInstructions.binary(Opcode.EXT, IRDataType.BYTE, newReg, byteIndexTr.resultReg), null)
|
||||
indexRegType == IRDataType.WORD && indexDt == IRDataType.LONG ->
|
||||
|
||||
IRDataType.WORD if indexDt == IRDataType.LONG ->
|
||||
addInstr(result, IRInstructions.binary(Opcode.LSIGW, IRDataType.LONG, newReg, byteIndexTr.resultReg), null)
|
||||
indexRegType == IRDataType.BYTE && indexDt == IRDataType.WORD ->
|
||||
|
||||
IRDataType.BYTE if indexDt == IRDataType.WORD ->
|
||||
addInstr(result, IRInstructions.binary(Opcode.LSIGB, IRDataType.WORD, newReg, byteIndexTr.resultReg), null)
|
||||
indexRegType == IRDataType.BYTE && indexDt == IRDataType.LONG ->
|
||||
|
||||
IRDataType.BYTE if indexDt == IRDataType.LONG ->
|
||||
addInstr(result, IRInstructions.binary(Opcode.LSIGB, IRDataType.LONG, newReg, byteIndexTr.resultReg), null)
|
||||
|
||||
else -> throw IllegalArgumentException("unexpected index conversion $indexDt -> $indexRegType")
|
||||
}
|
||||
indexReg = newReg
|
||||
@@ -2276,24 +2269,30 @@ class IRCodeGen(
|
||||
if(effectiveDt == indexRegType)
|
||||
return indexTr.resultReg
|
||||
val newReg = registers.next(indexRegType)
|
||||
when {
|
||||
indexRegType==IRDataType.WORD && indexTr.dt==IRDataType.BYTE ->
|
||||
when (indexRegType) {
|
||||
IRDataType.WORD if indexTr.dt==IRDataType.BYTE ->
|
||||
addInstr(result, IRInstructions.binary(Opcode.EXT, IRDataType.BYTE, newReg, indexTr.resultReg), null)
|
||||
indexRegType==IRDataType.WORD && indexTr.dt==IRDataType.LONG ->
|
||||
|
||||
IRDataType.WORD if indexTr.dt==IRDataType.LONG ->
|
||||
addInstr(result, IRInstructions.binary(Opcode.LSIGW, IRDataType.LONG, newReg, indexTr.resultReg), null)
|
||||
indexRegType==IRDataType.WORD && indexTr.dt==IRDataType.POINTER ->
|
||||
|
||||
IRDataType.WORD if indexTr.dt==IRDataType.POINTER ->
|
||||
addInstr(result, IRInstructions.binary(Opcode.LSIGW, IRDataType.LONG, newReg, indexTr.resultReg), null)
|
||||
indexRegType==IRDataType.BYTE && indexTr.dt==IRDataType.WORD ->
|
||||
|
||||
IRDataType.BYTE if indexTr.dt==IRDataType.WORD ->
|
||||
addInstr(result, IRInstructions.binary(Opcode.LSIGB, IRDataType.WORD, newReg, indexTr.resultReg), null)
|
||||
indexRegType==IRDataType.BYTE && indexTr.dt==IRDataType.LONG ->
|
||||
|
||||
IRDataType.BYTE if indexTr.dt==IRDataType.LONG ->
|
||||
addInstr(result, IRInstructions.binary(Opcode.LSIGB, IRDataType.LONG, newReg, indexTr.resultReg), null)
|
||||
indexRegType==IRDataType.BYTE && indexTr.dt==IRDataType.POINTER -> {
|
||||
|
||||
IRDataType.BYTE if indexTr.dt==IRDataType.POINTER -> {
|
||||
val ptrType = options.compTarget.pointerIRType
|
||||
if(ptrType==IRDataType.WORD)
|
||||
addInstr(result, IRInstructions.binary(Opcode.LSIGB, IRDataType.WORD, newReg, indexTr.resultReg), null)
|
||||
else
|
||||
addInstr(result, IRInstructions.binary(Opcode.LSIGB, IRDataType.LONG, newReg, indexTr.resultReg), null)
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("unexpected index conversion ${indexTr.dt} -> $indexRegType")
|
||||
}
|
||||
return newReg
|
||||
|
||||
@@ -481,8 +481,7 @@ jump p8_label_gen_2
|
||||
val delta = arithmeticDelta(ins)
|
||||
val nextDelta = arithmeticDelta(nextInstr)
|
||||
if(delta != null && nextDelta != null) {
|
||||
val newDelta = delta + nextDelta
|
||||
when(newDelta) {
|
||||
when(val newDelta = delta + nextDelta) {
|
||||
0 -> {
|
||||
chunk.instructions[idx] = IRInstructions.simple(Opcode.NOP)
|
||||
chunk.instructions[idx+1] = IRInstructions.simple(Opcode.NOP)
|
||||
@@ -624,8 +623,7 @@ jump p8_label_gen_2
|
||||
}
|
||||
}
|
||||
Opcode.OR -> {
|
||||
val imm = ins.immediate?.integerValue
|
||||
if (imm == null) return@forEach
|
||||
val imm = ins.immediate?.integerValue ?: return@forEach
|
||||
if (imm == 0) {
|
||||
chunk.instructions.removeAt(idx)
|
||||
changed = true
|
||||
@@ -643,8 +641,7 @@ jump p8_label_gen_2
|
||||
}
|
||||
}
|
||||
Opcode.XOR -> {
|
||||
val imm = ins.immediate?.integerValue
|
||||
if (imm == null) return@forEach
|
||||
val imm = ins.immediate?.integerValue ?: return@forEach
|
||||
if (imm == 0) {
|
||||
chunk.instructions.removeAt(idx)
|
||||
changed = true
|
||||
@@ -866,7 +863,7 @@ jump p8_label_gen_2
|
||||
var foundIdx: Int? = null
|
||||
var foundIns: IRInstruction? = null
|
||||
for((midIdx, midIns) in indexedInstructions) {
|
||||
if(midIdx <= loadmIdx || midIdx >= incIdx) continue
|
||||
if(midIdx !in (loadmIdx + 1)..<incIdx) continue
|
||||
if(midIdx in toRemove || midIdx in toReplace) continue
|
||||
if(midIns.type == IRDataType.FLOAT) continue
|
||||
val indirect = midIns.memory as? MemoryReference.Indirect ?: continue
|
||||
@@ -1158,13 +1155,18 @@ jump p8_label_gen_2
|
||||
mods.add(Mod(i+1, IRInstructions.binaryImmediate(Opcode.AND, IRDataType.LONG, outer.requireDest().registerNumber, 0xffff)))
|
||||
mods.add(Mod(i, IRInstructions.move(IRDataType.LONG, outer.requireDest().registerNumber, inner.requireSrcA().registerNumber)))
|
||||
}
|
||||
// truncate-long/word-to-byte followed by zero-extend byte-to-word == mask with $ff
|
||||
// truncate-long/word-to-byte followed by zero-extend byte-to-word == mask with $ff.
|
||||
// This is only valid when the source and destination have the same type, because
|
||||
// the rewritten loadr would otherwise access the source register with a different
|
||||
// type than its definition (e.g. long source to word dest).
|
||||
inner.opcode==Opcode.LSIGB && inner.type in setOf(IRDataType.WORD, IRDataType.LONG)
|
||||
&& outer.opcode==Opcode.EXT && outer.type==IRDataType.BYTE
|
||||
&& mid!=null && mid==outer.srcA?.register && inner.srcA!=null && outer.dest!=null
|
||||
&& readCounts.getValue(mid)==1 -> {
|
||||
mods.add(Mod(i+1, IRInstructions.binaryImmediate(Opcode.AND, inner.type!!, outer.requireDest().registerNumber, 0xff)))
|
||||
mods.add(Mod(i, IRInstructions.move(inner.type!!, outer.requireDest().registerNumber, inner.requireSrcA().registerNumber)))
|
||||
&& readCounts.getValue(mid)==1
|
||||
&& inner.type == outer.requireDest().type -> {
|
||||
val resultType = outer.requireDest().type
|
||||
mods.add(Mod(i+1, IRInstructions.binaryImmediate(Opcode.AND, resultType, outer.requireDest().registerNumber, 0xff)))
|
||||
mods.add(Mod(i, IRInstructions.move(resultType, outer.requireDest().registerNumber, inner.requireSrcA().registerNumber)))
|
||||
}
|
||||
else -> continue
|
||||
}
|
||||
@@ -1215,8 +1217,7 @@ jump p8_label_gen_2
|
||||
}
|
||||
|
||||
fun writesMem(ins: IRInstruction, key: MemKey): Boolean {
|
||||
if(memKey(ins) != key) return false
|
||||
return ins.memoryEffect == MemoryEffect.WRITE || ins.memoryEffect == MemoryEffect.READ_WRITE
|
||||
return memKey(ins) == key && (ins.memoryEffect == MemoryEffect.WRITE || ins.memoryEffect == MemoryEffect.READ_WRITE)
|
||||
}
|
||||
|
||||
val groups = mutableListOf<AddrGroup>()
|
||||
@@ -1367,7 +1368,6 @@ jump p8_label_gen_2
|
||||
if (stInstr.opcode != Opcode.STOREM || stInstr.srcA == null) { idx++; continue }
|
||||
val loopvarSymbol = stInstr.memory?.symbolName
|
||||
if (loopvarSymbol == null) { idx++; continue }
|
||||
val loopvar = loopvarSymbol
|
||||
val loopReg = stInstr.requireSrcA().register
|
||||
val loopType = stInstr.type ?: IRDataType.WORD
|
||||
// next chunk should be the loop body start with a label
|
||||
@@ -1383,7 +1383,7 @@ jump p8_label_gen_2
|
||||
for (k in idx + 1 until sub.chunks.size) {
|
||||
val c = sub.chunks[k]
|
||||
if (c !is IRCodeChunk) continue
|
||||
val hasIncDec = c.instructions.any { it.opcode in setOf(Opcode.INCM, Opcode.DECM) && it.memory?.symbolName == loopvar }
|
||||
val hasIncDec = c.instructions.any { it.opcode in setOf(Opcode.INCM, Opcode.DECM) && it.memory?.symbolName == loopvarSymbol }
|
||||
if (!hasIncDec) continue
|
||||
|
||||
// old shape: INCM then (same chunk or next chunk) JUMP loopLabel
|
||||
@@ -1403,7 +1403,7 @@ jump p8_label_gen_2
|
||||
val i1 = nxt.instructions[0]
|
||||
val i2 = nxt.instructions[1]
|
||||
val i3 = nxt.instructions[2]
|
||||
if (i1.opcode == Opcode.LOADM && i1.memory?.symbolName == loopvar && i1.dest != null &&
|
||||
if (i1.opcode == Opcode.LOADM && i1.memory?.symbolName == loopvarSymbol && i1.dest != null &&
|
||||
i2.opcode == Opcode.CMP && (i2.srcA?.register == i1.dest?.register || i2.srcB?.register == i1.dest?.register) &&
|
||||
i3.opcode == Opcode.BSTNE && i3.labelTarget == loopLabel) {
|
||||
tail = Tail(k, k + 1, false)
|
||||
@@ -1424,7 +1424,7 @@ jump p8_label_gen_2
|
||||
if (ch !is IRCodeChunk) continue
|
||||
for (ins in ch.instructions) {
|
||||
// any store/inc/dec that writes to loopvar memory
|
||||
if (ins.memory?.symbolName == loopvar && ins.opcode in setOf(Opcode.STOREM, Opcode.STOREIM, Opcode.STOREZM, Opcode.STOREX, Opcode.STOREZX, Opcode.INCM, Opcode.DECM, Opcode.ADDIM, Opcode.SUBIM)) {
|
||||
if (ins.memory?.symbolName == loopvarSymbol && ins.opcode in setOf(Opcode.STOREM, Opcode.STOREIM, Opcode.STOREZM, Opcode.STOREX, Opcode.STOREZX, Opcode.INCM, Opcode.DECM, Opcode.ADDIM, Opcode.SUBIM)) {
|
||||
// exclude the initial STOREM itself (not in body) and the inc chunk (not in body)
|
||||
bodyWrites = true; break
|
||||
}
|
||||
@@ -1444,7 +1444,7 @@ jump p8_label_gen_2
|
||||
for (k in tailEnd + 1 until sub.chunks.size) {
|
||||
val ch = sub.chunks[k] as? IRCodeChunk ?: continue
|
||||
for (ins in ch.instructions) {
|
||||
if (ins.opcode == Opcode.LOADM && ins.memory?.symbolName == loopvar) { liveOut = true; break }
|
||||
if (ins.opcode == Opcode.LOADM && ins.memory?.symbolName == loopvarSymbol) { liveOut = true; break }
|
||||
}
|
||||
if (liveOut) break
|
||||
}
|
||||
@@ -1461,7 +1461,7 @@ jump p8_label_gen_2
|
||||
val ch = sub.chunks[b] as? IRCodeChunk ?: continue
|
||||
for (i in ch.instructions.indices) {
|
||||
val ins = ch.instructions[i]
|
||||
if (ins.opcode == Opcode.LOADM && ins.memory?.symbolName == loopvar) {
|
||||
if (ins.opcode == Opcode.LOADM && ins.memory?.symbolName == loopvarSymbol) {
|
||||
val dest = ins.requireDest().register
|
||||
if (dest == loopReg) {
|
||||
// Already holds the correct value; remove the reload
|
||||
@@ -1477,7 +1477,7 @@ jump p8_label_gen_2
|
||||
val incChunk = sub.chunks[incIdx] as IRCodeChunk
|
||||
for (i in incChunk.instructions.indices) {
|
||||
val ins = incChunk.instructions[i]
|
||||
if (ins.memory?.symbolName == loopvar && ins.opcode in setOf(Opcode.INCM, Opcode.DECM)) {
|
||||
if (ins.memory?.symbolName == loopvarSymbol && ins.opcode in setOf(Opcode.INCM, Opcode.DECM)) {
|
||||
val newOpcode = if (ins.opcode == Opcode.INCM) Opcode.INC else Opcode.DEC
|
||||
incChunk.instructions[i] = IRInstructions.unary(newOpcode, loopType, loopReg.num)
|
||||
break
|
||||
@@ -1488,7 +1488,7 @@ jump p8_label_gen_2
|
||||
val cmpChunk = sub.chunks[newCmpIdx] as IRCodeChunk
|
||||
for (i in cmpChunk.instructions.indices) {
|
||||
val ins = cmpChunk.instructions[i]
|
||||
if (ins.opcode == Opcode.LOADM && ins.memory?.symbolName == loopvar) {
|
||||
if (ins.opcode == Opcode.LOADM && ins.memory?.symbolName == loopvarSymbol) {
|
||||
val dest = ins.requireDest().register
|
||||
if (dest == loopReg) {
|
||||
cmpChunk.instructions[i] = IRInstructions.simple(Opcode.NOP)
|
||||
|
||||
@@ -8,6 +8,18 @@ class IRUnusedCodeRemover(
|
||||
private val irprog: IRProgram,
|
||||
private val errors: IErrorReporter
|
||||
) {
|
||||
/**
|
||||
* The symbols that struct instance initializers take the address of. These are plain data
|
||||
* references: nothing ever calls them, so without this the code they point at is considered
|
||||
* unused and removed, leaving a dangling reference in the instance initializer.
|
||||
* The same reasoning applies to the array initializers of variables, which are handled
|
||||
* separately at each of the use sites below.
|
||||
*/
|
||||
private fun structInstanceInitRefs(): Sequence<IRStSymbolicReference.Symbol> =
|
||||
irprog.st.allStructInstances()
|
||||
.flatMap { instance -> instance.values.asSequence().map { it.value } }
|
||||
.filterIsInstance<IRStSymbolicReference.Symbol>()
|
||||
|
||||
fun optimize(): Int {
|
||||
var numRemoved = removeUnusedSubroutines() + removeUnusedAsmSubroutines()
|
||||
|
||||
@@ -52,6 +64,14 @@ class IRUnusedCodeRemover(
|
||||
return // symbol occurs in an initializer value (address-of this symbol)_
|
||||
}
|
||||
}
|
||||
|
||||
irprog.st.allStructInstances().forEach { instance ->
|
||||
if(instance.values.any { fieldValue ->
|
||||
val ref = fieldValue.value
|
||||
ref is IRStSymbolicReference.Symbol && ref.name.startsWith(blockLabel)
|
||||
})
|
||||
return // symbol occurs in a struct instance initializer value
|
||||
}
|
||||
}
|
||||
|
||||
val blockStructs = irprog.st.allStructDefs().filter { it.name.startsWith(prefix) }
|
||||
@@ -221,6 +241,11 @@ class IRUnusedCodeRemover(
|
||||
}
|
||||
}
|
||||
|
||||
// ... and likewise the ones referenced in struct instance initializers:
|
||||
structInstanceInitRefs()
|
||||
.filter { irprog.st.lookup(it.name) == null }
|
||||
.forEach { ref -> irprog.getChunkWithLabel(ref.name).let { reachable += it } }
|
||||
|
||||
fun grow() {
|
||||
val new = mutableSetOf<IRCodeChunkBase>()
|
||||
reachable.forEach {
|
||||
@@ -277,6 +302,11 @@ class IRUnusedCodeRemover(
|
||||
}
|
||||
}
|
||||
|
||||
// ... and likewise the chunks referenced in struct instance initializers:
|
||||
structInstanceInitRefs()
|
||||
.filter { irprog.st.lookup(it.name) == null }
|
||||
.forEach { ref -> allLabeledChunks[ref.name]?.let { linkedChunks += it } }
|
||||
|
||||
irprog.foreachCodeChunk { chunk ->
|
||||
chunk.next?.let { next -> linkedChunks += next }
|
||||
chunk.instructions.forEach {
|
||||
|
||||
@@ -428,9 +428,8 @@ object RegisterPacker {
|
||||
}
|
||||
|
||||
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))
|
||||
return t1 == t2 || (t1 == IRDataType.POINTER && t2 in setOf(IRDataType.WORD, IRDataType.LONG)) ||
|
||||
(t2 == IRDataType.POINTER && t1 in setOf(IRDataType.WORD, IRDataType.LONG))
|
||||
}
|
||||
|
||||
private fun conflictsWithRegister(
|
||||
|
||||
@@ -2,6 +2,7 @@ package prog8.codegen.intermediate
|
||||
|
||||
import prog8.code.*
|
||||
import prog8.code.ast.PtStructField
|
||||
import prog8.code.core.Position
|
||||
import prog8.intermediate.*
|
||||
|
||||
/**
|
||||
@@ -10,7 +11,7 @@ import prog8.intermediate.*
|
||||
*/
|
||||
private class StToIrConverter(val romable: Boolean) {
|
||||
fun convert(struct: StStruct): IRStStructDef =
|
||||
IRStStructDef(struct.scopedNameString, makeIrStructFieldsFrom(struct.fields), struct.size)
|
||||
IRStStructDef(struct.scopedNameString, makeIrStructFieldsFrom(struct.fields), struct.size, struct.isUnion)
|
||||
|
||||
private fun makeIrStructFieldsFrom(fields: List<PtStructField>): List<IRStStructField> =
|
||||
fields.map { IRStStructField(it.type, it.name, it.arraySize) }
|
||||
@@ -76,9 +77,9 @@ private class StToIrConverter(val romable: Boolean) {
|
||||
|
||||
val name = if('.' in variable.name) variable.name else variable.scopedNameString
|
||||
val hasInit = initValue != null
|
||||
val inBss = !hasInit || (romable && hasInit)
|
||||
val inBss = !hasInit || romable
|
||||
val readonly = romable && hasInit
|
||||
return IRStStaticVariable(name, variable.dt, initValue, variable.length, variable.zpwish, variable.align, variable.dirty, inBss, readonly)
|
||||
return IRStStaticVariable(name, variable.dt, initValue, variable.length, variable.zpwish, variable.align, variable.dirty, inBss, readonly, variable.shared, variable.astNode?.position ?: Position.DUMMY)
|
||||
}
|
||||
|
||||
fun convert(variable: StMemVar): IRStMemVar {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -24,17 +24,19 @@ internal fun extractStructArrayIndexInfo(
|
||||
val baseAddrOf: PtAddressOf
|
||||
val fieldOffset: Int
|
||||
val unwrapped = unwrapCast(addressExpr)
|
||||
when {
|
||||
unwrapped is PtAddressOf -> {
|
||||
when (unwrapped) {
|
||||
is PtAddressOf -> {
|
||||
baseAddrOf = unwrapped
|
||||
fieldOffset = 0
|
||||
}
|
||||
unwrapped is PtBinaryExpression && unwrapped.operator == "+" -> {
|
||||
|
||||
is PtBinaryExpression if 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
|
||||
@@ -46,7 +48,7 @@ internal fun extractStructArrayIndexInfo(
|
||||
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
|
||||
if(structSize !in 1..65535) return null
|
||||
if(fieldOffset !in 0..65535) return null
|
||||
return StructArrayIndexInfo(arrayIdent.name, idxExpr, structSize, fieldOffset)
|
||||
}
|
||||
@@ -196,7 +196,7 @@ fun prefixSymbols(program: PtProgram, options: CompilationOptions, st: SymbolTab
|
||||
}
|
||||
|
||||
|
||||
internal fun prefixScopedName(name: String, type: Char): String {
|
||||
internal fun prefixScopedName(name: String, type: Char, st: SymbolTable? = null): String {
|
||||
if('.' !in name) {
|
||||
if(name.startsWith(GENERATED_LABEL_PREFIX))
|
||||
return name
|
||||
@@ -206,7 +206,11 @@ internal fun prefixScopedName(name: String, type: Char): String {
|
||||
val firstPrefixed = "p8b_${parts[0]}"
|
||||
val lastPart = parts.last()
|
||||
val lastPrefixed = if(lastPart.startsWith(GENERATED_LABEL_PREFIX)) lastPart else "p8${type}_$lastPart"
|
||||
val inbetweenPrefixed = parts.drop(1).dropLast(1).map{ "p8s_$it" }
|
||||
val inbetweenPrefixed = parts.drop(1).dropLast(1).mapIndexed { index, part ->
|
||||
val scopedName = parts.take(index + 2).joinToString(".")
|
||||
val middleType = st?.lookup(scopedName)?.type ?: StNodeType.SUBROUTINE
|
||||
"p8${typePrefixChar(middleType)}_$part"
|
||||
}
|
||||
val prefixed = listOf(firstPrefixed) + inbetweenPrefixed + listOf(lastPrefixed)
|
||||
return prefixed.joinToString(".")
|
||||
}
|
||||
@@ -229,7 +233,7 @@ internal fun typePrefixChar(targetNt: StNodeType): Char {
|
||||
|
||||
|
||||
private fun PtVariable.prefixVariableOrThis(parent: PtNode, st: SymbolTable): PtVariable {
|
||||
name = prefixScopedName(name, 'v')
|
||||
name = prefixScopedName(name, 'v', st)
|
||||
if(value==null)
|
||||
return this
|
||||
|
||||
@@ -256,13 +260,25 @@ private fun PtVariable.prefixVariableOrThis(parent: PtNode, st: SymbolTable): Pt
|
||||
is PtFunctionCall if elt.builtin -> {
|
||||
if (elt.name != "prog8_lib_structalloc")
|
||||
throw AssemblyError("weird array value element $elt")
|
||||
else
|
||||
else {
|
||||
// The struct initializer's arguments are not reachable via children, so
|
||||
// collectRefs never visits them; prefix the address-of ones here.
|
||||
elt.children.forEachIndexed { idx, arg ->
|
||||
if (arg is PtAddressOf && arg.definingBlock()?.options?.noSymbolPrefixing != true) {
|
||||
val newAddr = PtAddressOf(arg.type, false, arg.position)
|
||||
newAddr.add(arg.identifier!!.prefixIdentifierOrThis(newAddr, st))
|
||||
if (arg.arrayIndexExpr != null)
|
||||
newAddr.add(arg.arrayIndexExpr!!)
|
||||
elt.setChild(idx, newAddr)
|
||||
}
|
||||
}
|
||||
newValue.add(elt)
|
||||
}
|
||||
}
|
||||
else -> throw AssemblyError("weird array value element $elt")
|
||||
}
|
||||
}
|
||||
val result = PtVariable(name, type, isSplitWordArray, zeropage, align, dirty, newValue, arraySize, position)
|
||||
val result = PtVariable(name, type, isSplitWordArray, zeropage, align, dirty, newValue, arraySize, position, shared)
|
||||
result.parent = parent
|
||||
result
|
||||
}
|
||||
@@ -290,7 +306,7 @@ private fun PtIdentifier.prefixIdentifierOrThis(parent: PtNode, st: SymbolTable)
|
||||
}
|
||||
|
||||
val prefixType = typePrefixChar(targetNt)
|
||||
val newName = prefixScopedName(name, prefixType)
|
||||
val newName = prefixScopedName(name, prefixType, st)
|
||||
val node = PtIdentifier(newName, type, position)
|
||||
node.parent = parent
|
||||
return node
|
||||
|
||||
@@ -6,12 +6,9 @@ import prog8.code.assembly.IAssemblyProgram
|
||||
import prog8.code.ast.PtProgram
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.code.core.OutputFiles
|
||||
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
|
||||
import prog8.intermediate.*
|
||||
|
||||
class VmCodeGen(val retainSSA: Boolean,
|
||||
private val preassignedCallSiteIds: Map<String, UByte> = emptyMap()
|
||||
@@ -54,8 +51,9 @@ internal class VmAssemblyProgram(
|
||||
get() = irProgram.countUsedRegisters()
|
||||
|
||||
override fun assemble(options: CompilationOptions, errors: IErrorReporter): Boolean {
|
||||
// the VM reads the IR file from disk.
|
||||
IRFileWriter(irProgram, null).write()
|
||||
// the VM reads the IR file from disk; for the virtual target the IR file is the program artifact.
|
||||
val programFile = OutputFiles.of(options, irProgram.name).programFile
|
||||
IRFileWriter(irProgram, programFile).write()
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -514,6 +514,51 @@ class TestIRPeepholeOpt: FunSpec({
|
||||
instr[2].requireImmediateInt() shouldBe 42
|
||||
}
|
||||
|
||||
test("collapse lsigb.w + ext.b into loadr.w + and.w") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.WORD, 7, IRMemory.direct("src")),
|
||||
IRInstructions.binary(Opcode.LSIGB, IRDataType.WORD, 8, 7),
|
||||
IRInstructions.binary(Opcode.EXT, IRDataType.BYTE, 9, 8),
|
||||
IRInstructions.binary(Opcode.ADDR, IRDataType.WORD, 2, 9),
|
||||
IRInstructions.returnVoid()
|
||||
))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.any { it.opcode == Opcode.LSIGB } shouldBe false
|
||||
instr.any { it.opcode == Opcode.EXT && it.requireDest().register == VirtualRegister.int(9) } shouldBe false
|
||||
instr.any {
|
||||
it.opcode == Opcode.LOADR &&
|
||||
it.type == IRDataType.WORD &&
|
||||
it.requireDest().register == VirtualRegister.int(9) &&
|
||||
it.requireSrcA().register == VirtualRegister.int(7)
|
||||
} shouldBe true
|
||||
instr.any {
|
||||
it.opcode == Opcode.AND &&
|
||||
it.type == IRDataType.WORD &&
|
||||
it.requireDest().register == VirtualRegister.int(9) &&
|
||||
it.requireImmediateInt() == 0xff
|
||||
} shouldBe true
|
||||
}
|
||||
|
||||
test("do not collapse lsigb.l + ext.b when source and dest types differ") {
|
||||
// lsigb.l produces a byte from a long; ext.b widens it to word. Rewriting this as
|
||||
// loadr + and would require accessing the long source as word, which conflicts with
|
||||
// the register type system. The optimizer must leave the original sequence intact.
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.LONG, 7, IRMemory.direct("src")),
|
||||
IRInstructions.binary(Opcode.LSIGB, IRDataType.LONG, 8, 7),
|
||||
IRInstructions.binary(Opcode.EXT, IRDataType.BYTE, 9, 8),
|
||||
IRInstructions.binary(Opcode.ADDR, IRDataType.WORD, 2, 9),
|
||||
IRInstructions.returnVoid()
|
||||
))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.any { it.opcode == Opcode.LSIGB } shouldBe true
|
||||
instr.any { it.opcode == Opcode.EXT && it.requireDest().register == VirtualRegister.int(9) } shouldBe true
|
||||
}
|
||||
|
||||
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)),
|
||||
|
||||
@@ -34,6 +34,17 @@ class TestSymbolPrefixer : FunSpec({
|
||||
prefixScopedName("lib.module.func", 's') shouldBe "p8b_lib.p8s_module.p8s_func"
|
||||
}
|
||||
|
||||
test("prefixScopedName uses the symbol type for middle components") {
|
||||
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)), Position.DUMMY)
|
||||
block.add(struct)
|
||||
program.add(block)
|
||||
val st = SymbolTableMaker(program, basicTestOptions()).make()
|
||||
|
||||
prefixScopedName("main.Point.field", 'v', st) shouldBe "p8b_main.p8t_Point.p8v_field"
|
||||
}
|
||||
|
||||
test("prefixScopedName generated label is not prefixed") {
|
||||
val genLabel = "p8_label_gen_42"
|
||||
prefixScopedName(genLabel, 's') shouldBe genLabel
|
||||
|
||||
@@ -46,6 +46,7 @@ dependencies {
|
||||
implementation(project(":codeCore"))
|
||||
implementation(project(":intermediate"))
|
||||
implementation("com.github.ajalt.clikt:clikt:5.1.0")
|
||||
implementation("com.github.ajalt.mordant:mordant:3.1.0")
|
||||
|
||||
// Test dependencies - Kotest BOM is provided by root build.gradle.kts
|
||||
testImplementation("io.kotest:kotest-runner-junit5")
|
||||
|
||||
@@ -53,6 +53,16 @@ Passes the linker script (see below).
|
||||
|
||||
## The linker script (`link.ld`)
|
||||
|
||||
The compiler resolves the linker script for a target in this order:
|
||||
|
||||
1. `link.ld` in the target's library directory (the `library` setting). This is how a custom
|
||||
target, defined outside of the compiler, supplies its own memory layout.
|
||||
2. `/prog8lib/<targetname>/link.ld`, for targets built into the compiler.
|
||||
3. `/prog8lib/qemu68k/link.ld` as the fallback, which is the script shown below.
|
||||
|
||||
Only the `ELF` output type links at all. The `AMIGAHUNK` output type (used by the built-in
|
||||
Amiga targets) assembles straight to a hunk executable and ignores the linker script.
|
||||
|
||||
```
|
||||
ENTRY(_start)
|
||||
SECTIONS
|
||||
|
||||
@@ -38,13 +38,13 @@ internal fun IRInstruction.forwardableImmediateFor(register: VirtualRegister, ty
|
||||
* It may contain registers that are NOT call arguments.
|
||||
* For every call argument register, the loaded immediate value is forwarded
|
||||
* directly into the argument's hardware register instead of going through the
|
||||
* register file (move #imm,dX instead of move p8_regfile+N,dX, and
|
||||
* fmovecr or a const-pool load into fpX instead of moving through p8_fregfile).
|
||||
* register file. That is move #imm,dX instead of move p8_regfile+N,dX, and
|
||||
* fmovecr or a const-pool load into fpX instead of moving through p8_fregfile.
|
||||
*
|
||||
* [deadRegisters] are the registers from [loads] that are not read anywhere
|
||||
* else in the subroutine (their only use is the forwarded call argument);
|
||||
* their register-file stores are omitted entirely. Note that this can also
|
||||
* remove stores for registers that are not call arguments at all.
|
||||
* else in the subroutine; their only use is the forwarded call argument, so
|
||||
* their register-file stores are omitted entirely. This can also remove
|
||||
* stores for registers that are not call arguments at all.
|
||||
*
|
||||
* The read check covers the subroutine's whole flattened instruction list
|
||||
* rather than only the part after the call (see `isRegisterReadElsewhere`),
|
||||
@@ -103,7 +103,7 @@ internal fun nativeFloatConst(value: Double): String? = when (value) {
|
||||
* instead. Return values are passed via virtual registers mapped back to the caller's result register.
|
||||
*/
|
||||
|
||||
internal class AsmGen(val program: IRProgram, internal val target: ICompilationTarget) {
|
||||
internal class AsmGen(val program: IRProgram, internal val target: ICompilationTarget, internal val errors: IErrorReporter? = null) {
|
||||
private val output = StringBuilder()
|
||||
private val regsUsed by lazy { program.registersUsed() }
|
||||
internal val cpu get() = target.cpu
|
||||
@@ -111,6 +111,10 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
companion object {
|
||||
const val REGFILE_LABEL = "p8_regfile"
|
||||
const val FLOAT_REGFILE_LABEL = "p8_fregfile"
|
||||
// frames larger than this are reported with an INFO message; deep recursion multiplies the frame per call
|
||||
const val LARGE_FRAME_SIZE = 512
|
||||
// consecutive STOREZM LONG frame stores from which a dbra clear loop is smaller than straight clr.l
|
||||
const val ZERO_CLEAR_LOOP_THRESHOLD = 16
|
||||
}
|
||||
|
||||
init {
|
||||
@@ -281,6 +285,54 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
output.appendLine(code)
|
||||
}
|
||||
|
||||
// === stack frame context ===
|
||||
// true while emitting a subroutine that has a frame (local area and/or incoming argument
|
||||
// area); the return translation emits `unlk a5` before each rts when this is set.
|
||||
internal var frameActive = false
|
||||
|
||||
// the code chunks of the subroutine currently being emitted, to tell a jump that stays
|
||||
// inside it apart from one that leaves it
|
||||
private var currentSubChunks: Set<IRCodeChunkBase> = emptySet()
|
||||
private var previousInsn: IRInstruction? = null
|
||||
|
||||
// Virtual registers of the subroutine being emitted, by register number -> frame offset.
|
||||
// A re-entrant subroutine (one that can have two live activations) keeps them per-activation in
|
||||
// its stack frame; every other subroutine uses the flat program-static register file.
|
||||
private var frameVregSlots: Map<Int, Int> = emptyMap()
|
||||
|
||||
private fun frameVregAddr(reg: Int): String? = frameVregSlots[reg]?.let { "$it(a5)" }
|
||||
|
||||
/** emits the frame teardown for the subroutine currently being translated, if it has a frame */
|
||||
internal fun emitFrameUnlk() {
|
||||
if (frameActive)
|
||||
emitLine("unlk a5")
|
||||
}
|
||||
|
||||
/**
|
||||
* true when a jump to this label lands outside the subroutine currently being emitted.
|
||||
* An unresolvable label counts as inside: without the chunk map we cannot tell, and guessing
|
||||
* 'outside' would tear down frames of jumps that stay in the subroutine.
|
||||
*/
|
||||
internal fun labelIsOutsideCurrentSub(labelName: String): Boolean {
|
||||
val target = program.resolveCodeTarget(CodeReference.Label(labelName)) ?: return false
|
||||
return target !in currentSubChunks
|
||||
}
|
||||
|
||||
internal fun jumpTableStaysInCurrentSub(pointer: VirtualRegister): Boolean {
|
||||
val load = previousInsn ?: return false
|
||||
if (load.opcode != Opcode.LOADX) return false
|
||||
if (load.dest?.register != pointer) return false
|
||||
val indexed = load.memory as? MemoryReference.Indexed ?: return false
|
||||
val symbol = indexed.base as? AddressBase.Symbol ?: return false
|
||||
val variable = program.st.lookup(symbol.name) as? IRStStaticVariable ?: return false
|
||||
val initializer = variable.initializationValue as? IRVariableInitializer.Array ?: return false
|
||||
if (initializer.elements.isEmpty()) return false
|
||||
return initializer.elements.all { element ->
|
||||
val label = (element as? IRStSymbolicReference.Symbol)?.name ?: return@all false
|
||||
!labelIsOutsideCurrentSub(label)
|
||||
}
|
||||
}
|
||||
|
||||
// === virtual register file layout (1, 2 or 4 bytes per slot depending on type, word-aligned) ===
|
||||
private data class RegFileLayout(val offsets: Map<Int, Int>, val totalSize: Int)
|
||||
|
||||
@@ -328,6 +380,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
}
|
||||
|
||||
fun regAddr(reg: Int): String {
|
||||
frameVregAddr(reg)?.let { return it }
|
||||
val offset = regFileLayout.offsets[reg] ?: error("register r$reg has no layout info")
|
||||
return "$REGFILE_LABEL+$offset"
|
||||
}
|
||||
@@ -335,9 +388,11 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
private fun regNumForOffset(offset: Int): Int? =
|
||||
regFileLayout.offsets.entries.find { it.value == offset }?.key
|
||||
|
||||
// Resolve a register-file address string ("p8_regfile" or "p8_regfile+N")
|
||||
// back to a virtual register number, or null if it is not a regfile slot.
|
||||
// Resolve a register-file address string ("p8_regfile" or "p8_regfile+N", or a frame slot
|
||||
// like "-8(a5)" for a per-activation virtual register) back to a virtual register number,
|
||||
// or null if it is not a register-file slot.
|
||||
fun regNumForAddress(address: String): Int? {
|
||||
frameVregSlots.entries.firstOrNull { "$it.value(a5)" == address }?.let { return it.key }
|
||||
if (!address.startsWith(REGFILE_LABEL))
|
||||
return null
|
||||
val suffix = address.removePrefix(REGFILE_LABEL)
|
||||
@@ -350,6 +405,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
}
|
||||
|
||||
fun regAddrByte(reg: Int, byteOffset: Int): String {
|
||||
frameVregSlots[reg]?.let { return "${it + byteOffset}(a5)" }
|
||||
val offset = regFileLayout.offsets[reg] ?: error("register r$reg has no layout info")
|
||||
return "$REGFILE_LABEL+${offset + byteOffset}"
|
||||
}
|
||||
@@ -380,6 +436,10 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
|
||||
fun loadPointerToA0(reg: Int) {
|
||||
// load a 32-bit pointer from the register file into a0
|
||||
frameVregAddr(reg)?.let {
|
||||
emitLine("movea.l $it, a0")
|
||||
return
|
||||
}
|
||||
val offset = regFileLayout.offsets[reg] ?: error("register r$reg has no layout info")
|
||||
val addr = if (offset == 0) REGFILE_LABEL else "$REGFILE_LABEL+$offset"
|
||||
emitLine("movea.l $addr, a0")
|
||||
@@ -408,8 +468,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
|
||||
|
||||
fun resolveSymbolRef(name: String): String {
|
||||
val node = program.st.lookup(name)
|
||||
return when (node) {
|
||||
return when (val node = program.st.lookup(name)) {
|
||||
is IRStMemVar -> {
|
||||
val addr = node.address.toInt()
|
||||
$$"$${addr.toUInt().toString(16).padStart(8, '0')}"
|
||||
@@ -454,6 +513,12 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
|
||||
private fun resolveMemoryBase(base: AddressBase, displacement: Int): String {
|
||||
val resolved = when (base) {
|
||||
// combine slot offset and reference displacement into a single a5-relative displacement
|
||||
is AddressBase.FrameSlot -> {
|
||||
val total = base.offset + displacement
|
||||
if (total !in -32768..32767) error("frame displacement $total out of 16-bit range")
|
||||
return "$total(a5)"
|
||||
}
|
||||
is AddressBase.Symbol -> resolveSymbolRef(base.name)
|
||||
is AddressBase.Absolute -> base.address.value.toHex()
|
||||
}
|
||||
@@ -535,6 +600,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
// === FPU helpers (M680x0 with 68881/68882) ===
|
||||
|
||||
fun floatRegFileAddr(reg: RegisterNum): String {
|
||||
frameVregAddr(reg.value)?.let { return it }
|
||||
val offset = floatRegFileLayout.offsets[reg.value] ?: error("float register fr${reg.value} has no layout info")
|
||||
return "$FLOAT_REGFILE_LABEL+$offset"
|
||||
}
|
||||
@@ -577,7 +643,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
}
|
||||
|
||||
val options = program.options
|
||||
val asmFile = options.outputDir.resolve("${program.name}.asm")
|
||||
val asmFile = OutputFiles.of(options, program.name).asm()
|
||||
try {
|
||||
if (options.optimize) {
|
||||
val asmLines = output.toString().lines().toMutableList()
|
||||
@@ -604,6 +670,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
val options = program.options
|
||||
emitRaw("; Program: ${program.name}")
|
||||
emitRaw("; Generated by prog8-m68kgen (Motorola 68000 codegen)")
|
||||
emitRaw("; prog8 compiler version: ${options.compilerVersion}")
|
||||
emitRaw("; Target CPU: ${cpu.name}")
|
||||
emitRaw("; Output: ${options.output.name}")
|
||||
emitRaw("")
|
||||
@@ -638,6 +705,9 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
// emit all Prog8 constants as vasm symbols for inline asm use
|
||||
emitConstants()
|
||||
|
||||
// emit struct/union type sizes and member offsets as vasm symbols for inline asm use
|
||||
emitStructDefs()
|
||||
|
||||
// Set up stack pointer and jump to program start
|
||||
emitLabel("prog8_program_start")
|
||||
|
||||
@@ -725,6 +795,14 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
val entrypointNames = setOf("p8b_main.p8s_start", "main.start")
|
||||
if(sub.label in entrypointNames)
|
||||
emitLine("bsr run_global_inits")
|
||||
frameActive = sub.hasFrame
|
||||
frameVregSlots = sub.frameVregSlots
|
||||
currentSubChunks = mutableSetOf<IRCodeChunkBase>().also { set -> sub.forEachChunk { set.add(it) } }
|
||||
if (sub.hasFrame) {
|
||||
emitRaw("; stack frame: ${sub.frameSize} bytes of locals, ${sub.incomingSize} bytes of arguments")
|
||||
emitLine(if (sub.frameSize > 0) "link a5,#-${sub.frameSize}" else "link a5,#0")
|
||||
reportFrameSize(sub)
|
||||
}
|
||||
val livenessInstructions = sub.chunks.filterIsInstance<IRCodeChunk>().flatMap { it.instructions }
|
||||
val deadStoreSuppressionAllowed = canSuppressDeadStores(sub)
|
||||
var instructionOffset = 0
|
||||
@@ -756,6 +834,9 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
}
|
||||
emitRaw("; End of subroutine: $subLabel")
|
||||
emitRaw("")
|
||||
frameActive = false
|
||||
frameVregSlots = emptyMap()
|
||||
currentSubChunks = emptySet()
|
||||
}
|
||||
|
||||
private fun emitAsmSubroutine(sub: IRAsmSubroutine) {
|
||||
@@ -775,6 +856,16 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
private fun unscopedName(scopedName: String): String =
|
||||
scopedName.substringAfterLast('.')
|
||||
|
||||
/** emits an informational message for unusually large frames; deep recursion multiplies the frame per call */
|
||||
private fun reportFrameSize(sub: IRSubroutine) {
|
||||
if (sub.frameSize > LARGE_FRAME_SIZE)
|
||||
errors?.info("subroutine '${userFacingName(sub.label)}' uses a ${sub.frameSize} byte stack frame; deep recursion multiplies this per call", sub.position)
|
||||
}
|
||||
|
||||
/** the scoped IR label without its internal scope markers, for use in user-facing messages */
|
||||
private fun userFacingName(scopedName: String): String =
|
||||
scopedName.split('.').joinToString(".") { it.removePrefix("p8b_").removePrefix("p8s_") }
|
||||
|
||||
// Dead-store suppression checks whether a register is read anywhere else in the
|
||||
// subroutine (see isRegisterReadElsewhere), which is independent of control
|
||||
// flow and therefore sound regardless of branch topology (loops, indirect
|
||||
@@ -841,6 +932,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
deadStoreSuppressionAllowed: Boolean = true,
|
||||
preserveLoopCounter: Boolean = false
|
||||
) {
|
||||
previousInsn = null
|
||||
invalidateD0Cache() // new basic block / branch target
|
||||
emitSourceComment(chunk.sourceLinesPositions)
|
||||
val callOptimizations = mutableMapOf<Int, ImmediateCallOptimization>()
|
||||
@@ -876,8 +968,19 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
}
|
||||
}
|
||||
}
|
||||
for (index in chunk.instructions.indices) {
|
||||
var index = 0
|
||||
while (index < chunk.instructions.size) {
|
||||
val insn = chunk.instructions[index]
|
||||
// Long ascending STOREZM LONG runs lower to a dbra loop; short runs keep clr.l.
|
||||
val zeroRun = frameZeroRunLength(chunk.instructions, index)
|
||||
if (zeroRun >= ZERO_CLEAR_LOOP_THRESHOLD) {
|
||||
val mem = insn.memory as MemoryReference.Direct
|
||||
val slot = mem.base as AddressBase.FrameSlot
|
||||
emitFrameZeroLoop(slot.offset + mem.displacement, zeroRun)
|
||||
previousInsn = null
|
||||
index += zeroRun
|
||||
continue
|
||||
}
|
||||
val clobbersLoopCounter = preserveLoopCounter && mayClobberLoopCounter(insn)
|
||||
if (clobbersLoopCounter)
|
||||
emitLine("move.w d7,-(sp)", "save loop counter around call")
|
||||
@@ -886,11 +989,60 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
callOptimizations[index],
|
||||
index in deadLoadIndices
|
||||
)
|
||||
previousInsn = insn
|
||||
if (clobbersLoopCounter)
|
||||
emitLine("move.w (sp)+,d7", "restore loop counter after call")
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts consecutive STOREZM LONG instructions on ascending frame-slot displacements
|
||||
* starting at [instructions[from]], stepping by one longword with nothing interleaved.
|
||||
* Returns 0 when [instructions[from]] is not such a store.
|
||||
*/
|
||||
private fun frameZeroRunLength(instructions: List<IRInstruction>, from: Int): Int {
|
||||
val first = instructions[from]
|
||||
if (first.opcode != Opcode.STOREZM || first.type != IRDataType.LONG) return 0
|
||||
val firstMem = first.memory as? MemoryReference.Direct ?: return 0
|
||||
if (firstMem.base !is AddressBase.FrameSlot) return 0
|
||||
var expected = (firstMem.base as AddressBase.FrameSlot).offset + firstMem.displacement
|
||||
var count = 0
|
||||
var index = from
|
||||
while (index < instructions.size) {
|
||||
val insn = instructions[index]
|
||||
if (insn.opcode != Opcode.STOREZM || insn.type != IRDataType.LONG) break
|
||||
val mem = insn.memory as? MemoryReference.Direct ?: break
|
||||
val slot = mem.base as? AddressBase.FrameSlot ?: break
|
||||
if (slot.offset + mem.displacement != expected) break
|
||||
count++
|
||||
expected += 4
|
||||
index++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* Lowers [longwords] consecutive longword zero-clears at [startDisp](a5)
|
||||
* to a dbra loop using scratch d0/d1/a0 while preserving d7.
|
||||
* Assumes scratch registers are free at chunk boundaries (true at prologues).
|
||||
*/
|
||||
private fun emitFrameZeroLoop(startDisp: Int, longwords: Int) {
|
||||
invalidateD0Cache() // moveq clobbers d0
|
||||
emitLine("moveq #0, d0")
|
||||
emitLine("lea $startDisp(a5), a0")
|
||||
val iterations = longwords - 1
|
||||
if (iterations in 0..127)
|
||||
emitLine("moveq #$iterations, d1")
|
||||
else
|
||||
emitLine("move.w #$iterations, d1")
|
||||
val loopLabel = makeLabel("zeroloop")
|
||||
emitLabel(loopLabel)
|
||||
emitLine("move.l d0,(a0)+")
|
||||
emitLine("dbra d1,$loopLabel")
|
||||
invalidateD0Cache()
|
||||
}
|
||||
|
||||
private fun mayClobberLoopCounter(insn: IRInstruction): Boolean {
|
||||
if (insn.opcode in setOf(Opcode.CALL, Opcode.CALLI, Opcode.CALLFAR, Opcode.CALLFARVB, Opcode.SYSCALL))
|
||||
return true
|
||||
@@ -962,7 +1114,10 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
forwardedImmediateCall: ImmediateCallOptimization? = null,
|
||||
suppressRegfileStore: Boolean = false
|
||||
) {
|
||||
emitRaw(" ; $insn")
|
||||
// No IR echo for zero-clear stores: prologue clear runs would drown in
|
||||
// "; storezm" noise while the lowered clr lines are self-explanatory.
|
||||
if (insn.opcode != Opcode.STOREZM)
|
||||
emitRaw(" ; $insn")
|
||||
when (insn.opcode) {
|
||||
Opcode.NOP -> {}
|
||||
Opcode.BREAKPOINT -> emitLine("illegal")
|
||||
@@ -1036,7 +1191,10 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
} else {
|
||||
emptyList<IRStStaticVariable>() to initdVars
|
||||
}
|
||||
val structInstancesWithInit: List<IRStStructInstance> = program.st.allStructInstances().filter { it.values.isNotEmpty() }.toList()
|
||||
val structInstancesWithInit: List<IRStStructInstance> = program.st.allStructInstances().filter {
|
||||
val def = program.st.lookup(it.structName) as IRStStructDef
|
||||
it.values.isNotEmpty() && !def.isUnion
|
||||
}.toList()
|
||||
val (chipramStructs: List<IRStStructInstance>, normalStructs: List<IRStStructInstance>) =
|
||||
if (chipramBlocks.isNotEmpty()) {
|
||||
structInstancesWithInit.partition { it.name.substringBefore('.') in chipramBlocks }
|
||||
@@ -1328,10 +1486,32 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
emitRaw("${fixNameSymbols(mv.name)} = $${mv.address.toString(16)}")
|
||||
}
|
||||
|
||||
if (emitted.size > 0 || memvars.count() > 0)
|
||||
if (emitted.isNotEmpty() || memvars.count() > 0)
|
||||
emitRaw("")
|
||||
}
|
||||
|
||||
// Emit the size and member offsets of all struct/union types as vasm 'equ' symbols,
|
||||
// so inline asm can reference them by name (parity with the 64tass-based backends).
|
||||
// Offsets use the same packed layout (no alignment padding) that the compiler's own
|
||||
// member-access codegen uses: a plain fold over the target's memory sizes.
|
||||
private fun emitStructDefs() {
|
||||
val structs = program.st.allStructDefs().toList()
|
||||
if (structs.isEmpty()) return
|
||||
emitRaw("; struct type definitions")
|
||||
for (sd in structs) {
|
||||
val base = fixNameSymbols(sd.name)
|
||||
emitRaw("${base}_size equ ${sd.size}")
|
||||
var offset = 0
|
||||
for (field in sd.fields) {
|
||||
emitRaw("${base}.p8v_${field.name} equ $offset")
|
||||
if (!sd.isUnion)
|
||||
offset += target.memorySize(field.type, field.arraySize ?: 1)
|
||||
}
|
||||
emitRaw("")
|
||||
}
|
||||
emitRaw("")
|
||||
}
|
||||
|
||||
|
||||
private fun emitBssSection() {
|
||||
val chipramBlocks = if (target.name in setOf("amiga500", "amiga1200")) chipramBlockLabels() else emptySet()
|
||||
@@ -1344,7 +1524,10 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
} else {
|
||||
emptyList<IRStStaticVariable>() to allBssVars
|
||||
}
|
||||
val allStructsNoInit: List<IRStStructInstance> = program.st.allStructInstances().filter { it.values.isEmpty() }.toList()
|
||||
val allStructsNoInit: List<IRStStructInstance> = program.st.allStructInstances().filter {
|
||||
val def = program.st.lookup(it.structName) as IRStStructDef
|
||||
it.values.isEmpty() || def.isUnion
|
||||
}.toList()
|
||||
val (chipramStructsNoInit: List<IRStStructInstance>, normalStructsNoInit: List<IRStStructInstance>) =
|
||||
if (chipramBlocks.isNotEmpty()) {
|
||||
allStructsNoInit.partition { it.name.substringBefore('.') in chipramBlocks }
|
||||
|
||||
@@ -61,8 +61,92 @@ internal fun optimizeAssembly(lines: MutableList<String>) {
|
||||
if (fuseMods.isNotEmpty()) {
|
||||
applyModifications(fuseMods, lines, pretrimmed)
|
||||
}
|
||||
// M5: drop a `lea base,a0` when a0 already holds that same base.
|
||||
// NOT REGFILE-DEPENDENT: a0 is a real CPU register, so this pass is independent of the
|
||||
// p8_regfile layout and of the planned register allocator.
|
||||
val leaMods = optimizeRedundantLea(pretrimmed)
|
||||
if (leaMods.isNotEmpty()) {
|
||||
applyModifications(leaMods, lines, pretrimmed)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* M5: remove `lea base,a0` instructions that recompute a base a0 is already known to hold.
|
||||
*
|
||||
* Indexed array access is emitted as `lea base,a0` followed by `(a0,d0.w*2)`. The lea itself is
|
||||
* required - absolute-indexed addressing (`base(a0,d0.w*2)`) does not exist on the m68k - but in
|
||||
* a sequence of accesses to the same array the base is reloaded several times over.
|
||||
*
|
||||
* A tracked value is dropped (a0 treated as unknown) whenever the scan sees:
|
||||
* - a label: incoming control flow may not be the fallthrough path
|
||||
* - any control transfer: a branch may reach elsewhere, and a call clobbers a0 (it is a
|
||||
* scratch register in the m68k calling convention)
|
||||
* - any instruction that names a0 outside a parenthesised address, i.e. writes it
|
||||
*
|
||||
* Anything else - including pure reads such as `move.w (a0,d0.w),d1` - leaves it untouched.
|
||||
*/
|
||||
private fun optimizeRedundantLea(allPretrimmed: List<String>): List<Modification> {
|
||||
// Unconditional control flow, calls and returns end the tracked value: control leaves via a
|
||||
// path where a0's contents are unknown (and a call clobbers a0, a scratch register in the
|
||||
// m68k calling convention). The whole DBcc family is included as loop control.
|
||||
// Plain condition-code branches (bne, bge, ...) are deliberately NOT in this set: they do not
|
||||
// modify a0, and their target is always a label, which resets the tracked value below. So the
|
||||
// fallthrough keeps the tracked value and the branch target is re-entered conservatively.
|
||||
val controlTransfers = setOf(
|
||||
"bra", "bsr", "jmp", "jsr", "rts", "rtr", "rte", "trap",
|
||||
"dbcc", "dbcs", "dbhi", "dbhs", "dblo", "dbls", "dbne", "dbnz", "dbeq", "dbz",
|
||||
"dbmi", "dbpl", "dbge", "dblt", "dbgt", "dble", "dbvc", "dbvs", "dbtst", "dbset",
|
||||
"dbclr", "dbchg", "dbf", "dbra"
|
||||
)
|
||||
// A lea whose destination is a0, and the value it leaves there.
|
||||
fun leaBase(instr: String): String? {
|
||||
if (!instr.startsWith("lea ") && !instr.startsWith("lea\t"))
|
||||
return null
|
||||
val operands = instr.substringAfter(' ').substringBefore(';')
|
||||
val parts = operands.split(',', limit = 2)
|
||||
if (parts.size != 2 || parts[1].trim() != "a0")
|
||||
return null
|
||||
return parts[0].trim()
|
||||
}
|
||||
|
||||
val filtered = allPretrimmed.withIndex()
|
||||
.filter { it.value.isNotBlank() && !it.value.trimStart().startsWith(';') }
|
||||
.map { TrimmedLine(it.value.trimStart(), it.value.trimStart(), it.index) }
|
||||
|
||||
val mods = mutableListOf<Modification>()
|
||||
var a0Base: String? = null
|
||||
for (line in filtered) {
|
||||
val instr = line.instruction
|
||||
if (hasLabel(line.value)) {
|
||||
// fallthrough into a label is fine, but the label may also be entered from
|
||||
// elsewhere, so the tracked value is no longer known to hold
|
||||
a0Base = null
|
||||
continue
|
||||
}
|
||||
val mnemonic = instr.substringBefore(' ').substringBefore('\t').lowercase()
|
||||
if (mnemonic in controlTransfers) {
|
||||
a0Base = null
|
||||
continue
|
||||
}
|
||||
val base = leaBase(instr.lowercase())
|
||||
if (base != null) {
|
||||
if (base == a0Base)
|
||||
mods.add(Modification(line.index, remove = true, replacement = null))
|
||||
else
|
||||
a0Base = base
|
||||
continue
|
||||
}
|
||||
// Any non-parenthesised mention of a0 means it is written
|
||||
if (instr.replace(parenthesised, "").contains("a0", ignoreCase = true))
|
||||
a0Base = null
|
||||
}
|
||||
return mods
|
||||
}
|
||||
|
||||
/** Matches a parenthesised group, for stripping addressing modes. */
|
||||
private val parenthesised = Regex("""\([^)]*\)""")
|
||||
private val d0IndexOperand = Regex("""\(\s*a0\s*,\s*d0\.w(?:\*\d+)?\s*\)""")
|
||||
|
||||
/** 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?)
|
||||
|
||||
@@ -85,7 +169,7 @@ private fun applyModifications(modifications: List<Modification>, lines: Mutable
|
||||
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(';')) {
|
||||
if (pretrim.isNullOrBlank() || pretrim.startsWith(';')) {
|
||||
lines.removeAt(idx)
|
||||
} else if (hasLabel(pretrim)) {
|
||||
val label = keepLabel(pretrim)
|
||||
@@ -588,11 +672,19 @@ private fun isSlotReferencedInLine(slot: String, line: String): Boolean {
|
||||
* or for 68020+ JUMPI/CALLI:
|
||||
* jmp/jsr ([p8_regfile+M]) ; memory-indirect
|
||||
*
|
||||
* Fused (preserves current unscaled semantics; no *4 scaling):
|
||||
* The same sequence with the scale folded into the addressing mode instead of
|
||||
* into d0 (68020+; this is what a pointer-array dispatch such as on..goto emits):
|
||||
* lea table_label, a0
|
||||
* move.l (a0,d0.w*4), d0
|
||||
* move.l d0, p8_regfile+M
|
||||
* movea.l p8_regfile+M, a0
|
||||
* jmp/jsr (a0)
|
||||
*
|
||||
* Fused, keeping whichever index operand was matched so the scale is preserved:
|
||||
* moveq #0, d0
|
||||
* move.b p8_regfile+N, d0
|
||||
* lea table_label, a0
|
||||
* move.l (a0,d0.w), a0
|
||||
* move.l (a0,d0.w), a0 ; or (a0,d0.w*4), a0 on 68020+
|
||||
* jmp/jsr (a0)
|
||||
*/
|
||||
private fun optimizeFuseLoadxJumpi(allPretrimmed: List<String>, lines: MutableList<String>): List<Modification> {
|
||||
@@ -603,11 +695,13 @@ private fun optimizeFuseLoadxJumpi(allPretrimmed: List<String>, lines: MutableLi
|
||||
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 loadIndexedOperand(instr: String): String? {
|
||||
// matches "move.l (a0,d0.w), d0" (68000, index pre-scaled into d0) and the scaled
|
||||
// "move.l (a0,d0.w*4), d0" forms (68020+, scale folded into the addressing mode).
|
||||
// The matched operand is returned so the fused instruction keeps the same scale.
|
||||
if (!instr.startsWith("move.l")) return null
|
||||
if (!(instr.trim().endsWith(", d0") || instr.trim().endsWith(",d0"))) return null
|
||||
return d0IndexOperand.find(instr)?.value
|
||||
}
|
||||
|
||||
fun isSpill(instr: String): String? {
|
||||
@@ -636,7 +730,7 @@ private fun optimizeFuseLoadxJumpi(allPretrimmed: List<String>, lines: MutableLi
|
||||
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 loadOperand = loadIndexedOperand(filtered[loadIdx].instruction) ?: continue
|
||||
val slotM = isSpill(filtered[spillIdx].instruction) ?: continue
|
||||
|
||||
// Check jump/call variant: 68000 uses movea reload + jmp/jsr (a0),
|
||||
@@ -663,7 +757,7 @@ private fun optimizeFuseLoadxJumpi(allPretrimmed: List<String>, lines: MutableLi
|
||||
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[loadIdx].index, false, "${loadIndent}move.l $loadOperand, a0"))
|
||||
mods.add(Modification(filtered[spillIdx].index, true, null))
|
||||
mods.add(Modification(filtered[nextIdx].index, true, null))
|
||||
alreadyModified.add(filtered[loadIdx].index)
|
||||
@@ -676,7 +770,7 @@ private fun optimizeFuseLoadxJumpi(allPretrimmed: List<String>, lines: MutableLi
|
||||
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[loadIdx].index, false, "${loadIndent}move.l $loadOperand, a0"))
|
||||
mods.add(Modification(filtered[spillIdx].index, true, null))
|
||||
mods.add(Modification(filtered[nextIdx].index, false, "${jmpIndent}$mnemonic (a0)"))
|
||||
alreadyModified.add(filtered[loadIdx].index)
|
||||
|
||||
@@ -1,24 +1,37 @@
|
||||
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.core.*
|
||||
import prog8.code.target.Amiga1200Target
|
||||
import prog8.code.target.Amiga500Target
|
||||
import java.nio.file.Files
|
||||
import prog8.code.target.Qemu68kTarget
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.readText
|
||||
import kotlin.io.path.writeText
|
||||
|
||||
class AssemblyProgramM68k(override val name: String, private val outputDir: Path) : IAssemblyProgram {
|
||||
class AssemblyProgramM68k(override val name: String) : IAssemblyProgram {
|
||||
|
||||
override val irInstructionCount: Int = 0
|
||||
override val irChunkCount: Int = 0
|
||||
override val irRegisterCount: Int = 0
|
||||
|
||||
private val assemblyFile = outputDir.resolve("$name.asm")
|
||||
private fun elfFile(outs: OutputFiles): Path = outs.programFile
|
||||
|
||||
fun elfFile(): Path = outputDir.resolve("$name.elf")
|
||||
/**
|
||||
* A target supplies its own memory layout as a "link.ld" file. A target defined by a
|
||||
* config file (i.e. living outside of the compiler) keeps it in its library directory, so
|
||||
* that is looked at first. Targets built into the compiler are looked up as a
|
||||
* /prog8lib/<targetname>/link.ld resource. Anything else falls back to the qemu68k layout.
|
||||
*/
|
||||
private fun resolveLinkerScript(target: ICompilationTarget): String {
|
||||
target.libraryPath?.resolve("link.ld")?.takeIf { it.exists() }?.let { return it.readText() }
|
||||
val targetResource = "/prog8lib/${target.name}/link.ld"
|
||||
AssemblyProgramM68k::class.java.getResource(targetResource)?.let { return it.readText() }
|
||||
return AssemblyProgramM68k::class.java.getResource("/prog8lib/${Qemu68kTarget.NAME}/link.ld")?.readText()
|
||||
?: error("cannot find $targetResource resource, and no qemu68k fallback either")
|
||||
}
|
||||
|
||||
private fun runProcess(command: List<String>, quiet: Boolean, tool: String? = null): Boolean {
|
||||
val proc = ProcessBuilder(command).redirectErrorStream(true)
|
||||
@@ -27,11 +40,11 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
|
||||
val process = try {
|
||||
proc.start()
|
||||
} catch (e: Exception) {
|
||||
when {
|
||||
tool=="vasm" -> {
|
||||
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" -> {
|
||||
"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 -> {
|
||||
@@ -55,12 +68,13 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
|
||||
else -> error("invalid cpu type for m68k codegen ${options.compTarget.cpu}")
|
||||
}
|
||||
val assemblerCpu = if(options.compTarget.name == "amiga500" && options.floats) "68020" else cpu
|
||||
val outs = OutputFiles.of(options, name)
|
||||
|
||||
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 objFile = outs.obj()
|
||||
val listFile = outs.list()
|
||||
val assembleCmd = mutableListOf(
|
||||
"vasmm68k_mot",
|
||||
"-m$assemblerCpu",
|
||||
@@ -71,7 +85,7 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
|
||||
"-ldots",
|
||||
"-spaces",
|
||||
"-o", objFile.toString(),
|
||||
assemblyFile.toString()
|
||||
outs.asm().toString()
|
||||
)
|
||||
if (options.asmListfile)
|
||||
assembleCmd.addAll(listOf("-L", listFile.toString()))
|
||||
@@ -80,14 +94,12 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
|
||||
if (!runProcess(assembleCmd, options.quiet, "vasm"))
|
||||
return false
|
||||
// clean up any leftover ELF/obj files from previous builds
|
||||
Files.deleteIfExists(outputDir.resolve("$name.bin"))
|
||||
outs.binFile().deleteIfExists()
|
||||
|
||||
// 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 linkScript = outs.linkScript()
|
||||
val elfFile = elfFile(outs)
|
||||
linkScript.writeText(resolveLinkerScript(options.compTarget))
|
||||
|
||||
val linkCmd = listOf(
|
||||
"vlink",
|
||||
@@ -98,15 +110,15 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
|
||||
objFile.toString()
|
||||
)
|
||||
val linkOk = runProcess(linkCmd, options.quiet, "vlink")
|
||||
Files.deleteIfExists(linkScript)
|
||||
linkScript.deleteIfExists()
|
||||
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 exefile = outs.programFile
|
||||
val listfile = outs.list()
|
||||
val assembleCmd = when(options.compTarget) {
|
||||
is Amiga500Target -> {
|
||||
// amiga 500 with kickstart 1.3
|
||||
@@ -122,7 +134,7 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
|
||||
"-spaces",
|
||||
"-nosym", // no debug symbols
|
||||
"-o", exefile.toString(),
|
||||
assemblyFile.toString()
|
||||
outs.asm().toString()
|
||||
).also { cmd ->
|
||||
if (options.asmListfile) {
|
||||
cmd.add("-L")
|
||||
@@ -146,7 +158,7 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
|
||||
"-spaces",
|
||||
"-nosym", // no debug symbols
|
||||
"-o", exefile.toString(),
|
||||
assemblyFile.toString()
|
||||
outs.asm().toString()
|
||||
).also { cmd ->
|
||||
if (options.asmListfile) {
|
||||
cmd.add("-L")
|
||||
|
||||
@@ -227,7 +227,7 @@ internal fun AsmGen.translateArithmetic(insn: IRInstruction) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLoadD0(srcReg, IRDataType.BYTE)
|
||||
emitLine($$"and.l #\$ff, d0", "zero extend")
|
||||
emitLine($$"and.l #$ff, d0", "zero extend")
|
||||
invalidateD0Cache()
|
||||
emitLine("mulu.w d0, d0")
|
||||
invalidateD0Cache()
|
||||
|
||||
@@ -195,19 +195,30 @@ private fun AsmGen.shiftOpcode(isLeft: Boolean, isArithmetic: Boolean, isRotate:
|
||||
// === 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.
|
||||
// is one instruction and optimal, but QEMU's 68020 emulation is broken for
|
||||
// word-sized memory *right* shifts in both addressing forms:
|
||||
// `asr.w addr` -> sign-extends the 16-bit operand instead of zero-extending
|
||||
// it, producing a logical shift result for negative values
|
||||
// `lsr.w (a0)` -> sign-extends, producing an arithmetic shift result
|
||||
// The register form (`move.w addr,d0` / `op.w #1,d0` / `move.w d0,addr`) is
|
||||
// correct in QEMU, and both the absolute and `(a0)` forms are correct on a real
|
||||
// 68000 (vamos). Left shifts and rotates are unaffected, so they keep the
|
||||
// compact memory form. d0 is scratch for the shift, matching the callers which
|
||||
// already invalidate their d0 cache.
|
||||
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")
|
||||
when (target.name) {
|
||||
Qemu68kTarget.NAME if (op == "asr" || op == "lsr") -> {
|
||||
emitLine("move.w $address, d0")
|
||||
emitLine("$op.w #1, d0")
|
||||
emitLine("move.w d0, $address")
|
||||
}
|
||||
Qemu68kTarget.NAME -> {
|
||||
emitLine("lea $address, a0")
|
||||
emitLine("$op.w (a0)")
|
||||
}
|
||||
else -> {
|
||||
emitLine("$op.w $address")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,40 +28,46 @@ import prog8.intermediate.requireSrcB
|
||||
import prog8.intermediate.requireTarget
|
||||
|
||||
internal fun AsmGen.translateBranch(insn: IRInstruction) {
|
||||
val label: String = when (val target = insn.requireTarget()) {
|
||||
val target = insn.requireTarget()
|
||||
val label: String = when (target) {
|
||||
is CodeReference.Label -> fixNameSymbols(target.name)
|
||||
is CodeReference.Absolute -> target.address.toHex()
|
||||
is CodeReference.Indirect -> error("branch needs a static target")
|
||||
}
|
||||
val leavesSubroutine = when (target) {
|
||||
is CodeReference.Label -> labelIsOutsideCurrentSub(target.name)
|
||||
is CodeReference.Absolute -> true
|
||||
is CodeReference.Indirect -> false
|
||||
}
|
||||
|
||||
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)
|
||||
Opcode.BSTCC -> emitBranch("bcc", label, leavesSubroutine)
|
||||
Opcode.BSTCS -> emitBranch("bcs", label, leavesSubroutine)
|
||||
Opcode.BSTEQ -> emitBranch("beq", label, leavesSubroutine)
|
||||
Opcode.BSTNE -> emitBranch("bne", label, leavesSubroutine)
|
||||
Opcode.BSTNEG -> emitBranch("bmi", label, leavesSubroutine)
|
||||
Opcode.BSTPOS -> emitBranch("bpl", label, leavesSubroutine)
|
||||
Opcode.BSTVC -> emitBranch("bvc", label, leavesSubroutine)
|
||||
Opcode.BSTVS -> emitBranch("bvs", label, leavesSubroutine)
|
||||
|
||||
// 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.BGT -> cmpBranchUnsignedImm(insn, label, "bhi", leavesSubroutine)
|
||||
Opcode.BGE -> cmpBranchUnsignedImm(insn, label, "bhs", leavesSubroutine)
|
||||
Opcode.BLT -> cmpBranchUnsignedImm(insn, label, "blo", leavesSubroutine)
|
||||
Opcode.BLE -> cmpBranchUnsignedImm(insn, label, "bls", leavesSubroutine)
|
||||
|
||||
Opcode.BGTR -> cmpBranchUnsignedReg(insn, label, "bhi")
|
||||
Opcode.BGER -> cmpBranchUnsignedReg(insn, label, "bhs")
|
||||
Opcode.BGTR -> cmpBranchUnsignedReg(insn, label, "bhi", leavesSubroutine)
|
||||
Opcode.BGER -> cmpBranchUnsignedReg(insn, label, "bhs", leavesSubroutine)
|
||||
// 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.BGTS -> cmpBranchSignedImm(insn, label, "bgt", leavesSubroutine)
|
||||
Opcode.BGES -> cmpBranchSignedImm(insn, label, "bge", leavesSubroutine)
|
||||
Opcode.BLTS -> cmpBranchSignedImm(insn, label, "blt", leavesSubroutine)
|
||||
Opcode.BLES -> cmpBranchSignedImm(insn, label, "ble", leavesSubroutine)
|
||||
|
||||
Opcode.BGTSR -> cmpBranchSignedReg(insn, label, "bgt")
|
||||
Opcode.BGESR -> cmpBranchSignedReg(insn, label, "bge")
|
||||
Opcode.BGTSR -> cmpBranchSignedReg(insn, label, "bgt", leavesSubroutine)
|
||||
Opcode.BGESR -> cmpBranchSignedReg(insn, label, "bge", leavesSubroutine)
|
||||
// BLTSR doesn't exist in IR — uses BGTSR with swapped operands
|
||||
|
||||
else -> error("Unknown branch opcode: ${insn.opcode}")
|
||||
@@ -70,7 +76,7 @@ internal fun AsmGen.translateBranch(insn: IRInstruction) {
|
||||
|
||||
// === Unsigned comparisons: register vs immediate ===
|
||||
|
||||
private fun AsmGen.cmpBranchUnsignedImm(insn: IRInstruction, label: String, branchOp: String) {
|
||||
private fun AsmGen.cmpBranchUnsignedImm(insn: IRInstruction, label: String, branchOp: String, leavesSubroutine: Boolean) {
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
val reg = insn.requireSrcA().intNumber
|
||||
val imm = insn.requireImmediateInt()
|
||||
@@ -81,24 +87,24 @@ private fun AsmGen.cmpBranchUnsignedImm(insn: IRInstruction, label: String, bran
|
||||
emitLine("tst$s ${regAddr(reg)}")
|
||||
else
|
||||
emitLine("cmpi$s #$imm, ${regAddr(reg)}")
|
||||
emitBranch(branchOp, label)
|
||||
emitBranch(branchOp, label, leavesSubroutine)
|
||||
}
|
||||
|
||||
// === Unsigned comparisons: register vs register ===
|
||||
|
||||
private fun AsmGen.cmpBranchUnsignedReg(insn: IRInstruction, label: String, branchOp: String) {
|
||||
private fun AsmGen.cmpBranchUnsignedReg(insn: IRInstruction, label: String, branchOp: String, leavesSubroutine: Boolean) {
|
||||
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)
|
||||
emitBranch(branchOp, label, leavesSubroutine)
|
||||
}
|
||||
|
||||
// === Signed comparisons: register vs immediate ===
|
||||
|
||||
private fun AsmGen.cmpBranchSignedImm(insn: IRInstruction, label: String, branchOp: String) {
|
||||
private fun AsmGen.cmpBranchSignedImm(insn: IRInstruction, label: String, branchOp: String, leavesSubroutine: Boolean) {
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
val reg = insn.requireSrcA().intNumber
|
||||
val imm = insn.requireImmediateInt()
|
||||
@@ -109,22 +115,41 @@ private fun AsmGen.cmpBranchSignedImm(insn: IRInstruction, label: String, branch
|
||||
emitLine("tst$s ${regAddr(reg)}")
|
||||
else
|
||||
emitLine("cmpi$s #$imm, ${regAddr(reg)}")
|
||||
emitBranch(branchOp, label)
|
||||
emitBranch(branchOp, label, leavesSubroutine)
|
||||
}
|
||||
|
||||
// === Signed comparisons: register vs register ===
|
||||
|
||||
private fun AsmGen.cmpBranchSignedReg(insn: IRInstruction, label: String, branchOp: String) {
|
||||
private fun AsmGen.cmpBranchSignedReg(insn: IRInstruction, label: String, branchOp: String, leavesSubroutine: Boolean) {
|
||||
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)
|
||||
emitBranch(branchOp, label, leavesSubroutine)
|
||||
}
|
||||
|
||||
private fun AsmGen.emitBranch(branchOp: String, label: String) {
|
||||
private val inverseBranchOps = mapOf(
|
||||
"bcc" to "bcs", "bcs" to "bcc",
|
||||
"beq" to "bne", "bne" to "beq",
|
||||
"bmi" to "bpl", "bpl" to "bmi",
|
||||
"bvc" to "bvs", "bvs" to "bvc",
|
||||
"bhi" to "bls", "bls" to "bhi",
|
||||
"bhs" to "blo", "blo" to "bhs",
|
||||
"bgt" to "ble", "ble" to "bgt",
|
||||
"bge" to "blt", "blt" to "bge")
|
||||
|
||||
private fun AsmGen.emitBranch(branchOp: String, label: String, leavesSubroutine: Boolean) {
|
||||
invalidateD0Cache()
|
||||
emitLine("$branchOp $label")
|
||||
if (!leavesSubroutine || !frameActive) {
|
||||
emitLine("$branchOp $label")
|
||||
return
|
||||
}
|
||||
|
||||
val skipLabel = makeLabel("branch_keep_frame")
|
||||
emitLine("${inverseBranchOps.getValue(branchOp)} $skipLabel")
|
||||
emitFrameUnlk()
|
||||
emitLine("jmp $label")
|
||||
emitLabel(skipLabel)
|
||||
}
|
||||
@@ -26,8 +26,19 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
|
||||
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.Label -> {
|
||||
// a jump to a label in another subroutine ends this activation, so our frame has to be gone
|
||||
// before we leave: whatever runs next returns to our caller, not to us
|
||||
if (frameActive && labelIsOutsideCurrentSub(t.name))
|
||||
emitFrameUnlk()
|
||||
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 -> {
|
||||
// a fixed address can't be resolved to a chunk of this subroutine, so assume it
|
||||
// leaves for good: whatever runs there returns to our caller, not to us
|
||||
emitFrameUnlk()
|
||||
emitLine("jmp ${t.address.value.toHex()}")
|
||||
}
|
||||
is CodeReference.Indirect -> error("JUMP needs target")
|
||||
}
|
||||
}
|
||||
@@ -36,6 +47,9 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
|
||||
val ref = insn.requireTarget() as? CodeReference.Indirect ?: error("JUMPI needs an indirect target register")
|
||||
val reg = ref.pointer.intNumber
|
||||
invalidateD0Cache()
|
||||
// Keep the frame for a table whose targets all belong to this subroutine.
|
||||
if (!jumpTableStaysInCurrentSub(ref.pointer.register))
|
||||
emitFrameUnlk()
|
||||
if(program.options.compTarget.cpu >= CpuType.M68020) {
|
||||
// 68020+ supports memory-indirect addressing: fetch the target address from memory directly.
|
||||
emitLine("jmp ([${regAddr(reg)}])")
|
||||
@@ -99,8 +113,10 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
|
||||
// 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).
|
||||
// Multiple status flag returns in one multi-assign are protected by the IR:
|
||||
// it wraps each flag's extraction in PUSHST/POPST so they don't clobber each other.
|
||||
// On the 68000 cpu this fails, because it cannot save/restore the CCR with a
|
||||
// nonprivileged instruction (compile for 68010 or higher to use this).
|
||||
val isMultiReturn = callSite.results.size > 1
|
||||
for(ret in callSite.results) {
|
||||
if (ret.location is CallLocation.StatusFlag)
|
||||
@@ -129,6 +145,7 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
|
||||
|
||||
Opcode.RETURN -> {
|
||||
invalidateD0Cache()
|
||||
emitFrameUnlk()
|
||||
emitLine("rts")
|
||||
}
|
||||
|
||||
@@ -142,6 +159,7 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
|
||||
val reg = insn.requireIntSourceA().intNumber
|
||||
emitLoadD0(reg, type)
|
||||
}
|
||||
emitFrameUnlk()
|
||||
emitLine("rts")
|
||||
}
|
||||
|
||||
@@ -154,6 +172,7 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
|
||||
emitLine("moveq #$value, d0")
|
||||
else
|
||||
emitLine("move$s #$value, d0")
|
||||
emitFrameUnlk()
|
||||
emitLine("rts")
|
||||
}
|
||||
|
||||
@@ -178,21 +197,18 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
|
||||
// === Status flag stack ops via Scc ===
|
||||
|
||||
Opcode.PUSHST -> {
|
||||
// Move CCR to D0 (low byte), then push as byte.
|
||||
// Save CCR to the stack. The 68010+ memory form keeps the CCR save
|
||||
// register-neutral (no data register is clobbered) and word-aligned.
|
||||
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)")
|
||||
emitLine("move ccr, -(sp)")
|
||||
}
|
||||
|
||||
Opcode.POPST -> {
|
||||
// Pop byte into D0, then restore CCR
|
||||
// Restore CCR from the stack (register-neutral on 68010+).
|
||||
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")
|
||||
emitLine("move (sp)+, ccr")
|
||||
}
|
||||
|
||||
// === Flag manipulation ===
|
||||
@@ -634,16 +650,19 @@ private fun AsmGen.translateCall(fnLabel: String, callSite: CallSite, forwardedI
|
||||
// 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
|
||||
// Parameter names are resolved by argument index so each stack push can be
|
||||
// annotated with the parameter it fills; unknown callees get no annotation.
|
||||
val paramNames = program.allSubs().find { it.label == fnLabel }?.parameters?.map { it.name } ?: emptyList()
|
||||
fun paramNameFor(index: Int): String? =
|
||||
paramNames.getOrNull(index)?.substringAfterLast('.')?.removePrefix("p8v_")
|
||||
val forwardedRegs = forwardedImmediateCall?.loads?.keys ?: emptySet()
|
||||
for ((index, arg) in callSite.arguments.withIndex()) {
|
||||
if (arg.source.register in forwardedRegs) continue
|
||||
translateArgument(arg, fnLabel, forwardedImmediateCall, paramNameFor(index))
|
||||
}
|
||||
for (arg in nonForwarded) {
|
||||
translateArgument(arg, fnLabel, forwardedImmediateCall)
|
||||
}
|
||||
for (arg in forwarded) {
|
||||
translateArgument(arg, fnLabel, forwardedImmediateCall)
|
||||
for ((index, arg) in callSite.arguments.withIndex()) {
|
||||
if (arg.source.register !in forwardedRegs) continue
|
||||
translateArgument(arg, fnLabel, forwardedImmediateCall, paramNameFor(index))
|
||||
}
|
||||
|
||||
// Check if this call targets an inline asmsub — emit its body directly instead of jsr
|
||||
@@ -655,6 +674,23 @@ private fun AsmGen.translateCall(fnLabel: String, callSite: CallSite, forwardedI
|
||||
emitLine("bsr $fnLabel")
|
||||
}
|
||||
|
||||
// caller cleanup of the all-stack argument area:
|
||||
// each argument was pushed into its own word-packed slot (2-byte slots for byte/word args,
|
||||
// 4-byte slots for wide args); the total area size is always even
|
||||
val pushedSlots = callSite.arguments.count { it.location is CallLocation.FrameSlot }
|
||||
if (pushedSlots > 0) {
|
||||
val totalSize = callSite.arguments
|
||||
.filter { it.location is CallLocation.FrameSlot }
|
||||
.sumOf { incomingSlotSize(it.source.type, target) }
|
||||
val needsCcr = callSite.results.any { it.location is CallLocation.StatusFlag }
|
||||
when {
|
||||
// addq is compact but updates CCR; lea preserves CCR so status-flag returns
|
||||
// remain visible to the IR's branch pattern.
|
||||
totalSize in 1..8 && !needsCcr -> emitLine("addq.l #$totalSize,sp")
|
||||
else -> emitLine("lea $totalSize(sp),sp")
|
||||
}
|
||||
}
|
||||
|
||||
// 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:
|
||||
@@ -662,8 +698,10 @@ private fun AsmGen.translateCall(fnLabel: String, callSite: CallSite, forwardedI
|
||||
// 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.
|
||||
// Multiple status flag returns in one multi-assign are protected by the IR:
|
||||
// it wraps each flag's extraction in PUSHST/POPST so they don't clobber each other.
|
||||
// On the 68000 cpu this fails, because it cannot save/restore the CCR with a
|
||||
// nonprivileged instruction (compile for 68010 or higher to use this).
|
||||
val isMultiReturn = callSite.results.size > 1
|
||||
for (ret in callSite.results) {
|
||||
if (ret.location is CallLocation.StatusFlag)
|
||||
@@ -757,7 +795,8 @@ private fun AsmGen.emitInlineCopyLoop(iterations: Int, size: Int) {
|
||||
private fun AsmGen.translateArgument(
|
||||
arg: CallArgument,
|
||||
fnLabel: String? = null,
|
||||
forwardedImmediateCall: ImmediateCallOptimization? = null
|
||||
forwardedImmediateCall: ImmediateCallOptimization? = null,
|
||||
paramName: String? = null
|
||||
) {
|
||||
val argReg = arg.source
|
||||
val forwarded = forwardedImmediateCall?.loads?.get(argReg.register)
|
||||
@@ -807,6 +846,34 @@ private fun AsmGen.translateArgument(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Stack argument: push into the parameter's word-packed slot, landing at the slot
|
||||
// base byte (2-byte slots for byte/word args, 4-byte slots for wide args).
|
||||
// The caller pops the whole argument area after the return, so the callee never needs to know the argument count.
|
||||
if (arg.location is CallLocation.FrameSlot) {
|
||||
if (argReg.isFloat) {
|
||||
// the FPU has no absolute-long addressing mode, so the value has to travel
|
||||
// through an FPU register first (fp0 is translator scratch, never holds a live vreg).
|
||||
// The store itself reserves the slot via predecrement, so no separate subq is needed.
|
||||
emitLine("fmove.s ${floatRegFileAddr(argReg.floatNumber)},$FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC,-(sp)", paramName.orEmpty())
|
||||
} else {
|
||||
when (argReg.type) {
|
||||
IRDataType.BYTE ->
|
||||
// one-instruction push: on the stack pointer a byte predecrement
|
||||
// steps by 2 (not 1), so this reserves the 2-byte slot in one go;
|
||||
// the byte lands at the slot's base byte, matching the zero
|
||||
// within-slot displacement that byte parameters use
|
||||
emitLine("move.b ${regAddr(argReg.intNumber)},-(sp)",
|
||||
(if (paramName != null) "$paramName " else "") + "(note: pushed as a word)")
|
||||
// a 2-byte predecrement push reserves and stores the word slot in one instruction
|
||||
IRDataType.WORD ->
|
||||
emitLine("move.w ${regAddr(argReg.intNumber)},-(sp)", paramName.orEmpty())
|
||||
// longs and pointers fill the whole slot, so reserve and store in one push
|
||||
else -> emitLine("move.l ${regAddr(argReg.intNumber)},-(sp)", paramName.orEmpty())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
// 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) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import prog8.intermediate.CallSite
|
||||
import prog8.intermediate.IMSyscall
|
||||
import prog8.intermediate.IRDataType
|
||||
import prog8.intermediate.RegisterOperand
|
||||
import prog8.intermediate.floatNumber
|
||||
import prog8.intermediate.intNumber
|
||||
|
||||
|
||||
@@ -18,7 +19,12 @@ import prog8.intermediate.intNumber
|
||||
|
||||
internal fun AsmGen.translateSyscall(num: Int, site: CallSite) {
|
||||
when (num) {
|
||||
IMSyscall.STRING_CONTAINS.number -> translateSyscallStringContains(site)
|
||||
IMSyscall.BYTEARRAY_CONTAINS.number -> translateSyscallBytearrayContains(site)
|
||||
IMSyscall.WORDARRAY_CONTAINS.number -> translateSyscallWordarrayContains(site)
|
||||
IMSyscall.SPLIT_WORDARRAY_CONTAINS.number -> translateSyscallSplitWordarrayContains(site)
|
||||
IMSyscall.LONGARRAY_CONTAINS.number -> translateSyscallLongarrayContains(site)
|
||||
IMSyscall.FLOATARRAY_CONTAINS.number -> translateSyscallFloatarrayContains(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)
|
||||
@@ -59,24 +65,32 @@ private fun AsmGen.loadStringArg(reg: Int, dt: IRDataType, dreg: String) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun AsmGen.translateSyscallWordarrayContains(site: CallSite) {
|
||||
// Shared helper for array contains syscalls that scan a contiguous array.
|
||||
// The length argument is a word, so arrays up to the target's ARRAY_SIZE_LIMIT
|
||||
// are scanned correctly (m68k: 32768 elements, VM: 65535 elements).
|
||||
private fun AsmGen.translateSyscallArrayContains(site: CallSite, dt: IRDataType) {
|
||||
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")
|
||||
val labelLoop = makeLabel(".ac_loop")
|
||||
val labelFound = makeLabel(".ac_found")
|
||||
val labelDone = makeLabel(".ac_done")
|
||||
|
||||
emitLoadD0(regElem, IRDataType.WORD)
|
||||
emitLoadD0(regElem, dt)
|
||||
emitLine("move.l ${regAddr(regArr)}, a0")
|
||||
emitLine("moveq.l #0, d1")
|
||||
emitLine("move.b ${regAddrByte(regLen, 0)}, d1")
|
||||
emitLine("move.w ${regAddr(regLen)}, d1")
|
||||
emitLine("subq.w #1, d1")
|
||||
emitLine("bmi $labelDone ; length was 0 -> not found")
|
||||
emitRaw("$labelLoop:")
|
||||
emitLine("cmp.w (a0)+, d0")
|
||||
val cmpInstr = when (dt) {
|
||||
IRDataType.BYTE -> "cmp.b"
|
||||
IRDataType.WORD -> "cmp.w"
|
||||
IRDataType.LONG -> "cmp.l"
|
||||
else -> error("array contains: unsupported element type $dt")
|
||||
}
|
||||
emitLine("$cmpInstr (a0)+, d0")
|
||||
emitLine("beq $labelFound")
|
||||
emitLine("dbra d1, $labelLoop")
|
||||
emitLine("move.b #0, ${regAddrByte(resultReg, 0)}")
|
||||
@@ -86,6 +100,100 @@ private fun AsmGen.translateSyscallWordarrayContains(site: CallSite) {
|
||||
emitRaw("$labelDone:")
|
||||
}
|
||||
|
||||
private fun AsmGen.translateSyscallBytearrayContains(site: CallSite) {
|
||||
translateSyscallArrayContains(site, IRDataType.BYTE)
|
||||
}
|
||||
|
||||
private fun AsmGen.translateSyscallWordarrayContains(site: CallSite) {
|
||||
translateSyscallArrayContains(site, IRDataType.WORD)
|
||||
}
|
||||
|
||||
private fun AsmGen.translateSyscallLongarrayContains(site: CallSite) {
|
||||
translateSyscallArrayContains(site, IRDataType.LONG)
|
||||
}
|
||||
|
||||
private fun AsmGen.translateSyscallSplitWordarrayContains(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(".swc_loop")
|
||||
val labelFound = makeLabel(".swc_found")
|
||||
val labelDone = makeLabel(".swc_done")
|
||||
|
||||
emitLoadD0(regElem, IRDataType.WORD)
|
||||
emitLine("move.l ${regAddr(regArr)}, a0") // a0 = lsb array start
|
||||
emitLine("move.w ${regAddr(regLen)}, d1") // d1 = element count
|
||||
emitLine("move.w d1, d2")
|
||||
emitLine("add.l a0, d2") // d2 = msb array start (lsb + length)
|
||||
emitLine("subq.w #1, d1")
|
||||
emitLine("bmi $labelDone ; length was 0 -> not found")
|
||||
emitRaw("$labelLoop:")
|
||||
emitLine("move.b (a0,d1.w), d3") // d3 = lsb of element at index d1
|
||||
emitLine("move.b (d2,d1.w), d4") // d4 = msb of element at index d1
|
||||
emitLine("lsl.w #8, d4")
|
||||
emitLine("or.w d3, d4") // d4 = reconstructed word
|
||||
emitLine("cmp.w d0, d4")
|
||||
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.translateSyscallFloatarrayContains(site: CallSite) {
|
||||
val regNeedle = site.argument(0).floatNumber
|
||||
val regArr = site.argument(1).intNumber
|
||||
val regLen = site.argument(2).intNumber
|
||||
val resultReg = site.resultRegister(0).intNumber
|
||||
|
||||
val labelLoop = makeLabel(".fac_loop")
|
||||
val labelFound = makeLabel(".fac_found")
|
||||
val labelDone = makeLabel(".fac_done")
|
||||
|
||||
emitLine("fmove.s ${floatRegFileAddr(regNeedle)}, fp0")
|
||||
emitLine("move.l ${regAddr(regArr)}, a0")
|
||||
emitLine("move.w ${regAddr(regLen)}, d1")
|
||||
emitLine("subq.w #1, d1")
|
||||
emitLine("bmi $labelDone ; length was 0 -> not found")
|
||||
emitRaw("$labelLoop:")
|
||||
emitLine("fcmp.s (a0)+, fp0")
|
||||
emitLine("fbeq $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.translateSyscallStringContains(site: CallSite) {
|
||||
val regChar = site.argument(0).intNumber
|
||||
val regStr = site.argument(1).intNumber
|
||||
val resultReg = site.resultRegister(0).intNumber
|
||||
|
||||
val labelLoop = makeLabel(".sc_loop")
|
||||
val labelFound = makeLabel(".sc_found")
|
||||
val labelDone = makeLabel(".sc_done")
|
||||
|
||||
emitLine("move.b ${regAddrByte(regChar, 0)}, d0")
|
||||
emitLine("move.l ${regAddr(regStr)}, a0")
|
||||
emitRaw("$labelLoop:")
|
||||
emitLine("move.b (a0)+, d1")
|
||||
emitLine("beq $labelDone ; reached null terminator -> not found")
|
||||
emitLine("cmp.b d0, d1")
|
||||
emitLine("beq $labelFound")
|
||||
emitLine("bra $labelLoop")
|
||||
emitRaw("$labelFound:")
|
||||
emitLine("move.b #1, ${regAddrByte(resultReg, 0)}")
|
||||
emitLine("bra $labelDone")
|
||||
emitRaw("$labelDone:")
|
||||
emitLine("move.b #0, ${regAddrByte(resultReg, 0)}")
|
||||
}
|
||||
|
||||
private fun AsmGen.translateSyscallClamp(site: CallSite, dt: IRDataType, signed: Boolean) {
|
||||
val valueReg = site.argument(0).intNumber
|
||||
val minReg = site.argument(1).intNumber
|
||||
|
||||
@@ -7,6 +7,7 @@ import prog8.code.ast.PtProgram
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.codegen.intermediate.IRCodeGen
|
||||
import prog8.codegen.intermediate.StackFrameLayout
|
||||
import prog8.intermediate.IRDataType
|
||||
import prog8.intermediate.IRFileWriter
|
||||
import prog8.intermediate.VirtualRegister
|
||||
@@ -28,15 +29,18 @@ class M68kCodeGenerator(val retainSSA: Boolean): ICodeGeneratorBackend {
|
||||
}
|
||||
irProgram.verifyRegisterTypes(virtualRegisterTypes)
|
||||
|
||||
if (options.compTarget.supportsStackFrames)
|
||||
StackFrameLayout(irProgram, errors).apply()
|
||||
|
||||
if (options.dumpVariables)
|
||||
dumpVariables(irProgram)
|
||||
|
||||
IRFileWriter(irProgram, null).write()
|
||||
|
||||
val gen = AsmGen(irProgram, irProgram.options.compTarget)
|
||||
val gen = AsmGen(irProgram, irProgram.options.compTarget, errors)
|
||||
if (!gen.generate())
|
||||
throw RuntimeException("M68k assembly generation failed")
|
||||
|
||||
return AssemblyProgramM68k(irProgram.name, irProgram.options.outputDir)
|
||||
return AssemblyProgramM68k(irProgram.name)
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,11 @@ 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.code.core.OutputFiles
|
||||
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 = M68kCli()
|
||||
@@ -23,8 +25,12 @@ fun main(args: Array<String>) {
|
||||
when(target.cpu) {
|
||||
CpuType.M68000, CpuType.M68020 -> {
|
||||
val gen = AsmGen(program, target)
|
||||
gen.generate()
|
||||
println("Generated assembly: ${program.name}.asm")
|
||||
val ok = gen.generate()
|
||||
if(!ok) {
|
||||
System.err.println("Assembly failed.")
|
||||
exitProcess(1)
|
||||
}
|
||||
println("Generated assembly: ${OutputFiles.of(program.options, program.name).asm()}")
|
||||
}
|
||||
else -> {
|
||||
error("This code generator only works for M68000 or M68020 CPU.")
|
||||
|
||||
@@ -1208,6 +1208,41 @@ class TestInstructionSelectionOptimizations : FunSpec({
|
||||
lines.count { it.contains("(a0,d0.w*") } shouldBe 0
|
||||
}
|
||||
|
||||
test("loadx+jumpi fusion removes the regfile roundtrip and keeps the scaled index operand") {
|
||||
// a pointer-array dispatch (on..goto / rewritten when) emits the scale in the addressing mode
|
||||
val lines = generateAsm(
|
||||
tempRoot.resolve("test-m68k-fuse-loadx-jumpi-scaled"),
|
||||
listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.POINTER, 6, IRMemory.indexed("table", 3, IRDataType.WORD, 4)),
|
||||
IRInstructions.jumpIndirect(6)
|
||||
)
|
||||
)
|
||||
|
||||
lines.any { it == "move.l (a0,d0.w*4), a0" } shouldBe true
|
||||
lines.any { it == "jmp (a0)" } shouldBe true
|
||||
// the spill and the reload it feeds are gone
|
||||
lines.count { it.startsWith("move.l d0,p8_regfile") } shouldBe 0
|
||||
lines.count { it.startsWith("movea.l") } shouldBe 0
|
||||
lines.any { it.startsWith("jmp ([p8_regfile") } shouldBe false
|
||||
}
|
||||
|
||||
test("loadx+jumpi fusion keeps the unscaled index operand on 68000") {
|
||||
val lines = generateAsm(
|
||||
tempRoot.resolve("test-m68k-fuse-loadx-jumpi-unscaled"),
|
||||
listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.POINTER, 6, IRMemory.indexed("table", 3, IRDataType.WORD, 4)),
|
||||
IRInstructions.jumpIndirect(6)
|
||||
),
|
||||
target = Amiga500Target()
|
||||
)
|
||||
|
||||
// on 68000 the scale is applied to d0 by hand, so the addressing mode stays unscaled
|
||||
lines.any { it == "lsl.w #2,d0" } shouldBe true
|
||||
lines.any { it == "move.l (a0,d0.w), a0" } shouldBe true
|
||||
lines.any { it == "jmp (a0)" } shouldBe true
|
||||
lines.count { it.startsWith("move.l d0,p8_regfile") } shouldBe 0
|
||||
}
|
||||
|
||||
test("indirect memory lowering picks displacement addressing or explicit pointer adjustment") {
|
||||
val lines = generateAsm(
|
||||
tempRoot.resolve("test-m68k-indirect-displacement"),
|
||||
@@ -1255,7 +1290,7 @@ class TestInstructionSelectionOptimizations : FunSpec({
|
||||
lines.any { it == "move.w #1234,var" } shouldBe true
|
||||
}
|
||||
|
||||
// === extb.l gate robustification ===
|
||||
// === extb.l gate hardening ===
|
||||
|
||||
test("M68020 sign-extends byte to long with extb.l") {
|
||||
val lines = generateAsm(
|
||||
@@ -1279,4 +1314,118 @@ class TestInstructionSelectionOptimizations : FunSpec({
|
||||
lines.count { it == "ext.w d0" } shouldBe 1
|
||||
lines.count { it == "ext.l d0" } shouldBe 1
|
||||
}
|
||||
|
||||
test("M5 drops a lea when a0 already holds the same base") {
|
||||
val lines = mutableListOf(
|
||||
" lea arr,a0",
|
||||
" move.w d1,(a0,d0.w)",
|
||||
" lea arr,a0",
|
||||
" clr.w (a0,d0.w)"
|
||||
)
|
||||
optimizeAssembly(lines)
|
||||
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 1
|
||||
lines.any { it.trimStart().startsWith("clr.w") } shouldBe true
|
||||
}
|
||||
|
||||
test("M5 keeps a lea when a different base was loaded into a0") {
|
||||
val lines = mutableListOf(
|
||||
" lea arr,a0",
|
||||
" move.w d1,(a0,d0.w)",
|
||||
" lea other,a0",
|
||||
" lea arr,a0",
|
||||
" clr.w (a0,d0.w)"
|
||||
)
|
||||
optimizeAssembly(lines)
|
||||
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 2
|
||||
lines.count { it.trimStart().startsWith("lea other,a0") } shouldBe 1
|
||||
}
|
||||
|
||||
test("M5 keeps a lea when a0 is written in between") {
|
||||
val lines = mutableListOf(
|
||||
" lea arr,a0",
|
||||
" movea.l other,a0",
|
||||
" lea arr,a0",
|
||||
" clr.w (a0,d0.w)"
|
||||
)
|
||||
optimizeAssembly(lines)
|
||||
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 2
|
||||
}
|
||||
|
||||
test("M5 keeps a lea when a call intervenes") {
|
||||
val lines = mutableListOf(
|
||||
" lea arr,a0",
|
||||
" bsr some.sub",
|
||||
" lea arr,a0",
|
||||
" clr.w (a0,d0.w)"
|
||||
)
|
||||
optimizeAssembly(lines)
|
||||
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 2
|
||||
lines.any { it.trimStart().startsWith("bsr") } shouldBe true
|
||||
}
|
||||
|
||||
test("M5 forgets a0 across a label") {
|
||||
val lines = mutableListOf(
|
||||
" lea arr,a0",
|
||||
"somewhere:",
|
||||
" lea arr,a0",
|
||||
" clr.w (a0,d0.w)"
|
||||
)
|
||||
optimizeAssembly(lines)
|
||||
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 2
|
||||
lines.any { it.trimStart().startsWith("somewhere:") } shouldBe true
|
||||
}
|
||||
|
||||
test("M5 drops a lea across a conditional branch to a label") {
|
||||
// A conditional branch does not modify a0, and its target is a label where the tracked
|
||||
// value is dropped, so the fallthrough still knows a0 holds the base.
|
||||
val lines = mutableListOf(
|
||||
" lea arr,a0",
|
||||
" move.w (a0,d0.w),d0",
|
||||
" bge clamp.done",
|
||||
" lea arr,a0",
|
||||
" clr.w (a0,d0.w)",
|
||||
"clamp.done:"
|
||||
)
|
||||
optimizeAssembly(lines)
|
||||
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 1
|
||||
lines.any { it.trimStart().startsWith("clr.w") } shouldBe true
|
||||
}
|
||||
|
||||
test("qemu68k word right shifts avoid the memory forms its 68020 model mis-emulates") {
|
||||
// QEMU's 68020 sign-extends the operand for both `asr.w <abs>` and
|
||||
// `lsr.w (a0)`, so both memory forms give the wrong result. The
|
||||
// register form round-trips through d0 and is correct.
|
||||
val lines = generateAsm(
|
||||
tempRoot.resolve("test-m68k-qemu-word-rshift"),
|
||||
listOf(
|
||||
IRInstructions.memoryOp(Opcode.ASRM, IRDataType.WORD, IRMemory.direct("p8b_test.p8v_value")),
|
||||
IRInstructions.memoryOp(Opcode.LSRM, IRDataType.WORD, IRMemory.direct("p8b_test.p8v_other"))
|
||||
),
|
||||
Qemu68kTarget()
|
||||
)
|
||||
|
||||
lines.count { it.startsWith("asr.w") } shouldBe 1
|
||||
lines.count { it.startsWith("lsr.w") } shouldBe 1
|
||||
lines.count { it == "asr.w #1,d0" } shouldBe 1
|
||||
lines.count { it == "lsr.w #1,d0" } shouldBe 1
|
||||
lines.count { it == "move.w p8b_test.p8v_value,d0" } shouldBe 1
|
||||
lines.count { it == "move.w d0,p8b_test.p8v_value" } shouldBe 1
|
||||
lines.count { it.startsWith("lea") } shouldBe 0
|
||||
}
|
||||
|
||||
test("amiga500 keeps the compact word right shift memory form") {
|
||||
val lines = generateAsm(
|
||||
tempRoot.resolve("test-m68k-amiga-word-rshift"),
|
||||
listOf(
|
||||
IRInstructions.memoryOp(Opcode.ASRM, IRDataType.WORD, IRMemory.direct("p8b_test.p8v_value")),
|
||||
IRInstructions.memoryOp(Opcode.LSRM, IRDataType.WORD, IRMemory.direct("p8b_test.p8v_other"))
|
||||
),
|
||||
Amiga500Target()
|
||||
)
|
||||
|
||||
lines.count { it.startsWith("asr.w p8b_test.p8v_value") } shouldBe 1
|
||||
lines.count { it.startsWith("lsr.w p8b_test.p8v_other") } shouldBe 1
|
||||
lines.count { it == "asr.w #1,d0" } shouldBe 0
|
||||
lines.count { it == "lsr.w #1,d0" } shouldBe 0
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,473 @@
|
||||
package prog8tests.codegen.m68k
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.engine.spec.tempdir
|
||||
import io.kotest.matchers.shouldBe
|
||||
import prog8.code.core.*
|
||||
import prog8.code.target.Qemu68kTarget
|
||||
import prog8.codegen.m68k.AsmGen
|
||||
import prog8.intermediate.*
|
||||
import prog8tests.helpers.DummyStringEncoder
|
||||
import prog8tests.helpers.ErrorReporterForTests
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.readText
|
||||
|
||||
// Vertical Slice Prototype: m68k backend lowering of FrameSlot memory references
|
||||
// and the link/unlk frame prologue/epilogue.
|
||||
class TestStackFrameEmission : FunSpec({
|
||||
|
||||
val tempRoot = tempdir().toPath()
|
||||
|
||||
fun generateAsmWithIncoming(
|
||||
frameSize: Int,
|
||||
instructions: List<IRInstruction>,
|
||||
incomingSize: Int = 0,
|
||||
subLabel: String = "test.start",
|
||||
frameVregSlots: Map<Int, Int> = emptyMap(),
|
||||
errors: ErrorReporterForTests? = null,
|
||||
params: List<IRSubroutine.IRParam>? = null
|
||||
): List<String> {
|
||||
val options = CompilationOptions.builder(Qemu68kTarget())
|
||||
.output(OutputType.RAW)
|
||||
.zeropage(ZeropageType.DONTUSE)
|
||||
.floats(false)
|
||||
.compilerVersion("test")
|
||||
.memtopAddress(0xffffu)
|
||||
.optimize(true)
|
||||
.build()
|
||||
val program = IRProgram("test", IRSymbolTable(), options, DummyStringEncoder)
|
||||
program.options.outputDir = tempRoot
|
||||
val chunk = IRCodeChunk(subLabel, null)
|
||||
chunk.instructions.addAll(instructions)
|
||||
// word-packed slots: give callers a way to declare each parameter's real type,
|
||||
// since the default derivation from the incoming area size assumes uniform slots
|
||||
val paramList = params
|
||||
?: if (incomingSize > 0)
|
||||
List(incomingSize / 2) { IRSubroutine.IRParam("p8v_p$it", DataType.UBYTE) }
|
||||
else emptyList()
|
||||
val sub = IRSubroutine(subLabel, paramList, emptyList(), Position.DUMMY, frameSize, incomingSize, frameVregSlots)
|
||||
sub.chunks.add(chunk)
|
||||
val block = IRBlock("test", false, IRBlock.Options(), Position.DUMMY)
|
||||
block.children.add(sub)
|
||||
program.blocks.add(block)
|
||||
|
||||
tempRoot.toFile().deleteRecursively()
|
||||
tempRoot.toFile().mkdirs()
|
||||
AsmGen(program, Qemu68kTarget(), errors).generate()
|
||||
val asmFile = tempRoot.resolve("test.asm")
|
||||
check(asmFile.exists()) { "Assembly file not written: $asmFile" }
|
||||
return asmFile.readText().lines().map { it.trim() }
|
||||
}
|
||||
|
||||
fun generateAsm(frameSize: Int, instructions: List<IRInstruction>): List<String> =
|
||||
generateAsmWithIncoming(frameSize, instructions, 0, "test.start")
|
||||
|
||||
// builds a subroutine with a chunk per extra chunk label, for jump-target scope tests
|
||||
fun sub(label: String, frameSize: Int, instructions: List<IRInstruction>, extraChunks: List<IRCodeChunkBase> = emptyList()): IRSubroutine {
|
||||
val first = IRCodeChunk(label, null)
|
||||
first.instructions.addAll(instructions)
|
||||
val sub = IRSubroutine(label, emptyList(), emptyList(), Position.DUMMY, frameSize, 0, emptyMap())
|
||||
sub.chunks.add(first)
|
||||
sub.chunks.addAll(extraChunks)
|
||||
return sub
|
||||
}
|
||||
|
||||
fun generateAsmForSubs(vararg subs: IRSubroutine): List<String> {
|
||||
val options = CompilationOptions.builder(Qemu68kTarget())
|
||||
.output(OutputType.RAW)
|
||||
.zeropage(ZeropageType.DONTUSE)
|
||||
.floats(false)
|
||||
.compilerVersion("test")
|
||||
.memtopAddress(0xffffu)
|
||||
.optimize(true)
|
||||
.build()
|
||||
val program = IRProgram("test", IRSymbolTable(), options, DummyStringEncoder)
|
||||
program.options.outputDir = tempRoot
|
||||
val block = IRBlock("test", false, IRBlock.Options(), Position.DUMMY)
|
||||
block.children.addAll(subs)
|
||||
program.blocks.add(block)
|
||||
program.linkChunks()
|
||||
tempRoot.toFile().deleteRecursively()
|
||||
tempRoot.toFile().mkdirs()
|
||||
AsmGen(program, Qemu68kTarget(), null).generate()
|
||||
return tempRoot.resolve("test.asm").readText().lines().map { it.trim() }
|
||||
}
|
||||
|
||||
test("frameless subroutine emits no frame setup") {
|
||||
val lines = generateAsm(0, listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 1, IRMemory.direct("some.var")),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.any { it.startsWith("link") } shouldBe false
|
||||
lines.any { it.startsWith("unlk") } shouldBe false
|
||||
}
|
||||
|
||||
test("framed subroutine emits link prologue and unlk epilogue") {
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-1)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.any { it == "link a5,#-16" } shouldBe true
|
||||
lines.count { it == "unlk a5" } shouldBe 1
|
||||
lines.any { "-1(a5)" in it } shouldBe true
|
||||
// epilogue must directly precede the subroutine's return
|
||||
lines[lines.indexOf("unlk a5") + 1] shouldBe "rts"
|
||||
}
|
||||
|
||||
test("frame slot direct references lower to displacements relative to a5") {
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.storeImmediate(IRDataType.WORD, 5, IRMemory.frameDirect(-4)),
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 1, IRMemory.frameDirect(-1)),
|
||||
IRInstructions.memoryOpImmediate(Opcode.ADDIM, IRDataType.BYTE, IRMemory.frameDirect(-1), 3),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.any { it.contains("-4(a5)") && it.startsWith("move.w") } shouldBe true
|
||||
lines.any { it.contains("-1(a5)") } shouldBe true
|
||||
lines.any { it.startsWith("addq.b") || it.startsWith("add.b") } shouldBe true
|
||||
}
|
||||
|
||||
test("indexed frame slot access uses lea of the slot base plus index") {
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.BYTE, 1, IRMemory.frameIndexed(-8, 3, IRDataType.WORD, scale = 1)),
|
||||
IRInstructions.storeZero(Opcode.STOREZX, IRDataType.BYTE, IRMemory.frameIndexed(-8, 3, IRDataType.WORD, scale = 1)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.any { it.startsWith("lea") && "-8(a5)" in it } shouldBe true
|
||||
lines.count { it.contains("(a0,d0.w)") } shouldBe 2
|
||||
}
|
||||
|
||||
test("frame displacement combines slot offset and reference displacement") {
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-8, 4)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
// -8 slot + 4 displacement = -4(a5), a single combined displacement
|
||||
lines.any { it.contains("-4(a5)") } shouldBe true
|
||||
lines.any { it.contains("-8(a5)+4") } shouldBe false
|
||||
}
|
||||
|
||||
test("incoming-only frame still emits link and unlk") {
|
||||
val lines = generateAsmWithIncoming(0, listOf(IRInstructions.simple(Opcode.RETURN)), incomingSize = 12)
|
||||
lines.any { it == "link a5,#0" } shouldBe true
|
||||
lines.any { it == "unlk a5" } shouldBe true
|
||||
}
|
||||
|
||||
test("stack arguments are pushed word-aligned into their slots and popped by the caller") {
|
||||
val calls = listOf(
|
||||
IRInstructions.call(CallSite(
|
||||
target = CallTarget.Direct(CodeReference.Label("test.sub")),
|
||||
arguments = listOf(
|
||||
Calls.argument(1, IRDataType.BYTE, CallLocation.FrameSlot(16)),
|
||||
Calls.argument(2, IRDataType.WORD, CallLocation.FrameSlot(12)),
|
||||
Calls.argument(3, IRDataType.LONG, CallLocation.FrameSlot(8))
|
||||
)
|
||||
)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
)
|
||||
val lines = generateAsmWithIncoming(0, calls, incomingSize = 12, subLabel = "test.caller")
|
||||
// word-packed pushes: byte arguments use the single move.b predecrement instruction
|
||||
// (the byte lands at the slot's base byte), word and long push as themselves
|
||||
lines.count { it.startsWith("move.b ") && it.contains(",-(sp)") && it.contains("pushed as a word") } shouldBe 1
|
||||
lines.count { it.startsWith("move.w ") && it.contains(",-(sp)") } shouldBe 1
|
||||
lines.count { it.startsWith("move.l ") && it.contains(",-(sp)") } shouldBe 1
|
||||
lines.none { it == "subq.l #2,sp" } shouldBe true
|
||||
lines.any { it.startsWith("bsr") } shouldBe true
|
||||
lines.any { it == "addq.l #8,sp" } shouldBe true // 2+2+4 bytes
|
||||
// small argument areas use the short addq form
|
||||
val smallCalls = listOf(
|
||||
IRInstructions.call(CallSite(
|
||||
target = CallTarget.Direct(CodeReference.Label("test.sub")),
|
||||
arguments = listOf(Calls.argument(1, IRDataType.BYTE, CallLocation.FrameSlot(8)))
|
||||
)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
)
|
||||
val smallLines = generateAsmWithIncoming(0, smallCalls, incomingSize = 4, subLabel = "test.caller")
|
||||
smallLines.any { it == "addq.l #2,sp" } shouldBe true
|
||||
}
|
||||
|
||||
test("status-flag results use CCR-preserving cleanup") {
|
||||
val calls = listOf(
|
||||
IRInstructions.call(CallSite(
|
||||
target = CallTarget.Direct(CodeReference.Label("test.sub")),
|
||||
arguments = listOf(Calls.argument(1, IRDataType.BYTE, CallLocation.FrameSlot(8))),
|
||||
results = listOf(CallResult(null, CallLocation.StatusFlag(Statusflag.Pz)))
|
||||
)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
)
|
||||
val lines = generateAsmWithIncoming(0, calls, incomingSize = 4, subLabel = "test.caller")
|
||||
// a single byte argument (2-byte slot) would normally use addq.l #2,sp, but that
|
||||
// clobbers CCR, so status-flag returns must use lea instead.
|
||||
lines.any { it == "lea 2(sp),sp" } shouldBe true
|
||||
lines.none { it == "addq.l #2,sp" } shouldBe true
|
||||
}
|
||||
|
||||
test("float stack arguments travel through the FPU accumulator") {
|
||||
val calls = listOf(
|
||||
IRInstructions.call(CallSite(
|
||||
target = CallTarget.Direct(CodeReference.Label("test.sub")),
|
||||
arguments = listOf(Calls.argument(1, IRDataType.FLOAT, CallLocation.FrameSlot(8)))
|
||||
)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
)
|
||||
val lines = generateAsmWithIncoming(0, calls, incomingSize = 4, subLabel = "test.caller")
|
||||
// the 68881 has no absolute-long addressing mode: the value must pass through fp0,
|
||||
// and the store itself reserves the slot via predecrement
|
||||
lines.any { it.startsWith("fmove.s p8_fregfile+0,fp0") } shouldBe true
|
||||
lines.any { it == "fmove.s fp0,-(sp)" } shouldBe true
|
||||
lines.none { it == "subq.l #4,sp" } shouldBe true
|
||||
}
|
||||
test("per-activation virtual registers are addressed through the frame") {
|
||||
// a re-entrant subroutine keeps its virtual registers in its own frame (slice 3), so they
|
||||
// are addressed relative to a5 instead of the flat program-static register file
|
||||
val lines = generateAsmWithIncoming(8, listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.WORD, 5, IRMemory.direct("main.var")),
|
||||
IRInstructions.returnRegister(IRDataType.WORD, 5)
|
||||
), frameVregSlots = mapOf(5 to -2, 6 to -4))
|
||||
lines.any { it.startsWith("link ") } shouldBe true
|
||||
lines.any { it.startsWith("move") && it.contains("-2(a5)") } shouldBe true
|
||||
lines.none { it.startsWith("move") && it.contains("p8_regfile") } shouldBe true
|
||||
}
|
||||
|
||||
test("virtual registers without a frame slot keep using the static register file") {
|
||||
val lines = generateAsmWithIncoming(8, listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.WORD, 5, IRMemory.direct("main.var")),
|
||||
IRInstructions.returnRegister(IRDataType.WORD, 5)
|
||||
), frameVregSlots = mapOf(6 to -4))
|
||||
// register 5 has no frame slot, so it stays in the static register file
|
||||
lines.any { it.startsWith("move") && it.contains("p8_regfile") } shouldBe true
|
||||
lines.any { it.startsWith("move") && it.contains("(a5)") && it.contains("p8_regfile") } shouldBe false
|
||||
}
|
||||
|
||||
test("a byte-sized frame-resident register is addressed with its byte offset") {
|
||||
val lines = generateAsmWithIncoming(4, listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 5, IRMemory.direct("main.var")),
|
||||
IRInstructions.returnRegister(IRDataType.BYTE, 5)
|
||||
), frameVregSlots = mapOf(5 to -4))
|
||||
lines.any { it.startsWith("move.b") && it.contains("-4(a5)") } shouldBe true
|
||||
}
|
||||
|
||||
test("sub-byte extraction of a frame-resident register uses a valid a5 displacement") {
|
||||
// MSIGB on a long goes through regAddrByte, which must not emit "$(-4)(a5)"
|
||||
val lines = generateAsmWithIncoming(4, listOf(
|
||||
IRInstructions.binary(Opcode.MSIGB, IRDataType.LONG, 6, 5),
|
||||
IRInstructions.returnRegister(IRDataType.BYTE, 6)
|
||||
), frameVregSlots = mapOf(5 to -4))
|
||||
lines.any { it.startsWith("move.b") && it.contains("-4(a5)") } shouldBe true
|
||||
lines.none { it.contains("\$(-4)(a5)") } shouldBe true
|
||||
}
|
||||
|
||||
test("framed subroutine emits a stack frame listing comment") {
|
||||
val lines = generateAsmWithIncoming(24, listOf(
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-1)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
), incomingSize = 8)
|
||||
lines.any { it == "; stack frame: 24 bytes of locals, 8 bytes of arguments" } shouldBe true
|
||||
}
|
||||
|
||||
test("incoming-only frame emits a stack frame listing comment with zero locals") {
|
||||
val lines = generateAsmWithIncoming(0, listOf(IRInstructions.simple(Opcode.RETURN)), incomingSize = 8)
|
||||
lines.any { it == "; stack frame: 0 bytes of locals, 8 bytes of arguments" } shouldBe true
|
||||
}
|
||||
|
||||
test("frameless subroutine emits no stack frame listing comment") {
|
||||
val lines = generateAsm(0, listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 1, IRMemory.direct("some.var")),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.none { it.startsWith("; stack frame:") } shouldBe true
|
||||
}
|
||||
|
||||
test("subroutine boundary markers are emitted exactly once per subroutine") {
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-1)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.count { it == "; ---- Subroutine: test.start ----" } shouldBe 1
|
||||
lines.count { it == "; End of subroutine: test.start" } shouldBe 1
|
||||
}
|
||||
|
||||
test("large frames report an informational message") {
|
||||
val errors = ErrorReporterForTests()
|
||||
generateAsmWithIncoming(900, listOf(
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-1)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
), subLabel = "p8b_main.p8s_worker", errors = errors)
|
||||
errors.infos.any { it.contains("subroutine 'main.worker' uses a 900 byte stack frame; deep recursion multiplies this per call") } shouldBe true
|
||||
}
|
||||
|
||||
test("small frames produce no informational message") {
|
||||
val errors = ErrorReporterForTests()
|
||||
generateAsmWithIncoming(24, listOf(
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-1)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
), incomingSize = 8, errors = errors)
|
||||
errors.infos shouldBe emptyList()
|
||||
}
|
||||
|
||||
test("a long run of longword zero-clears lowers to a dbra loop") {
|
||||
val stores = (0 until 17).map { i ->
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.LONG, IRMemory.frameDirect(-68 + i * 4))
|
||||
}
|
||||
val lines = generateAsm(68, stores + IRInstructions.simple(Opcode.RETURN))
|
||||
lines.any { it == "moveq #0,d0" } shouldBe true
|
||||
lines.any { it == "lea -68(a5),a0" } shouldBe true
|
||||
lines.any { it == "moveq #16,d1" } shouldBe true
|
||||
lines.any { it == "move.l d0,(a0)+" } shouldBe true
|
||||
lines.count { it.startsWith("dbra d1,zeroloop_") } shouldBe 1
|
||||
lines.none { it.startsWith("clr.l") } shouldBe true
|
||||
}
|
||||
|
||||
test("a short run of longword zero-clears keeps straight stores") {
|
||||
val stores = (0 until 15).map { i ->
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.LONG, IRMemory.frameDirect(-60 + i * 4))
|
||||
}
|
||||
val lines = generateAsm(60, stores + IRInstructions.simple(Opcode.RETURN))
|
||||
lines.count { it.startsWith("clr.l") } shouldBe 15
|
||||
lines.none { it.startsWith("dbra") } shouldBe true
|
||||
}
|
||||
|
||||
test("an interleaved store splits the zero-clear run") {
|
||||
val first = (0 until 8).map { i ->
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.LONG, IRMemory.frameDirect(-68 + i * 4))
|
||||
}
|
||||
val second = (0 until 8).map { i ->
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.LONG, IRMemory.frameDirect(-32 + i * 4))
|
||||
}
|
||||
val instructions = first +
|
||||
IRInstructions.storeImmediate(IRDataType.BYTE, 0, IRMemory.frameDirect(-36)) +
|
||||
second +
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
val lines = generateAsm(68, instructions)
|
||||
// two runs of 8 stay below the loop threshold
|
||||
lines.none { it.startsWith("dbra") } shouldBe true
|
||||
lines.count { it.startsWith("clr.l") } shouldBe 16
|
||||
lines.any { it == "clr.b -36(a5)" } shouldBe true
|
||||
}
|
||||
|
||||
test("dispatch jump in a framed subroutine pops the frame before jumping") {
|
||||
// an on..goto leaves the subroutine for good, so the target's rts must find our caller's
|
||||
// return address: leaving our frame behind would make it pop garbage
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.jumpIndirect(5),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
val unlkIdx = lines.indexOf("unlk a5")
|
||||
unlkIdx shouldBe lines.indexOfFirst { it.startsWith("jmp") } - 1
|
||||
}
|
||||
|
||||
test("dispatch jump in a frameless subroutine emits no frame teardown") {
|
||||
val lines = generateAsm(0, listOf(
|
||||
IRInstructions.jumpIndirect(5),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.none { it.startsWith("unlk") } shouldBe true
|
||||
}
|
||||
|
||||
test("jump to an absolute address pops the frame before jumping") {
|
||||
// a fixed address can't be resolved to a chunk of this subroutine, so it must be assumed
|
||||
// to leave for good and the frame must be gone before the target's eventual rts
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.jump(codeAddress(0x1234u)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
val jmpIdx = lines.indexOfFirst { it.startsWith("jmp") }
|
||||
(jmpIdx > 0) shouldBe true
|
||||
lines[jmpIdx - 1] shouldBe "unlk a5"
|
||||
}
|
||||
|
||||
test("jump to an absolute address in a frameless subroutine emits no frame teardown") {
|
||||
val lines = generateAsm(0, listOf(
|
||||
IRInstructions.jump(codeAddress(0x1234u)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.none { it.startsWith("unlk") } shouldBe true
|
||||
}
|
||||
|
||||
test("conditional branch to an absolute address tears down only the taken path") {
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.branch(Opcode.BSTEQ, codeAddress(0x1234u)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
val branchIdx = lines.indexOfFirst { it.startsWith("bne") }
|
||||
val jmpIdx = lines.indexOfFirst { it.startsWith("jmp") && "$1234" in it }
|
||||
(branchIdx >= 0) shouldBe true
|
||||
(jmpIdx > branchIdx) shouldBe true
|
||||
lines[jmpIdx - 1] shouldBe "unlk a5"
|
||||
lines.count { it == "unlk a5" } shouldBe 2
|
||||
}
|
||||
|
||||
test("conditional branch to an external non-entry label tears down only the taken path") {
|
||||
val outsideInner = IRCodeChunk("test.outside.inner", null).also {
|
||||
it.instructions.add(IRInstructions.simple(Opcode.RETURN))
|
||||
}
|
||||
val lines = generateAsmForSubs(
|
||||
sub("test.start", frameSize = 16, instructions = listOf(
|
||||
IRInstructions.branch(Opcode.BSTEQ, CodeReference.Label("test.outside.inner")),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
)),
|
||||
sub("test.outside", frameSize = 0, instructions = listOf(IRInstructions.simple(Opcode.RETURN)),
|
||||
extraChunks = listOf(outsideInner))
|
||||
)
|
||||
val jmpIdx = lines.indexOfFirst { it.startsWith("jmp") && "outside.inner" in it }
|
||||
(jmpIdx > 0) shouldBe true
|
||||
lines[jmpIdx - 1] shouldBe "unlk a5"
|
||||
lines.any { it.startsWith("bne") } shouldBe true
|
||||
}
|
||||
|
||||
test("signed comparison branch to an absolute address tears down only the taken path") {
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.branchImmediate(Opcode.BGTS, IRDataType.WORD, 1, 1, codeAddress(0x1234u)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.any { it.startsWith("cmpi.w") } shouldBe true
|
||||
val branchIdx = lines.indexOfFirst { it.startsWith("ble") }
|
||||
val jmpIdx = lines.indexOfFirst { it.startsWith("jmp") && "$1234" in it }
|
||||
(branchIdx >= 0) shouldBe true
|
||||
(jmpIdx > branchIdx) shouldBe true
|
||||
lines[jmpIdx - 1] shouldBe "unlk a5"
|
||||
lines.count { it == "unlk a5" } shouldBe 2
|
||||
}
|
||||
|
||||
test("jump to a label in another subroutine pops the frame before jumping") {
|
||||
// jumping out of the subroutine is a documented way to end it, so our frame must not be
|
||||
// left behind for whatever runs next (and its eventual rts) to trip over
|
||||
val lines = generateAsmForSubs(
|
||||
sub("test.start", frameSize = 16, instructions = listOf(
|
||||
IRInstructions.jump(CodeReference.Label("test.outside")),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
)),
|
||||
sub("test.outside", frameSize = 0, instructions = listOf(IRInstructions.simple(Opcode.RETURN)))
|
||||
)
|
||||
val braIdx = lines.indexOfFirst { it.startsWith("bra") && "test.outside" in it }
|
||||
(braIdx > 0) shouldBe true
|
||||
lines[braIdx - 1] shouldBe "unlk a5"
|
||||
}
|
||||
|
||||
test("jump to a label in a chunk of the same subroutine keeps the frame") {
|
||||
val lines = generateAsmForSubs(
|
||||
sub("test.start", frameSize = 16, instructions = listOf(
|
||||
IRInstructions.jump(CodeReference.Label("test.start.second")),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
), extraChunks = listOf(
|
||||
IRCodeChunk("test.start.second", null).also {
|
||||
it.instructions.add(IRInstructions.storeImmediate(IRDataType.BYTE, 0, IRMemory.direct("test.var")))
|
||||
}
|
||||
))
|
||||
)
|
||||
lines.count { it == "unlk a5" } shouldBe 1 // only the return epilogue
|
||||
lines[lines.indexOf("unlk a5") + 1] shouldBe "rts"
|
||||
lines.any { it.startsWith("bra") && "test.start.second" in it } shouldBe true
|
||||
}
|
||||
|
||||
test("jump to a label inside a loop body of the same subroutine keeps the frame") {
|
||||
val loopBodyChunk = IRCodeChunk("test.start.loopbody", null)
|
||||
loopBodyChunk.instructions.add(IRInstructions.jump(CodeReference.Label("test.start.loopbody")))
|
||||
val lines = generateAsmForSubs(
|
||||
sub("test.start", frameSize = 16, instructions = listOf(
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
), extraChunks = listOf(IRLoopChunk("test.start.loop", 3, mutableListOf(loopBodyChunk))))
|
||||
)
|
||||
lines.count { it == "unlk a5" } shouldBe 1 // the jump to the loop body label must not tear the frame down
|
||||
}
|
||||
|
||||
})
|
||||
@@ -47,6 +47,7 @@ dependencies {
|
||||
implementation(project(":intermediate"))
|
||||
implementation("com.michael-bull.kotlin-result:kotlin-result-jvm:2.3.1")
|
||||
implementation("com.github.ajalt.clikt:clikt:5.1.0")
|
||||
implementation("com.github.ajalt.mordant:mordant:3.1.0")
|
||||
|
||||
// Test dependencies - Kotest BOM is provided by root build.gradle.kts
|
||||
testImplementation("io.kotest:kotest-runner-junit5")
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
package prog8.codegen.new6502
|
||||
|
||||
import prog8.code.StStructInstanceBlockName
|
||||
import prog8.code.core.*
|
||||
import prog8.codegen.new6502.optimization.PeepholeOptimizer
|
||||
import prog8.intermediate.*
|
||||
@@ -116,7 +117,7 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa
|
||||
emitBssSection()
|
||||
|
||||
val options = program.options
|
||||
val asmFile = options.outputDir.resolve("${program.name}.asm")
|
||||
val asmFile = OutputFiles.of(options, program.name).asm()
|
||||
try {
|
||||
asmFile.toFile().writeText(output.toString())
|
||||
} catch (e: Exception) {
|
||||
@@ -227,16 +228,32 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa
|
||||
name.startsWith('>') -> ">" to name.drop(1)
|
||||
else -> "" to name
|
||||
}
|
||||
return "$prefix${fixNameSymbols(rest)}"
|
||||
return when (val node = program.st.lookup(rest)) {
|
||||
is IRStStructInstance -> "$prefix${structInstanceRef(node, rest)}"
|
||||
else -> "$prefix${fixNameSymbols(rest)}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun structInstanceRef(node: IRStStructInstance, name: String): String {
|
||||
// Uninitialized instances (empty initializer, or unions) are emitted into
|
||||
// the prog8_struct_instances_bss block, but the IR bakes in the plain
|
||||
// prog8_struct_instances prefix into every reference. Rewrite those to
|
||||
// match the emitted block label.
|
||||
val def = program.st.lookup(node.structName) as IRStStructDef
|
||||
val fixed = fixNameSymbols(name)
|
||||
return if (node.values.isEmpty() || def.isUnion) {
|
||||
"${StStructInstanceBlockName}_bss.${fixed.substringAfter('.')}"
|
||||
} else {
|
||||
fixed
|
||||
}
|
||||
}
|
||||
|
||||
fun resolveSymbolRef(name: String): String {
|
||||
val node = program.st.lookup(name)
|
||||
return when (node) {
|
||||
return when (val node = program.st.lookup(name)) {
|
||||
is IRStConstant -> constLabel(name)
|
||||
is IRStStaticVariable, is IRStMemVar,
|
||||
is IRStMemorySlab,
|
||||
is IRStStructInstance -> fixNameSymbols(name)
|
||||
is IRStMemorySlab -> fixNameSymbols(name)
|
||||
is IRStStructInstance -> structInstanceRef(node, name)
|
||||
else -> name // external/library symbol, keep as-is
|
||||
}
|
||||
}
|
||||
@@ -352,6 +369,7 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa
|
||||
val options = program.options
|
||||
emitRaw("; Program: ${program.name}")
|
||||
emitRaw("; Generated by prog8-newgen (new 6502 codegen)")
|
||||
emitRaw("; prog8 compiler version: ${options.compilerVersion}")
|
||||
emitRaw("; Target CPU: ${cpu.name}")
|
||||
emitRaw("; Output: ${options.output.name} Launcher: ${options.launcher.name}")
|
||||
emitRaw("")
|
||||
@@ -936,34 +954,58 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa
|
||||
if (structs.isEmpty()) return
|
||||
emitRaw("; struct type definitions")
|
||||
for (sd in structs) {
|
||||
val paramFields = sd.fields.filter { it.arraySize == null }
|
||||
val structargs = paramFields.indices.joinToString(",") { "f$it" }
|
||||
emitRaw("${fixNameSymbols(sd.name)} .struct $structargs")
|
||||
var paramIdx = 0
|
||||
for (field in sd.fields) {
|
||||
val type = when {
|
||||
field.type.isBool || field.type.isUnsignedByte -> ".byte"
|
||||
field.type.isSignedByte -> ".char"
|
||||
field.type.isUnsignedWord || field.type.isPointer -> ".word"
|
||||
field.type.isSignedWord -> ".sint"
|
||||
field.type.isLong -> ".dint"
|
||||
field.type.isFloat -> ".byte"
|
||||
else -> ".byte"
|
||||
if(sd.isUnion) {
|
||||
// NB: .union takes no constructor parameters, unlike .struct
|
||||
emitRaw("${fixNameSymbols(sd.name)} .union")
|
||||
emitStructDefFields(sd) { "?" }
|
||||
emitRaw(" .endunion")
|
||||
emitRaw("${fixNameSymbols(sd.name)}_size=${sd.size}")
|
||||
} else {
|
||||
val paramFields = sd.fields.filter { it.arraySize == null }
|
||||
val structargs = paramFields.indices.joinToString(",") { "f$it" }
|
||||
emitRaw("${fixNameSymbols(sd.name)} .struct $structargs")
|
||||
var paramIdx = 0
|
||||
emitStructDefFields(sd) { "\\f${paramIdx++}" }
|
||||
emitRaw(" .endstruct")
|
||||
emitRaw("${fixNameSymbols(sd.name)}_size=${sd.size}")
|
||||
}
|
||||
emitRaw("")
|
||||
}
|
||||
emitRaw("")
|
||||
}
|
||||
|
||||
private fun emitStructDefFields(sd: IRStStructDef, scalarValue: () -> String) {
|
||||
for (field in sd.fields) {
|
||||
val fieldName = "p8v_${field.name}"
|
||||
val arrSize = field.arraySize
|
||||
val effectiveDt = if(arrSize != null) field.type.elementType() else field.type
|
||||
if (effectiveDt.isFloat) {
|
||||
val floatSize = target.FLOAT_MEM_SIZE.toInt()
|
||||
if (arrSize != null) {
|
||||
emitRaw(" $fieldName .fill ${floatSize*arrSize}, ?")
|
||||
} else {
|
||||
emitRaw(" $fieldName .fill $floatSize, ${scalarValue()}")
|
||||
}
|
||||
val fieldName = "p8v_${field.name}"
|
||||
val arrSize = field.arraySize
|
||||
} else {
|
||||
val type = structDefFieldAsmType(effectiveDt)
|
||||
if (arrSize != null) {
|
||||
val anon = List(arrSize) { "?" }.joinToString(",")
|
||||
emitRaw(" $fieldName $type $anon")
|
||||
} else {
|
||||
emitRaw(" $fieldName $type \\f$paramIdx")
|
||||
paramIdx++
|
||||
emitRaw(" $fieldName $type ${scalarValue()}")
|
||||
}
|
||||
}
|
||||
emitRaw(" .endstruct")
|
||||
emitRaw("")
|
||||
}
|
||||
emitRaw("")
|
||||
}
|
||||
|
||||
private fun structDefFieldAsmType(type: DataType): String = when {
|
||||
type.isBool || type.isUnsignedByte -> ".byte"
|
||||
type.isSignedByte -> ".char"
|
||||
type.isUnsignedWord || type.isPointer -> ".word"
|
||||
type.isSignedWord -> ".sint"
|
||||
type.isLong -> ".dint"
|
||||
type.isFloat -> ".byte"
|
||||
else -> ".byte"
|
||||
}
|
||||
|
||||
private fun emitFloatConstants() {
|
||||
@@ -1028,7 +1070,10 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa
|
||||
|
||||
// struct instances without init values -> BSS (zeroed at startup)
|
||||
val allInstances = program.st.allStructInstances().toList()
|
||||
val (instancesNoInit, instancesWithInit) = allInstances.partition { it.values.isEmpty() }
|
||||
val (instancesNoInit, instancesWithInit) = allInstances.partition {
|
||||
val def = program.st.lookup(it.structName) as IRStStructDef
|
||||
it.values.isEmpty() || def.isUnion
|
||||
}
|
||||
if (instancesNoInit.isNotEmpty()) {
|
||||
emitRaw(" .section BSS")
|
||||
emitRaw("prog8_struct_instances_bss .block")
|
||||
@@ -1053,13 +1098,17 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa
|
||||
for (fieldValue in si.values) {
|
||||
when (val fv = fieldValue.value) {
|
||||
is IRStSymbolicReference.Numeric -> {
|
||||
val v = fv.value.toInt()
|
||||
val size = when (fieldValue.dt) {
|
||||
BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.BOOL -> ".byte"
|
||||
BaseDataType.UWORD, BaseDataType.WORD -> ".word"
|
||||
else -> ".word"
|
||||
// FLOAT is 5 MFLPT bytes (6502), LONG is 4 bytes; both would
|
||||
// be truncated if emitted as a 2-byte .word, corrupting the
|
||||
// instance layout.
|
||||
when (fieldValue.dt) {
|
||||
BaseDataType.FLOAT -> emitLine(" .byte [${target.getFloatAsmBytes(fv.value)}] ; float ${fv.value}")
|
||||
BaseDataType.LONG -> emitLine(" .dint ${fv.value.toLong()}")
|
||||
BaseDataType.BYTE -> emitLine(" .char ${fv.value.toInt()}")
|
||||
BaseDataType.UBYTE, BaseDataType.BOOL -> emitLine(" .byte ${fv.value.toInt()}")
|
||||
BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.STR, BaseDataType.POINTER -> emitLine(" .word ${fv.value.toInt()}")
|
||||
else -> error("unsupported datatype for struct instance field value: ${fieldValue.dt}")
|
||||
}
|
||||
emitLine(" $size $v")
|
||||
}
|
||||
is IRStSymbolicReference.Symbol -> {
|
||||
emitLine(" .word ${fixNameSymbols(fv.name)}")
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
package prog8.codegen.new6502
|
||||
|
||||
import prog8.code.core.AssemblyError
|
||||
import prog8.code.cpu6502.byteShiftAddExpansion
|
||||
import prog8.code.cpu6502.wordShiftAddExpansion
|
||||
import prog8.intermediate.*
|
||||
|
||||
internal fun AsmGen.translateArithmetic(insn: IRInstruction) {
|
||||
@@ -722,20 +724,42 @@ internal fun AsmGen.mulImmediate(dstReg: Int, value: Int, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("ldy #${value and 0xff}")
|
||||
emitLine("jsr prog8_math.multiply_bytes")
|
||||
// small constant: expand inline as a Horner shift-add instead of calling the ~130
|
||||
// cycle multiply_bytes. Shared sequence with the classic 6502 backend.
|
||||
val expansion = byteShiftAddExpansion(value)
|
||||
if (expansion != null) {
|
||||
expansion.forEach { emitLine(it) }
|
||||
} else {
|
||||
emitLine("ldy #${value and 0xff}")
|
||||
emitLine("jsr prog8_math.multiply_bytes")
|
||||
}
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("lda #<${value and 0xffff}")
|
||||
emitLine("sta prog8_math.multiply_words.multiplier")
|
||||
emitLine("lda #>${value and 0xffff}")
|
||||
emitLine("sta prog8_math.multiply_words.multiplier+1")
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("ldy ${regAddrHi(dstReg)}")
|
||||
emitLine("jsr prog8_math.multiply_words")
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
emitLine("sty ${regAddrHi(dstReg)}")
|
||||
val expansion = wordShiftAddExpansion(value)
|
||||
if (expansion != null) {
|
||||
// set up the shared sequence's contract: A = r.lo, W1 = r, W2 = x
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("sta P8ZP_SCRATCH_W2")
|
||||
emitLine("sta P8ZP_SCRATCH_W1")
|
||||
emitLine("lda ${regAddrHi(dstReg)}")
|
||||
emitLine("sta P8ZP_SCRATCH_W2+1")
|
||||
emitLine("sta P8ZP_SCRATCH_W1+1")
|
||||
emitLine("lda P8ZP_SCRATCH_W1")
|
||||
expansion.forEach { emitLine(it) }
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
emitLine("sty ${regAddrHi(dstReg)}")
|
||||
} else {
|
||||
emitLine("lda #<${value and 0xffff}")
|
||||
emitLine("sta prog8_math.multiply_words.multiplier")
|
||||
emitLine("lda #>${value and 0xffff}")
|
||||
emitLine("sta prog8_math.multiply_words.multiplier+1")
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("ldy ${regAddrHi(dstReg)}")
|
||||
emitLine("jsr prog8_math.multiply_words")
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
emitLine("sty ${regAddrHi(dstReg)}")
|
||||
}
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
// multiplicand (dstReg) -> cx16.r12/r13, immediate multiplier -> cx16.r14/r15
|
||||
@@ -1535,6 +1559,41 @@ internal fun AsmGen.translateFloatArithmetic(insn: IRInstruction) {
|
||||
emitLine("jsr floats.MOVMF")
|
||||
}
|
||||
|
||||
Opcode.ADDIM -> {
|
||||
val value = insn.requireImmediateFloat()
|
||||
val constLabel = getFloatConstLabel(value)
|
||||
val target = resolveAddress(insn.requireMemory())
|
||||
emitLine("lda #<$target")
|
||||
emitLine("ldy #>$target")
|
||||
emitLine("jsr floats.MOVFM")
|
||||
emitLine("lda #<$constLabel")
|
||||
emitLine("ldy #>$constLabel")
|
||||
emitLine("jsr floats.FADD")
|
||||
emitLine("ldx #<$target")
|
||||
emitLine("ldy #>$target")
|
||||
emitLine("jsr floats.MOVMF")
|
||||
}
|
||||
|
||||
Opcode.SUBIM -> {
|
||||
// NOTE: FSUB does FAC1 = memory - FAC1, so we must use push/pop + FSUBT instead
|
||||
val value = insn.requireImmediateFloat()
|
||||
val constLabel = getFloatConstLabel(value)
|
||||
val target = resolveAddress(insn.requireMemory())
|
||||
emitLine("lda #<$target")
|
||||
emitLine("ldy #>$target")
|
||||
emitLine("jsr floats.MOVFM")
|
||||
emitLine("jsr floats.pushFAC1")
|
||||
emitLine("lda #<$constLabel")
|
||||
emitLine("ldy #>$constLabel")
|
||||
emitLine("jsr floats.MOVFM")
|
||||
emitLine("sec")
|
||||
emitLine("jsr floats.popFAC")
|
||||
emitLine("jsr floats.FSUBT")
|
||||
emitLine("ldx #<$target")
|
||||
emitLine("ldy #>$target")
|
||||
emitLine("jsr floats.MOVMF")
|
||||
}
|
||||
|
||||
Opcode.INCM -> {
|
||||
val target = resolveAddress(insn.requireMemory())
|
||||
emitLine("lda #<$target")
|
||||
|
||||
@@ -494,7 +494,7 @@ internal val CallResult.hardwareSlot: CallingConventionSlot?
|
||||
/**
|
||||
* The slot-based arguments, in the order they must be loaded to avoid register clobbering.
|
||||
* Order matches the old 6502 codegen: paired regs first (AX/AY/XY), then single regs (Y, X, A),
|
||||
* then float regs, then status flags. This ensures that loading a paired register
|
||||
* then float regs, then status flags. That way, loading a paired register
|
||||
* (which clobbers two hardware regs) doesn't overwrite a value needed for a later argument.
|
||||
* Within each group the original argument order is kept.
|
||||
*/
|
||||
@@ -554,8 +554,8 @@ private fun AsmGen.translateCall(fnLabel: String, site: CallSite) {
|
||||
// the IR generates LOADHR for them.
|
||||
// In single-return expression context, process 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.
|
||||
// Multiple status flag returns in one multi-assign are protected by the IR:
|
||||
// it wraps each flag's extraction in PUSHST/POPST (php/plp) so they don't clobber each other.
|
||||
val isMultiReturn = site.results.size > 1
|
||||
for (ret in site.results) {
|
||||
if (ret.location is CallLocation.StatusFlag)
|
||||
@@ -670,6 +670,8 @@ private fun AsmGen.translateArgument(arg: CallArgument, argIndex: Int = -1, fnLa
|
||||
// Syscall argument - value is already in the register file,
|
||||
// the syscall handler reads it from there.
|
||||
}
|
||||
is CallLocation.FrameSlot ->
|
||||
error("stack frame argument slots are only supported on m68k targets: $location")
|
||||
is CallLocation.HardwareRegister -> throw IllegalStateException("slot already handled")
|
||||
}
|
||||
}
|
||||
@@ -797,8 +799,7 @@ private fun CallSite.argumentRegister(index: Int, what: String): Int =
|
||||
|
||||
private fun AsmGen.translateSyscall(insn: IRInstruction) {
|
||||
val site = insn.requireCallSite()
|
||||
val syscallNum = (site.target as CallTarget.SystemCall).number
|
||||
when (syscallNum) {
|
||||
when (val syscallNum = (site.target as CallTarget.SystemCall).number) {
|
||||
IMSyscall.CLAMP_UBYTE.number -> translateSyscallClampUbyte(site)
|
||||
IMSyscall.CLAMP_BYTE.number -> translateSyscallClampByte(site)
|
||||
IMSyscall.CLAMP_UWORD.number -> translateSyscallClampUword(site)
|
||||
@@ -1428,8 +1429,7 @@ private fun AsmGen.translateFloatSign(insn: IRInstruction) {
|
||||
}
|
||||
|
||||
private fun AsmGen.jsrfarRoutine(): String {
|
||||
val targetName = program.options.compTarget.name
|
||||
return when (targetName) {
|
||||
return when (val targetName = program.options.compTarget.name) {
|
||||
"cx16" -> "cx16.JSRFAR"
|
||||
"c64" -> "c64.x16jsrfar"
|
||||
"c128" -> "c128.x16jsrfar"
|
||||
|
||||
@@ -1104,8 +1104,9 @@ internal fun AsmGen.copyRegisterLong(dstReg: Int, srcReg: Int) {
|
||||
emitLine("bpl -")
|
||||
}
|
||||
|
||||
/** the address of a [base] (+ [displacement], for a Symbol base only - matches the previous behavior
|
||||
* of ignoring any offset that came together with a fixed numeric address) */
|
||||
/** the address of a [base], plus [displacement]. The displacement is only added
|
||||
* for a Symbol base; this matches the previous behavior of ignoring any offset
|
||||
* that came together with a fixed numeric address. */
|
||||
internal fun AsmGen.resolveAddress(base: AddressBase, displacement: Int = 0): String {
|
||||
return when (base) {
|
||||
is AddressBase.Symbol -> {
|
||||
@@ -1113,6 +1114,7 @@ internal fun AsmGen.resolveAddress(base: AddressBase, displacement: Int = 0): St
|
||||
if (displacement != 0) "$resolved+$displacement" else resolved
|
||||
}
|
||||
is AddressBase.Absolute -> base.address.value.toHex()
|
||||
is AddressBase.FrameSlot -> error("stack frame slots are only supported on m68k targets, got $base")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ fun main(args: Array<String>) {
|
||||
val reader = IRFileReader()
|
||||
val source = Path(inputFile).readText()
|
||||
val program = reader.read(source)
|
||||
if(asmListfile == true)
|
||||
if(asmListfile)
|
||||
program.options.asmListfile = true
|
||||
val target = program.options.compTarget
|
||||
|
||||
|
||||
@@ -41,6 +41,6 @@ class New6502CodeGenerator(val retainSSA: Boolean,
|
||||
if(!success)
|
||||
return null
|
||||
|
||||
return AssemblyProgram6502(irProgram.name, irProgram.options.outputDir, irProgram.options.compTarget)
|
||||
return AssemblyProgram6502(irProgram.name, irProgram.options, irProgram.options.compTarget)
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ class ZeropageAllocator(
|
||||
if (name in zeropage.allocatedVariables)
|
||||
return true
|
||||
val v = program.st.lookup(name)
|
||||
return if (v is IRStMemVar) v.address <= 255u else false
|
||||
return v is IRStMemVar && v.address <= 255u
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,9 +100,9 @@ class ZeropageAllocator(
|
||||
?: (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
|
||||
val weight = when (insn.type) {
|
||||
IRDataType.WORD -> 2
|
||||
IRDataType.LONG -> 3
|
||||
else -> 1
|
||||
}
|
||||
scores[label] = (scores[label] ?: 0) + weight
|
||||
|
||||
@@ -319,12 +319,16 @@ class ConstExprEvaluator {
|
||||
NumericLiteral(BaseDataType.BYTE, value, func.position)
|
||||
}
|
||||
"log2" -> {
|
||||
val value = truncate(log2(args.single().number))
|
||||
NumericLiteral(BaseDataType.UBYTE, value, func.position)
|
||||
val arg = args.single().number
|
||||
if (arg <= 0.0)
|
||||
throw ExpressionError("log2 of a non-positive value is not defined", func.position)
|
||||
NumericLiteral(BaseDataType.UBYTE, truncate(log2(arg)), func.position)
|
||||
}
|
||||
"log2w" -> {
|
||||
val value = truncate(log2(args.single().number))
|
||||
NumericLiteral(BaseDataType.UWORD, value, func.position)
|
||||
val arg = args.single().number
|
||||
if (arg <= 0.0)
|
||||
throw ExpressionError("log2 of a non-positive value is not defined", func.position)
|
||||
NumericLiteral(BaseDataType.UBYTE, truncate(log2(arg)), func.position)
|
||||
}
|
||||
"atan2" -> {
|
||||
val x1f = args[0].number
|
||||
@@ -334,7 +338,12 @@ class ConstExprEvaluator {
|
||||
var radians = atan2(y2f-y1f, x2f-x1f)
|
||||
if(radians<0)
|
||||
radians+=2*PI
|
||||
NumericLiteral(BaseDataType.UWORD, floor(radians/2.0/PI*256.0), func.position)
|
||||
val value = floor(radians/2.0/PI*256.0)
|
||||
// the routine returns a ubyte, and a near-zero negative angle can round
|
||||
// up to exactly 2*PI, which lands on 256
|
||||
if(value !in 0.0..255.0)
|
||||
throw ExpressionError("atan2 result $value is out of range for a byte value", func.position)
|
||||
NumericLiteral(BaseDataType.UBYTE, value, func.position)
|
||||
}
|
||||
"diff" -> {
|
||||
val n1 = args[0].number
|
||||
|
||||
@@ -1013,11 +1013,12 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
|
||||
}
|
||||
in powersOfTwoFloat -> {
|
||||
val numshifts = powersOfTwoFloat.indexOf(cv)
|
||||
if (leftDt.isInteger) {
|
||||
// division by a power of two => logical shift right
|
||||
// NOTE: technically this optimization is only 100% accurate on unsigned integers
|
||||
// whereas shifthing signed integers can result in an off-by-1 rounding difference
|
||||
// (If you change the guard to only trigger this for unsigned integers the codegen will fall back to using division, which is slow but 100% accurate)
|
||||
if (leftDt.isUnsignedInteger) {
|
||||
// unsigned division by a power of two => logical shift right (exact)
|
||||
// Signed integers must NOT be folded here: the shift floors toward -inf, while
|
||||
// Prog8's signed '/' truncates toward zero, so x / 2^n would be off by one for
|
||||
// every negative dividend that is not an exact multiple. Leaving the division in
|
||||
// place lets the codegen emit a correct (slower) signed DIVS instead.
|
||||
return BinaryExpression(expr.left, ">>", NumericLiteral.optimalInteger(numshifts, expr.position), expr.position)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,11 +57,11 @@ fun Program.optimizeStatements(errors: IErrorReporter,
|
||||
return optimizationCount
|
||||
}
|
||||
|
||||
fun Program.inlineSubroutines(options: CompilationOptions): Int {
|
||||
fun Program.inlineSubroutines(errors: IErrorReporter, options: CompilationOptions): Int {
|
||||
// Skip inlining when optimizations are disabled (-noopt flag)
|
||||
if (!options.optimize) return 0
|
||||
|
||||
val inliner = Inliner(this, options)
|
||||
val inliner = Inliner(this, options, errors)
|
||||
inliner.visit(this)
|
||||
val mods = inliner.applyModifications()
|
||||
inliner.linkAffectedParents(namespace)
|
||||
|
||||
@@ -5,6 +5,7 @@ import prog8.ast.expressions.*
|
||||
import prog8.ast.statements.*
|
||||
import prog8.ast.walk.*
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.code.core.InternalCompilerException
|
||||
|
||||
|
||||
@@ -27,11 +28,13 @@ private class CannotInlineException : Exception()
|
||||
* - Subroutines must have a single functional statement in their body to be auto-inlined.
|
||||
* - Multi-return value subroutines are not yet supported for auto-inlining.
|
||||
*/
|
||||
class Inliner(private val program: Program, private val options: CompilationOptions): AstWalker() {
|
||||
class Inliner(private val program: Program, private val options: CompilationOptions, private val errors: IErrorReporter): AstWalker() {
|
||||
|
||||
private val suggestInlineReported = mutableSetOf<Subroutine>()
|
||||
|
||||
inner class DetermineInlineSubs(val program: Program): IAstVisitor {
|
||||
private val modifications = mutableListOf<AstModification>()
|
||||
private val visitedSubroutines = java.util.Collections.newSetFromMap(java.util.IdentityHashMap<Subroutine, Boolean>())
|
||||
private val visitedSubroutines = mutableSetOf<Subroutine>()
|
||||
|
||||
init {
|
||||
visit(program)
|
||||
@@ -59,10 +62,7 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
if (!visitedSubroutines.add(subroutine)) return
|
||||
|
||||
fun isBodyInlineable(stmt: Return): Boolean {
|
||||
if (stmt.values.isEmpty())
|
||||
return true
|
||||
|
||||
return stmt.values.all { value ->
|
||||
return stmt.values.isEmpty() || stmt.values.all { value ->
|
||||
if (isSimpleReturnExpression(value)) {
|
||||
val walker = object : AstWalker() {
|
||||
override fun after(identifier: IdentifierReference, parent: Node): Iterable<AstModification> {
|
||||
@@ -93,9 +93,7 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
// crash (literal argument has no identifier to assign to) or change semantics
|
||||
// (assigning to the caller's variable instead of the local parameter copy).
|
||||
val targetName = stmt.target.identifier?.nameInSource?.lastOrNull()
|
||||
if (targetName != null && subroutine.parameters.any { it.name == targetName })
|
||||
return false
|
||||
return if (stmt.value.isSimple) {
|
||||
return !(targetName != null && subroutine.parameters.any { it.name == targetName }) && if (stmt.value.isSimple) {
|
||||
val targetInline =
|
||||
if (stmt.target.identifier != null) {
|
||||
makeFullyScoped(stmt.target.identifier!!)
|
||||
@@ -156,9 +154,8 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
|
||||
if (hasOnlyBodyStatements) {
|
||||
if (subroutine !== program.entrypoint) {
|
||||
val bodyStmt = functionalStatements.firstOrNull()
|
||||
val isAutoInlineable =
|
||||
when (bodyStmt) {
|
||||
when (val bodyStmt = functionalStatements.firstOrNull()) {
|
||||
is Return -> isBodyInlineable(bodyStmt)
|
||||
is Assignment -> isBodyInlineable(bodyStmt)
|
||||
is FunctionCallStatement -> isBodyInlineable(bodyStmt)
|
||||
@@ -237,9 +234,12 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
|
||||
override fun after(functionCallStatement: FunctionCallStatement, parent: Node): Iterable<AstModification> {
|
||||
val sub = functionCallStatement.target.targetStatement(program.builtinFunctions) as? Subroutine
|
||||
if (sub == null) return noModifications
|
||||
|
||||
if (!sub.inline) return noModifications
|
||||
?: return noModifications
|
||||
|
||||
if (!sub.inline) {
|
||||
maybeSuggestInlineKeyword(sub)
|
||||
return noModifications
|
||||
}
|
||||
|
||||
val (canInline, _) = canInlineAtCallSiteWithReason(sub, functionCallStatement)
|
||||
if (!canInline) {
|
||||
@@ -258,15 +258,43 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
return false to "asmsub with parameters cannot be inlined"
|
||||
}
|
||||
|
||||
if (sub.parameters.size > 1) {
|
||||
return false to "subroutines with more than 1 parameter cannot be inlined in this compiler version"
|
||||
}
|
||||
|
||||
if (sub.parameters.any { it.registerOrPair != null }) {
|
||||
return false to "subroutines with register-mapped parameters cannot be inlined"
|
||||
}
|
||||
|
||||
if (sub.parameters.size == 1) {
|
||||
if (sub.parameters.size > 1) {
|
||||
// Multi-parameter inlining is only supported on m68k targets (stack-based
|
||||
// calling convention makes the call overhead worthwhile), and only for the
|
||||
// single-return-value expression case with a tiny body and simple args.
|
||||
if (!options.compTarget.cpu.is68k) {
|
||||
return false to "subroutines with more than 1 parameter cannot be inlined in this compiler version"
|
||||
}
|
||||
if (fcall.args.size != sub.parameters.size) {
|
||||
return false to "argument count does not match parameter count"
|
||||
}
|
||||
if (!fcall.args.all { it.isSimple }) {
|
||||
return false to "argument is too complex to inline (might have side effects)"
|
||||
}
|
||||
// Refuse to inline a sub that assigns directly to any of its parameters.
|
||||
// Substituting the argument into the target would crash for a literal
|
||||
// argument, or wrongly modify the caller's variable for a variable argument
|
||||
// (parameters are passed by value, so the sub only modifies its local copy).
|
||||
val paramNames = sub.parameters.map { it.name }.toSet()
|
||||
val bodyStmt = sub.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
|
||||
if ((bodyStmt as? Assignment)?.target?.identifier?.nameInSource?.lastOrNull() in paramNames)
|
||||
return false to "cannot inline subroutine that assigns to its parameter"
|
||||
// v1 only supports the single-return-value expression case on this path.
|
||||
// Void and multi-value statement call sites keep the previous restriction.
|
||||
val returnStmt = bodyStmt as? Return
|
||||
?: return false to "multi-parameter inlining requires a single return statement"
|
||||
if (returnStmt.values.size != 1) {
|
||||
return false to "multi-parameter inlining requires a single return value"
|
||||
}
|
||||
if (!isFairlySimpleReturnExpression(returnStmt.values[0], paramNames)) {
|
||||
return false to "multi-parameter inline body is too complex"
|
||||
}
|
||||
// fall through to the 6502-independent checks below (none apply to m68k)
|
||||
} else if (sub.parameters.size == 1) {
|
||||
val arg = fcall.args.first()
|
||||
if (!arg.isSimple) {
|
||||
return false to "argument is too complex to inline (might have side effects)"
|
||||
@@ -298,10 +326,49 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
return canInlineAtCallSiteWithReason(sub, fcall).first
|
||||
}
|
||||
|
||||
private fun fitsM68kMultiParamInline(sub: Subroutine): Boolean {
|
||||
if (!options.compTarget.cpu.is68k)
|
||||
return false
|
||||
if (sub.inline)
|
||||
return false
|
||||
if (sub.isAsmSubroutine && sub.parameters.isNotEmpty())
|
||||
return false
|
||||
if (sub.parameters.size <= 1)
|
||||
return false
|
||||
if (sub.parameters.any { it.registerOrPair != null })
|
||||
return false
|
||||
if (sub === program.entrypoint)
|
||||
return false
|
||||
if (sub.statements.any { it is Subroutine || (it is VarDecl && it.origin != VarDeclOrigin.SUBROUTINEPARAM) })
|
||||
return false
|
||||
val functionalCount = sub.statements.count { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
|
||||
if (functionalCount != 1 && functionalCount != 2)
|
||||
return false
|
||||
val bodyStmt = sub.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
|
||||
val returnStmt = bodyStmt as? Return ?: return false
|
||||
if (returnStmt.values.size != 1)
|
||||
return false
|
||||
if (functionalCount == 2 && sub.statements.lastOrNull()?.let(::isEmptyReturn) != true)
|
||||
return false
|
||||
val paramNames = sub.parameters.map { it.name }.toSet()
|
||||
return isFairlySimpleReturnExpression(returnStmt.values[0], paramNames)
|
||||
}
|
||||
|
||||
private fun maybeSuggestInlineKeyword(sub: Subroutine) {
|
||||
if (sub in suggestInlineReported)
|
||||
return
|
||||
if (!fitsM68kMultiParamInline(sub))
|
||||
return
|
||||
suggestInlineReported.add(sub)
|
||||
errors.info("subroutine '${sub.name}' would likely benefit from 'inline'", sub.position)
|
||||
}
|
||||
|
||||
override fun before(functionCallExpr: FunctionCallExpression, parent: Node): Iterable<AstModification> {
|
||||
val sub = functionCallExpr.target.targetStatement(program.builtinFunctions) as? Subroutine
|
||||
if (sub == null) return noModifications
|
||||
if (!sub.inline) return noModifications
|
||||
val sub = functionCallExpr.target.targetStatement(program.builtinFunctions) as? Subroutine ?: return noModifications
|
||||
if (!sub.inline) {
|
||||
maybeSuggestInlineKeyword(sub)
|
||||
return noModifications
|
||||
}
|
||||
|
||||
val (canInline, _) = canInlineAtCallSiteWithReason(sub, functionCallExpr)
|
||||
if (!canInline) {
|
||||
@@ -422,6 +489,78 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* m68k-only predicate for multi-parameter inlining (v1: expression case only).
|
||||
* Allows any number of parameters but bounds the duplicated work instead:
|
||||
* single return expression, expression-tree depth <= MAX_DEPTH, total nodes
|
||||
* <= MAX_NODES, and total parameter occurrences <= MAX_PARAM_USES.
|
||||
* Numeric literals and constant identifiers count as free (cost 0).
|
||||
*/
|
||||
private fun isFairlySimpleReturnExpression(expr: Expression, paramNames: Set<String>): Boolean {
|
||||
val maxDepth = 3
|
||||
val maxNodes = 12
|
||||
val maxParamUses = 6
|
||||
|
||||
var nodes = 0
|
||||
var paramUses = 0
|
||||
var ok = true
|
||||
|
||||
fun isFreeLeaf(ident: IdentifierReference): Boolean {
|
||||
// Constants count as free (usually already constant-folded to literals).
|
||||
if (ident.constValue(program) != null)
|
||||
return true
|
||||
val target = try {
|
||||
ident.targetStatement()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
return target is VarDecl && target.type == VarDeclType.CONST
|
||||
}
|
||||
|
||||
fun visit(e: Expression, depth: Int) {
|
||||
if (!ok)
|
||||
return
|
||||
if (depth > maxDepth) {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
when (e) {
|
||||
is NumericLiteral -> {
|
||||
// free, costs no node budget
|
||||
}
|
||||
is IdentifierReference -> {
|
||||
if (!isFreeLeaf(e)) {
|
||||
nodes++
|
||||
if (nodes > maxNodes) {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
if (e.nameInSource.lastOrNull() in paramNames) {
|
||||
paramUses++
|
||||
if (paramUses > maxParamUses)
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
}
|
||||
is PrefixExpression -> visit(e.expression, depth + 1)
|
||||
is BinaryExpression -> {
|
||||
nodes++
|
||||
if (nodes > maxNodes) {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
visit(e.left, depth + 1)
|
||||
visit(e.right, depth + 1)
|
||||
}
|
||||
is TypecastExpression -> visit(e.expression, depth + 1)
|
||||
else -> ok = false
|
||||
}
|
||||
}
|
||||
|
||||
visit(expr, 0)
|
||||
return ok
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitutes parameter references in a statement with argument values.
|
||||
*/
|
||||
@@ -434,7 +573,11 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
?: return noModifications
|
||||
|
||||
// Only handle inlining if sub is marked as such and call site is okay
|
||||
if (!sub.inline || !canInlineAtCallSite(sub, fcall))
|
||||
if (!sub.inline) {
|
||||
maybeSuggestInlineKeyword(sub)
|
||||
return noModifications
|
||||
}
|
||||
if (!canInlineAtCallSite(sub, fcall))
|
||||
return noModifications
|
||||
|
||||
val toInline = sub.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM } as? Return ?: return noModifications
|
||||
@@ -477,6 +620,8 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
}
|
||||
|
||||
private fun substituteParameters(sub: Subroutine, fcall: IFunctionCall, node: Node): Node? {
|
||||
if (fcall.args.size != sub.parameters.size)
|
||||
return null
|
||||
val paramVarDecls = sub.parameters.map { param ->
|
||||
sub.statements.filterIsInstance<VarDecl>().find { it.origin == VarDeclOrigin.SUBROUTINEPARAM && it.name == param.name }
|
||||
?: throw FatalAstException("parameter ${param.name} not found in subroutine ${sub.name}")
|
||||
|
||||
@@ -486,15 +486,19 @@ class StatementOptimizer(private val program: Program,
|
||||
return listOf(AstReplaceNode(assignment, ifstmt, parent))
|
||||
}
|
||||
|
||||
if(v2 is NumericLiteral || v2 is IdentifierReference) {
|
||||
if(v2 is NumericLiteral) {
|
||||
// x = min(expression, 100) -> x=expression, if x>100 x=100
|
||||
// Only safe with a real constant: the assignment to x happens before the
|
||||
// comparison, so comparing against anything that x could alias (such as a
|
||||
// variable that happens to be the assignment target) would read the already
|
||||
// overwritten value.
|
||||
val assign = Assignment(assignment.target.copy(), v1, AssignmentOrigin.OPTIMIZER, assignment.position)
|
||||
val ifstmt = makeMinMaxCheckAndAssignRight(assign.target.toExpression(), ">", v2, assignment.target, assignment.position)
|
||||
return listOf(
|
||||
AstInsert.after(assignment, ifstmt, parent as IStatementContainer),
|
||||
AstReplaceNode(assignment, assign, parent)
|
||||
)
|
||||
} else if(v1 is NumericLiteral || v1 is IdentifierReference) {
|
||||
} else if(v1 is NumericLiteral) {
|
||||
// x = min(100, expression) -> x=expression, if x>100 x=100
|
||||
val assign = Assignment(assignment.target.copy(), v2, AssignmentOrigin.OPTIMIZER, assignment.position)
|
||||
val ifstmt = makeMinMaxCheckAndAssignRight(assign.target.toExpression(), ">", v1, assignment.target, assignment.position)
|
||||
@@ -519,15 +523,17 @@ class StatementOptimizer(private val program: Program,
|
||||
return listOf(AstReplaceNode(assignment, ifstmt, parent))
|
||||
}
|
||||
|
||||
if(v2 is NumericLiteral || v2 is IdentifierReference) {
|
||||
if(v2 is NumericLiteral) {
|
||||
// x = max(expression, 100) -> x=expression, if x<100 x=100
|
||||
// See the note in the min__ case above: the compared operand must be a
|
||||
// real constant, never a variable that the assignment could have aliased.
|
||||
val assign = Assignment(assignment.target.copy(), v1, AssignmentOrigin.OPTIMIZER, assignment.position)
|
||||
val ifstmt = makeMinMaxCheckAndAssignRight(assign.target.toExpression(), "<", v2, assignment.target, assignment.position)
|
||||
return listOf(
|
||||
AstInsert.after(assignment, ifstmt, parent as IStatementContainer),
|
||||
AstReplaceNode(assignment, assign, parent)
|
||||
)
|
||||
} else if(v1 is NumericLiteral || v1 is IdentifierReference) {
|
||||
} else if(v1 is NumericLiteral) {
|
||||
// x = max(100, expression) -> x=expression, if x<100 x=100
|
||||
val assign = Assignment(assignment.target.copy(), v2, AssignmentOrigin.OPTIMIZER, assignment.position)
|
||||
val ifstmt = makeMinMaxCheckAndAssignRight(assign.target.toExpression(), "<", v1, assignment.target, assignment.position)
|
||||
@@ -585,25 +591,91 @@ class StatementOptimizer(private val program: Program,
|
||||
}
|
||||
}
|
||||
|
||||
if(whenStmt.betterAsOnGoto(program, options)) {
|
||||
val indexOffset = whenStmt.onGotoIndexOffset(program, options)
|
||||
if(indexOffset!=null) {
|
||||
// rewrite when into a on..goto , which is faster and also smaller for ~5+ cases
|
||||
var elseScope: AnonymousScope? = null
|
||||
var elseJump: Jump? = null
|
||||
val jumps = mutableListOf<Pair<Int, Jump>>()
|
||||
val jumps = mutableListOf<Pair<Int, IdentifierReference>>()
|
||||
val choiceLabels = mutableMapOf<WhenChoice, String>()
|
||||
whenStmt.choices.forEach { choice ->
|
||||
if(choice.values==null) {
|
||||
elseJump = choice.statements.statements.single() as Jump
|
||||
elseScope = choice.statements
|
||||
elseJump = choice.statements.statements.singleOrNull() as? Jump
|
||||
} else {
|
||||
val simpleJump = choice.statements.statements.singleOrNull() as? Jump
|
||||
val simpleTarget = simpleJump?.target as? IdentifierReference
|
||||
val generatedLabel = if(simpleTarget==null) {
|
||||
program.makeLabel("whencase").also { choiceLabels[choice] = it }
|
||||
} else null
|
||||
val label = simpleTarget?.copy()
|
||||
?: IdentifierReference(listOf(requireNotNull(generatedLabel)), choice.position)
|
||||
choice.values!!.forEach { value ->
|
||||
jumps.add(value.constValue(program)!!.number.toInt() to choice.statements.statements.single() as Jump)
|
||||
jumps.add(value.constValue(program)!!.number.toInt() to label)
|
||||
}
|
||||
}
|
||||
}
|
||||
if(choiceLabels.isNotEmpty() && parent !is Subroutine && parent !is Block)
|
||||
return noModifications
|
||||
|
||||
val jumpLabels = jumps.sortedBy { it.first }.map { it.second.target as IdentifierReference }
|
||||
val labelsByValue = jumps.associate { it.first to it.second.copy() }
|
||||
val span = jumps.maxOf { it.first } - indexOffset + 1
|
||||
val gapTarget = (elseJump?.target as? IdentifierReference)?.copy()
|
||||
val elseIsEmpty = elseScope?.statements?.isEmpty() ?: true
|
||||
val needsEndLabel = choiceLabels.isNotEmpty() || (span > jumps.size && elseIsEmpty && gapTarget==null)
|
||||
val endLabel = if(needsEndLabel) program.makeLabel("whenend", whenStmt.position) else null
|
||||
val gapLabel = if(span > jumps.size && gapTarget==null && !elseIsEmpty)
|
||||
program.makeLabel("ongotogap", whenStmt.position)
|
||||
else null
|
||||
val effectiveGapTarget = gapTarget
|
||||
?: gapLabel?.let { IdentifierReference(listOf(it.name), it.position) }
|
||||
?: endLabel?.let { IdentifierReference(listOf(it.name), it.position) }
|
||||
// the table spans the whole value range; a value that isn't a choice jumps to the else target.
|
||||
// every slot needs its own node instance, since a choice can cover several values
|
||||
val jumpLabels = (indexOffset..jumps.maxOf { it.first }).map {
|
||||
labelsByValue[it]?.copy() ?: requireNotNull(effectiveGapTarget).copy()
|
||||
}
|
||||
|
||||
val elsePart = if(elseJump==null) null else AnonymousScope(mutableListOf(elseJump), elseJump.position)
|
||||
val onGoto = OnGoto(false, whenStmt.condition, jumpLabels, elsePart, whenStmt.position)
|
||||
return listOf(AstReplaceNode(whenStmt, onGoto, parent))
|
||||
// the jump table is indexed from the lowest choice value upwards, and unsigned arithmetic
|
||||
// wraps so values below it simply fall out of the table's bounds check
|
||||
val indexExpr: Expression = if(indexOffset==0) whenStmt.condition
|
||||
else BinaryExpression(whenStmt.condition.copy(), "-", NumericLiteral.optimalInteger(indexOffset, whenStmt.position), whenStmt.position)
|
||||
|
||||
// Put the gap target in the else body when one exists. When there is no else part
|
||||
// (or an empty one), generated case bodies follow the on..goto in place of it, so an
|
||||
// out-of-bounds value must jump to the end label instead of falling through to them.
|
||||
val elsePart = if(elseIsEmpty) {
|
||||
if(choiceLabels.isNotEmpty())
|
||||
AnonymousScope(mutableListOf<Statement>(program.jumpLabel(requireNotNull(endLabel))), whenStmt.position)
|
||||
else
|
||||
null
|
||||
} else {
|
||||
val s = requireNotNull(elseScope)
|
||||
val statements = if(gapLabel==null)
|
||||
s.statements
|
||||
else
|
||||
(listOf<Statement>(gapLabel) + s.statements).toMutableList()
|
||||
AnonymousScope(statements, s.position)
|
||||
}
|
||||
val onGoto = OnGoto(false, indexExpr, jumpLabels, elsePart, whenStmt.position)
|
||||
val modifications = mutableListOf<AstModification>()
|
||||
var anchor: Statement = whenStmt
|
||||
choiceLabels.forEach { (choice, labelName) ->
|
||||
val label = Label(labelName, choice.position)
|
||||
modifications += AstInsert.after(anchor, label, parent as IStatementContainer)
|
||||
anchor = label
|
||||
choice.statements.statements.forEach { statement ->
|
||||
modifications += AstInsert.after(anchor, statement, parent as IStatementContainer)
|
||||
anchor = statement
|
||||
}
|
||||
val endJump = program.jumpLabel(requireNotNull(endLabel))
|
||||
modifications += AstInsert.after(anchor, endJump, parent as IStatementContainer)
|
||||
anchor = endJump
|
||||
}
|
||||
if(endLabel!=null)
|
||||
modifications += AstInsert.after(anchor, endLabel, parent as IStatementContainer)
|
||||
modifications += AstReplaceNode(whenStmt, onGoto, parent)
|
||||
return modifications
|
||||
}
|
||||
|
||||
return noModifications
|
||||
|
||||
@@ -391,12 +391,10 @@ class UnusedCodeRemover(private val program: Program,
|
||||
if (definingSub == null) false // global: unsafe
|
||||
else {
|
||||
val target = stmt.target.targetStatement(program.builtinFunctions)
|
||||
if (target is Subroutine) {
|
||||
!isNestedWithin(target, definingSub)
|
||||
} else if (target is BuiltinFunctionPlaceholder) {
|
||||
target.name !in setOf("call", "callfar", "callfar2")
|
||||
} else {
|
||||
true
|
||||
when (target) {
|
||||
is Subroutine -> !isNestedWithin(target, definingSub)
|
||||
is BuiltinFunctionPlaceholder -> target.name !in setOf("call", "callfar", "callfar2")
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ dependencies {
|
||||
implementation(project(":intermediate"))
|
||||
implementation(project(":virtualmachine"))
|
||||
implementation("com.github.ajalt.clikt:clikt:5.1.0")
|
||||
implementation("com.github.ajalt.mordant:mordant:3.0.2")
|
||||
implementation("com.github.ajalt.mordant:mordant:3.1.0")
|
||||
implementation("com.michael-bull.kotlin-result:kotlin-result-jvm:2.3.1")
|
||||
|
||||
testImplementation(project(":codeCore"))
|
||||
@@ -32,20 +32,66 @@ configurations.all {
|
||||
exclude(group = "org.antlr", module = "antlr4")
|
||||
}
|
||||
|
||||
// Post-process the generated launcher scripts: pass --enable-native-access only to
|
||||
// JVMs that understand it (JDK 18+), to avoid JNA restricted-method warnings on newer JDKs.
|
||||
fun patchLaunchScripts(scriptDir: File) {
|
||||
val unixScript = scriptDir.resolve("prog8c")
|
||||
if (unixScript.canRead()) {
|
||||
val marker = "eval \"set -- $("
|
||||
val text = unixScript.readText()
|
||||
check(marker in text) { "unexpected POSIX launch script layout for prog8c" }
|
||||
val probe = $$"""if "$JAVACMD" --enable-native-access=ALL-UNNAMED -version >/dev/null 2>&1; then
|
||||
JAVA_OPTS="$JAVA_OPTS --enable-native-access=ALL-UNNAMED"
|
||||
fi
|
||||
|
||||
"""
|
||||
unixScript.writeText(text.replace(marker, probe + marker))
|
||||
}
|
||||
val windowsScript = scriptDir.resolve("prog8c.bat")
|
||||
if (windowsScript.canRead()) {
|
||||
val marker = "@rem Execute prog8c"
|
||||
val text = windowsScript.readText()
|
||||
check(marker in text) { "unexpected Windows launch script layout for prog8c" }
|
||||
val probe = """@rem Prog8: add --enable-native-access on JVMs that support it (JDK 18+) to avoid JNA restricted-method warnings
|
||||
"%JAVA_EXE%" --enable-native-access=ALL-UNNAMED -version >NUL 2>&1
|
||||
if not errorlevel 1 set JAVA_OPTS=%JAVA_OPTS% --enable-native-access=ALL-UNNAMED
|
||||
|
||||
"""
|
||||
windowsScript.writeText(text.replace(marker, probe + marker))
|
||||
}
|
||||
}
|
||||
|
||||
tasks.startScripts {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
tasks.withType<org.gradle.jvm.application.tasks.CreateStartScripts>().configureEach {
|
||||
doLast {
|
||||
patchLaunchScripts(outputDir!!)
|
||||
}
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass.set("prog8.CompilerMainKt")
|
||||
applicationName = "prog8c"
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
manifest {
|
||||
attributes("Main-Class" to "prog8.CompilerMainKt")
|
||||
attributes("Enable-Native-Access" to "ALL-UNNAMED")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.shadowJar {
|
||||
archiveBaseName.set("prog8c")
|
||||
archiveVersion.set(version.toString())
|
||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||
mergeServiceFiles()
|
||||
manifest {
|
||||
attributes("Main-Class" to "prog8.CompilerMainKt")
|
||||
attributes("Enable-Native-Access" to "ALL-UNNAMED")
|
||||
}
|
||||
filesMatching("META-INF/LICENSE") {
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
%import shared_m68k_adpcm
|
||||
|
||||
adpcm {
|
||||
%option ignore_unused, private_symbols
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
%import exec
|
||||
|
||||
ahi {
|
||||
%option no_symbol_prefixing, ignore_unused
|
||||
|
||||
; Playback-only binding for ahi.device, high-level (CMD_WRITE) API.
|
||||
; AHI v4 compatible: ahir_Version is 4 and only 8/16-bit sample types
|
||||
; are exposed (32-bit types need version 6+).
|
||||
; The unit to play on is selectable via ahi.open() (pass 0 = AHI_DEFAULT_UNIT
|
||||
; for the user's default output, or 1-3 for another configured unit; AHI_NO_UNIT
|
||||
; only makes sense for low-level-only access and is not supported here).
|
||||
; The low-level control and audio-mode query LVOs (audio channel handles)
|
||||
; are exposed as extsubs below; their hooks and requester are deliberately
|
||||
; not covered.
|
||||
; Requires a system with ahi.device installed (real hardware or full
|
||||
; emulation); not testable under vamos. Amiga1200 target only (68020+,
|
||||
; Kickstart 3+): uses CreateMsgPort/CreateIORequest for port handling.
|
||||
; Struct offsets verified against NDK3.2 exec/io.h (IOStdReq) and the
|
||||
; AHI v6 dev kit (Developer/Include/Asm/devices/ahi.i, AHIRequest).
|
||||
|
||||
; ---- constants ----
|
||||
|
||||
const uword AHI_VERSION = 4 ; ahir_Version preset before OpenDevice
|
||||
|
||||
const ubyte AHI_DEFAULT_UNIT = 0
|
||||
const ubyte AHI_NO_UNIT = 255 ; low-level API only, no hardware allocated
|
||||
|
||||
const long AHI_INVALID_ID = $ffffffff ; audio id sentinel, not a valid mode
|
||||
const long AHI_DEFAULT_ID = 0 ; AllocAudioA: use the default mode
|
||||
|
||||
; sample formats for ahir_Type (all V4-compatible)
|
||||
const long AHIST_M8S = 0 ; mono, 8-bit signed
|
||||
const long AHIST_M16S = 1 ; mono, 16-bit signed (big-endian WORDs)
|
||||
const long AHIST_S8S = 2 ; stereo, 8-bit signed
|
||||
const long AHIST_S16S = 3 ; stereo, 16-bit signed (big-endian WORDs)
|
||||
|
||||
; volume and stereo position are 16.16 Fixed
|
||||
const long FULL_VOLUME = $10000
|
||||
const long PAN_LEFT = 0
|
||||
const long PAN_CENTER = $8000
|
||||
const long PAN_RIGHT = $10000
|
||||
|
||||
; error codes (AHIE_*, seen in io_Error alongside exec IO errors)
|
||||
const byte AHIE_OK = 0
|
||||
const byte AHIE_NOMEM = 1
|
||||
const byte AHIE_BADSOUNDTYPE = 2
|
||||
const byte AHIE_BADSAMPLETYPE = 3
|
||||
const byte AHIE_ABORTED = 4
|
||||
const byte AHIE_UNKNOWN = 5
|
||||
const byte AHIE_HALFDUPLEX = 6
|
||||
|
||||
; ---- low-level LVO API ----
|
||||
; These call the device's library-style routines through its base
|
||||
; (sys.AHIBase, @bank 101), which ahi.open() sets from the opened
|
||||
; device's io_Device. Register assignments and offsets verified against
|
||||
; the AHI dev kit ahi_lib.fd/sfd (bias 42). The synth routines need an
|
||||
; AudioCtrl handle from ahi.AllocAudioA(); the query trio reads the
|
||||
; audio-mode database, useful for diagnostics. Taglists are plain
|
||||
; 'pointer' parameters (like the intuition bindings): build them as
|
||||
; ^^utility.TagItem arrays of {tag, data} pairs, terminated by
|
||||
; utility.TAG_END. Data is a pointer-or-value per the tag's semantics.
|
||||
|
||||
extsub @bank 101 -42 = AllocAudioA(pointer tagList @A1) -> pointer @D0 ; returns AudioCtrl, or 0 (AF_NULL) if failed
|
||||
extsub @bank 101 -48 = FreeAudio(pointer audioCtrl @A2)
|
||||
extsub @bank 101 -60 = ControlAudioA(pointer audioCtrl @A2, pointer tagList @A1) -> long @D0
|
||||
extsub @bank 101 -66 = SetVol(uword channel @D0, long volume @D1, long pan @D2, pointer audioCtrl @A2, long flags @D3)
|
||||
extsub @bank 101 -72 = SetFreq(uword channel @D0, long freq @D1, pointer audioCtrl @A2, long flags @D2)
|
||||
extsub @bank 101 -78 = SetSound(uword channel @D0, uword sound @D1, long offset @D2, long length @D3, pointer audioCtrl @A2, long flags @D4)
|
||||
extsub @bank 101 -90 = LoadSound(uword sound @D0, long sampletype @D1, pointer info @A0, pointer audioCtrl @A2) -> long @D0
|
||||
extsub @bank 101 -96 = UnloadSound(uword sound @D0, pointer audioCtrl @A2)
|
||||
extsub @bank 101 -102 = NextAudioID(long lastID @D0) -> long @D0 ; next id, or AHI_INVALID_ID at the end
|
||||
extsub @bank 101 -108 = GetAudioAttrsA(long id @D0, pointer audioCtrl @A2, pointer tagList @A1) -> long @D0 ; 1=ok, 0=invalid id
|
||||
extsub @bank 101 -114 = BestAudioIDA(pointer tagList @A1) -> long @D0 ; best matching audio id, AHI_INVALID_ID if none
|
||||
|
||||
; For ahi.BestAudioIDA(): hard requirements go in the main taglist, and
|
||||
; AHIB_Dizzy points at a second taglist of soft preferences - after the
|
||||
; hard ones are applied, the mode with the most of the wanted (and fewest
|
||||
; of the unwanted) features wins. Without it the call hardly does what its
|
||||
; name suggests, so it is worth passing.
|
||||
const long AHIB_Dizzy = $800000be
|
||||
|
||||
; ---- low-level tag constants ----
|
||||
; AHIC_* go to ahi.ControlAudioA(), AHIDB_* to ahi.GetAudioAttrsA(),
|
||||
; AHIB_* to ahi.BestAudioIDA().
|
||||
; Boolean AHIDB_* tags return 0/1; the R-bit string tags (driver, name,
|
||||
; author, copyright, version, annotation) are OUT parameters: the device
|
||||
; writes a string into the buffer you pass (size it via AHIDB_BufferLen).
|
||||
; Tag base is utility.TAG_USER.
|
||||
const long AHIC_Play = $80000050
|
||||
const long AHIC_Record = $80000051
|
||||
const long AHIC_MonitorVolume = $80000052
|
||||
const long AHIC_MonitorVolume_Query = $80000053
|
||||
const long AHIC_MixFreq_Query = $80000054
|
||||
const long AHIC_InputGain = $80000055
|
||||
const long AHIC_InputGain_Query = $80000056
|
||||
const long AHIC_OutputVolume = $80000057
|
||||
const long AHIC_OutputVolume_Query = $80000058
|
||||
const long AHIC_Input = $80000059
|
||||
const long AHIC_Input_Query = $8000005a
|
||||
const long AHIC_Output = $8000005b
|
||||
const long AHIC_Output_Query = $8000005c
|
||||
|
||||
const long AHIDB_AudioID = $80000064
|
||||
const long AHIDB_Driver = $80008065 ; R
|
||||
const long AHIDB_Volume = $80000067
|
||||
const long AHIDB_Panning = $80000068
|
||||
const long AHIDB_Stereo = $80000069
|
||||
const long AHIDB_HiFi = $8000006a
|
||||
const long AHIDB_PingPong = $8000006b
|
||||
const long AHIDB_Name = $8000806d ; R
|
||||
const long AHIDB_Bits = $8000006e
|
||||
const long AHIDB_MaxChannels = $8000006f
|
||||
const long AHIDB_MinMixFreq = $80000070
|
||||
const long AHIDB_MaxMixFreq = $80000071
|
||||
const long AHIDB_Record = $80000072
|
||||
const long AHIDB_Frequencies = $80000073
|
||||
const long AHIDB_FrequencyArg = $80000074
|
||||
const long AHIDB_Frequency = $80000075
|
||||
const long AHIDB_Author = $80008076 ; R
|
||||
const long AHIDB_Copyright = $80008077 ; R
|
||||
const long AHIDB_Version = $80008078 ; R
|
||||
const long AHIDB_Annotation = $80008079 ; R
|
||||
const long AHIDB_BufferLen = $8000007a
|
||||
const long AHIDB_IndexArg = $8000007b
|
||||
const long AHIDB_Index = $8000007c
|
||||
const long AHIDB_Realtime = $8000007d
|
||||
const long AHIDB_MaxPlaySamples = $8000007e
|
||||
const long AHIDB_MaxRecordSamples = $8000007f
|
||||
const long AHIDB_FullDuplex = $80000081
|
||||
const long AHIDB_MinMonitorVolume = $80000082
|
||||
const long AHIDB_MaxMonitorVolume = $80000083
|
||||
const long AHIDB_MinInputGain = $80000084
|
||||
const long AHIDB_MaxInputGain = $80000085
|
||||
const long AHIDB_MinOutputVolume = $80000086
|
||||
const long AHIDB_MaxOutputVolume = $80000087
|
||||
const long AHIDB_Inputs = $80000088
|
||||
const long AHIDB_InputArg = $80000089
|
||||
const long AHIDB_Input = $8000008a
|
||||
const long AHIDB_Outputs = $8000008b
|
||||
const long AHIDB_OutputArg = $8000008c
|
||||
const long AHIDB_Output = $8000008d
|
||||
|
||||
; ---- struct ----
|
||||
|
||||
struct AHIRequest { ; total size: 80
|
||||
; struct IOStdReq base (48 bytes, same layout as exec.IOStdReq)
|
||||
^^exec.Node Succ ; 0
|
||||
^^exec.Node Pred ; 4
|
||||
ubyte Type ; 8
|
||||
byte Pri ; 9
|
||||
str Name ; 10
|
||||
^^exec.MsgPort ReplyPort ; 14
|
||||
uword Length ; 18
|
||||
pointer Device ; 20
|
||||
long Unit ; 24
|
||||
uword Command ; 28
|
||||
ubyte Flags ; 30
|
||||
byte Error ; 31
|
||||
long Actual ; 32
|
||||
long IOStdReq_Length ; 36
|
||||
pointer Data ; 40
|
||||
long Offset ; 44
|
||||
; AHI extension
|
||||
uword Version ; 48
|
||||
uword Pad ; 50
|
||||
long Private1 ; 52
|
||||
long Private2 ; 56
|
||||
long SampleType ; 60
|
||||
long Frequency ; 64
|
||||
long Volume ; 68
|
||||
long Position ; 72
|
||||
pointer Link ; 76
|
||||
}
|
||||
|
||||
; Two request blocks for gapless double-buffered playback, used
|
||||
; ping-pong fashion and chained via ahir_Link. Allocated with
|
||||
; CreateIORequest (Kickstart 2.04+), sharing a single reply port.
|
||||
^^AHIRequest Req0
|
||||
^^AHIRequest Req1
|
||||
private ^^exec.MsgPort port
|
||||
private ubyte queued ; number of outstanding writes (0..2)
|
||||
private bool play_alt ; next play uses Req1
|
||||
private bool wait_alt ; next wait reaps Req1
|
||||
private bool opened
|
||||
|
||||
; ---- high level playback interface ----
|
||||
|
||||
sub open(ubyte unit) -> bool {
|
||||
; -- Open ahi.device for playback on the given unit (0 = default,
|
||||
; 1-3 other configured units; see the header note above).
|
||||
; Returns false if unavailable.
|
||||
if opened
|
||||
return true
|
||||
port = exec.CreateMsgPort()
|
||||
if port == 0
|
||||
return false
|
||||
Req0 = exec.CreateIORequest(port, sizeof(ahi.AHIRequest))
|
||||
if Req0 == 0 {
|
||||
exec.DeleteMsgPort(port)
|
||||
return false
|
||||
}
|
||||
Req0.Version = AHI_VERSION
|
||||
if exec.OpenDevice("ahi.device", unit, Req0, 0)!=0 {
|
||||
exec.DeleteIORequest(Req0)
|
||||
exec.DeleteMsgPort(port)
|
||||
return false
|
||||
}
|
||||
Req1 = exec.CreateIORequest(port, sizeof(ahi.AHIRequest))
|
||||
if Req1 == 0 {
|
||||
exec.CloseDevice(Req0)
|
||||
exec.DeleteIORequest(Req0)
|
||||
exec.DeleteMsgPort(port)
|
||||
return false
|
||||
}
|
||||
Req1.Device = Req0.Device
|
||||
Req1.Unit = Req0.Unit
|
||||
Req1.Version = AHI_VERSION
|
||||
sys.AHIBase = Req0.Device ; base for the low-level LVO calls above
|
||||
queued = 0
|
||||
play_alt = false
|
||||
wait_alt = false
|
||||
opened = true
|
||||
return true
|
||||
}
|
||||
|
||||
sub close() {
|
||||
; -- Abort pending audio, close the device, free the port and requests.
|
||||
if not opened
|
||||
return
|
||||
stop()
|
||||
exec.CloseDevice(Req0)
|
||||
exec.DeleteIORequest(Req0)
|
||||
exec.DeleteIORequest(Req1)
|
||||
exec.DeleteMsgPort(port)
|
||||
sys.AHIBase = 0
|
||||
opened = false
|
||||
}
|
||||
|
||||
sub write_sync(pointer samples, long num_bytes, long sampletype, uword frequency, long volume, long position) -> byte {
|
||||
; -- Play a sample synchronously, blocks until finished. Returns io_Error.
|
||||
while queued>0
|
||||
wait_oldest()
|
||||
num_bytes = setup(Req0, 0, samples, num_bytes, sampletype, frequency, volume, position)
|
||||
if num_bytes==0
|
||||
return AHIE_OK
|
||||
void exec.DoIO(Req0)
|
||||
return Req0.Error
|
||||
}
|
||||
|
||||
sub play(pointer samples, long num_bytes, long sampletype, uword frequency, long volume, long position) {
|
||||
; -- Play a sample asynchronously on the (mixed) output.
|
||||
; If chunks were queued earlier with play_queued, those are waited for first,
|
||||
; so this never reuses a request block that still has a write in flight.
|
||||
while queued>0
|
||||
wait_oldest()
|
||||
num_bytes = setup(Req0, 0, samples, num_bytes, sampletype, frequency, volume, position)
|
||||
if num_bytes==0
|
||||
return
|
||||
BeginIO(Req0)
|
||||
queued = 1
|
||||
play_alt = true
|
||||
wait_alt = false
|
||||
; sound now plays asynchronously.
|
||||
}
|
||||
|
||||
sub play_queued(pointer samples, long num_bytes, long sampletype, uword frequency, long volume, long position) {
|
||||
; -- Queue a sample chunk for gapless playback (ahir_Link chaining).
|
||||
; Unlike play(), this may be called while the previous chunk is still playing:
|
||||
; AHI starts it the moment the previous chunk ends.
|
||||
; If both slots are full this waits for the oldest chunk to finish first.
|
||||
if queued==2
|
||||
wait_oldest()
|
||||
^^AHIRequest io = Req0
|
||||
pointer link = 0
|
||||
if play_alt {
|
||||
io = Req1
|
||||
link = Req0
|
||||
} else {
|
||||
if queued>0
|
||||
link = Req1
|
||||
}
|
||||
num_bytes = setup(io, link, samples, num_bytes, sampletype, frequency, volume, position)
|
||||
if num_bytes==0
|
||||
return
|
||||
play_alt = not play_alt
|
||||
BeginIO(io)
|
||||
queued = queued+1
|
||||
; chunk plays (or stays queued) asynchronously.
|
||||
}
|
||||
|
||||
sub wait() {
|
||||
; -- Wait for all queued audio to finish playing.
|
||||
while queued>0
|
||||
wait_oldest()
|
||||
}
|
||||
|
||||
sub stop() {
|
||||
; -- Abort pending playback. Only our own requests are aborted;
|
||||
; CMD_FLUSH would kill other programs' audio too and must not be used here.
|
||||
while queued>0 {
|
||||
^^AHIRequest io = get_slot(wait_alt)
|
||||
exec.AbortIO(io)
|
||||
void exec.WaitIO(io)
|
||||
wait_alt = not wait_alt
|
||||
queued = queued-1
|
||||
}
|
||||
play_alt = false
|
||||
wait_alt = false
|
||||
}
|
||||
|
||||
inline sub is_playing() -> bool {
|
||||
; -- True while chunks are queued but not yet reaped by wait().
|
||||
; Note: a finished chunk still counts until wait() reaps it.
|
||||
return queued>0
|
||||
}
|
||||
|
||||
sub frame_size(long sampletype) -> ubyte {
|
||||
; -- Bytes per sample frame for the given AHIST_ type.
|
||||
when sampletype {
|
||||
AHIST_M8S -> return 1
|
||||
AHIST_M16S, AHIST_S8S -> return 2
|
||||
else -> return 4
|
||||
}
|
||||
}
|
||||
|
||||
private sub get_slot(bool alt) -> ^^AHIRequest {
|
||||
; request block taking the next play/wait (primary or alternate).
|
||||
if alt
|
||||
return Req1
|
||||
return Req0
|
||||
}
|
||||
|
||||
private sub wait_oldest() {
|
||||
; reap the oldest outstanding write.
|
||||
if queued==0
|
||||
return
|
||||
^^AHIRequest io = get_slot(wait_alt)
|
||||
void exec.WaitIO(io)
|
||||
wait_alt = not wait_alt
|
||||
queued = queued-1
|
||||
if queued==0 {
|
||||
play_alt = false
|
||||
wait_alt = false
|
||||
}
|
||||
}
|
||||
|
||||
private sub setup(^^AHIRequest io, pointer link, pointer samples, long num_bytes, long sampletype, uword frequency, long volume, long position) -> long {
|
||||
; -- Fill a request block. Rounds the length down to whole sample frames
|
||||
; (io_Length must be a multiple of the frame size); io_Offset must be 0.
|
||||
; Returns the rounded length (0 = nothing to play).
|
||||
ubyte frame = frame_size(sampletype)
|
||||
num_bytes = num_bytes / frame * frame
|
||||
if num_bytes==0
|
||||
return 0
|
||||
io.Command = exec.CMD_WRITE
|
||||
io.Flags = 0
|
||||
io.Data = samples
|
||||
io.IOStdReq_Length = num_bytes
|
||||
io.Offset = 0
|
||||
io.SampleType = sampletype
|
||||
io.Frequency = frequency
|
||||
io.Volume = volume
|
||||
io.Position = position
|
||||
io.Link = link
|
||||
return num_bytes
|
||||
}
|
||||
|
||||
private asmsub BeginIO(^^AHIRequest io @A1) clobbers (D0, D1, A0, A1, A6) {
|
||||
; Calls the device's BeginIO vector directly, instead of exec DoIO/SendIO,
|
||||
; because those clear io_Flags.
|
||||
%asm {{
|
||||
move.l 20(a1), a6 ; a6 = io_Device (offset 20 in IORequest)
|
||||
jsr -30(a6) ; device BeginIO vector
|
||||
rts
|
||||
}}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
%import shared_amiga_asyncio
|
||||
|
||||
asyncio {
|
||||
%option no_symbol_prefixing, ignore_unused
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
%import shared_amiga_diskfont
|
||||
|
||||
diskfont {
|
||||
%option no_symbol_prefixing, ignore_unused
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
%import shared_amiga_easygfx
|
||||
|
||||
easygfx {
|
||||
%option ignore_unused
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
%import shared_amiga_emudbg
|
||||
|
||||
emudbg {
|
||||
%option ignore_unused
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
%import shared_amiga_lowlevel
|
||||
|
||||
lowlevel {
|
||||
%option no_symbol_prefixing, ignore_unused
|
||||
}
|
||||
@@ -29,6 +29,7 @@ p8_sys_startup {
|
||||
; }
|
||||
|
||||
sub init_system() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
; save CLI arguments
|
||||
move.l d0,-(sp)
|
||||
@@ -89,6 +90,7 @@ proc_CLI = 172
|
||||
}
|
||||
|
||||
sub init_system_phase2() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
moveq #0,d0
|
||||
moveq #0,d1
|
||||
@@ -110,6 +112,7 @@ proc_CLI = 172
|
||||
}
|
||||
|
||||
sub cleanup_at_exit() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
movem.l d0,-(sp) ; keep return code
|
||||
|
||||
@@ -124,6 +127,8 @@ proc_CLI = 172
|
||||
|
||||
if sys.RexxSysBase != 0 exec.CloseLibrary(sys.RexxSysBase)
|
||||
if sys.IFFParseBase != 0 exec.CloseLibrary(sys.IFFParseBase)
|
||||
if sys.AsyncIOBase != 0 exec.CloseLibrary(sys.AsyncIOBase)
|
||||
if sys.LowLevelBase != 0 exec.CloseLibrary(sys.LowLevelBase)
|
||||
if sys.UtilityBase != 0 exec.CloseLibrary(sys.UtilityBase)
|
||||
if sys.IconBase != 0 exec.CloseLibrary(sys.IconBase)
|
||||
if sys.IntuitionBase != 0 exec.CloseLibrary(sys.IntuitionBase)
|
||||
|
||||
@@ -39,19 +39,24 @@ sys {
|
||||
pointer @shared UtilityBase
|
||||
pointer @shared RexxSysBase ; loaded from disk on demand
|
||||
pointer @shared IFFParseBase ; loaded from disk on demand
|
||||
pointer @shared AsyncIOBase ; loaded from disk on demand
|
||||
pointer @shared LowLevelBase ; kickstart 2.0+, loaded from disk on demand
|
||||
pointer @shared DiskfontBase ; loaded from disk on demand
|
||||
pointer @shared TimerBase ; opened on demand
|
||||
pointer @shared AHIBase ; ahi.device base, opened on demand by ahi.open(); for the low-level ahi.* LVO calls
|
||||
|
||||
^^ubyte @shared arguments ; CLI argument string (null-terminated), or NULL if Workbench launch
|
||||
|
||||
sub reset_system() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
move.l 4.w,a6
|
||||
jmp -726(a6) ; ColdReboot()
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub wait(long jiffies @D1) {
|
||||
; --- wait approximately the given number of jiffies (1/60th seconds)
|
||||
asmsub wait(long ticks @D1) {
|
||||
; --- wait approximately the given number of ticks (1/50th seconds, i.e. 20ms each)
|
||||
%asm {{
|
||||
move.l sys.DOSBase,a6
|
||||
jmp -198(a6) ; Delay
|
||||
@@ -68,17 +73,18 @@ sys {
|
||||
}}
|
||||
}
|
||||
|
||||
sub exit(ubyte returnvalue) {
|
||||
asmsub exit(word returnvalue @D0) {
|
||||
; -- exit the program with a return code in D0. When invoked from Prog8 via sys.exit(), all active defers in the call chain are unwound program-wide (LIFO) before system cleanup. sys.reset_system() does not run defers.
|
||||
%asm {{
|
||||
moveq.l #0,d0
|
||||
move.b sys.exit.returnvalue,d0
|
||||
and.l #$ffff,d0
|
||||
move.l p8_sys_startup.orig_stackpointer,sp
|
||||
jmp p8_sys_startup.cleanup_at_exit
|
||||
}}
|
||||
}
|
||||
|
||||
|
||||
sub set_carry() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
; set both C (comparison carry) and X (rotate carry) bits
|
||||
moveq #$11,d0
|
||||
@@ -87,6 +93,7 @@ sys {
|
||||
}
|
||||
|
||||
sub clear_carry() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
; clear C and X bits
|
||||
moveq #0,d0
|
||||
@@ -125,12 +132,96 @@ sys {
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub stack_size() -> long @D0 {
|
||||
; -- Return the amount of stack space still available, in bytes.
|
||||
%asm {{
|
||||
move.l 4.w,a6 ; ExecBase
|
||||
move.l $114(a6),a0 ; ThisTask
|
||||
move.l sp,d0
|
||||
sub.l $3a(a0),d0 ; minus tc_SPLower
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub cpuAtLeast68020() clobbers (A6, D0) -> bool @Pz {
|
||||
%asm {{
|
||||
move.l 4.w,a6
|
||||
move.w 296(a6),d0
|
||||
andi.w #$008e,d0
|
||||
seq d0
|
||||
tst.b d0
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
sub die(uword code, str message) {
|
||||
%asm {{
|
||||
moveq #0,d0
|
||||
move.w 12(a5),d0
|
||||
lea .alert(pc),a1
|
||||
lea .alert_limit(pc),a3
|
||||
move.w #20,(a1)+
|
||||
move.b #16,(a1)+
|
||||
lea .prefix(pc),a2
|
||||
.copy_prefix:
|
||||
move.b (a2)+,d1
|
||||
beq.s .copy_code
|
||||
move.b d1,(a1)+
|
||||
bra.s .copy_prefix
|
||||
.copy_code:
|
||||
move.w 12(a5),d2
|
||||
moveq #3,d4
|
||||
.copy_code_digit:
|
||||
move.w d2,d3
|
||||
lsr.w #8,d3
|
||||
lsr.w #4,d3
|
||||
andi.w #$000f,d3
|
||||
cmpi.w #10,d3
|
||||
bcs.s .copy_code_number
|
||||
addi.w #'A'-10,d3
|
||||
bra.s .copy_code_store
|
||||
.copy_code_number:
|
||||
addi.w #'0',d3
|
||||
.copy_code_store:
|
||||
move.b d3,(a1)+
|
||||
lsl.w #4,d2
|
||||
dbra d4,.copy_code_digit
|
||||
clr.b (a1)+
|
||||
move.b #1,(a1)+
|
||||
move.w #20,(a1)+
|
||||
move.b #32,(a1)+
|
||||
.copy_message:
|
||||
move.l 8(a5),a0
|
||||
.copy_message_loop:
|
||||
cmpa.l a3,a1
|
||||
bhi.s .message_full
|
||||
move.b (a0)+,d1
|
||||
move.b d1,(a1)+
|
||||
bne.s .copy_message_loop
|
||||
clr.b (a1)
|
||||
bra.s .display
|
||||
.message_full:
|
||||
subq.l #1,a1
|
||||
clr.b (a1)
|
||||
clr.b 1(a1)
|
||||
.display:
|
||||
moveq #48,d1
|
||||
movea.l sys.IntuitionBase,a6
|
||||
lea .alert(pc),a0
|
||||
jsr -90(a6)
|
||||
moveq #0,d0
|
||||
move.w 12(a5),d0
|
||||
jmp sys.exit
|
||||
|
||||
even
|
||||
.alert:
|
||||
ds.b 158
|
||||
.alert_limit:
|
||||
ds.b 2
|
||||
.alert_end:
|
||||
.prefix:
|
||||
dc.b "PROGRAM DIED code $",0
|
||||
; !notreached!
|
||||
}}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 348 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user