Compare commits

...
155 Commits
Author SHA1 Message Date
Irmen de Jong b2520db872 add compiler version in assembly file 2026-10-22 19:26:02 +02:00
Irmen de Jong ebe26d0cf9 Fix block alias 2026-10-22 19:08:36 +02:00
Irmen de Jong 63e2a45f09 Require explicit via for linked-list for loops 2026-10-20 01:42:39 +02:00
Irmen de Jong 6076b01f6d Require explicit via for linked-list for loops
Removes the header shorthand, extracts shared validation into
LinkedForLoopChecks.kt, fixes until-validation for pointer sentinels,
and updates docs/examples/tests.
2026-10-20 00:25:27 +02:00
Irmen de Jong d0bf2c3dcb fix doc build for RTD 2026-10-19 23:07:18 +02:00
Irmen de Jong d9bf6f090e antlr4 parser tweaks 2026-10-19 22:57:49 +02:00
Irmen de Jong e08a995067 amiga: get rid of local var address escape warnings 2026-10-19 22:49:03 +02:00
Irmen de Jong da2288db93 improved variable source line position 2026-10-19 22:16:53 +02:00
Irmen de Jong 31b5cddb96 apply new linked list iteration for loop syntax 2026-10-19 22:10:19 +02:00
Irmen de Jong 264b894909 Add linked-list for-loop iteration with via/until syntax 2026-10-19 21:59:56 +02:00
Irmen de Jong 09ae0c6322 Fix 6502 codegen for pointer-to-pointer struct field double-deref 2026-10-19 19:44:44 +02:00
Irmen de Jong 9ca5db3436 fix 6502 pointer deref clobbering bare pointer variable 2026-10-19 04:01:12 +02:00
Irmen de Jong 375214b5e3 Add emudbg library module for amiga500/amiga1200 targets
New Prog8 standard library module for emulator debug output on the Amiga
targets, targeting the Copperline emulator (with partial WinUAE support).
2026-10-19 01:29:04 +02:00
Irmen de Jong 1da37e4119 skill 2026-10-17 23:45:22 +02:00
Irmen de Jong 16f7d588a7 add '-emu2' for amiga targets, -emu now launches the actual 'copperline' emulator, -emu2 still launches vamos. 2026-10-17 14:01:03 +02:00
Irmen de Jong f6c8a3f2ed defer: keep static per-subroutine mask, fix m68k carry bug, document recursion limitation
Roll back the per-activation defer-mask change because its code/RAM cost
outweighed the benefit. The pending-defer state is again a single static
mask variable per subroutine.

Keep the m68k bugfix: the handler now tests each defer bit with
`mask & bit != 0` instead of `mask >>= 1; bcc`, because the `>>=` store
on m68k uses MOVE which clears carry before the branch can read it.

Document that recursive/mutually recursive subroutines containing defer
are not supported on stack-frame targets, and remove the recursion-sound
defer todo item.
2026-10-17 05:00:02 +02:00
Irmen de Jong 5619898113 reduce test memory footprint 2026-10-17 02:43:39 +02:00
Irmen de Jong ae0414631e fix compiler crash on peek/poke/popp used in the wrong statement/expression context
fixes #248
2026-10-17 02:27:51 +02:00
Irmen de Jong e3ca9ae4a8 Fix virtual target typed pop results 2026-10-17 02:12:17 +02:00
Irmen de Jong 38c60a2d75 harden AstWalker detached-subtree skipping and extract shared canSkipExplicitCmpi helper 2026-10-17 01:46:35 +02:00
Irmen de Jong 5e83c44300 fix wrong result for short-circuit boolean and/or on the virtual target 2026-10-17 01:43:08 +02:00
Irmen de Jong 99ff5768c5 fix compiler crash on 'not' combined with or/and in one expression
cherry-picked from v12 maintenance b71787a929
2026-10-17 01:18:09 +02:00
Irmen de Jong fba4bb5272 6502 codegen bug: subtraction with a register-clobbering right operand 2026-10-16 00:58:49 +02:00
Irmen de Jong 17322a08ec 6502 math: convert lerp/lerpw/interpolate/interpolatew to 6502 assembly 2026-10-16 00:07:46 +02:00
Irmen de Jong 06d2f9ebbe when->on..goto: calibrate the jump-table sizeLimit per target 2026-10-15 22:10:52 +02:00
Irmen de Jong 3a5f2c33ab when->on..goto: allow arbitrary choice bodies by generating labels and jump tables 2026-10-15 20:23:02 +02:00
Irmen de Jong d523ecd1e1 when->on..goto: synthesize a gap label so gapped value ranges also use jumpi tables instead of the compare chain 2026-10-15 04:57:02 +02:00
Irmen de Jong 159634a99f Fix m68k frame teardown for intra-subroutine dispatch 2026-10-15 04:29:33 +02:00
Irmen de Jong 55dcb45230 when->on..goto: let the else part be any body, not just a single goto
Only the value choices have to be goto's, since those become entries in the
jump table. The else is taken over verbatim as the on..goto's else part, so a
common `else -> { ... }` body no longer disqualifies the whole when.
2026-10-15 04:05:45 +02:00
Irmen de Jong de75342198 fix when->on..goto rewrite for signed-byte conditions and non-zero-based values. Also made it smarter to allow a nonzero start value index 2026-10-15 03:29:02 +02:00
Irmen de Jong 30d3b76787 LSP: extract more document symbols
SymbolExtractor now also emits symbols for enum members, struct
fields, aliases, subroutine parameters, local variables and labels,
and finds labels/declarations nested in control-flow bodies.
Also refresh the LSP feature list in the introduction docs.
2026-10-15 01:47:45 +02:00
Irmen de Jong 8bbe5bd209 label names hash numbers are now stable 2026-10-14 19:35:49 +02:00
Irmen de Jong da7ea294ef todo cleanup 2026-10-14 01:29:22 +02:00
Irmen de Jong 05d8acc4bb optimized VM initialization and update Mordant library to fix a FFM crash 2026-10-13 22:48:29 +02:00
Irmen de Jong 6cac707fd1 code cleanups 2026-10-13 22:40:19 +02:00
Irmen de Jong dea43c99bb m68k: remove word packed args plan doc 2026-10-13 21:42:57 +02:00
Irmen de Jong 0b902f06b5 m68k: word-packed stack argument slots 2026-10-13 21:37:43 +02:00
Irmen de Jong 733a3fb752 add missing helper routines doc 2026-10-13 17:49:31 +02:00
Irmen de Jong 460adb9e02 Remove Exec-specific for-loop over linked lists, disallow pointer as struct field name 2026-10-13 01:01:24 +02:00
Irmen de Jong 3c22dfc11a fix compiler crash on indexed reads/writes through ^^pointer struct fields 2026-10-13 00:45:21 +02:00
Irmen de Jong dfb3257c04 reserve the 'pointer' datatype name, it can no longer be used as a symbol name 2026-10-13 00:00:26 +02:00
Irmen de Jong 8bcd176746 fix invalid parsing of ^^pointer 2026-10-12 20:45:12 +02:00
Irmen de Jong fd3add76e4 rename amiga_gfx to easygfx 2026-10-11 14:39:37 +02:00
Irmen de Jong 4d0a7ff8bf added amiga/tictactoe example 2026-10-11 06:42:29 +02:00
Irmen de Jong e1f2faabe7 added diskfont.p8 2026-10-11 05:33:47 +02:00
Irmen de Jong 25bd60368a fix undefined symbol error 2026-10-11 03:23:49 +02:00
Irmen de Jong f61c1dc7ef fix mouse click offsets and aspect ratio 2026-10-11 02:41:42 +02:00
Irmen de Jong eda21a6ebf Fix in operator on arrays larger than 255 elements 2026-10-11 02:05:21 +02:00
Irmen de Jong c0267e37b0 added amiga_gfx module and amiga/gfxdemo example 2026-10-11 01:03:59 +02:00
Irmen de Jong 78f9df203c inline math trig routines 2026-10-10 20:46:10 +02:00
Irmen de Jong d3e10b4974 m68k: optimize log2 and gcd routines, added log2l 2026-10-10 19:36:04 +02:00
Irmen de Jong c4679bd8af m68k: optimize lerp/interpolate/CRC routines; add interpolatew 2026-10-10 18:28:12 +02:00
Irmen de Jong e7dfe79919 m68k: add log2, diff, lerp, interpolate, gcd and crc16/32; fix qemu68k word shifts and const-eval of log2/atan2 2026-10-10 17:49:29 +02:00
Irmen de Jong 31dbbc3fe1 m68k: implement bcd library routines 2026-10-10 15:41:32 +02:00
Irmen de Jong 55b7239553 improve skills 2026-10-10 14:19:38 +02:00
Irmen de Jong 5702e5aa2a 14.0-beta2 release 2026-10-10 13:25:11 +02:00
Irmen de Jong 592637856f m68k: cleaner recursive defer errors 2026-10-10 02:02:25 +02:00
Irmen de Jong 233ca21492 m68k: frameable inline asm, word-sized sys.exit, and sys.die alerts 2026-10-10 00:19:22 +02:00
Irmen de Jong 1a84616363 update skills 2026-10-09 20:06:56 +02:00
Irmen de Jong d32b70bb75 m68k: tear down frames on external conditional branches 2026-10-08 20:50:39 +02:00
Irmen de Jong 73241b0261 m68k: tear down frames before absolute jumps 2026-10-08 20:41:35 +02:00
Irmen de Jong e515dee561 m68k: suggest @shared for locals referenced from inline assembly 2026-10-08 19:08:27 +02:00
Irmen de Jong 3d78415088 better vm error for %asm 2026-10-08 04:37:19 +02:00
Irmen de Jong 0189903a63 m68k: pin @shared variables to static storage 2026-10-08 04:17:40 +02:00
Irmen de Jong 9c9552174e m68k: stackful coroutines fixed with per-task stacks, new %option noframe 2026-10-08 01:59:50 +02:00
Irmen de Jong be61d37fd1 update gradle wrapper 2026-10-08 00:30:07 +02:00
Irmen de Jong 70bb199e12 m68k: pop frame on jumps leaving subroutine; reject dispatch to subs with parameters 2026-10-07 23:46:48 +02:00
Irmen de Jong 09737187ad fix %jmptable crashes on expression and unqualified entries 2026-10-07 22:25:23 +02:00
Irmen de Jong de39533fba qemu68k: new recursive maze solver example 2026-10-07 19:06:16 +02:00
Irmen de Jong 8bc2b177a0 m68k: made textelite native recursion 2026-10-07 19:06:16 +02:00
Irmen de Jong 09eba995ae m68k: storing or returning the address of a variable in call stack frame is now an error 2026-10-07 19:06:16 +02:00
Irmen de Jong b739d7b663 m68k: print call arg names in asm comments 2026-10-07 19:06:16 +02:00
Irmen de Jong 5e709fca7e m68k: stack-frame zero-clear coalescing 2026-10-07 19:06:16 +02:00
Irmen de Jong cc8df33418 m68k: plan for optimized stack frame zeroing 2026-10-07 19:06:16 +02:00
Irmen de Jong 125af552ff amiga: added recursive fractal-tree example 2026-10-07 19:06:16 +02:00
Irmen de Jong 29254e892b m68k: no longer print recursion warnings 2026-10-07 19:06:16 +02:00
Irmen de Jong a1a48405b0 m68k: fix a5 frame pointer register clobbering 2026-10-07 19:06:16 +02:00
Irmen de Jong acc8705ea8 bugfix for m68k stack memory model 2026-10-07 19:06:16 +02:00
Irmen de Jong bbff472457 doc cleanup 2026-10-07 19:06:16 +02:00
Irmen de Jong 353b0c84b7 slice 5 for m68k stack memory model
(slice 4 was skipped)
2026-10-07 19:06:16 +02:00
Irmen de Jong 329b3df4a3 slice 3 for m68k stack memory model 2026-10-07 19:06:16 +02:00
Irmen de Jong fe90ca572d slice 2 for m68k stack mememory model 2026-10-07 19:06:16 +02:00
Irmen de Jong 26bbd6c1fe vertical slice prototype for m68k stack mememory model 2026-10-07 19:06:16 +02:00
Irmen de Jong 822cfd4f35 tweak textelite 2026-10-07 02:24:17 +02:00
Irmen de Jong 0807d07ee5 cleanups 2026-10-07 02:00:31 +02:00
Irmen de Jong 2e3aa76a2f m68k: improve subroutine inlining 2026-10-07 01:17:14 +02:00
Irmen de Jong 3beb565c84 m68k targets: add sys.stack_size() function that gives remaining stack size in bytes 2026-10-07 00:39:11 +02:00
Irmen de Jong b1642f4f7a m68k: fix some subtle memset/memclear errors 2026-10-06 22:28:40 +02:00
Irmen de Jong 1f8b42fae5 make user docs and some code comments less convoluted 2026-10-05 20:00:32 +02:00
Irmen de Jong 13828ea932 fix defer stack-save ordering for multi-value returns 2026-10-04 07:55:51 +02:00
Irmen de Jong e550143ed0 amiga: added dos.read_file helper routine to read complete files 2026-10-04 04:46:33 +02:00
Irmen de Jong 47e6066a36 fix sizeof for large structs 2026-10-04 04:44:08 +02:00
Irmen de Jong 266f4ab380 amiga: fixed lowlevel-timer example 2026-10-04 02:53:35 +02:00
Irmen de Jong e5954542dd mention stack alignment rule in m68k skill 2026-10-04 02:31:23 +02:00
Irmen de Jong 8c2bc15b41 Fix IR peephole crash on lsigb.l + ext.b with mismatched register types
regression was introduced in commit 2ce7a63c1e
2026-10-03 14:01:56 +02:00
Irmen de Jong c9a7dc05d9 better error message 2026-10-03 02:08:57 +02:00
Irmen de Jong 9d023c9568 fix .p8ir file system case (in)sensitivity fragility 2026-10-02 23:41:42 +02:00
Irmen de Jong a1ce45e409 doc updates 2026-10-02 23:24:07 +02:00
Irmen de Jong 09d469714b whole enum aliasing (by aliasing each member) 2026-10-02 22:53:25 +02:00
Irmen de Jong 38db571f20 implemented -o / --output to specify specific output file 2026-10-02 22:33:43 +02:00
Irmen de Jong 5f3b587af1 docs
outputfile todo
2026-10-02 21:28:18 +02:00
Irmen de Jong e74c34a5ab fix builtins rejecting a cx16 virtual register as their result register 2026-10-01 02:54:53 +02:00
Irmen de Jong 2b156ec652 amiga: add lowlevel.library timer example 2026-09-30 22:56:29 +02:00
Irmen de Jong e38f71fd2d keep routines referenced from struct instance initializers, so that unusedcoderemover doesn't strip them 2026-09-30 21:33:26 +02:00
Irmen de Jong 6c107379df fix broken struct-array initializer prefixing for address-of arguments
fixes #246
2026-09-30 21:15:32 +02:00
Irmen de Jong 99f29cb9b9 fix broken min/max rewrite when the target aliases an operand.
fixes #247
2026-09-30 21:15:32 +02:00
Irmen de Jong d6ea0782e2 amiga: added lowlevel.library support 2026-09-30 19:23:32 +02:00
Irmen de Jong bdee93dc8f eliminate redundant lea for repeated indexed array access 2026-09-28 21:51:03 +02:00
Irmen de Jong 3d675a460e don't fold signed division by a power of two into a shift 2026-09-28 21:22:01 +02:00
Irmen de Jong bd1747926c slightly optimize long multiplication 2026-09-28 02:29:25 +02:00
Irmen de Jong cff7732189 fix unsound index-reload peephole and extend it to two indexed accesses 2026-09-28 00:48:00 +02:00
Irmen de Jong fa966086e9 corrections 2026-09-28 00:39:57 +02:00
Irmen de Jong ec4b47ac0d support custom m68k targets: select the codegen by CpuType and allow a per-target link.ld 2026-09-27 11:50:33 +02:00
Irmen de Jong 5222562493 move sqr range check into prog8_math.square instead of inlining it at each call site 2026-09-27 11:49:49 +02:00
Irmen de Jong c8cda7947a 6502 optimization for uword * ubyte 2026-09-26 05:15:41 +02:00
Irmen de Jong ffe855f295 x*x correctness fix 2026-09-26 04:57:40 +02:00
Irmen de Jong 8a66dabf6d adjust benchmark weights and bump benchmark version number 2026-09-26 04:49:28 +02:00
Irmen de Jong b8c975704a 6502 byte shift optimization for const multiplications 2026-09-26 03:25:25 +02:00
Irmen de Jong 2ef0ddd42c 6502 word shift optimization for const multiplications 2026-09-26 02:45:57 +02:00
Irmen de Jong 9a83b7455a possible 6502 codegen optimization opportunities 2026-09-25 23:58:59 +02:00
Irmen de Jong 9fce07565c some romable tests, fix romable callfar crash and update stale VM label address test 2026-09-25 22:40:36 +02:00
Irmen de Jong d703d678b4 fix VM label-valued initializers 2026-09-25 02:22:19 +02:00
Irmen de Jong 90a7c41586 fix IR symbol prefixing 2026-09-25 02:04:43 +02:00
Irmen de Jong b1a9ba929d remove ARRAY_POINTER and make pointer arrays target-independent 2026-09-25 01:18:32 +02:00
Irmen de Jong 75ab3ccfbd amiga1200: added ahi library and demo programs 2026-09-25 00:03:50 +02:00
Irmen de Jong c031737da0 fixup docs about amiga targets (missing amiga1200 mostly) 2026-09-24 22:31:03 +02:00
Irmen de Jong 2f905ef6b6 update reg design 2026-09-24 01:23:33 +02:00
Irmen de Jong 4cbf93da85 fix Status flag extraction clobbers a register return value 2026-09-24 01:00:49 +02:00
Irmen de Jong 976b64185d fix struct size truncation to 256 bytes on targets that allow larger structs (m68k) 2026-09-24 00:22:23 +02:00
Irmen de Jong 2050fcb4ee m68k: emit struct field definitions in the asm 2026-09-23 23:55:57 +02:00
Irmen de Jong 69525ae67f 6502 codegen: emit _size symbol for struct and union memory size 2026-09-23 23:14:11 +02:00
Irmen de Jong 4eaba62f1e 6502 codegen: inline clamp() on word/uword when having compile-time constant bounds 2026-09-23 22:54:57 +02:00
Irmen de Jong bf696bcf5e fix multiple status flag returns in new codegens 2026-09-23 03:10:59 +02:00
Irmen de Jong 34bbb46514 fix a few new6502 codegen bugs 2026-09-23 01:27:40 +02:00
Irmen de Jong f086918e6f add union types: structs where every field is at offset 0, thus saving memory 2026-09-23 00:07:44 +02:00
Irmen de Jong 521f7ffbe6 tweak amiga audio 2026-09-22 22:49:09 +02:00
Irmen de Jong 2d070aaec4 Fix 6502 pointer-field augmented assignment clobbering scratch pointer
Fixes #245
2026-09-21 22:51:11 +02:00
Irmen de Jong 044a4a1789 amiga: added trackdisk device support 2026-09-21 00:13:17 +02:00
Irmen de Jong 6de6a81d4b amiga: added asyncio.library support 2026-09-20 05:12:10 +02:00
Irmen de Jong 815f4109a5 cx16/gfx_lores: optimize text() by setting VERA address once per character 2026-09-19 22:06:05 +02:00
Irmen de Jong a49f080ca7 cx16/gfx_hires: optimize text() by hoisting color dispatch and using local tables 2026-09-19 19:04:20 +02:00
Irmen de Jong ac4ae0440f vera64 restructure 2026-09-19 17:50:30 +02:00
Irmen de Jong 773f1f2292 more vera64 code 2026-09-19 16:25:52 +02:00
Irmen de Jong efee8bde68 added preliminary VERA64 support library for the c64 target
vera64 = a board for the c64 containing a VERA graphics adapter
2026-09-19 00:14:11 +02:00
Irmen de Jong ddfdd9097d fix adpcm private symbols 2026-09-18 22:40:59 +02:00
Irmen de Jong ef64e2f9a4 fix typed pointer arithmetic p2-p1 to yield the number of elements. fixes #244 2026-09-18 21:22:07 +02:00
Irmen de Jong 2f6ab002d8 amiga: adpcmbench, m68k adpcm decoder optimized further, fix wavc output byte order format 2026-09-18 03:46:00 +02:00
Irmen de Jong ba76fbfe85 fix code generation for "return <void function call>" 2026-09-17 22:39:53 +02:00
Irmen de Jong 7f3b2e8aa6 m68k asm versions of adpcm decoder routines, now byte at a time instead of nibble 2026-09-17 21:53:13 +02:00
Irmen de Jong f254fc2b62 m68k asm versions of adpcm decoder routines 2026-09-17 02:53:48 +02:00
Irmen de Jong 457d55b392 fix potential endless loop in wavfile library, also made a correct implementation of it for m68k targets (little/big endianness problem resolved) 2026-09-17 00:04:34 +02:00
Irmen de Jong b21a984dbb even more optimized adpcm decoding routine 2026-09-16 19:04:49 +02:00
Irmen de Jong 7427e6ddad changed adpcm decoder by a table driven one, almost twice as fast now 2026-09-16 05:03:13 +02:00
Irmen de Jong 192e0086f9 update plans 2026-09-16 00:52:40 +02:00
Irmen de Jong c8972c9561 add jdk option --enable-native-access=ALL-UNNAMED to manifest and launcher scripts to suppress newer JDK warning about JNI/JNA calling native libraries 2026-09-14 23:10:12 +02:00
348 changed files with 27350 additions and 5339 deletions

No files matched your search

+65
View File
@@ -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`.
+47
View File
@@ -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`
+50 -22
View File
@@ -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.
+1
View File
@@ -14,6 +14,7 @@ build/
out/
output/
gradle/
Libs/
.gradle/
.kotlin/
.git/
+4 -1
View File
@@ -45,4 +45,7 @@ compiler/lib/
.gradle
sd*.img
*.d64
AsyncIO/
AsyncIO.info
m68k-amigaos-ahi/
Libs/
-1
View File
@@ -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>
+2 -2
View File
@@ -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>
+59 -12
View File
@@ -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
+20 -3
View File
@@ -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
View File
@@ -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
+5 -7
View File
@@ -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 }
}
}
}
+107 -99
View File
@@ -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")
}
+6 -5
View File
@@ -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()
+5 -6
View File
@@ -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/")
}
}
}
+5 -4
View File
@@ -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/")
}
}
}
+6 -5
View File
@@ -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()
+16 -42
View File
@@ -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")
}
+11 -9
View File
@@ -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")
}
@@ -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")
}
}
+109 -1
View File
@@ -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
+1
View File
@@ -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")
+10
View File
@@ -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
+199 -16
View File
@@ -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)
}
}
+8 -2
View File
@@ -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
}
})
+473
View File
@@ -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
}
})
+1
View File
@@ -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)
+168 -23
View File
@@ -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
}
}
}
+47 -1
View File
@@ -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
}
+5
View File
@@ -0,0 +1,5 @@
%import shared_m68k_adpcm
adpcm {
%option ignore_unused, private_symbols
}
+377
View File
@@ -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)
+96 -5
View File
@@ -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