Compare commits

..
522 Commits
Author SHA1 Message Date
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
Irmen de Jong 7ba2ee9857 added named struct field initializers 2026-09-14 00:56:22 +02:00
Irmen de Jong a025d68291 added %assert expression, "message" 2026-09-13 23:04:56 +02:00
Irmen de Jong 81794aded3 use Mordant library to make some terminal output more sophisticated
this library is already used by the Clikt option parser library so no new dependency
2026-09-13 21:28:32 +02:00
Irmen de Jong 9621552056 add 'arena' allocator library
6502 targets use uword sizes with no alignment overhead; m68k/virtual use
long sizes with even-address alignment. Updated the six pointer examples that used to have their own arena implementation.
2026-09-13 05:43:57 +02:00
Irmen de Jong 5685a1f229 6502: tighter for loop code for word and long loop vars 2026-09-13 04:29:59 +02:00
Irmen de Jong b014c78c94 m68k: better error when trying to use cx16 virtual register, those don't exist anymore 2026-09-13 04:28:25 +02:00
Irmen de Jong ca729d279e update history and syntax files 2026-09-12 23:14:50 +02:00
Irmen de Jong 3b0faa749a Merge branch 'master' into structured-IR
Port end-exclusive for-loop codegen and M4 loop-counter peephole to the
structured operand API (IRInstructions factories, memory?.symbolName,
labelTarget, requireDest/requireSrcA).
2026-09-12 00:48:30 +02:00
Irmen de Jong 22a2f8cbc5 IR: end-exclusive increment+compare for step +-1 for-loops; fix DECM/BSTPOS on VM 2026-09-12 00:07:02 +02:00
Irmen de Jong 468850bc47 structured IR 2026-09-11 20:20:40 +02:00
Irmen de Jong 20374f4059 Merge branch 'v12maintenance'
# Conflicts:
#	compilerAst/src/prog8/ast/antlr/Antlr2KotlinVisitor.kt
#	compilerAst/src/prog8/ast/statements/AstStatements.kt
#	examples/test.p8
2026-09-11 19:37:10 +02:00
Irmen de Jong ceffa4d83d todos 2026-09-11 19:25:43 +02:00
Irmen de Jong dbba6a94fa Merge branch 'v12maintenance'
# Conflicts:
#	codeGenCpu6502/src/prog8/codegen/cpu6502/assignment/AssignmentAsmGen.kt
#	compiler/src/prog8/compiler/astprocessing/AstIdentifiersChecker.kt
#	compiler/test/ast/TestVariousCompilerAst.kt
#	compiler/test/codegeneration/TestExecution6502.kt
#	examples/test.p8
#	gradle.properties
2026-09-10 21:45:47 +02:00
Irmen de Jong 302e4bdf8e m68k targets are experimental not for production code use 2026-09-10 20:41:00 +02:00
Irmen de Jong 4c65049817 proper error message when using a builtin function name as block name 2026-09-09 04:05:31 +02:00
Irmen de Jong a7cbc2c5bf Review fixes: pointer read desugaring and poke->assignment optimizer 2026-09-09 02:31:21 +02:00
Irmen de Jong d51ca62431 Fix poke->assignment optimizer dropping AddressOf index
StatementOptimizer rewrote pokel(&arena[1], x) to arena = x, ignoring
AddressOf.arrayIndex (and dereference), which miscompiled implicit
indexed-base struct field writes such as arena[1].s = X. All four
poke/pokew/pokel/pokef rewrites now require a plain variable address.
2026-09-09 02:05:20 +02:00
Irmen de Jong 9ac60eec40 fix some more pointer syntax related issues 2026-09-09 01:33:43 +02:00
Irmen de Jong c55b16f4cb todo 2026-09-09 01:01:16 +02:00
Irmen de Jong bd0457f262 Allow const expressions for 2D array dimensions (not just numeric literals) 2026-09-08 23:13:54 +02:00
Irmen de Jong ce2b298d6f code cleanups 2026-09-08 22:36:54 +02:00
Irmen de Jong cf7afaf77a update libraries, update to kotlin 2.4.20 2026-09-08 21:41:53 +02:00
Irmen de Jong 54507ad9b3 Merge branch 'v12maintenance'
# Conflicts:
#	gradle.properties
2026-09-07 19:19:52 +02:00
Irmen de Jong 619a215a87 fix test size expectation 2026-09-07 19:14:58 +02:00
Irmen de Jong c12e27dacc cx16: added verafx-accelerated line routine, added cobramk3 example that uses it next to the cpu only version 2026-09-06 22:51:12 +02:00
Irmen de Jong db32b81d7a m68k: d0 caching 2026-09-06 20:32:16 +02:00
Irmen de Jong 687d86d36e added amiga1200 target (68020 + FPU) 2026-09-06 18:25:15 +02:00
Irmen de Jong e074fe614f avoid extremely long array data source lines, and redundant label comments 2026-09-06 16:18:31 +02:00
Irmen de Jong bbfb09e467 IR: uniform traversal 2026-09-05 00:06:30 +02:00
Irmen de Jong bc62b2de81 IR: fix CONCAT register direction 2026-09-04 19:42:31 +02:00
Irmen de Jong 58f544fe14 Implement counted-loop IR (IRLoopChunk)
Replace the early lowering of constant repeat/for loops into p8_regfile
counter sequences with a first-class IRLoopChunk that carries the trip
count and body. This removes the fragile REGFILE-DEPENDENT m68k dbra
peephole and gives each backend explicit control.
2026-09-04 00:01:00 +02:00
Irmen de Jong 7dc05688bf Add peephole optimization to fold array[variable+const] index offsets
Adds three new passes to the 6502 assembly optimizer:

- optimizeFoldIndexOffset (byte/bool arrays)
- optimizeFoldIndexOffsetSplitWord (split word arrays)
- optimizeFoldIndexOffsetNonSplitWord (@nosplit word arrays)

fixes github issue #234
2026-09-03 21:36:29 +02:00
Irmen de Jong aac7d409dc split AssignmentAsmGen into 3 helper objects to reduce massive file size 2026-09-03 03:39:29 +02:00
Irmen de Jong 0771105694 fix sorting module on virtual target 2026-09-03 03:39:07 +02:00
Irmen de Jong 9a461628f2 Merge branch 'v12maintenance' 2026-09-02 19:14:57 +02:00
Irmen de Jong b0f1f655c3 codeGenIntermediate: fold struct-array field access to single LOADX/STOREX
Replace arr[idx].field (arr:Struct[Sz], idx variable) from
EXT+MUL #Sz + ADDR + ADD #fieldOff + LOADI/STOREI (5 ops) to
single indexed op: LOADX/STOREX arr+fieldOff,S=Sz and
STOREZX for zero. Reuses existing ,S=N + +off encoding.
2026-09-02 01:35:30 +02:00
Irmen de Jong 59c41ad129 ir: add scaled indexing (S=) to LOADX/STOREX/STOREZX
Remove byte-offset pre-scaling from IR generation and encode
element index + explicit scale in the IR.  Effective address is
base + index*scale + disp (disp = symbolOffset).
2026-09-02 00:34:23 +02:00
Irmen de Jong 60eeaba791 streamline IR optimization proposals 2026-09-01 23:15:03 +02:00
Irmen de Jong 30266c1b92 Merge branch 'v12maintenance'
# Conflicts:
#	gradle.properties
2026-09-01 22:22:39 +02:00
Irmen de Jong 9d537ab3c0 warn when %zpreserve/%zpallowed excludes fixed zeropage scratch locations 2026-09-01 04:21:49 +02:00
Irmen de Jong 1ba08ec6ec make -dumpvars work on all compilation targets 2026-09-01 03:51:13 +02:00
Irmen de Jong 199c3e4efb Merge branch 'new-codegens' 2026-08-30 23:54:10 +02:00
Irmen de Jong a7c035f4d1 attempt to fix github runner OOM 2026-08-30 23:44:05 +02:00
Irmen de Jong 05cbe74a60 attempt to fix github runner OOM 2026-08-30 23:04:47 +02:00
Irmen de Jong d652d7a924 Merge branch 'new-codegens'
# Conflicts:
#	gradle.properties
2026-08-30 22:47:38 +02:00
Irmen de Jong 9c53791ef4 m68k: optimize small memcopies to inline copy 2026-08-30 21:35:48 +02:00
Irmen de Jong 420a9b7249 m68k: optimize load of constant args into CPU registers for subroutine call 2026-08-30 19:20:12 +02:00
Irmen de Jong 9bf8318b1c Support arrays of struct instances
- Parser: parse `StructType[N] var` via elementToArray() for
  STRUCT_INSTANCE (Prog8ANTLR.g4, Antlr2KotlinVisitor)
- Desugar: struct-instance bases via addressOfStructInstance(),
  whole-struct `a[i]=a[j]` -> sys.memcopy, inferred `[[...]]`
  and explicit `Struct : [...]` (without ^^) for value arrays;
  reject `^^Struct : [...]` in instance arrays
- 6502: computeStructArrayElementAddress() with 16-bit
  index*structSize scaling (AssignmentAsmGen, AsmGen)
- IR/VM/m68k/new6502: STRUCTDEFS round-trip, loaders, gens
- AstChecker/IdentifiersChecker: reject struct fields that are
  arrays of structs; fix isArray guards
- Docs: structpointers.rst/variables.rst, todo.rst, examples/test.p8
- Tests: TestArraysOfStructs (pointer vs value init, IR round-trip,
  16-bit strides)
2026-08-30 18:24:14 +02:00
Irmen de Jong 94822b8662 Add arrays of struct instances (StructType[N] vars)
Parser: accept `Type[N] name` between type and identifier
(Prog8ANTLR.g4, Antlr2KotlinVisitor) via elementToArray()
for STRUCT_INSTANCE; CodeDesugarer: handle struct-instance
bases, addressOfStructInstance(), whole-struct memcopy
(points[a]=points[b]), and fix variable-index 16-bit scaling
in 6502 AssignmentAsmGen.computeStructArrayElementAddress();
IR: preserve STRUCTDEFS round-trip, VM loader support;
m68k/new6502/intermediate backends; docs + todo updated.
Limitations: not as struct fields (AstChecker/
AstIdentifiersChecker error) nor as sub params.
2026-08-30 14:14:49 +02:00
Irmen de Jong b0d50c2a68 preserve block comments in compiler AST and AST output 2026-08-30 02:33:30 +02:00
Irmen de Jong deae2e6422 Unify variable and memory slab relocation with vars address configuration 2026-08-29 01:17:44 +02:00
Irmen de Jong c8d29e6dc9 ir: more IR peephole optimizations 2026-08-29 00:29:50 +02:00
Irmen de Jong a7b14c2931 ir: more optimized inplace array assignments 2026-08-28 23:38:54 +02:00
Irmen de Jong a14a8f4e62 ir: deduplicate index canonicalization and strengthen regression tests 2026-08-28 23:00:51 +02:00
Irmen de Jong cf50372515 ir: canonicalize LOADX/STOREX/STOREZX index width and remove guessed-type workaround 2026-08-28 22:23:55 +02:00
Irmen de Jong da0df8f133 amiga: use defer for resource freeing, change custom.grab_system and restore_system value storage so they are now regular subroutines instead of inline 2026-08-28 21:27:02 +02:00
Irmen de Jong 27070f31e8 Merge branch 'master' into new-codegens
# Conflicts:
#	codeCore/src/prog8/code/core/DataTypes.kt
#	docs/source/todo.rst
2026-08-28 05:23:30 +02:00
Irmen de Jong 277e9c09bb making defer program-wide: also unwinds at program exit()/return from start() 2026-08-28 05:10:09 +02:00
Irmen de Jong 70521ce301 amiga: fix modplay file close 2026-08-27 23:05:13 +02:00
Irmen de Jong 93b5faa038 m68k: revert LSIGB/LSIGW direct p8_regfile loads - caused code growth 2026-08-27 19:49:21 +02:00
Irmen de Jong f33814a594 m68k: fuse LOADX+JUMPI/CALLI in asm peephole and document %jmptable as 6502-only 2026-08-27 04:50:07 +02:00
Irmen de Jong 986ae35821 m68k: optimize LSIGB/LSIGW to direct p8_regfile byte loads 2026-08-27 03:29:56 +02:00
Irmen de Jong 937b520838 core: fix POINTER equalsSize target-aware for 32-bit 2026-08-27 03:15:57 +02:00
Irmen de Jong 250ddda11b ir,m68k: add LOADP_INC/STOREP_INC for pointer post-inc (a0)+ 2026-08-27 02:57:54 +02:00
Irmen de Jong bde52cc134 ir: fix byte->long/pointer extension for 32-bit targets (single-type EXTL) 2026-08-26 23:55:17 +02:00
Irmen de Jong 8e739febf0 ir: m68k status-flag fusion - skip redundant CMPI #0 on ops that set Z 2026-08-26 23:41:36 +02:00
Irmen de Jong 3fcb4b4262 ir: canonicalize LOADX index width, 2026-08-26 23:18:06 +02:00
Irmen de Jong 56643f43df tweak m68k tests and docs 2026-08-26 22:31:05 +02:00
Irmen de Jong a9cad9b51d fix some problems in several examples 2026-08-26 00:49:02 +02:00
Irmen de Jong 8647b136bd 6502: fix constant byte store via pointer+const offset storing garbage
fixes regression from commit 102992911
2026-08-25 23:59:41 +02:00
Irmen de Jong 4179f2c571 disable RMB-menus, doc updates 2026-08-25 03:31:15 +02:00
Irmen de Jong 02a118ec0e deref assignment targets: use target pointer width for address temps and IR casts 2026-08-24 23:38:45 +02:00
Irmen de Jong 561f31329d support pointer assignment targets like (expr as ^^Struct).field = v and func().field = v (grammar + CodeDesugarer poke desugar, IR pointer-cast widening, tests/docs) 2026-08-24 23:00:23 +02:00
Irmen de Jong c3920de251 update to new ksim65 version 2026-08-24 00:25:01 +02:00
Irmen de Jong 14bf07907b amiga: tweak modplay and ptplayer library to be able to supply playback feedback 2026-08-23 23:00:33 +02:00
Irmen de Jong c657935f10 m68k: fix passing 0 in m68k address register codegen 2026-08-23 22:30:50 +02:00
Irmen de Jong c08def79ae Merge branch 'master' into new-codegens 2026-08-23 21:04:23 +02:00
Irmen de Jong 4199ab4200 fix broken unit test dependency on vasm 2026-08-23 16:07:27 +02:00
Irmen de Jong fe6850816c m68k: restore zero-extension for ADDR/SUBR widening and byte array indices
this broke in d978be2f9d
2026-08-23 15:34:17 +02:00
Irmen de Jong eff1a9fc53 m68k: loop dbra optimization 2026-08-23 14:58:44 +02:00
Irmen de Jong 5857382242 add %bssaddress and %slabsaddress directives to directly control the addresses of those segments 2026-08-23 14:16:06 +02:00
Irmen de Jong dfda43e747 more 6502 optimizations 2026-08-23 10:18:05 +02:00
Irmen de Jong 1029929111 6502 optimizations 2026-08-23 04:15:47 +02:00
Irmen de Jong ae55109a18 m68k: peepholes 2026-08-23 03:24:20 +02:00
Irmen de Jong 2ce7a63c1e IR: resolve loadx index-width type conflicts in usedRegisters; add conversion-collapse peephole 2026-08-23 02:41:12 +02:00
Irmen de Jong da6934ea33 m68k: prefer BSR/BRA over JSR/JMP 2026-08-23 01:09:15 +02:00
Irmen de Jong 2c8ccb706e added sfxgen library 2026-08-23 00:39:55 +02:00
Irmen de Jong 0d0d707976 Merge branch 'master' into new-codegens
# Conflicts:
#	simpleAst/src/prog8/code/ast/AstExpressions.kt
2026-08-23 00:25:27 +02:00
Irmen de Jong 79dd8d6b92 vm: rnd() is now identical to the xorshift algorithm of the other compilation targets
m68k: implemented clamp IR syscalls
new6502: implement EXTL
2026-08-22 22:29:41 +02:00
Irmen de Jong f750caa833 68k: fix vasm parse issue with .b, .l etc symbols -> now name-mangled 2026-08-22 21:46:16 +02:00
Irmen de Jong d17c978892 Merge branch 'master' into new-codegens
# Conflicts:
#	compiler/test/TestCompilerOnExamples.kt
#	examples/c64/cube3d.p8
#	examples/cube3d-gfx.p8
#	examples/cx16/cobramk3-gfx.p8
#	examples/cx16/cube3d.p8
2026-08-22 19:12:06 +02:00
Irmen de Jong 5b890ac5d5 amiga: more strongly typed pointers in the library structs and routines 2026-08-22 18:23:01 +02:00
Irmen de Jong 894a3e90db codegen: fix pointer-typed struct field reads on 32-bit targets to use peekl and long address arithmetic 2026-08-22 12:40:34 +02:00
Irmen de Jong 234468690e resolve aliased/qualified struct types via StructTypeResolver with proper errors, infer for-loop node type in the list declaration's scope so imported module lists work, and fix 6502 codegen crash comparing against &struct.field 2026-08-21 23:00:00 +02:00
Irmen de Jong 5f50b254a2 rename list -> lists 2026-08-21 21:22:17 +02:00
Irmen de Jong c5b17ae880 finalize amiga audio routines 2026-08-21 21:08:28 +02:00
Irmen de Jong 4b320013cf Merge branch 'master' into new-codegens
# Conflicts:
#	codeGenCpu6502/src/prog8/codegen/cpu6502/assignment/PointerAssignmentsGen.kt
#	compiler/test/TestPointers.kt
#	docs/source/todo.rst
#	examples/test.p8
2026-08-21 19:14:29 +02:00
Irmen de Jong f3de26bf6c for-loop support for list/node iteration, and reverse str/array iteration using step -1 2026-08-21 07:28:39 +02:00
Irmen de Jong 08ac5eff3e amiga: forward sys.memcopy to exec.CopyMem 2026-08-20 23:46:34 +02:00
Irmen de Jong 7de39bbd62 amiga: fix audio.wait_all() 2026-08-20 23:28:19 +02:00
Irmen de Jong 198cf4d722 IR: fix for loop iteration overflow for 256 iters 2026-08-20 23:20:42 +02:00
Irmen de Jong b0b8513cb8 amiga.audio using static allocations 2026-08-20 23:19:08 +02:00
Irmen de Jong 82cc66c0cc fix amiga audio (needs BeginIO) 2026-08-20 20:15:33 +02:00
Irmen de Jong c81d9b25ac Merge branch 'master' into new-codegens
# Conflicts:
#	compiler/test/ast/TestConst.kt
#	docs/source/todo.rst
2026-08-18 23:52:02 +02:00
Irmen de Jong c89f870dd0 amiga: adding audio.p8 for audio.device support
but test program still doesn't produce any audible sound...
2026-08-18 23:42:17 +02:00
Irmen de Jong ffb0a4d603 amiga: custom now includes interrupt vector offset symbols, cpuVBR and isPAL (set in grab_system) 2026-08-18 22:07:13 +02:00
Irmen de Jong df659d285b optimize imports 2026-08-18 01:28:35 +02:00
Irmen de Jong 2c2664c91f Merge branch 'vm32bit' into new-codegens 2026-08-18 00:12:59 +02:00
Irmen de Jong e0147a4a7e fix hardcoded uword pointer-size assumptions for 32-bit targets 2026-08-18 00:09:10 +02:00
Irmen de Jong 859223c98b fix hardcoded uword pointer-size assumptions for 32-bit targets 2026-08-17 23:57:37 +02:00
Irmen de Jong 7bdb9bc853 Merge branch 'new-codegens' into vm32bit 2026-08-17 22:19:46 +02:00
Irmen de Jong 4e87aa6b85 m68k: preserve immediate loads for named call arguments 2026-08-17 22:19:03 +02:00
Irmen de Jong 02667e9916 fix str/long type comparison 2026-08-17 20:36:39 +02:00
Irmen de Jong 9d57cbe263 m68k: fix float sgn instruction format 2026-08-17 20:06:52 +02:00
Irmen de Jong 8b25ff2390 Merge branch 'master' into vm32bit 2026-08-17 04:08:23 +02:00
Irmen de Jong 2cb753caf8 fix floating point issue for IR syscalls and m68k codegen 2026-08-17 04:03:09 +02:00
Irmen de Jong 34f0bea4e8 fix pointer unit tests, consolidate pointer type selection for target 2026-08-17 03:30:17 +02:00
Irmen de Jong 82828606e6 removed some string routines from shared because virtual target versions now need a 32 bit compatible implementation 2026-08-17 01:39:31 +02:00
Irmen de Jong ae751c4af4 Merge branch 'new-codegens' into vm32bit 2026-08-16 23:41:35 +02:00
Irmen de Jong 51b98204e9 fix unit test build issue 2026-08-16 23:39:55 +02:00
Irmen de Jong 12f375e070 Merge branch 'new-codegens' into vm32bit
# Conflicts:
#	examples/test.p8
2026-08-16 23:28:39 +02:00
Irmen de Jong 54a1a0704a allow strings to be used to initialize arrays in struct instances 2026-08-16 23:25:41 +02:00
Irmen de Jong 4fdd12929a vm: switched to m68k like 32 bits registers 2026-08-16 21:14:15 +02:00
Irmen de Jong cc4f27d121 vm: making VM 32 bit
work in progress: see vm32.md there are failing tests still
2026-08-16 19:49:28 +02:00
Irmen de Jong 61041f4502 m68k: now calls IR register type verifier properly 2026-08-16 18:07:42 +02:00
Irmen de Jong 673ed91ee3 pointer and float size moved to memsizer, fix memory size of pass-by-ref to properly be size of pointer 2026-08-16 16:13:42 +02:00
Irmen de Jong 65b6280618 fix string compares on 32 bits targets 2026-08-16 15:55:30 +02:00
Irmen de Jong e50e175522 amiga: fix negative LVO offsets in library calls in p8ir file 2026-08-16 14:34:35 +02:00
Irmen de Jong fa1ceab78f M68K codegen: extend immediate call-argument forwarding and dead-store removal to floating-point registers and `p8_fregfile`. 2026-08-16 08:08:02 +02:00
Irmen de Jong 7eab0278b3 Merge branch 'master' into new-codegens 2026-08-16 07:32:34 +02:00
Irmen de Jong 69eaaf29c4 amiga: added sys.arguments for any CLI argument string 2026-08-16 07:08:22 +02:00
Irmen de Jong 69def73f3f m68k: allow assigning string constant to long/pointer variable (to get its address) 2026-08-16 06:18:33 +02:00
Irmen de Jong 6c93254758 m68k assembly peephole optimizer 2026-08-16 01:58:17 +02:00
Irmen de Jong a68cdd13a6 IR: added EXTL and EXTLS opcodes to help with pointer math 2026-08-15 22:58:15 +02:00
Irmen de Jong 295dfada61 amiga: add custom.filter_on() and filter_off() 2026-08-15 22:56:42 +02:00
Irmen de Jong d978be2f9d m68k: remove needless 0-extensions 2026-08-15 19:48:30 +02:00
Irmen de Jong 0a4601721a docs 2026-08-15 19:22:37 +02:00
Irmen de Jong d870c39aa4 m68k: use moveq and movea for call argument loading, drop +0 offset
- use moveq for small immediate call args (bytes 128-255 map to signed -128..-1)
- use movea.l for pointer/long args into address registers, dropping +0 suffix
- loadPointerToA0 also emits movea.l without the redundant +0 offset
- update stale C64 multiplexer expected output size
2026-08-15 18:49:28 +02:00
Irmen de Jong 2b93efbb33 m68k: remove redundant 0-extension, optimize and fix MSB/LSB extraction 2026-08-15 18:21:16 +02:00
Irmen de Jong c563562a6f m68k: optimized indirect JSR/JMP on 68020+ 2026-08-15 16:49:54 +02:00
Irmen de Jong 92a0d3cb1b m68k: optimize MUL and DIV instructions 2026-08-15 16:39:01 +02:00
Irmen de Jong 21d45a0a90 m68k: optimize CMP instruction, fix % 2026-08-15 16:00:26 +02:00
Irmen de Jong c87afc03ef m68k: optimize function call arg loading for immediates 2026-08-15 15:31:43 +02:00
Irmen de Jong 8dc9c4d3c2 optimize agent skills 2026-08-14 22:33:20 +02:00
Irmen de Jong e264aff607 added amiga3d example, fixing amiga blitter routines 2026-08-14 19:39:58 +02:00
Irmen de Jong 9591aad814 improving the C64 sprite multiplexer 2026-08-14 02:54:09 +02:00
Irmen de Jong 3208ef1f1a fix custom.set_aga_color 2026-08-14 00:33:50 +02:00
Irmen de Jong 36fcfa6965 implemented unary operations through pointers 2026-08-13 23:45:24 +02:00
Irmen de Jong ba7bf3f4fe cache IRProgram.registersUsed() 2026-08-13 23:25:00 +02:00
Irmen de Jong fcc93b385d IR: implement array in place / and % 2026-08-13 23:13:53 +02:00
Irmen de Jong 4c6d9b8f5f even more IR peephole optimizations 2026-08-13 23:01:41 +02:00
Irmen de Jong 6a3ee52bd5 more IR peephole optimizations 2026-08-13 22:50:50 +02:00
Irmen de Jong 8dec449a1c get rid of Either 2026-08-13 22:40:51 +02:00
Irmen de Jong a09942254d migrate CLI argument parsing from the obsolete kotlinx-cli library to Clikt 2026-08-13 22:33:16 +02:00
Irmen de Jong d702ace09f add support for pointer sizes >2 to configurable targets 2026-08-13 22:23:47 +02:00
Irmen de Jong f011d3f100 new language feature: %option private_symbols, and public keyword - to manage visibility 2026-08-13 22:05:58 +02:00
Irmen de Jong e404530ba4 Merge branch 'master' into new-codegens
# Conflicts:
#	compiler/src/prog8/compiler/Compiler.kt
#	compiler/src/prog8/compiler/CompilerDaemon.kt
#	gradle.properties
2026-08-13 18:32:47 +02:00
Irmen de Jong 2e529f8849 amiga: added copper and blitter modules and amigablitcop example 2026-08-12 22:59:42 +02:00
Irmen de Jong 79a5ddcdd8 amiga: fix custom chip register names and offsets 2026-08-12 02:33:24 +02:00
Irmen de Jong 7aecb8d6eb amiga: add floating point support (assumes 68020+ and a FPU to run) 2026-08-11 23:12:49 +02:00
Irmen de Jong 73768e1876 new syntax feature done 2026-08-11 21:45:56 +02:00
Irmen de Jong 3514bd61ab new syntax feature: const type can be omitted (defaults to long for integers now) 2026-08-11 21:27:07 +02:00
Irmen de Jong a7f60a571d m68k: fix byte/word index value type mismatch 2026-08-11 20:58:13 +02:00
Irmen de Jong 3705064157 m68k: moveq improvements 2026-08-11 06:25:25 +02:00
Irmen de Jong 208205faea step 4 to allow for loop 'step' to be variable not only const: various correctness 2026-08-11 06:17:34 +02:00
Irmen de Jong 5af6afef61 step 3 to allow for loop 'step' to be variable not only const: 6502 codegen 2026-08-11 05:29:55 +02:00
Irmen de Jong a5a535806b step 2 to allow for loop 'step' to be variable not only const: step type casting done earlier 2026-08-11 04:20:00 +02:00
Irmen de Jong d582c1edf6 step 1 to allow for loop 'step' to be variable not only const 2026-08-11 04:07:56 +02:00
Irmen de Jong 7275137782 update some amiga stuff 2026-08-11 00:15:13 +02:00
Irmen de Jong f11da0488a fix false recursion message 2026-08-10 21:49:41 +02:00
Irmen de Jong ed1abe7e6b allow (optional) explicit for loop variable type to override the inferred type 2026-08-10 21:20:37 +02:00
Irmen de Jong e70a1ed403 fix STOREZX.BYTE register type determination on m68k targets
STOREZX uses reg1 as the index register (not the value register),
so it should return indexRegType (WORD on 32-bit targets) regardless
of the instruction type. The code handled this for FLOAT/WORD/LONG
but missed the BYTE type case, causing register type conflicts
when indexing arrays with uword variables on m68k targets.
2026-08-09 23:18:46 +02:00
Irmen de Jong c53e72c5b2 Merge branch 'master' into new-codegens
# Conflicts:
#	compiler/test/TestCompilerOnExamples.kt
2026-08-09 23:05:04 +02:00
Irmen de Jong b353904d30 fix for loop index register type issue in IR on m68k targets 2026-08-09 21:57:22 +02:00
Irmen de Jong a58a643d0d fixing unit test 2026-08-09 19:10:35 +02:00
Irmen de Jong 823ecfac6f add amiga ptplayer module (protracker module player) 2026-08-09 18:28:11 +02:00
Irmen de Jong 22035077af clean up for loop vardecls in examples 2026-08-09 17:05:34 +02:00
Irmen de Jong da8c3eda92 new language feature: implicit for loop variable declaration 2026-08-09 14:49:19 +02:00
Irmen de Jong 41c2b9c039 fix bit shift code for shifts by 16/32 bits or more 2026-08-09 14:26:45 +02:00
Irmen de Jong c7e25e323a some script fixes and izx0 image format 2026-08-09 12:26:59 +02:00
Irmen de Jong 1e2fc4d6f9 fix unit test on github hopefully 2026-08-09 00:48:43 +02:00
Irmen de Jong 799486eb1b Merge branch 'master' into new-codegens 2026-08-09 00:23:23 +02:00
Irmen de Jong ed67a259de m68k: added compression module with ZX0 decompressor 2026-08-09 00:22:58 +02:00
Irmen de Jong e0329e4457 added amiga iff load example 2026-08-08 22:13:53 +02:00
Irmen de Jong a3161fa117 implemented endian-aware msb/lsb handling 2026-08-08 19:34:12 +02:00
Irmen de Jong 9c8e81b566 IR: fix m68k indexing datatype size 2026-08-08 18:14:30 +02:00
Irmen de Jong e2e72a1791 Merge branch 'master' into new-codegens
# Conflicts:
#	compiler/src/prog8/CompilerMain.kt
2026-08-08 16:45:49 +02:00
Irmen de Jong b3b983c7ec fix untyped pointer indexing on 32 bits targets (long vs uword) 2026-08-08 16:01:28 +02:00
Irmen de Jong 14ae579f00 amiga: add iffparse library module 2026-08-08 14:57:07 +02:00
Irmen de Jong 45f48d2e88 amiga: added %option amiga_chipram in blocks to allocate them in CHIP ram (for sound and graphics data) 2026-08-08 02:01:04 +02:00
Irmen de Jong 8d6ed7712d fix ir block cleanup bug 2026-08-08 01:24:09 +02:00
Irmen de Jong eea185bb83 keep label at top of block instead of moving vardecls before it, IR: fix bug that removed labels in blocks without code 2026-08-08 01:06:42 +02:00
Irmen de Jong 99d28ae557 fix typecast error for certain long/word address-of 2026-08-08 00:47:43 +02:00
Irmen de Jong 528762d961 add amiga screen example 2026-08-07 23:49:12 +02:00
Irmen de Jong c6e9a45293 fix m68k floating point code generation (68020 + FPU required) 2026-08-07 22:40:44 +02:00
Irmen de Jong 6880bd2a05 prog8 coder skill 2026-08-07 21:43:08 +02:00
Irmen de Jong 7547fb7747 array and string size limit raised to 32K on m68k targets 2026-08-07 20:47:06 +02:00
Irmen de Jong ff0e922674 dumpsymbols no longer includes compiler daemon info messages 2026-08-07 19:33:05 +02:00
Irmen de Jong 71f66171c4 amiga: fix const types in generated library files 2026-08-07 19:32:33 +02:00
Irmen de Jong c1f76cf073 m68k: prog8 doesn't have to do BSS clearing itself, the exe loaders already do this for us automatically 2026-08-07 19:07:40 +02:00
Irmen de Jong c2d151920d new docs about register allocation 2026-08-07 17:43:46 +02:00
Irmen de Jong c916570abd m68k now has long mul and div on 68000, uses utility.library on amiga if available 2026-08-07 12:32:47 +02:00
Irmen de Jong bdab975d14 fix amiga library pointer params 2026-08-07 11:28:29 +02:00
Irmen de Jong 1693e4ff9c Merge branch 'refs/heads/master' into new-codegens
# Conflicts:
#	examples/test.p8
2026-08-06 23:34:07 +02:00
Irmen de Jong 6bf304b67b amiga500: add arexx library, timer.device manually opened 2026-08-06 23:17:28 +02:00
Irmen de Jong a5978a913b m68k: added coroutines module 2026-08-06 22:08:59 +02:00
Irmen de Jong e528f0ccaa amiga500: update generated library modules 2026-08-06 20:35:55 +02:00
Irmen de Jong e5f488dcfb new6502: fix align and 0-assign into array 2026-08-06 20:13:42 +02:00
Irmen de Jong c47a6bde0e IR: fix register type error when 0-assigning into an array 2026-08-06 19:31:05 +02:00
Irmen de Jong b8d38eccca new6502: fix long comparisons and STZ on 6502 2026-08-06 19:08:09 +02:00
Irmen de Jong 4c2144eb0d m68k: optimized bit shifts 2026-08-05 19:08:30 +02:00
Irmen de Jong 68399e8370 m68k: optimized bit manipulations 2026-08-05 13:50:12 +02:00
Irmen de Jong 7b61b066e6 m68k: optimized branches 2026-08-05 13:31:07 +02:00
Irmen de Jong bd2a47fb54 m68k: added agent skill 2026-08-05 13:20:13 +02:00
Irmen de Jong 2521df21e7 m68k: optimize bitwise not and zero loads 2026-08-05 12:50:34 +02:00
Irmen de Jong ddb6b597d6 m68k: optimize bitwise ops 2026-08-05 12:38:19 +02:00
Irmen de Jong c24fc9e81d emit floats as dc.s 2026-08-05 12:12:49 +02:00
Irmen de Jong 68f0d2285f Merge branch 'master' into new-codegens
# Conflicts:
#	docs/source/_static/symboldumps/skeletons-c128.txt
#	docs/source/_static/symboldumps/skeletons-c64.txt
#	docs/source/_static/symboldumps/skeletons-cx16.txt
#	docs/source/_static/symboldumps/skeletons-pet32.txt
#	docs/source/_static/symboldumps/skeletons-virtual.txt
#	docs/source/compiling.rst
#	gradle.properties
2026-08-05 11:05:02 +02:00
Irmen de Jong 9937990618 Merge branch 'master' into new-codegens
# Conflicts:
#	examples/test.p8
2026-08-05 03:20:14 +02:00
Irmen de Jong e007d127f0 IR: smarter handling of x<<16 and x>>16 if x is a long 2026-08-05 02:01:21 +02:00
Irmen de Jong e5d9609db1 IR: added new multi-bit shifting opcodes 2026-08-05 01:42:49 +02:00
Irmen de Jong 051f3809cd IR: improved add/sub of small const values 2026-08-05 00:10:09 +02:00
Irmen de Jong b5fd07bb91 IR: added STOREIM instruction for immediate store constant to memory 2026-08-04 23:11:07 +02:00
Irmen de Jong 4097d51514 IR: more efficient code for comparisons against constants 2026-08-04 22:28:59 +02:00
Irmen de Jong 577815a1f4 m68k: cmp -> tst optimization 2026-08-04 22:06:20 +02:00
Irmen de Jong c2a942dd45 fix jump table on m68k 2026-08-04 21:43:39 +02:00
Irmen de Jong 82342268da fix too conservative Inliner gates, and possible endinanness problem. Optimized nested msb/lsb on m68k. 2026-08-04 21:01:29 +02:00
Irmen de Jong 30e576e253 defensive fix for wrong datatype in pointer comparisons 2026-08-04 20:44:05 +02:00
Irmen de Jong a5782c300a fix compiler crash on out of range pointer address values 2026-08-04 20:24:50 +02:00
Irmen de Jong 84f720b63b fix pokew/pokel pointer size mismatch 2026-08-04 19:46:49 +02:00
Irmen de Jong b8cded4373 revert back to fast (but slightly inaccurate) signed bitshifting for division optimization 2026-08-04 19:41:14 +02:00
Irmen de Jong 368dce93d9 new6502 codegen: implement long multiplications and divisions 2026-08-04 19:09:41 +02:00
Irmen de Jong 644226f925 update prog8 coder skill about multiple statements per line 2026-08-04 18:52:58 +02:00
Irmen de Jong 42e8e91034 disable signed div bias-shift method because of code bloat 2026-08-04 18:38:35 +02:00
Irmen de Jong af80ec6502 fix invalid optimization of signed division into a shift (wrong rounding result) - IR codegen 2026-08-04 17:42:12 +02:00
Irmen de Jong cf714d8e0c fix invalid optimization of signed division into a shift (wrong rounding result) - 6502 codegen only 2026-08-04 16:16:38 +02:00
Irmen de Jong 86c2f711ed fix invalid const-folding of certain strings char classification routines such as isupper() 2026-08-04 15:59:08 +02:00
Irmen de Jong 86c01e9d7b remaining m68k optimization issues noted in doc 2026-08-04 15:45:29 +02:00
Irmen de Jong 7138ee024b fix memory-byte read sign extension error when the byte was signed 2026-08-04 15:45:14 +02:00
Irmen de Jong b9f65186cb fix expression operand swap bug when there are side effects 2026-08-04 14:49:01 +02:00
Irmen de Jong 27aeafef0d more ast optimization gatings for m68k 2026-08-04 13:58:03 +02:00
Irmen de Jong 16901212d4 m68k: fix divmod() and lmh() call convention 2026-08-04 13:40:30 +02:00
Irmen de Jong 49f5c30e8c 6502: better error message for missing signed divmod support 2026-08-04 13:09:06 +02:00
Irmen de Jong bef1678c2a IR: divmod no longer uses stack to return the quotient and remainer 2026-08-04 12:46:36 +02:00
Irmen de Jong 3459ffcdb9 m68k: fix wrong divm/mulm code gen 2026-08-04 05:19:53 +02:00
Irmen de Jong 4cfe9b2729 Merge branch 'master' into new-codegens 2026-08-04 04:58:22 +02:00
Irmen de Jong 97e5a6f7bb amiga500 textio: use dos VPrintf instead of separate conv calls 2026-08-04 04:50:01 +02:00
Irmen de Jong 79c05ae396 m68k: fix divs sign extension bug 2026-08-04 03:05:13 +02:00
Irmen de Jong f25ea95bdf m68k: fix extb.l on 68000, checks for long division/multiplication on 68000 2026-08-04 01:51:14 +02:00
Irmen de Jong 45bf8ba72a m68k: fix long and float variable dc.l/dc.s, fix 68020 long division instruction syntax 2026-08-04 01:00:27 +02:00
Irmen de Jong c64fdcffa8 IR: fix register type error for power of 2 multiplies as shifts 2026-08-04 00:05:13 +02:00
Irmen de Jong c0b8cf827d fix m68k eor instruction syntax 2026-08-04 00:04:19 +02:00
Irmen de Jong fc38847af4 fix addq/subq value range to 1-8 2026-08-03 23:33:30 +02:00
Irmen de Jong fea52c6145 uword comparison optimizations not for m68k 2026-08-03 22:45:46 +02:00
Irmen de Jong 4b20440efe another bit shift optimization not for m68k 2026-08-03 22:30:30 +02:00
Irmen de Jong c39410afc2 x/256 ast optimization not for m68k 2026-08-03 22:19:29 +02:00
Irmen de Jong 93b953cf60 implement IR ADDIM and SUBIM in the new codegens 2026-08-03 21:53:54 +02:00
Irmen de Jong 01c82f92eb Merge branch 'refs/heads/master' into new-codegens
# Conflicts:
#	examples/test.p8
#	intermediate/src/prog8/intermediate/IRInstructions.kt
2026-08-03 19:36:25 +02:00
Irmen de Jong 8405e6f2a3 fix endianness issue with msw/lsw on m68k 2026-08-03 15:49:39 +02:00
Irmen de Jong 491b28f6b6 chained expression splitting is only beneficial on the 6502 2026-08-03 15:26:52 +02:00
Irmen de Jong 9286d6b2c8 fix amiga sysinit 2026-08-03 15:21:33 +02:00
Irmen de Jong d144235dc9 Merge branch 'master' into new-codegens 2026-08-03 14:35:47 +02:00
Irmen de Jong 2fa82ff3ad fix ast remainder optimization check 2026-08-03 13:26:11 +02:00
Irmen de Jong 3e03980478 tweaking ast optimizations for m68k 2026-08-03 12:45:48 +02:00
Irmen de Jong e042756453 add timer.device 2026-08-03 08:09:11 +02:00
Irmen de Jong 84d5247b78 fix m68k enum field symbols 2026-08-03 05:30:09 +02:00
Irmen de Jong 4682462c19 added utility.library 2026-08-03 04:16:10 +02:00
Irmen de Jong b6e503a67b fix amiga textio and m68k alignment issues 2026-08-03 02:33:19 +02:00
Irmen de Jong dedae29d72 Merge branch 'master' into new-codegens 2026-08-02 17:42:33 +02:00
Irmen de Jong 092116de1d custom grab_system() and return_system() added for demo/game hardware banging programs
added amigarasters example
2026-07-16 23:04:14 +02:00
Irmen de Jong bf0733f8b3 amiga LVOs (extsub library callfars) are now represented by their familiar negative decimal offset number instead of a hex unsigned long.
Actual JSR to the library LVO uses the symbolic name if possible to create code that is easier to debug
2026-07-16 21:42:04 +02:00
Irmen de Jong b0cf649c2d register packer does't work well 2026-07-16 20:33:27 +02:00
Irmen de Jong 3499121f9c better amiga program startup 2026-07-16 00:35:44 +02:00
Irmen de Jong c43cacdc2f added a IR RegisterPacking optimization step that reduces the number of registers through reuse of register slots 2026-07-15 22:39:19 +02:00
Irmen de Jong ead1e5e879 make exit() work on m68k 2026-07-15 00:18:40 +02:00
Irmen de Jong 4c1f193d53 amiga custom chip registers 2026-07-14 22:20:12 +02:00
Irmen de Jong abaaea33d8 more intuition constants 2026-07-14 20:13:03 +02:00
Irmen de Jong fe42040d48 structs in symboldumps 2026-07-14 08:04:32 +02:00
Irmen de Jong 7565d04cf5 fix missing constants in amiga lib dumps 2026-07-14 07:27:15 +02:00
Irmen de Jong 0d76eea1be unit test for bitwise long const errors 2026-07-14 07:24:01 +02:00
Irmen de Jong 9a2851a9d5 fix const evaluation type problem in bitwise operations 2026-07-14 07:12:52 +02:00
Irmen de Jong 5e1612f204 improved amiga libconvert script (now requires clang to parse the C header files) 2026-07-14 06:49:29 +02:00
Irmen de Jong 316181b971 fix skill to allow fixed-size arrays as struct fields 2026-07-14 06:26:08 +02:00
Irmen de Jong e753eed4d4 amiga lib dumps with structs 2026-07-14 04:52:45 +02:00
Irmen de Jong b52a07c7c4 tweak m68k roxl/roxr semantix (X bit)
work on m68k conv routines
2026-07-14 02:49:19 +02:00
Irmen de Jong f210c8b0db amiga library calls 2026-07-13 22:58:34 +02:00
Irmen de Jong f865a9801f output memory mapped variables on m68k asm 2026-07-12 19:18:17 +02:00
Irmen de Jong 6a165e951b amiga500 target 2026-07-12 19:05:13 +02:00
Irmen de Jong 9322fbb234 better syntax highlighting in docs
added m68k qemubootinfo example
2026-07-12 14:20:31 +02:00
Irmen de Jong 4b7e2dce9d better syntax highlighting in docs 2026-07-12 13:05:05 +02:00
Irmen de Jong 7bda2e987c remove redundant DONTCARE split pointer array conversions on non-6502 targets 2026-07-12 12:55:00 +02:00
Irmen de Jong e645b0abe1 add pokep() and peekp() builtin functions 2026-07-12 12:54:29 +02:00
Irmen de Jong f10a58ee1b fix m68k long/string arrays 2026-07-12 12:09:44 +02:00
Irmen de Jong f498f598bf fix m68k concat 2026-07-12 04:49:13 +02:00
Irmen de Jong 4dfcb59b6f fix daemon 2026-07-12 04:03:10 +02:00
Irmen de Jong 4506a3cb3e pass compilation target to more places to be able to determine correct types 2026-07-12 03:52:57 +02:00
Irmen de Jong 247559858f fix bitwise instructions on m68k 2026-07-11 23:40:46 +02:00
Irmen de Jong 8a8cf3abc7 Merge branch 'master' into new-codegens
# Conflicts:
#	compiler/test/TestCompilerOnExamples.kt
2026-07-11 23:13:04 +02:00
Irmen de Jong 0cf488b586 fix some array issues 2026-07-11 23:01:23 +02:00
Irmen de Jong a019f22f47 all arena allocators now return pointer type 2026-07-11 22:26:14 +02:00
Irmen de Jong 9587e1b724 Merge branch 'master' into new-codegens 2026-07-11 22:17:25 +02:00
Irmen de Jong 7adbdf39ac specific for 6502 2026-07-11 22:02:08 +02:00
Irmen de Jong 57714efb40 added pushp() and popp() builtin functions 2026-07-11 21:49:22 +02:00
Irmen de Jong dafb0906c3 fix some m68k pointer/uword stuff 2026-07-11 21:13:21 +02:00
Irmen de Jong 96bb64aebb fix some m68k fp and alignment stuff 2026-07-11 20:36:03 +02:00
Irmen de Jong 57254233e4 Merge branch 'master' into new-codegens
# Conflicts:
#	codeCore/src/prog8/code/target/NormalMemSizer.kt
2026-07-11 18:22:11 +02:00
Irmen de Jong 9ec4b35de9 fix m68k memclear 2026-07-11 18:20:41 +02:00
Irmen de Jong 9adbd77ce4 vasm package is borked 2026-07-11 17:28:15 +02:00
Irmen de Jong 8957e8fff7 needs devmode as well 2026-07-11 17:25:16 +02:00
Irmen de Jong 3aa0344c4a needs beta channel 2026-07-11 17:23:14 +02:00
Irmen de Jong 54de2c5fbf attempt to install vasm in github actions 2026-07-11 17:21:12 +02:00
Irmen de Jong 0e978b250b added 'pointer' as type alias for uword or long depending on the compilation target's pointer size 2026-07-11 16:59:24 +02:00
Irmen de Jong 67e6888009 fix ast check for m68k to also consider library files, align code on even addresses, start implementing syscalls 2026-07-11 14:59:46 +02:00
Irmen de Jong debb91965b fix ast check for m68k to also consider library files, align code on even addresses 2026-07-11 13:43:14 +02:00
Irmen de Jong d0b3fefd57 fix peek/poke to take long addresses on m68k 2026-07-11 13:16:16 +02:00
Irmen de Jong 5cc3623a2a m68 adding input routine, fix linkerscript, qemu bootinfo 2026-07-11 12:41:35 +02:00
Irmen de Jong d08f532a10 m68k float support 2026-07-11 02:39:14 +02:00
Irmen de Jong 35b7e4a4de fix reference return types vs pointer 2026-07-11 00:07:36 +02:00
Irmen de Jong 38b77d8817 ir: fix usage of STOREZ on pointers 2026-07-10 23:49:48 +02:00
Irmen de Jong 3078f402bb fix uword literal assignment to pointer on m68k 2026-07-10 22:35:07 +02:00
Irmen de Jong f977daafbb value casting now gets passed the compilation target (needed for correct pointer type casting) 2026-07-10 22:23:33 +02:00
Irmen de Jong aabd99e9eb fix uword to pointer assignment problems 2026-07-10 21:32:59 +02:00
Irmen de Jong e7dbab1822 fix defer on m68k by making mask variable a word so correct memory LSR instruction can be used 2026-07-10 17:40:47 +02:00
Irmen de Jong cf6efd76f0 making sure memory() is long on m68k 2026-07-10 04:51:54 +02:00
Irmen de Jong 8a7922ffcc fix split words arrays on m68k to be not split 2026-07-10 01:47:18 +02:00
Irmen de Jong c7a8e84fd0 fix more pointer type handling on m68k 2026-07-09 03:44:41 +02:00
Irmen de Jong cb57bea862 fix inline asmsub handling, more optimized strings and sys routines 2026-07-09 03:25:30 +02:00
Irmen de Jong b24ce80945 tweaking m68k library 2026-07-09 00:13:20 +02:00
Irmen de Jong eb18397035 Merge branch 'master' into new-codegens 2026-07-08 23:37:48 +02:00
Irmen de Jong 37a86f3899 switch back to M68020 CPU so code migth eventually run on a stock Amiga 1200 2026-07-08 23:03:10 +02:00
Irmen de Jong 5700ae446f improve m68k codegen, add integer sqrt 2026-07-08 22:28:00 +02:00
Irmen de Jong cb240f6a4b Merge branch 'master' into new-codegens 2026-07-08 21:02:17 +02:00
Irmen de Jong 54df69a2b3 allow array args for pointer params in m68k 2026-07-07 22:14:38 +02:00
Irmen de Jong 88b3291785 added ELF/RAW output type selection for m68k, fix div garbage register data 2026-07-07 21:49:45 +02:00
Irmen de Jong c9c68f7d8c clean up some pointer type stuff in m68k 2026-07-07 20:36:00 +02:00
Irmen de Jong 2d3f0baa51 add a few m68k math routines
properly output data section in m68k
2026-07-07 20:03:13 +02:00
Irmen de Jong bd4184252f add sys.MAX_LONG and sys.MIN_LONG for m68k 2026-07-07 05:02:47 +02:00
Irmen de Jong f043dba23d Merge branch 'master' into new-codegens 2026-07-07 04:39:09 +02:00
Irmen de Jong e6a267bd3f fix 32 bits address-of 2026-07-07 04:27:47 +02:00
Irmen de Jong a532fb411b IR POINTER size tweaks 2026-07-07 02:58:05 +02:00
Irmen de Jong 1f4ffdffc1 fix IR constant prefixing/no prefixing 2026-07-07 01:45:55 +02:00
Irmen de Jong 1053a9a761 Merge branch 'master' into new-codegens 2026-07-07 01:00:13 +02:00
Irmen de Jong 7f51bbd777 working on encoding support for variable sized pointers in the IR 2026-07-07 00:06:28 +02:00
Irmen de Jong 5c36e21948 working on fixing pointer arithmethic
but see transformWithPointerArithmetic() still unfinished
2026-07-06 02:20:32 +02:00
Irmen de Jong a46443417e disable zeropage usage on non-6502 targets 2026-07-05 20:02:14 +02:00
Irmen de Jong 57055b3b55 map m68k cpu registers to and from slots 2026-07-05 16:25:20 +02:00
Irmen de Jong 559521c6df m68k better BSS 2026-07-05 07:07:19 +02:00
Irmen de Jong 11e3b39486 m68k (incomplete) attempt to fix 32 bits pointers 2026-07-05 06:31:24 +02:00
Irmen de Jong 1c50e397de m68k registers 2026-07-05 03:41:59 +02:00
Irmen de Jong 99ead2c74c m68k 2026-07-04 23:18:03 +02:00
Irmen de Jong f557d2bee0 Merge branch 'master' into new-codegens 2026-07-04 18:28:14 +02:00
Irmen de Jong 5278f9a595 new codegen fix floatparse / mul 2026-07-04 18:00:49 +02:00
Irmen de Jong dd1a886b66 Merge branch 'master' into new-codegens 2026-07-04 00:04:50 +02:00
Irmen de Jong 7f95377ad7 newcodegen: fixing more comparisons and branches 2026-07-04 00:01:53 +02:00
Irmen de Jong 903c553f94 newcodegen: fixing more comparisons and branches 2026-07-03 22:15:39 +02:00
Irmen de Jong 0e7237c64a newcodegen: fixing branches 2026-07-03 21:13:27 +02:00
Irmen de Jong 122af69cdc newcodegen: fixing comparisons 2026-07-03 05:14:07 +02:00
Irmen de Jong 786d742e76 newcodegen: fixing floats 2026-07-03 02:04:43 +02:00
Irmen de Jong c4c46fd282 fix stack byte order in new codegen 2026-07-02 22:49:10 +02:00
Irmen de Jong 5a49c97e5d fix zeropage var inits in new codegen, better 65c02 instruction usage 2026-07-02 22:12:43 +02:00
Irmen de Jong 2cd195b343 fix string encoding in new codegen 2026-07-02 21:03:54 +02:00
Irmen de Jong 36238267d0 reuse AssemblyProgram6502 in newcodegen for 6502 2026-07-02 01:36:25 +02:00
Irmen de Jong 52fe2f2c0d adjust compilation options etc for the newCodegen flag 2026-07-02 01:21:18 +02:00
Irmen de Jong 4bf0eba2f8 Merge branch 'master' into new-codegens
# Conflicts:
#	.idea/compiler.xml
#	codeCore/src/prog8/code/core/CompilationOptions.kt
#	codeGenIntermediate/test/TestVmCodeGen.kt
#	codeGenNew6502/src/prog8/codegen/new6502/New6502CodeGenerator.kt
#	compiler/src/prog8/CompilerMain.kt
#	compiler/src/prog8/compiler/Compiler.kt
#	compiler/src/prog8/compiler/CompilerDaemon.kt
#	compiler/test/TestCompilerOnExamples.kt
#	compiler/test/helpers/compileXyz.kt
#	scripts/clean.sh
#	settings.gradle
2026-07-02 01:14:55 +02:00
Irmen de Jong 9d1bcfc0c6 remove codegenExperimental module 2026-07-02 00:54:11 +02:00
Irmen de Jong 15756a8977 rename -expericodegen into -newcodegen that should switch all existing 6502 codegens to using the new one based on IR 2026-07-02 00:51:45 +02:00
Irmen de Jong 0ab36bcbf6 added qemu68k compilation target 2026-07-01 23:47:13 +02:00
Irmen de Jong 36eef51e1c new package structure for new codegens 2026-07-01 23:03:32 +02:00
Irmen de Jong 309c25a993 add new codegens that are based on the IR 2026-07-01 22:22:53 +02:00
640 changed files with 109052 additions and 17015 deletions

No files matched your search

+154 -241
View File
@@ -1,291 +1,204 @@
---
name: asm6502-coder
description: Writing or understanding 6502/65C02 assembly code using 64tass syntax, for use as stand alone .asm or inline within Prog8 programs
description: Writing or understanding 6502/65C02 assembly in 64tass syntax, standalone or embedded in Prog8
---
# 65(C)02 Assembly Coder Skill
# 6502 Assembly Coder
You are an expert MOS 6502 assembly development assistant. Keep responses concise and practical — prefer short, correct code examples over lengthy prose.
You are working with **6502/65C02 assembly** using **64tass syntax**, in separate `*.asm` files or embedded in a Prog8 program (inside `%asm {{ }}` blocks or `asmsub` routines). Follow all rules below.
Use 64tass syntax, not ca65 or another assembler. Keep answers concise and
practical. This applies to standalone `.asm` files and Prog8 `%asm {{ }}` or
`asmsub` blocks.
## Assembler: 64tass Syntax
### Formatting Rules
- Labels start in the first column of the line.
- Instructions are indented at least 4 spaces.
- Two spaces between the instruction opcode and its operand (e.g., `lda #1`, `sta $d020`).
- End-of-line comments are preceded by two spaces before the `;` (e.g., `sta $d020 ; border color`).
- If a comment is the only thing on a line, it starts in the first column (no indentation).
- Opcodes and operands are written in lowercase (e.g., `lda`, not `LDA`; `$d020`, not `$D020`).
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.
- **NOT ca65/cc65** or other assemblers. Key differences:
- `.proc` / `.pend` for procedures (scoping)
- `_label` for local labels (prefixed with underscore, scoped to `.proc`)
- **Symbol aliases**: inside a `.proc`, use `_name = P8ZP_SCRATCH_B1` to give a scratch variable a descriptive name.
- **Anonymous labels**: defined as `+` (forward) or `-` (backward) at the start of a line.
- **CRITICAL DISTINCTION**: Label **definitions** are always just a single `+` or `-`. Label **references** use `+`, `++`, `+++` to refer to the 1st, 2nd, 3rd upcoming forward label, and `-`, `--`, `---` for the 1st, 2nd, 3rd preceding backward label.
- `+` refers to the NEXT upcoming `+` label, `++` refers to the ONE AFTER that, etc.
- `-` refers to the MOST RECENT `-` label, `--` to the one before that, etc.
- When a `+` label is passed, the forward-reference count resets (so `++` then refers to the next one after that new label)
- Example:
```asm
- dex ; backward label '-'
bne - ; branch to most recent '-' (the dex above)
ldx #5
+ dex ; forward label '+'
bne + ; branch to this same '+' (forward)
sta $400
+ lda #0 ; second '+' label definition (just '+')
bne ++ ; branch to the '++' below (third forward '+')
rts
+ inc $d020 ; third forward label definition (just '+', referred to as '++')
```
- Data directives: `.byte`, `.word`, `.dword`, `.fill` (allocate space)
- Equates: `label = value` (not `label .equ value` or `#define`)
- **Number Literals**: Hex `$1234`, Binary `%10101010`, Decimal `123`.
- Zero-page variables defined with `=`
- `.text` for inline string data
- **Conditional assembly**: `.if`, `.elsif`, `.else`, `.endif`
- **Memory Sections**: `.section <name>`, `.send <name>` (common sections: `CODE`, `DATA`, `BSS`, `BSS_NOCLEAR`)
## Syntax
## Instructions
- **Instructions like `rol`, `ror`, `asl`, `lsr` (and `inc`, `dec` on 65C02) require an explicit operand for accumulator**: write `rol a`, `inc a`, etc., not just `rol` or `inc`.
- Standard 6502 addressing modes: implied, immediate (`#`), zero-page (`zp`), zero-page,X (`zp,x`), absolute (`abs`), absolute,X (`abs,x`), absolute,Y (`abs,y`), indirect (`(abs)`), indirect,X (`(zp,x)`), indirect,Y (`(zp),y`), relative (branches), accumulator
- Branches: `bne`, `beq`, `bmi`, `bpl`, `bcs`, `bcc`, `bvs`, `bvc` (relative, max +127/-128 bytes)
- Jumps: `jmp` (absolute or indirect), `jsr`/`rts` (subroutine call/return)
- No `push`/`pop` mnemonics — use `pha`/`pla` (byte) and `txa`/`phx`/`plx`/`tay`/`phy`/`ply` for registers
- Labels begin in column 1. Instructions are indented at least four spaces.
- Use lowercase opcodes and operands, two spaces between opcode and operand,
and two spaces before end-of-line comments.
- Hex is `$1234`, binary is `%1010`, decimal is `123`.
- Use `.byte`, `.word`, `.dword`, `.fill`, `.text`, and equates such as
`name = value`.
- Sections use `.section name` and `.send name`.
- Procedures use `.proc` and `.pend`; labels beginning with `_` are local to a
procedure.
- Conditional assembly uses `.if`, `.elsif`, `.else`, and `.endif`.
### Instruction Side Effects (Flags)
- **`Z` (Zero)**: Set if the result of an operation is 0.
- **`N` (Negative)**: Set if bit 7 of the result is 1.
- **`C` (Carry)**: Used for unsigned overflow and shifts. `cmp` sets `C` if `Register >= Operand`.
- **`V` (Overflow)**: Set if a signed arithmetic operation overflowed.
- **Commonly affected by**: `lda`, `ldx`, `ldy`, `inx`, `dex`, `tax`, `tay`, `txa`, `tya`, `and`, `ora`, `eor`, `asl`, `lsr`, `rol`, `ror`, `adc`, `sbc`, `cmp`, `cpx`, `cpy`, `bit`.
- **Note**: `lda`, `ldx`, `ldy` do NOT affect the Carry flag. Only `adc`, `sbc`, `cmp`, and shift/rotate instructions affect Carry.
## Anonymous Labels
## 6502 vs 65C02
- **CX16 target only**: can use WDC 65C02 instructions — `stz`, `phx`, `plx`, `phy`, `ply`, `bra`, `trb`, `tsb`, `stp`, `wai`, `inc a`, `dec a`, `bit #imm`, `bit zp,x`, `bit abs,x`, `jmp (abs,x)`, and `(zp)` indirect addressing mode (e.g., `lda (zp)`).
- **C64, C128, PET32 targets**: original 6502 only — no `stz`, no `phx`/`plx`/`phy`/`ply`, no `bra` etc.
- **Note**: The Rockwell/bit-manipulation instructions (`rmb`, `smb`, `bbr`, `bbs`) are **NOT** available.
- Check the target before using 65C02-specific instructions
- **6502 / 65C02 instruction reference table**: https://www.pagetable.com/c64ref/6502/?cpu=65c02&tab=4 (provides exact instruction details for all opcodes: operation, addressing modes, byte length, and cycle count).
Anonymous label definitions are always a single `+` or `-` in column 1.
References use repeated signs:
## Commander X16 Memory Map
- `+` is the next upcoming `+` definition, `++` the second, `+++` the third.
- `-` is the most recent `-` definition, `--` the preceding one, and so on.
- Passing a `+` definition resets the forward-reference count.
```asm
- dex
bne - ; most recent '-' above
ldx #5
+ dex ; first upcoming '+'
bne + ; this '+'
sta $400
+ lda #0 ; second '+' definition
bne ++ ; next '+' after this one
rts
+ inc $d020 ; third '+' definition
```
Bank Offset Content
$0000-$9EFF Fixed/System RAM
$9F00-$9FFF I/O Area (VIA, VERA, YM2151)
$00-$FF:$A000-$BFFF Banked RAM (max 2 MiB, often 512 KiB) (256x8K banks)
$00-$1F:$C000-$FFFF System ROM (512 KiB) (32x16K banks)
```
- Bank registers (in zeropage): `$00` selects the current RAM bank (0-255), `$01` selects the current ROM bank (0-31). For JSRFAR/banked calls from Prog8, use `extsub @bank ...` instead of manipulating these directly.
- Full hardware specs at https://ayce.dev/emptyx16.html#emptyx16---x16-hardware-specs
## Calling Convention / Register Conventions
- **Accumulator (A)**: 8-bit, used for most arithmetic, data movement, return values
- **X register**: 8-bit, often used for indexing, loop counters
- **Y register**: 8-bit, often used for indirect addressing index
- **Processor Status (P)**: flags — carry (C), zero (Z), negative/N (sign bit 7), overflow (V), decimal (D), interrupt (I), break (B)
- **No caller-saved vs callee-saved convention** — list all modified registers in `clobbers (A, X, Y)` when writing `asmsub`
- The CPU stack (SP, $0100-$01FF) is limited (usually ~128 bytes free). Do not overflow it
## CPU Differences
## Assembly within Prog8 Programs
- `cx16` is the only Prog8 target that supports 65C02 instructions such as
`stz`, `phx`, `plx`, `phy`, `ply`, `bra`, `trb`, `tsb`, `wai`, `stp`, and
`bit #imm`. C64, C128, and PET32 use the original 6502.
- Rockwell instructions (`rmb`, `smb`, `bbr`, `bbs`) are not available.
- Use an explicit `a` for accumulator forms such as `rol a`, `lsr a`, `inc a`,
and `dec a`.
- Branches are relative. Prog8's assembler invocation enables `--long-branch`
for out-of-range branches.
- `lda`, `ldx`, and `ldy` do not affect carry. `cmp` sets carry for
`register >= operand`.
- On NMOS 6502, `jmp ($xxff)` wraps the high-byte fetch within the page.
- `BRK` skips the byte after its opcode. The byte is commonly a handler
signature.
- `bit` memory forms set N and V from the operand and Z from `A AND operand`.
65C02 `bit #imm` sets only Z.
- Decimal mode can persist into NMOS IRQ handlers. Use `cld` at handler entry
and explicitly control D before `adc` or `sbc`.
### `%asm {{ }}` blocks
- Embed arbitrary 64tass assembly directly in your Prog8 source
- Access Prog8 symbols using their prefixed names (see below)
- Can be placed inside subroutines or at block level
## Prog8 Integration
### `asmsub` (assembly subroutine)
- For kernel (ROM) routines or low-level assembly
- Parameters passed via registers: `@A`, `@X`, `@Y`, `@AX` (A low, X high), `@AY` (A low, Y high), `@R0`-`@R15`, `@FAC1`/`@FAC2` (float), `@Pc` (carry), `@Pz` (zero)
- Return value: `-> type @register` — also via `@Pz`/`@Pc` for flags
- Clobbers: `clobbers (A, X, Y)` — MUST list all modified registers
- **CRITICAL**: Parameter names in `asmsub` are **documentation only**. You MUST use the actual registers in your assembly code, NOT the parameter names (unless you create aliases yourself).
- Create symbolic aliases at assembly top for clarity: `x1 = cx16.r0`, `y1 = cx16.r0L`
- Accessing Prog8 parameters if they were NOT mapped to registers: use `p8v_paramname`. (Mapping to registers is preferred for speed).
`asmsub` parameter names are documentation only. Use the actual registers or
define assembly aliases, and list every modified register in `clobbers`.
Example:
```prog8
asmsub line(uword x1 @R0, ubyte y1 @A, uword x2 @R1, ubyte y2 @Y) clobbers (A, X, Y) {
asmsub increment(uword value @R0) clobbers (A, X) {
%asm {{
x1 = cx16.r0
x2 = cx16.r1
lda x1 ; use alias, not "_x1"
lda cx16.r0L
clc
adc #1
sta cx16.r0L
}}
}
```
### `asmsub` parameter annotation reference
| Annotation | Register | Size |
|------------|----------|------|
| `@A` | Accumulator | 8-bit |
| `@X` | X register | 8-bit |
| `@Y` | Y register | 8-bit |
| `@AX` | A (low) + X (high) | 16-bit |
| `@AY` | A (low) + Y (high) | 16-bit |
| `@R0`-`@R15` | cx16 virtual registers | 16-bit each |
| `@FAC1`/`@FAC2` | Floating-point accumulators | 5-byte float |
| `@Pc` | Carry flag | bool |
| `@Pz` | Zero flag | bool |
Annotations include `@A`, `@X`, `@Y`, `@AX`, `@AY`, `@R0`-`@R15`, `@FAC1`,
`@FAC2`, `@Pc` (carry), and `@Pz` (zero). Unmapped parameters are accessed as
`p8v_name`.
## Accessing Prog8 Symbols from Assembly
All Prog8 symbols are prefixed when accessed from assembly:
Prog8 symbols use these prefixes: `p8v_` variables and parameters, `p8s_`
subroutines, `p8b_` blocks, `p8c_` constants, `p8l_` labels, and `p8t_` struct
types. Fully qualified names may look like
`p8b_main.p8s_helper.p8v_local`. `%option no_symbol_prefixing` disables the
prefixes. Split word arrays use `_lsb` and `_msb` symbols.
| Prefix | Refers to | Example |
|--------|-----------|---------|
| `p8v_` | Variables, parameters | `p8v_myvar` |
| `p8s_` | Subroutines | `p8s_mysub` |
| `p8b_` | Blocks | `p8b_myblock` |
| `p8c_` | Constants, enum members | `p8c_myconst`, `p8c_MyEnum_Member` |
| `p8l_` | Labels | `p8l_mylabel` |
| `p8t_` | Struct types | `p8t_MyStruct` |
| `p8_` | Other symbols | |
## Zeropage and Memory
- **Fully qualified**: e.g., `p8b_myblock.p8v_myvar`, `p8b_myblock.p8s_mysub.p8v_localvar`
- Within a `.proc` (subroutine) in your assembly, short names often work (assembler scoping)
- **`%option no_symbol_prefixing`**: disables all prefixes. Stdlib modules (`cbm`, `cx16`, `txt`) use this — you can write `cbm.CHROUT` directly
- **Split word arrays**: two separate byte arrays — append `_lsb` and `_msb` to the name: `p8v_myarray_lsb`, `p8v_myarray_msb`
- Never hardcode zeropage addresses. Use `P8ZP_SCRATCH_B1`,
`P8ZP_SCRATCH_REG`, `P8ZP_SCRATCH_W1`, `P8ZP_SCRATCH_W2`, and
`P8ZP_SCRATCH_PTR`, or the appropriate `cx16.r0`-`cx16.r15` symbols.
- Scratch variables are not necessarily consecutive. CX16 virtual registers
are consecutive and have `L` and `H` byte names.
- Allocate additional temporary storage in BSS.
- The hardware stack occupies `$0100-$01ff` and is limited. Do not overflow
it.
- Prog8 words are little-endian: load the low byte first, then the high byte.
## Zeropage Usage
- Do NOT use arbitrary zeropage locations. Only use these predefined scratch variables:
- `P8ZP_SCRATCH_B1` (byte)
- `P8ZP_SCRATCH_REG` (byte)
- `P8ZP_SCRATCH_W1` (word)
- `P8ZP_SCRATCH_W2` (word)
- `P8ZP_SCRATCH_PTR` (word)
- On CX16: virtual registers `cx16.r0`-`cx16.r15` are in zeropage (and their low/high bytes: `cx16.r0L`, `cx16.r0H`, etc.)
- Virtual registers `cx16.r0`-`cx16.r15` are available on ALL targets, but only on CX16 in zeropage
- Assume the scratch variables are not consecutive in zeropage. The CX16 virtual registers ARE consecutive in memory though.
- For additional temporary storage, allocate regular variables in BSS
- **CRITICAL: NEVER use hardcoded zeropage addresses** (like `$1e`, `$22`, `$7c`, etc.) to reference P8ZP_SCRATCH registers or cx16 virtual registers. Their addresses differ per compilation target. **ALWAYS** use the symbolic names (`P8ZP_SCRATCH_W1`, `cx16.r14`, `cx16.r14+1`, etc.) so the assembler resolves the correct address for the target.
## Common 6502 Patterns
### Common Branch Logic (Comparisons)
| Logic | Unsigned | Signed |
|-------|----------|--------|
| `A == imm` | `cmp #imm`, `beq label` | (Same) |
| `A != imm` | `cmp #imm`, `bne label` | (Same) |
| `A < imm` | `cmp #imm`, `bcc label` | `sec`, `sbc #imm`, `bvc *+4`, `eor #$80`, `bmi label` |
| `A >= imm` | `cmp #imm`, `bcs label` | `sec`, `sbc #imm`, `bvc *+4`, `eor #$80`, `bpl label` |
| `A <= imm` | `beq label`, `bcc label` | (Use complex signed logic or reorder) |
| `A > imm` | `beq +`, `bcs label`, `+` | (Use complex signed logic or reorder) |
### Looping (downto with BNE)
```asm
ldx #count
loop ; do work here
dex
bne loop ; loop while X != 0 (runs 'count' times)
```
### Self-Modifying Code (SMC) — Avoid When Possible
**Do NOT use self-modifying code unless absolutely necessary** — self-modifying code writes to the code segment at runtime, which means it **cannot run from ROM** and is generally fragile, hard to debug, and violates the expected behavior of modern tooling (emulators, debuggers, etc.). Prefer lookup tables, indirect jumps via a vector in RAM, or alternative algorithms that keep code in place.
### Self-Modifying Code (SMC) Detection
Look for `sta`, `stx`, or `sty` pointing into code labels:
```asm
lda #$42
sta _target+1 ; Modifies the immediate operand of the LDA at _target
...
_target lda #$00 ; This #$00 will be replaced by #$42 at runtime
```
### Indirect indexed read (table of data)
```asm
ldy #index
lda (ptr),y ; read byte at address stored in zp ptr + Y
```
### 16-bit arithmetic (word add)
```asm
clc
lda word1_lo
adc word2_lo
sta result_lo
lda word1_hi
adc word2_hi
sta result_hi
lda word1
adc word2
sta result
lda word1+1
adc word2+1
sta result+1
```
### Calling a Prog8 subroutine from assembly
## Common Patterns
```asm
jsr p8s_myblock.p8s_mysub
ldy #index
lda (ptr),y
jsr p8s_main.p8s_helper
```
### Reading a word variable
```asm
lda p8v_myword ; loads LSB
ldy p8v_myword+1 ; loads MSB (word variables are stored LSB-first)
```
Avoid self-modifying code unless it is explicitly required. It cannot run
from ROM and complicates debugging and tooling. Prefer lookup tables, RAM
vectors, or alternative algorithms.
## CPU Quirks and Pitfalls
## Reference
### JMP ($xxFF) Page Wrap Bug (NMOS 6502)
- **Problem**: On original 6502 CPUs, `jmp ($caff)` will fetch the LSB from `$caff` but the MSB from `$ca00` (instead of `$cb00`).
- **Target**: Affects C64, C128, PET32.
- **Solution**: Avoid placing indirect jump vectors on a page boundary, or use the CX16 (65C02) which fixed this bug.
### Primary: 6502 Family CPU Reference
### BRK Instruction and the "Signature Byte"
- **Behavior**: After a `BRK` instruction, the return address on the stack is incremented by **2**. This means the CPU skips the byte immediately following the `BRK` opcode.
- **Usage**: This skipped byte is often used as a "signature" or parameter byte for the BRK handler.
<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.
### BIT Instruction Flags
- **Absolute/Zero-page**: `bit $1234` copies bit 7 of the memory value to the **N** flag and bit 6 to the **V** flag. The **Z** flag is set based on `A AND memory`.
- **Immediate (65C02 only)**: `bit #$01` only affects the **Z** flag; it does **NOT** modify N or V.
- **NMOS 6502**: Does **not** support `bit #imm`.
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.
### The "B" (Break) Flag
- **Quirk**: The B flag (bit 4 of the status register) doesn't actually exist in the hardware status register. It only exists on the stack after a `PHP` or `BRK` instruction (set to 1) or a hardware IRQ/NMI (set to 0).
- **Detection**: To tell if an interrupt was caused by `BRK` or a hardware IRQ, your handler must `pla`, `and #$10`, and check the result.
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.
### Decimal Mode Flag (D) Persistence
- **Pitfall**: On NMOS 6502, the `D` flag is **not** cleared on interrupt. Always use `cld` in IRQ handlers. On 65C02, it is cleared automatically, but `cld` is still good practice.
- **ADC/SBC**: Be extremely careful with arithmetic if you haven't explicitly set or cleared the `D` flag, as its state might be unknown.
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.
## Optimization Tips
- **`stz` (65C02 only)**: Saves cycles and bytes compared to `lda #0`, `sta ...`.
- **`bra` (65C02 only)**: Shorter and usually faster than `jmp`.
- **Avoid `clc` before `bcc`**: `cmp` already sets the carry flag correctly for `bcc`/`bcs`.
- **`inx` / `dex` vs `clc`+`adc #1`**: Incrementing/decrementing is faster and doesn't affect the carry flag.
- **Zero-page usage**: Accessing variables in zeropage is 1 cycle faster and 1 byte shorter than absolute addressing.
- **Cycle Counting**: Most instructions take 2-4 cycles. `jsr` takes 6, `rts` takes 6. Branches take 2 (no branch), 3 (branch taken), or 4 (branch taken across page boundary).
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.
## 64tass Macros
Define macros for common tasks:
```asm
pushax .macro
pha
txa
pha
.endm
### Secondary: WDC 65C02 reference
popax .macro
pla
tax
pla
.endm
```
Call them with `#pushax` and `#popax`.
<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.
## IRQ Handler Best Practices
- **Clear the Decimal Flag (`cld`)**: On the original 6502, the decimal flag (`D`) is **not** automatically cleared when an interrupt occurs. If the interrupted code was in decimal mode, your handler will also run in decimal mode, causing arithmetic errors. Always call `cld` at the beginning of your handler. The 65C02 clears it automatically, but `cld` is still recommended for portability.
### Neither reference covers the assembler
## Invoking the Assembler (64tass)
If you need to manually invoke `64tass` to assemble a generated `.asm` file, you should be aware of the default arguments that `prog8c` supplies to ensure compatibility with the generated code:
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'`.
- `--ascii`: **CRITICAL.** Prog8 generates character and string data in ASCII. Without this flag, `64tass` defaults to PETSCII, which will garble your strings.
- `--case-sensitive`: Prog8 is case-sensitive and expects the assembler to be as well.
- `--long-branch`: Enables automatic conversion of relative branches (`beq`, `bne`, etc.) to absolute jumps if the target is out of range. Prog8 relies on this.
- `-Wno-implied-reg`: Suppresses warnings when the accumulator `a` is omitted from instructions like `rol`, `lsr`, etc. (though the skill recommends always using `rol a` for clarity).
- `-Wall`: Enables all warnings.
- `--cbm-prg` (or `--atari-xex` / `--nostart`): Sets the output format and adds the appropriate load address header.
## Tools
### Optional but Recommended for Debugging:
- `--vice-labels --labels=labels.txt`: Generates a label file that can be loaded into the VICE monitor (`load_labels "labels.txt"`) to see your Prog8 symbol names while debugging.
- `--list=listing.txt`: Generates a full assembly listing file with addresses and opcodes.
Prog8 invokes 64tass with `--ascii --case-sensitive --long-branch -Wall` plus
the target output option such as `--cbm-prg`. For manual assembly:
### Example manual invocation:
```bash
64tass --ascii --case-sensitive --long-branch -Wall --cbm-prg -o myprogram.prg myprogram.asm
64tass --ascii --case-sensitive --long-branch -Wall --cbm-prg \
-o myprogram.prg myprogram.asm
```
For debugging, add `--vice-labels --labels=labels.txt` and/or
`--list=listing.txt`.
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`.
+197
View File
@@ -0,0 +1,197 @@
---
name: m68k-coder
description: Writing or understanding Motorola 68000/68020 assembly in vasm mot syntax, standalone or embedded in Prog8
---
# M68K Assembly Coder
Use Motorola syntax for `vasm`, not Devpac or other assemblers. Keep answers
concise and practical.
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.
- Use lowercase opcodes and operands, two spaces between opcode and operand,
and two spaces before end-of-line comments.
- Use `.b`, `.w`, and `.l` for byte, word, and long operations. Do not rely on
default sizes.
- Hex is `$1234`, binary is `%1010`, decimal is `123`, and characters use
`#'A'`.
- Local labels start with `.`. Equates use `name = value`.
- Data directives are `dc.b`, `dc.w`, `dc.l`; allocation directives are
`ds.b`, `ds.w`, `ds.l`.
- Sections use forms such as `SECTION .text,code` and `SECTION .bss,bss`.
```asm
moveq #0,d0
move.w value,d0
add.l d1,d0
.loop
dbra d0,.loop
```
## FPU Size Suffixes
FPU instructions that access memory **must** include a size suffix: `.s`
(single, 4 bytes), `.d` (double, 8 bytes), or `.x` (extended, 12 bytes).
Without a suffix, vasm defaults to `.x`, reading 12 bytes and corrupting
results when the operand is smaller.
**Use `.s` with memory operands:**
```asm
fmove.s value(pc),fp0 ; load float from memory
fcmp.s other(pc),fp0 ; compare float in memory
ftst.s value(pc) ; test float in memory
```
**Omit the suffix for register-to-register operations:**
```asm
fadd fp1,fp0 ; fp0 = fp0 + fp1
fcmp fp1,fp0 ; compare fp0 vs fp1
ftst fp0 ; test fp0
fintrz fp0,fp0 ; truncate fp0
```
The FPU registers are always 80-bit internally, so no size suffix is needed
when both operands are registers.
## Architecture
- Registers `D0-D7` and `A0-A7` are 32-bit; `A7` is the stack pointer.
- Pointers are 32-bit and must normally be accessed with `.l`.
- Memory is big-endian: `$1234` is stored as `$12,$34` and a long's low byte
is at offset `+3`.
- Word and long accesses must be even-aligned on 68000. 68020 permits
misalignment but it is slower.
- **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`.
- On 68000, sign-extend a byte with `ext.w` followed by `ext.l`.
- `move.b` and `move.w` to a data register do not clear its upper bits. Like
`move.w` only writes the lower 16 bits, `move.b` only writes the lower 8 bits;
both leave all other bits of the register untouched. So zero-extending a byte
to long requires `moveq #0,d0` first, then `move.b src,d0`, then `move.l d0,dst`
(or mask with `and.l #$ff` / `and.l #$ffff`). Relying on `move.b` to clear the
upper bits is a common bug that corrupts values.
- `movea` does not set condition codes; `move` does. Keep the stack word
aligned and push words or longs, never bytes.
## Registers and Calls
- Return values use `D0` (primary), `D1` and `D2` (additional), or `A0` for a
pointer.
- `D0-D1` and `A0-A1` are scratch registers. Preserve registers required by
the surrounding calling convention, especially `D2-D7` and `A2-A6`.
- 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`
parameter names are documentation only: use the actual annotated registers.
Declare every modified register in `clobbers`.
```prog8
asmsub add100(uword value @D0) -> uword @D0 clobbers (D0) {
%asm {{
add.w #100,d0
}}
}
```
Do not use `%option no_sysinit` on `amiga500` or `qemu68k` targets when the goal
is to create normal runnable programs. It skips critical startup logic including
library initialization and CLI argument handling, causing programs to crash on
start.
Supported annotations include `@D0`-`@D7`, `@A0`-`@A6`, virtual `@R0`-`@R15`,
floating-point `@FP0`-`@FP7`, and flags `@Pc` and `@Pz`.
Prog8 assembly symbols are prefixed:
| Prefix | Symbol |
| --- | --- |
| `p8v_` | variable or parameter |
| `p8s_` | subroutine |
| `p8b_` | block |
| `p8c_` | constant or enum member |
| `p8l_` | label |
| `p8t_` | struct type |
Use fully qualified names when needed, such as
`p8b_main.p8v_value`. `%option no_symbol_prefixing` disables prefixes.
```asm
move.l p8v_pointer,a0
move.w p8v_word,d0
jsr p8s_main.p8s_helper
```
## Useful Patterns
```asm
lea source,a0
lea dest,a1
.copy
move.b (a0)+,(a1)+
bne.s .copy
```
Use `moveq` for small constants, `addq`/`subq` for constants 1 through 8, and
`lea` for address arithmetic. Check divisors for zero and division overflow.
## Tools
For QEMU 68k, Prog8 invokes `vasmm68k_mot` with `-m68020 -m68881 -Felf
-opt-speed -warnunaligned -ldots -spaces`, then links with `vlink`. For raw
binary output use `-Fbin` and `-join=0x<addr>`. Add `-L listing.txt` when a
listing is needed.
Prog8 runs `amiga500` programs with `vamos --cpu 68000` and `qemu68k` programs
with `qemu-system-m68k -M virt -cpu m68020 -m 1M -kernel program.elf -nographic`.
+141 -221
View File
@@ -1,248 +1,168 @@
---
name: prog8-coder
description: Writing or understanding Prog8 programs
description: Writing or understanding Prog8 programs and Prog8 IR
---
# Prog8 Coder Skill
# Prog8 Coder
You are an expert retro system development assistant, versed in the Prog8 programming language. Keep responses concise and practical — prefer short, correct code examples over lengthy prose.
You are working with **Prog8** source code (`.p8` files) or its Intermediate Representation (`.p8ir` files). Prog8 targets 8-bit systems (C64, CX16, C128, PET32) with the 6502 CPU, plus a `virtual` target for testing.
Follow ALL the rules below carefully.
You are an expert Prog8 assistant. Keep answers concise and practical. This
skill covers `.p8` source and `.p8ir` intermediate representation for the
6502, M68000, and virtual targets.
## General & Setup
- A program = a `main` block containing a `start` subroutine entry point, plus optional other subroutines/blocks
- Add `%zeropage basicsafe` at the top of your program to allow clean return on exit (instead of resetting the machine/emulator)
- Module imports: `%import modulename` — no `as` aliasing. Use the module's defined prefix (e.g., `%import textio` → `txt.xxx`)
- `.p8ir` files are the **Intermediate Representation** — target-independent, executable via `prog8c -vm file.p8ir`
- **Library search**: `prog8c -libsearch <regex>` to find routines in the embedded stdlib (e.g., `prog8c -libsearch "txt\."`)
- **Library dump**: `prog8c -libdump <dir>` to extract all embedded library source files
- **`sys` module**: always available, no import needed
- **CX16 programs**: add `%encoding iso`, call `txt.iso()` in `start()`, end with `sys.poweroff_system()`
Various reference documentation is available locally in
`~/Documents/prog8-dev-resources`:
## Datatypes & Variables
- Primitives: `bool`, `byte`, `ubyte`, `word`, `uword`, `long`, `float`, `str`
- `ubyte`/`uword` = unsigned; `long` = signed 4-byte; `float` = 5-byte MS format; `str` = 0-terminated ubytes (max 255 chars)
- **float requires `%import floats`** at top of file, else compiler errors
- Arrays: max 256 bytes (512 for split word arrays). For larger data, use `memory()` + pointers
- `memory(name, size)` returns a `uword` address to a statically reserved block of memory
- To point a typed pointer at a `memory()` block, assign the uword directly: `^^MyStruct ptr = memory("name", size)`. The `^^Type:expression` syntax (below) only works with array literals, not general uword expressions.
- Struct initialization: `^^StructType ptr = ^^StructType:[val1,val2,...]` (the `^^StructType:` can be omitted if inferable). This `^^Type:[...]` syntax does NOT work with variables or `memory()` — only literal arrays.
- Struct definitions must be inside a block, not at file level.
- Struct fields: simple types, `str`, and fixed-size arrays. Array fields: `ubyte[108] name`. `str` in struct = `^^ubyte`
- Word/pointer arrays split into LSB/MSB by default. Override with `@nosplit`
- **No call stack**: all variables statically allocated. No recursion without manual stack management
- Variables zero-initialized (globals at start, locals on subroutine entry)
- `@shared`, `@zp`, `@requirezp`, `@dirty`, `@nosplit` are **tags** that go on variable declarations. **Place them after the datatype (and array specifiers), before the variable name(s):**
```
private ubyte[8] @shared vera_storage ; single var with tag
ubyte @zp @shared varname ; multiple tags allowed
ubyte @requirezp var1 ; require zeropage
ubyte[] @shared names ; array with tag
```
The grammar is: `[private] datatype [arraydims] [tags...] identifierlist`
- `@shared` marks variables as "used by external code" (assembly), prevents the optimizer from removing them
- `@zp`/`@requirezp`: use sparingly — only for pointers (limited zeropage space)
- Pointer-like typed pointers (`^^type`) support C-style scaled arithmetic; `uword` pointers always treat element as 1 byte
- `&` = untyped address (uword); `&&` = typed pointer
- Available zeropage scratch: `P8ZP_SCRATCH_B1`, `P8ZP_SCRATCH_REG`, `P8ZP_SCRATCH_W1`, `P8ZP_SCRATCH_W2`, `P8ZP_SCRATCH_PTR` — and cx16 virtual registers R0-R15 on all targets
- Virtual registers (`cx16.r0`–`cx16.r15`): global 16-bit, NOT preserved across calls.
- **WARNING: Virtual registers in ISR/IRQ handlers**: The virtual registers R0-R15 are *not preserved* across the IRQ handler call. If your handler uses them, it will corrupt the interrupted program's state. Either avoid using them in the handler, or save/restore with `cx16.save_virtual_registers()` / `cx16.restore_virtual_registers()`. This applies to all targets, not just CX16.
- **WARNING: Long operations clobber R12-R15**: Some operations on `long` values use R12-R15 as temporary storage and will silently overwrite them. Do not rely on R12-R15 values when working with longs, and avoid using R12-R15 explicitly if your code uses long arithmetic.
- **WARNING: VERA registers in ISR handlers (CX16)**: If your IRQ handler reads or writes VERA control registers (e.g., `cx16.VERA_DATA0`, `cx16.VERA_ADDR_L`, etc.), you must save and restore the VERA context around the handler's work using `cx16.save_vera_context()` / `cx16.restore_vera_context()`. Without this, the handler will corrupt any VERA operations (tilemap updates, sprite positioning, etc.) happening in the interrupted main program.
- **IRQ handler best practices**: Keep handlers extremely short and fast — they run with interrupts disabled and steal cycles from the main program. Do NOT do lengthy processing, I/O, or complex subroutine calls inside the handler. Instead, set a boolean flag or semaphore that the main loop checks periodically, and do the actual work there.
- Math performance: integer trig (`math.sin8`, `math.cos8`) uses fast LUTs; float trig (`floats.sin`/`cos`) is much slower
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!
## Recursion & Stack Management
No call stack for variable storage — recursion overwrites locals. To handle it:
1. **CPU hardware stack**: `push()`/`pushw()`/`pushl()`/`pushf()` and `pop()`/`popw()`/`popl()`/`popf()`. Save/restore locals around recursive calls
2. **Software stacks**: `buffers.stack` (uword) / `buffers.smallstack` (ubyte). Both provide `push_b()`/`push_w()` and `pop_b()`/`pop_w()`
3. **Iterative rewrite (preferred)**: Many recursive algorithms work as `repeat` loops with explicit bounds — avoids all stack overhead
## Strings, Arrays & Pointers
- `str` / array: max 256 bytes. `long[]` limited to 64 entries (64x4=256). `str[]` for string arrays: `str[5] names = ["a","b","c","d","e"]`
- 2D arrays: `type[rows][cols] name`, access `name[r][c]`. Flat init list only (no nested `[[...]]`). Total size still ≤ 256 bytes
- str/array passed as pointer to subroutine (receiving subroutine gets `^^ubyte` or `^^element`)
- **No const pointers** or pointer-to-pointer currently supported
- **Parsing limitation**: `pointer[index].field` as assignment target needs `^^`: `pointer[index]^^.field = value`
- **Pre-allocate buffers**: `str buffer = "." * 50` (empty `""` allocates nothing — `strings.append()` will fail)
- **No reassignment**: can't `buffer = "new text"` after declaration. Use `strings.copy()`/`strings.ncopy()`
- String concat is expensive on 6502. Prefer separate prints over concatenation
- **Efficient buffer iteration**: prefer `ptr++` + `@(ptr)` over `@(buffer + offset)`. Exception: if offset is a `ubyte` (≤ 255), `buffer[offset]` works fine
- `@(ptr)` = peek/poke byte. For words: `peekw`/`pokew`, longs: `peekl`/`pokel`, floats: `peekf`/`pokef`, bools: `peekbool`/`pokebool`
- Assigning a uword address to a `str` field of a struct is done by direct assignment. Compute the address in a uword variable first, then assign: `cx16.r0 = &namebufs + offset; entry.name = cx16.r0`. The `^^ubyte:(expr)` cast syntax does not parse — use a temp uword instead.
- Use `len(array)` instead of hardcoded sizes
- Array indexing is 0-based: `arr[0]` is first element
- Static memory only — real dynamic allocation impossible, but can emulate with a simple arena allocator over a `memory()` slab
## Essentials
## Logic & Control Flow
- Logical operators (short-circuit, bool only): `and`, `or`, `xor`, `not`
- In `a and b`: if `a` is false, `b` is NOT evaluated
- In `a or b`: if `a` is true, `b` is NOT evaluated
- Important when `b` has side effects
- Bitwise operators: `&`, `|`, `^`, `~`, `<<`, `>>`
- Bit rotation: `rol()`/`ror()` (through carry), `rol2()`/`ror2()` (no carry)
- CPU status flag branches: `if_cs`, `if_cc`, `if_z`, `if_nz` (compile to single 6502 branch instructions)
- If-expressions for simple value assignments based on a choice
- **Optional braces in if/else**: when the `if` or `else` body is a single statement, the `{ }` can be omitted. Place the statement on the next line, indented. Example:
```
if x < 5
txt.print("small")
```
- `defer` defers statement execution until scope exit. Multiple defers fire in **reverse registration order** (LIFO / stack order — last deferred runs first). A defer is only registered if execution reaches that statement — conditional paths that skip the `defer` line will not register it.
- `goto`, labels, jump lists allowed
- **Common mistake**: `and`/`or` for bitmasking — use `&`/`|` instead!
- A program normally has a `main` block with a parameterless `start` entry
subroutine. Other blocks and subroutines may be added.
- `%import modulename` imports a module without aliases. Use its defined prefix,
such as `%import textio` followed by `txt.print(...)`.
- `sys` is always available and needs no import.
- `.p8ir` is target-independent IR and can be run with
`prog8c -vm program.p8ir`.
- Use `%zeropage basicsafe` in test programs when a clean return is needed.
- Find library routines with `prog8c -libsearch <regex>` or extract them with
`prog8c -libdump <dir>`.
- 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).
### The when Statement
The `when` statement is a control flow construct that enables you to execute a specific action based on the value of an expression. It is generally more readable and often more efficient than a sequence of `if-else if` statements, as the compiler can optimize it into more efficient branching structures, such as a jump table.
- **Expression**: Evaluates an expression and compares it against case values.
- **Cases**: Defined by a value followed by the `->` operator.
- **Blocks**: Use `{ }` to enclose multiple statements for a case.
- **Else Clause**: Serves as a default handler; mandatory unless the expression type is fully covered.
- **Efficiency**: Recommended for handling sets of fixed choices as it typically results in better assembly code.
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()`.
## Loop Constructs
Prog8 supports these loop types. All support `break` and `continue` (except `unroll`).
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.
### `for` loop — iterate over a range or array
- Loop variable **must be declared separately** before the `for` statement
- Works with `ubyte`, `byte`, `uword`, `word`, `long`, pointer types (NOT `float`)
- Iterates over ranges (`start to end`), descending ranges (`start downto end`), or arrays/strings
- Optional `step <constant>` for non-unit step sizes
- Loop variable value **after the loop is undefined** — don't rely on it
- Descending loops with `downto` usually produce more efficient 6502 code
```
ubyte i
for i in 20 to 155 {
; body
break ; exit loop
continue ; next iteration
}
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
modules, IRQ handlers, boot stubs, or code that deliberately avoids those
libraries.
; descending
for i in 155 downto 20 {}
for i in 155 to 20 step -1 {}
## Types and Memory
; iterate over array elements
uword[] fib = [0, 1, 1, 2, 3, 5, 8, 13]
uword num
for num in fib {
...
}
- Primitive types include `bool`, `byte`, `ubyte`, `word`, `uword`, `long`,
`float`, `str`, and `pointer`.
- `pointer` is target-sized: 16-bit on 6502 targets, 32-bit on `amiga500`,
`qemu68k`, and `virtual`. Use it for portable addresses, not `uword` or
`long` chosen by assumption.
- M68000 targets are big-endian and use 32-bit pointers. Load the `m68k-coder`
skill for assembly details.
- `float` requires `%import floats`.
- 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()`
for larger data.
- Word arrays are split into LSB/MSB arrays by default. Use `@nosplit` when
contiguous storage is required.
- Variable tags follow the type and array dimensions, before the name:
```prog8
ubyte[8] @shared vera_storage
uword @requirezp address
```
### `while` loop — repeat while condition is true
```
while condition {
; body
break
continue
}
```
Use `@shared` for values accessed externally by assembly. Use `@zp` and
`@requirezp` sparingly because zeropage is limited. Never assume virtual
registers survive calls. Long operations may clobber `R12-R15`.
### `do`-`until` loop — always executes body at least once
```
do {
; body
break
continue
} until condition
```
## Syntax Pitfalls
### `repeat` loop — repeat a fixed number of times (most efficient)
- Most efficient code generation — prefer over `for` when loop variable not needed
- Omit count for infinite loop (still supports `break`)
```
repeat 15 {
; body
break
continue
}
- Hex uses `$FF`, binary uses `%1010`, and casts use `expression as type`.
- There is no automatic type widening 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
`~` for bitwise operations.
- `and` and `or` short-circuit, so the right operand may not be evaluated.
- There is no `elif`, bare block, or semicolon statement separator. Semicolon
starts a comment. Prefer one statement per line and four-space indentation.
- `defer` executes registered statements in reverse registration order.
- Array indexing starts at zero. Use `len(array)` rather than hardcoded sizes.
- There is no function overloading. Call type-specific library routines such as
`txt.print_ub` or `txt.print_w`.
; infinite:
repeat {
; body
break if x==5
}
```
## Control Flow
### `unroll` loop — compile-time code duplication
- Not a real loop — duplicates body N times at compile time
- No `break`/`continue` allowed
- Only simple statements (assignments, calls) in body
- Constant iteration count required
```
unroll 80 {
cx16.VERA_DATA0 = 255
}
```
Prog8 supports `if`/`else`, `when`, `for`, `while`, `do`/`until`, `repeat`,
`unroll`, `break`, `continue`, `goto`, and labels. `unroll` duplicates a
constant-count body at compile time and does not support `break` or `continue`.
`repeat` is usually more efficient than `for` when no loop variable is needed.
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 & Return Values
- **Don't use `private` on subroutines and variables** (including nested ones) unless the user asks for it. Everything is public by default in Prog8 — follow that convention.
- `inline` keyword for subroutines to suggest inlining
- No **function overloading** (except builtins) and no **polymorphism** in general. This means you must call specific routines for different types (e.g., `txt.print_ub(val)` vs `txt.print_w(val)` instead of a generic `print(val)`). Cannot use builtin names (msw, lsw, msb, lsb, mkword, mklong, peek, peekw, peekl, etc.) as variable/sub names
- Can return 0, 1, or multiple values: `a, b, c = routine()`. Use `void` to skip: `void routine()`, `a, void, c = routine()`
- Nested subroutines access parent scope variables directly
## Subroutines and Assembly
## Assembly Subroutines (asmsub)
For low-level assembly that gets arguments via registers and returns values in registers.
- **Syntax**: `[private] [inline] asmsub subname(params) [clobbers(regs)] [-> returns] { %asm {{ ... }} }`
- **Body Restriction**: The body of an `asmsub` **must only contain** a single `%asm {{ ... }}` node. Regular Prog8 statements or nested blocks are NOT allowed.
- **Parameters**: `type name @register` (e.g., `ubyte val @A`, `uword addr @AX`, `float f @FAC1`).
- **Return Values**: `-> type @register` (e.g., `-> ubyte @A`, `-> bool @Pz` for immediate branch use).
- **Registers**:
- **8-bit**: `@A`, `@X`, `@Y`
- **16-bit**: `@AX`, `@AY`, `@XY` (register pairs)
- **Float**: `@FAC1`, `@FAC2` (Floating Point Accumulators)
- **Virtual**: `@R0`–`@R15` (16-bit), `@R0R1`–`@R14R15` (32-bit combined)
- **Status Flags (for returns)**: `@Pc` (Carry), `@Pz` (Zero), `@Pv` (Overflow), `@Pn` (Negative)
- **Clobbers**: `clobbers (A, X, Y)` — list all hardware registers modified by the routine.
- **Parameter names** are for documentation and type checking only. Use the registers in your assembly code.
- **Inlining**: `inline asmsub` will paste the assembly code directly at the call site, avoiding `jsr`/`rts` overhead.
- Subroutines can return zero, one, or multiple values:
`a, b = routine()` or `void routine()`.
- 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
instructions, zero-page rules, and symbol references. Load `m68k-coder` for
M68000 assembly.
## External Subroutines (extsub)
Used to call routines at fixed memory addresses (like ROM KERNAL routines or third-party drivers).
- **Syntax**: `[private] extsub [@bank <value>] address = subname(params) [clobbers(regs)] [-> returns]`
- **Address**: Can be a hex literal (`$C000`) or a constant expression.
- **Bank (optional)**: `@bank <integer>` (constant bank) or `@bank <identifier>` (variable bank).
- **No Body**: Unlike `asmsub`, `extsub` has no `{ }` body; it just maps a signature to an address.
- **Example**:
```prog8
; CX16 KERNAL CHROUT
extsub $FFD2 = chrout(ubyte char @A) clobbers(A, X, Y)
## Interrupts and Hardware
; Routine in a specific RAM bank
extsub @bank 10 $C09F = audio_init() clobbers(A, X, Y) -> bool @Pc
```
- Keep IRQ handlers short. Set a flag and do substantial work in the main loop.
- Virtual registers `R0-R15` are not preserved across IRQ handlers. Avoid them
or save and restore them with the library routines.
- CX16 IRQ handlers that touch VERA registers must save and restore VERA
context.
- Use only symbolic scratch names such as `P8ZP_SCRATCH_W1` and
`cx16.r0`; never hardcode zeropage addresses.
## Inline Assembly
When writing inline assembly (`%asm {{ }}` blocks or `asmsub` routines), load the **asm6502-coder** skill for formatting rules, symbol references, anonymous labels, and 64tass syntax details.
## Verification
## Standard Library
- Find routines, functions, variables, modules and signatures in the symbol dump file for the given compilation target.
- Online location: https://prog8.readthedocs.io/en/latest/libraries.html#low-fi-variable-and-subroutine-definitions-in-all-available-library-modules they are linked there 1 for each compilation target
- Structure: builtin functions, then module sections with variables/constants (`type name`) and subroutines (`name (params) -> returntype`)
- Text output: `textio` module (`txt.print`, `txt.chrout`, `txt.print_b`/`_ub`/`_w`/`_uw`/`_l`/`_bool`, `txt.print_f` for floats, `txt.spc()`, `txt.nl()`). **Note: Prog8 has no function overloading**, so you cannot use `txt.print(number)` — you must call the specific routine for the type (e.g., `txt.print_ub(val)` for an unsigned byte).
- Math: `math` module — integer trig (`sin8`, `cos8`) via fast LUTs; `math.rnd()` for random numbers
- String conversion: `conv` module (`str_uword`, `str2word`, etc.) — for printing numbers use txt routines instead
- Char operations: `strings` module (`isdigit`, `isxdigit`, `isupper`, `islower`, `isletter`, `isspace`, `isprint`) — use these instead of manual ASCII/PETSCII arithmetic
- String functions return useful lengths — capture them: `len = strings.copy(dest, src)`, `len = strings.append(buf, text)`, `len = strings.upper(mystr)`
## Syntax & Formatting
- Hex: `$FF` (not `0xFF`); Binary: `%1010` (not `0b1010`). Underscores for readability: `25_000_000`
- 4-digit hex `$0000` = uword. No type suffixes (no `0L`). Cast: `expr as type`
- Augmented assignment: `+=`, `-=`, `*=`, `/=`, `%=`, `&=`, `|=`, `^=`, `<<=`, `>>=`
- **`;` starts a comment to end of line** — NOT a statement separator. One statement per line
- **No `elif`**: use nested `else { if ... }`
- Type casting: `expression as type` (e.g., `bytevar as word`). `as` has very low precedence (lower than arithmetic)
- **No automatic type widening**: `byte*byte=byte` (overflow possible!). Cast explicitly
- **No bare `{ }` blocks** like C/Java
- Indentation: 4 spaces for .p8 and .asm files (no tabs)
- Character encoding: 6502 targets use PETSCII by default (call `txt.lowercase()` at start for lowercase). Virtual target uses ISO (`%encoding iso` + `txt.iso()`)
- **Test programs for CBM targets must only use lowercase text in string literals** (the C64/C128/PET32/CX16 targets all use PETSCII). In PETSCII, uppercase letters A-Z are the *graphics* symbols, so any uppercase text you put in a string literal is rendered as unpredictable graphics characters instead of readable letters. Use only lowercase (a-z) for any printed test output, labels, or expected-match strings on these targets; reserve uppercase for intentional PETSCII graphics. The `virtual` target (ISO) is unaffected and may use either case.
- Array size inferred from initializer: `str[] types = ["a", "b", "c"]`
- Enums: `Enum::Value` syntax (double colon), declared inside a block. They are syntactic sugar for a list of `const` declarations, not a type. Use enums for related values, `const` for standalone
- Avoid `globals.XXXX` — move constants closer to where they're used
- Member access through pointers: use `.` for both direct and pointer access. The compiler infers the type. For complex assignment targets, `^^` may be needed: `ptr^^.field = value`
- Qualified names: must use full path from top level (e.g., `cx16.r0`, not relative)
- Use the `virtual` target for behavioral tests when possible:
`prog8c -target virtual -emu program.p8`.
- Use `-check` for syntax and semantic checks without output generation.
- Use `-noopt` to determine whether a failure is optimizer-related.
- For IR execution, use `-vmtrace` when control flow needs inspection.
+1
View File
@@ -14,6 +14,7 @@ build/
out/
output/
gradle/
Libs/
.gradle/
.kotlin/
.git/
+1 -2
View File
@@ -31,7 +31,7 @@ jobs:
run: ./gradlew compileKotlin compileTestKotlin --console=plain
- name: Test
run: ./gradlew test --continue --console=plain
run: ./gradlew test --continue --max-workers=3 -PtestConcurrency=2 -PtestMaxHeap=8g --console=plain
- name: Package
run: |
@@ -52,4 +52,3 @@ jobs:
with:
name: test-results
path: compiler/build/test-results/test/
+6 -1
View File
@@ -15,6 +15,8 @@ out-old/
.*cache/
*.directory
*.prg
*.elf
*.o
*.bin
*.p8ir
*.bankedcalls
@@ -43,4 +45,7 @@ compiler/lib/
.gradle
sd*.img
*.d64
AsyncIO/
AsyncIO.info
m68k-amigaos-ahi/
Libs/
+1 -1
View File
@@ -14,4 +14,4 @@
<module name="prog8.codeGenNew6502.test" target="17" />
</bytecodeTargetLevel>
</component>
</project>
</project>
+2 -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>
@@ -12,6 +11,8 @@
<option value="$PROJECT_DIR$/codeCore" />
<option value="$PROJECT_DIR$/codeGenCpu6502" />
<option value="$PROJECT_DIR$/codeGenIntermediate" />
<option value="$PROJECT_DIR$/codeGenM68k" />
<option value="$PROJECT_DIR$/codeGenNew6502" />
<option value="$PROJECT_DIR$/codeOptimizers" />
<option value="$PROJECT_DIR$/compiler" />
<option value="$PROJECT_DIR$/compilerAst" />
+2 -2
View File
@@ -15,6 +15,6 @@
</component>
<component name="KotlinJpsPluginSettings">
<option name="externalSystemId" value="Gradle" />
<option name="version" value="2.4.10" />
<option name="version" value="2.4.21" />
</component>
</project>
</project>
-31
View File
@@ -1,31 +0,0 @@
{
"context": {
"fileName": "AGENTS.md"
},
"$version": 3,
"permissions": {
"allow": [
"Bash(prog8c *)",
"Bash(gradle *)",
"Bash(mkdir *)",
"Bash(ls *)",
"Bash(cat *)",
"Bash(find *)",
"Bash(sed *)",
"Bash(grep *)",
"Bash(xargs *)",
"Bash(wc *)",
"Bash(diff *)",
"Bash(paste *)",
"Bash(echo *)",
"Bash(cp *)",
"Bash(time)",
"Bash(kotlinc *)",
"Bash(python3 *)",
"Bash(jar *)",
"Bash(od *)",
"Bash(xxd *)",
"Bash(gcc *)"
]
}
}
+63 -13
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.
@@ -35,6 +41,7 @@
- ANTLR4 version 4.13 is used for the parser implementation.
- Dependent library versions can be found in 'build.gradle.kts' and in the IntelliJ IDEA configuration files in .idea/libraries
- The compiler main entrypoint is in the "compiler" module, in src/prog8/CompilerMain.kt
- **Codebase knowledge graph**: For a structured overview of modules, their dependencies, key entities, the end-to-end compilation pipeline, the standard library layout, and the test structure, consult `docs/CODEBASE-KNOWLEDGE-GRAPH.md`. Read it when you need to understand how the compiler fits together before diving into specific files.
## Compilation Flow (High-Level)
```
@@ -53,7 +60,9 @@ Source → parseMainModule() → processAst() → optimizeAst() → postprocessA
### Target Differences
- **CPU instruction set differences**: Only the CommanderX16 target (cx16) can use 65C02 instructions such as STZ. The other targets (C64, C128, PET32) can only use original 6502 instructions.
- When writing or understanding assembly code, load the `asm6502-coder` skill for 64tass syntax and conventions.
- When writing or understanding 6502/65C02 assembly code, load the `asm6502-coder` skill for 64tass syntax and conventions.
- When writing or understanding M68K assembly code (amiga500, 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
@@ -125,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
@@ -196,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 codegen module** (one that reads `.p8ir` files): Run `gradle :<module>:test --console=plain` instead of full `gradle build`. Compiler unit tests are irrelevant for IR-level changes. Do run full `gradle build` before final verification.
- **When working on a standalone code gen module** (one that reads `.p8ir` files): Run `gradle :<module>:test --console=plain` instead of full `gradle build`. Compiler unit tests are irrelevant for IR-level changes. Do run full `gradle build` before final verification if the change touched compiler sources.
### Build command summary
@@ -216,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
@@ -242,15 +267,25 @@ When the task involves writing or understanding `.p8` (Prog8 source) or `.p8ir`
- Never assume your changes simply work, always test!
- If the project does not have any testing tools, scripts, MCP tools, skills, etc. available for testing, ask the user whether testing should be skipped.
- in prog8 code being tested, use '@shared' variables to avoid const-folding
### Automated Tests (gradle)
Always use the `--console=plain` flag with these commands to avoid messing up the agent's text interface.
- `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:
@@ -281,9 +316,17 @@ gradle :compiler:compileTestKotlin --info 2>&1 | grep "^e:"
- Unit tests use KoTest (FunSpec style)
- Tests are in the "compiler" module's `test` directory (and some other modules)
- Test config is in root `build.gradle.kts`; tests run in parallel
- **When writing test programs**, add at the top: `%zeropage basicsafe` and `%option no_sysinit`
- **When writing test programs**, add at the top: `%zeropage basicsafe` and `%option no_sysinit` - the latter ONLY when NOT writing for the amiga500 or qemu68k targets
- When a test fails, the output shows "There were failing tests. See the report at:" - **read that HTML report**
### 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)
@@ -291,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:
@@ -307,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
+9 -3
View File
@@ -4,13 +4,14 @@ PayPal: [![PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)]
[![Documentation](https://readthedocs.org/projects/prog8/badge/?version=latest)](https://prog8.readthedocs.io/)
Prog8 - Structured Programming Language for 8-bit 6502/65c02 microprocessors
============================================================================
Prog8 - Structured Programming Language for 6502/65c02 (8-bit) and 68000 (32-bit) microprocessors
=================================================================================================
*Written by Irmen de Jong (irmen@razorvine.net)*
This is a structured programming language for the 8-bit 6502/6510/65c02 microprocessor from the late 1970's and 1980's
as used in many home computers from that era. It is a medium to low level programming language,
as used in many home computers from that era. It also supports the 32-bit Motorola 68000 CPU, for which the
Commodore Amiga is the intended machine. It is a medium to low level programming language,
which aims to provide many conveniences over raw assembly code (even when using a macro assembler).
**Want to buy me a coffee or a pizza perhaps?**
@@ -104,9 +105,14 @@ What does Prog8 provide?
- "c64": Commodore-64 (6502 like CPU)
- "c128": Commodore-128 (6502 like CPU - the Z80 cpu mode is not supported)
- "pet32": Commodore PET (limited support)
- "amiga500": Commodore Amiga 500 (16/32-bit 68000 CPU; experimental)
- "amiga1200": Commodore Amiga 1200 (32-bit 68020 CPU with optional FPU; experimental)
- "qemu68k": Qemu M68k simulator (experimental)
- via external configurable targets: Atari 800 XL, Neo6502, NES, C64 OS, Foenix F256, ...
- If you only use standard kernal and prog8 library routines, it is possible to compile the *exact same program* for different machines (just change the compiler target flag)
The m68k targets (amiga500, amiga1200, qemu68k) and the optional `-newcodegen` 6502 code generator are experimental and not yet suitable for production code.
Additional required tools
+13 -12
View File
@@ -41,7 +41,7 @@ rotate3d {
for i in 0 to len(matrix_math.xcoor)-1 {
rz = matrix_math.rotatedz[i]
if rz >= 10 {
persp = 600 + rz/64
persp = 600 + (rz>>5)
sx = matrix_math.rotatedx[i] / persp as byte + WIDTH/2
sy = matrix_math.rotatedy[i] / persp as byte + HEIGHT/2
;; txt.setcc(sx as ubyte, sy as ubyte, 46, 7)
@@ -51,7 +51,7 @@ rotate3d {
for i in 0 to len(matrix_math.xcoor)-1 {
rz = matrix_math.rotatedz[i]
if rz < 10 {
persp = 600 + rz/64
persp = 600 + (rz>>5)
sx = matrix_math.rotatedx[i] / persp as byte + WIDTH/2
sy = matrix_math.rotatedy[i] / persp as byte + HEIGHT/2
;; txt.setcc(sx as ubyte, sy as ubyte, 81, 7)
@@ -84,18 +84,19 @@ matrix_math {
word wcosc = math.cos8(az)
word wsinc = math.sin8(az)
word wcosa_sinb = wcosa*wsinb / 128
word wsina_sinb = wsina*wsinb / 128
; instead of (slow) /128, we simply shift 7 bits (and take the roundoff error)
word wcosa_sinb = wcosa*wsinb >> 7
word wsina_sinb = wsina*wsinb >> 7
word Axx = wcosa*wcosb / 128
word Axy = (wcosa_sinb*wsinc - wsina*wcosc) / 128
word Axz = (wcosa_sinb*wcosc + wsina*wsinc) / 128
word Ayx = wsina*wcosb / 128
word Ayy = (wsina_sinb*wsinc + wcosa*wcosc) / 128
word Ayz = (wsina_sinb*wcosc - wcosa*wsinc) / 128
word Axx = wcosa*wcosb >> 7
word Axy = (wcosa_sinb*wsinc - wsina*wcosc) >> 7
word Axz = (wcosa_sinb*wcosc + wsina*wsinc) >> 7
word Ayx = wsina*wcosb >> 7
word Ayy = (wsina_sinb*wsinc + wcosa*wcosc) >> 7
word Ayz = (wsina_sinb*wcosc - wcosa*wsinc) >> 7
word Azx = -wsinb
word Azy = wcosb*wsinc / 128
word Azz = wcosb*wcosc / 128
word Azy = wcosb*wsinc >> 7
word Azz = wcosb*wcosc >> 7
ubyte @zp i
for i in 0 to len(xcoor)-1 {
+3 -3
View File
@@ -220,10 +220,10 @@ btree {
arena {
; extremely trivial arena allocator (that never frees)
uword buffer = memory("arena", 2000, 0)
uword next = buffer
pointer buffer = memory("arena", 2000, 0)
pointer next = buffer
sub alloc(ubyte size) -> uword {
sub alloc(ubyte size) -> pointer {
defer next += size
return next
}
+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
+12 -6
View File
@@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
kotlin("jvm") version "2.4.10"
kotlin("jvm") version "2.4.21"
}
allprojects {
@@ -52,8 +52,8 @@ subprojects {
// Common test dependencies via Kotest BOM (Bill of Materials)
// This manages all Kotest module versions centrally
dependencies {
testImplementation(platform("io.kotest:kotest-bom:6.2.3"))
// implementation("com.github.irmen:ksim65:v2.1")
testImplementation(platform("io.kotest:kotest-bom:6.2.4"))
// implementation("org.codeberg.irmen:ksim65:v2.2")
}
// Common sourceSets configuration
@@ -72,16 +72,22 @@ subprojects {
// Enable JUnit 5 (required for Kotest)
useJUnitPlatform()
// 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
// Set parallelism to number of CPU cores
jvmArgs("-Dkotest.framework.parallelism=${Runtime.getRuntime().availableProcessors()}")
// Allow CI to override parallelism for memory-constrained runners
jvmArgs("-Dkotest.framework.parallelism=$testConcurrency")
// Disable Kotest autoscan warning
jvmArgs("-Dkotest.framework.classpath.scanning.autoscan.disable=true")
// Enable Gradle's parallel test execution (runs multiple test classes concurrently)
// Use 50% of available processors to avoid over-subscription
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
maxParallelForks = testConcurrency
// Allow filtering tests via -PtestFilter="*TestName*" from command line
// Example: gradle test -PtestFilter="*TestLookup*"
+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,38 +1,29 @@
package prog8.codegen.cpu6502
package prog8.code.assembly
import prog8.code.GENERATED_LABEL_PREFIX
import prog8.code.IAssemblyProgram
import prog8.code.core.CompilationOptions
import prog8.code.core.ICompilationTarget
import prog8.code.core.IErrorReporter
import prog8.code.core.OutputType
import prog8.code.core.*
import prog8.code.target.C128Target
import prog8.code.target.C64Target
import prog8.code.target.PETTarget
import java.nio.file.Path
internal class AssemblyProgram(
override val name: String,
outputDir: Path,
private val compTarget: ICompilationTarget) : IAssemblyProgram {
class AssemblyProgram6502(
override val name: String,
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)
@@ -45,7 +36,7 @@ internal class AssemblyProgram(
// 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")
@@ -56,10 +47,10 @@ internal class AssemblyProgram(
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}.")
}
@@ -67,7 +58,7 @@ internal class AssemblyProgram(
// 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")
@@ -78,16 +69,16 @@ internal class AssemblyProgram(
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")
@@ -98,9 +89,9 @@ internal class AssemblyProgram(
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}.")
}
@@ -108,7 +99,7 @@ internal class AssemblyProgram(
// 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")
@@ -119,7 +110,7 @@ internal class AssemblyProgram(
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)
@@ -131,8 +122,9 @@ internal class AssemblyProgram(
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}")
}
val proc = ProcessBuilder(assemblerCommand)
@@ -159,16 +151,16 @@ internal class AssemblyProgram(
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 @@ internal class AssemblyProgram(
}
}
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,6 +181,7 @@ internal class AssemblyProgram(
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")
}
}
@@ -0,0 +1,12 @@
package prog8.code.assembly
import prog8.code.core.CompilationOptions
import prog8.code.core.IErrorReporter
interface IAssemblyProgram {
val name: String
val irInstructionCount: Int
val irChunkCount: Int
val irRegisterCount: Int
fun assemble(options: CompilationOptions, errors: IErrorReporter): Boolean
}
@@ -33,7 +33,7 @@ class FSignature(val pure: Boolean, // does it have side effects?
val returnTypes: Array<BaseDataType>,
vararg val parameters: FParam) {
fun callConvention(actualParamTypes: List<BaseDataType>): CallConvention {
fun callConventionFor6502(actualParamTypes: List<BaseDataType>): CallConvention {
val returns: ReturnConvention
if(returnTypes.isEmpty())
returns = ReturnConvention(null, null)
@@ -41,7 +41,7 @@ class FSignature(val pure: Boolean, // does it have side effects?
val returnType = returnTypes[0]
returns = when (returnType) {
BaseDataType.UBYTE, BaseDataType.BYTE -> ReturnConvention(returnType, RegisterOrPair.A)
BaseDataType.UWORD, BaseDataType.WORD -> ReturnConvention(returnType, RegisterOrPair.AY)
BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.POINTER -> ReturnConvention(returnType, RegisterOrPair.AY)
BaseDataType.LONG -> ReturnConvention(returnType, RegisterOrPair.R14R15)
BaseDataType.FLOAT -> ReturnConvention(returnType, RegisterOrPair.FAC1)
in IterableDatatypes -> ReturnConvention(returnType, RegisterOrPair.AY)
@@ -49,7 +49,7 @@ class FSignature(val pure: Boolean, // does it have side effects?
// return type depends on arg type
when (val paramType = actualParamTypes.first()) {
BaseDataType.UBYTE, BaseDataType.BYTE -> ReturnConvention(paramType, RegisterOrPair.A)
BaseDataType.UWORD, BaseDataType.WORD -> ReturnConvention(paramType, RegisterOrPair.AY)
BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.POINTER -> ReturnConvention(paramType, RegisterOrPair.AY)
BaseDataType.LONG -> ReturnConvention(returnType, RegisterOrPair.R14R15)
BaseDataType.FLOAT -> ReturnConvention(paramType, RegisterOrPair.FAC1)
in IterableDatatypes -> ReturnConvention(paramType, RegisterOrPair.AY)
@@ -109,8 +109,8 @@ val BuiltinFunctions: Map<String, FSignature> = mapOf(
"prog8_lib_square_word" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("value", BaseDataType.WORD, BaseDataType.UWORD)),
"prog8_lib_square_long" to FSignature(true, arrayOf(BaseDataType.LONG), FParam("value", BaseDataType.LONG)),
"prog8_lib_structalloc" to FSignature(true, arrayOf(BaseDataType.UWORD)),
"prog8_lib_copylong" to FSignature(false, emptyArray(), FParam("pointer1", BaseDataType.UWORD), FParam("pointer2", BaseDataType.UWORD)),
"prog8_lib_copyfloat" to FSignature(false, emptyArray(), FParam("pointer1", BaseDataType.UWORD), FParam("pointer2", BaseDataType.UWORD)),
"prog8_lib_copylong" to FSignature(false, emptyArray(), FParam("pointer1", BaseDataType.POINTER, BaseDataType.UWORD, BaseDataType.LONG), FParam("pointer2", BaseDataType.POINTER, BaseDataType.UWORD, BaseDataType.LONG)),
"prog8_lib_copyfloat" to FSignature(false, emptyArray(), FParam("pointer1", BaseDataType.POINTER, BaseDataType.UWORD, BaseDataType.LONG), FParam("pointer2", BaseDataType.POINTER, BaseDataType.UWORD, BaseDataType.LONG)),
"abs" to FSignature(true, arrayOf(BaseDataType.UNDEFINED), FParam("value", *NumericDatatypes)),
"abs__byte" to FSignature(true, arrayOf(BaseDataType.UBYTE), FParam("value", BaseDataType.BYTE)),
"abs__word" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("value", BaseDataType.WORD)),
@@ -158,33 +158,37 @@ val BuiltinFunctions: Map<String, FSignature> = mapOf(
"max__word" to FSignature(true, arrayOf(BaseDataType.WORD), FParam("val1", BaseDataType.WORD), FParam("val2", BaseDataType.WORD)),
"max__uword" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("val1", BaseDataType.UWORD), FParam("val2", BaseDataType.UWORD)),
"max__long" to FSignature(true, arrayOf(BaseDataType.LONG), FParam("val1", BaseDataType.LONG), FParam("val2", BaseDataType.LONG)),
"peek" to FSignature(true, arrayOf(BaseDataType.UBYTE), FParam("address", BaseDataType.UWORD)),
"peekbool" to FSignature(true, arrayOf(BaseDataType.BOOL), FParam("address", BaseDataType.UWORD)),
"peekw" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("address", BaseDataType.UWORD)),
"peekl" to FSignature(true, arrayOf(BaseDataType.LONG), FParam("address", BaseDataType.UWORD)),
"peekf" to FSignature(true, arrayOf(BaseDataType.FLOAT), FParam("address", BaseDataType.UWORD)),
"poke" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD), FParam("value", BaseDataType.UBYTE, BaseDataType.BYTE)),
"pokebool" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD), FParam("value", BaseDataType.BOOL)),
"pokebowl" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD), FParam("value", BaseDataType.BOOL)),
"pokew" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD), FParam("value", BaseDataType.UWORD, BaseDataType.WORD)),
"pokel" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD), FParam("value", BaseDataType.LONG)),
"pokef" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD), FParam("value", BaseDataType.FLOAT)),
"pokemon" to FSignature(false, arrayOf(BaseDataType.UBYTE), FParam("address", BaseDataType.UWORD), FParam("value", BaseDataType.UBYTE)),
"peek" to FSignature(true, arrayOf(BaseDataType.UBYTE), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER)),
"peekbool" to FSignature(true, arrayOf(BaseDataType.BOOL), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER)),
"peekw" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER)),
"peekl" to FSignature(true, arrayOf(BaseDataType.LONG), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER)),
"peekf" to FSignature(true, arrayOf(BaseDataType.FLOAT), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER)),
"peekp" to FSignature(true, arrayOf(BaseDataType.POINTER), FParam("address", BaseDataType.POINTER, BaseDataType.UWORD, BaseDataType.LONG)),
"poke" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER), FParam("value", BaseDataType.UBYTE, BaseDataType.BYTE)),
"pokebool" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER), FParam("value", BaseDataType.BOOL)),
"pokebowl" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER), FParam("value", BaseDataType.BOOL)),
"pokew" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER), FParam("value", BaseDataType.UWORD, BaseDataType.WORD)),
"pokel" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER), FParam("value", BaseDataType.LONG)),
"pokef" to FSignature(false, emptyArray(), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER), FParam("value", BaseDataType.FLOAT)),
"pokep" to FSignature(false, emptyArray(), FParam("address", BaseDataType.POINTER, BaseDataType.UWORD, BaseDataType.LONG), FParam("value", BaseDataType.POINTER)),
"pokemon" to FSignature(false, arrayOf(BaseDataType.UBYTE), FParam("address", BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.POINTER), FParam("value", BaseDataType.UBYTE)),
"rsave" to FSignature(false, emptyArray()),
"rrestore" to FSignature(false, emptyArray()),
"memory" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("name", BaseDataType.STR), FParam("size", BaseDataType.UWORD), FParam("alignment", BaseDataType.UWORD)),
"memory__ref" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("name", BaseDataType.STR)),
"callfar" to FSignature(false, arrayOf(BaseDataType.UWORD), FParam("bank", BaseDataType.UBYTE), FParam("address", BaseDataType.UWORD), FParam("arg", BaseDataType.UWORD)),
"callfar2" to FSignature(false, arrayOf(BaseDataType.UWORD), FParam("bank", BaseDataType.UBYTE), FParam("address", BaseDataType.UWORD), FParam("argA", BaseDataType.UBYTE), FParam("argX", BaseDataType.UBYTE), FParam("argY", BaseDataType.UBYTE), FParam("argC", BaseDataType.BOOL)),
"call" to FSignature(false, arrayOf(BaseDataType.UWORD), FParam("address", BaseDataType.UWORD)),
"memory" to FSignature(true, arrayOf(BaseDataType.POINTER), FParam("name", BaseDataType.STR), FParam("size", BaseDataType.UWORD), FParam("alignment", BaseDataType.UWORD)),
"memory__ref" to FSignature(true, arrayOf(BaseDataType.POINTER), FParam("name", BaseDataType.STR)),
"callfar" to FSignature(false, arrayOf(BaseDataType.UWORD), FParam("bank", BaseDataType.UBYTE), FParam("address", BaseDataType.POINTER), FParam("arg", BaseDataType.UWORD)),
"callfar2" to FSignature(false, arrayOf(BaseDataType.UWORD), FParam("bank", BaseDataType.UBYTE), FParam("address", BaseDataType.POINTER), FParam("argA", BaseDataType.UBYTE), FParam("argX", BaseDataType.UBYTE), FParam("argY", BaseDataType.UBYTE), FParam("argC", BaseDataType.BOOL)),
"call" to FSignature(false, arrayOf(BaseDataType.UWORD), FParam("address", BaseDataType.POINTER)),
"push" to FSignature(false, emptyArray(), FParam("value", BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.BOOL)),
"pushw" to FSignature(false, emptyArray(), FParam("value", BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.POINTER)),
"pushl" to FSignature(false, emptyArray(), FParam("value", BaseDataType.LONG)),
"pushf" to FSignature(false, emptyArray(), FParam("value", BaseDataType.FLOAT)),
"pushp" to FSignature(false, emptyArray(), FParam("value", BaseDataType.POINTER)),
"pop" to FSignature(false, arrayOf(BaseDataType.UBYTE)),
"popw" to FSignature(false, arrayOf(BaseDataType.UWORD)),
"popl" to FSignature(false, arrayOf(BaseDataType.LONG)),
"popf" to FSignature(false, arrayOf(BaseDataType.FLOAT)),
"popp" to FSignature(false, arrayOf(BaseDataType.POINTER)),
)
val InplaceModifyingBuiltinFunctions = setOf(
@@ -4,6 +4,7 @@ import java.nio.file.Path
import kotlin.io.path.Path
@Suppress("unused")
class CompilationOptions(val output: OutputType,
val launcher: CbmPrgLauncherType,
val zeropage: ZeropageType,
@@ -17,6 +18,7 @@ class CompilationOptions(val output: OutputType,
// these are set later, based on command line arguments or options in the source code:
var loadAddress: UInt,
var memtopAddress: UInt,
var varsAddress: UInt? = null,
var warnSymbolShadowing: Boolean = false,
var warnImplicitTypeCast: Boolean = false,
var optimize: Boolean = false,
@@ -25,16 +27,17 @@ class CompilationOptions(val output: OutputType,
var includeSourcelines: Boolean = false,
var dumpVariables: Boolean = false,
var dumpSymbols: Boolean = false,
var newCodegen: Boolean = false,
var varsHighBank: Int? = null,
var varsGolden: Boolean = false,
var slabsHighBank: Int? = null,
var slabsGolden: Boolean = false,
var breakpointCpuInstruction: String? = null,
var ignoreFootguns: Boolean = false,
var outputDir: Path = Path(""),
var quiet: Boolean = false,
var profilingInstrumentation: Boolean = false,
var symbolDefs: Map<String, String> = emptyMap()
var symbolDefs: Map<String, String> = emptyMap(),
var privateSymbols: Boolean = false,
var outputFile: Path? = null
) {
init {
compTarget.initializeMemoryAreas(this)
@@ -57,6 +60,7 @@ class CompilationOptions(val output: OutputType,
private var compilerVersion: String = "unknown"
private var loadAddress: UInt = compTarget.PROGRAM_LOAD_ADDRESS
private var memtopAddress: UInt = compTarget.PROGRAM_MEMTOP_ADDRESS
private var varsAddress: UInt? = null
private var warnSymbolShadowing: Boolean = false
private var warnImplicitTypeCast: Boolean = false
private var optimize: Boolean = false
@@ -67,14 +71,14 @@ class CompilationOptions(val output: OutputType,
private var dumpSymbols: Boolean = false
private var varsHighBank: Int? = null
private var varsGolden: Boolean = false
private var slabsHighBank: Int? = null
private var slabsGolden: Boolean = false
private var breakpointCpuInstruction: String? = null
private var ignoreFootguns: Boolean = false
private var outputDir: Path = Path("")
private var quiet: Boolean = false
private var profilingInstrumentation: Boolean = false
private var newCodegen: Boolean = false
private var symbolDefs: Map<String, String> = emptyMap()
private var outputFile: Path? = null
fun output(output: OutputType) = apply { this.output = output }
fun launcher(launcher: CbmPrgLauncherType) = apply { this.launcher = launcher }
@@ -87,6 +91,7 @@ class CompilationOptions(val output: OutputType,
fun compilerVersion(compilerVersion: String) = apply { this.compilerVersion = compilerVersion }
fun loadAddress(loadAddress: UInt) = apply { this.loadAddress = loadAddress }
fun memtopAddress(memtopAddress: UInt) = apply { this.memtopAddress = memtopAddress }
fun varsAddress(varsAddress: UInt?) = apply { this.varsAddress = varsAddress }
fun warnSymbolShadowing(warnSymbolShadowing: Boolean) = apply { this.warnSymbolShadowing = warnSymbolShadowing }
fun warnImplicitTypeCast(warnImplicitTypeCast: Boolean) = apply { this.warnImplicitTypeCast = warnImplicitTypeCast }
fun optimize(optimize: Boolean) = apply { this.optimize = optimize }
@@ -97,23 +102,23 @@ class CompilationOptions(val output: OutputType,
fun dumpSymbols(dumpSymbols: Boolean) = apply { this.dumpSymbols = dumpSymbols }
fun varsHighBank(varsHighBank: Int?) = apply { this.varsHighBank = varsHighBank }
fun varsGolden(varsGolden: Boolean) = apply { this.varsGolden = varsGolden }
fun slabsHighBank(slabsHighBank: Int?) = apply { this.slabsHighBank = slabsHighBank }
fun slabsGolden(slabsGolden: Boolean) = apply { this.slabsGolden = slabsGolden }
fun breakpointCpuInstruction(breakpointCpuInstruction: String?) = apply { this.breakpointCpuInstruction = breakpointCpuInstruction }
fun ignoreFootguns(ignoreFootguns: Boolean) = apply { this.ignoreFootguns = ignoreFootguns }
fun outputDir(outputDir: Path) = apply { this.outputDir = outputDir }
fun quiet(quiet: Boolean) = apply { this.quiet = quiet }
fun profilingInstrumentation(profilingInstrumentation: Boolean) = apply { this.profilingInstrumentation = profilingInstrumentation }
fun symbolDefs(symbolDefs: Map<String, String>) = apply { this.symbolDefs = symbolDefs }
fun newCodegen(newCodegen: Boolean) = apply { this.newCodegen = newCodegen }
fun outputFile(outputFile: Path?) = apply { this.outputFile = outputFile }
fun build(): CompilationOptions {
return CompilationOptions(
output, launcher, zeropage, zpReserved, zpAllowed, floats, noSysInit, romable, compTarget, compilerVersion,
loadAddress, memtopAddress, warnSymbolShadowing, warnImplicitTypeCast, optimize, asmQuiet, asmListfile,
includeSourcelines, dumpVariables, dumpSymbols, varsHighBank, varsGolden,
slabsHighBank, slabsGolden, breakpointCpuInstruction, ignoreFootguns, outputDir, quiet,
loadAddress, memtopAddress, varsAddress, warnSymbolShadowing, warnImplicitTypeCast, optimize, asmQuiet, asmListfile,
includeSourcelines, dumpVariables, dumpSymbols, newCodegen, varsHighBank, varsGolden,
breakpointCpuInstruction, ignoreFootguns, outputDir, quiet,
profilingInstrumentation, symbolDefs
)
).apply { this.outputFile = this@Builder.outputFile }
}
}
}
+199 -114
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), subtype is whatever type each element points to
UNDEFINED;
@@ -30,7 +31,7 @@ enum class BaseDataType {
this.isByteOrBool -> false
this.isWord -> other.isByteOrBool
this == LONG -> other.isByteOrBool || other.isWord
this == STR && other == UWORD || this == UWORD && other == STR -> false
this == STR && (other == UWORD || other == LONG) || this == UWORD && other == STR -> false
this.isArray && other.isArray -> false
this.isArray -> other.isByteOrBool
this == STR -> other.isByteOrBool
@@ -38,14 +39,20 @@ enum class BaseDataType {
else -> true
}
fun equalsSize(other: BaseDataType) =
fun equalsSize(other: BaseDataType): Boolean = equalsSize(other, null)
fun equalsSize(other: BaseDataType, memsizer: IMemSizer?): Boolean =
when {
this == other -> true
this.isArray && other.isArray -> true
this.isByteOrBool -> other.isByteOrBool
this.isWord -> other.isWord || other.isPointer
this.isPointer -> other.isWord
this == STR && other== UWORD || this== UWORD && other== STR -> true
this.isWord -> other.isWord || (other.isPointer && (memsizer == null || memsizer.POINTER_MEM_SIZE == 2u))
this.isLong -> other.isLong || (other.isPointer && memsizer?.POINTER_MEM_SIZE != null && memsizer.POINTER_MEM_SIZE != 2u)
this.isPointer -> when(memsizer?.POINTER_MEM_SIZE) {
null -> other.isWord || other.isPointer
2u -> other.isWord || other.isPointer
else -> other.isLong || other.isPointer
}
this == STR && (other == UWORD || other == LONG) || (this == UWORD || this == LONG) && other == STR -> true
this == STR && other.isArray -> true
this.isArray && other == STR -> true
else -> false
@@ -68,6 +75,15 @@ val BaseDataType.isSignedWord get() = this == BaseDataType.WORD
val BaseDataType.isLong get() = this == BaseDataType.LONG
val BaseDataType.isFloat get() = this == BaseDataType.FLOAT
val BaseDataType.isInteger get() = this in setOf(BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.LONG)
val BaseDataType.maxUnsignedValue: Double
get() = when (this) {
BaseDataType.UBYTE -> 255.0
BaseDataType.BYTE -> 127.0
BaseDataType.UWORD -> 65535.0
BaseDataType.WORD -> 32767.0
BaseDataType.LONG -> 2147483647.0
else -> Double.MAX_VALUE
}
val BaseDataType.isIntegerOrBool get() = this in setOf(BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.LONG, BaseDataType.BOOL)
val BaseDataType.isWordOrByteOrBool get() = this in setOf(BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.BOOL)
val BaseDataType.isNumeric get() = this == BaseDataType.FLOAT || this.isInteger
@@ -76,12 +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.isSplitWordArray get() = this == BaseDataType.ARRAY_SPLITW || this == BaseDataType.ARRAY_POINTER // pointer arrays are also always stored as split uwords
val BaseDataType.isIterable get() = this in setOf(BaseDataType.STR, BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW, BaseDataType.ARRAY_POINTER)
val BaseDataType.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
@@ -112,18 +126,21 @@ 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 && subType==null && subTypeFromAntlr==null)
if(base.isSplitWordArray)
require(sub != null)
// subType and subTypeFromAntlr allowed for arrays with struct element types
// (from parser creating ARRAY(LONG) directly for 32-bit targets, or from VariousCleanups conversion)
if(base == BaseDataType.ARRAY_SPLITW)
require(sub == BaseDataType.UWORD || sub == BaseDataType.WORD)
}
base==BaseDataType.STR -> require(sub==BaseDataType.UBYTE) { "string subtype should be ubyte" }
@@ -139,10 +156,15 @@ class DataType private constructor(
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is DataType) return false
return base == other.base && sub == other.sub && (subType==other.subType || subType!!.sameas(other.subType!!))
if (base != other.base || sub != other.sub || pointeeSub != other.pointeeSub) return false
val st1 = subType
val st2 = other.subType
if (st1 == st2) return true
if (st1 == null || st2 == null) return false
return st1.sameas(st2)
}
override fun hashCode(): Int = Objects.hash(base, sub, subType)
override fun hashCode(): Int = Objects.hash(base, sub, pointeeSub, subType)
fun setActualSubType(actualSubType: ISubType) {
subType = actualSubType
@@ -155,61 +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.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)
TODO("cannot use struct instance as a data type (yet) - use a pointer instead")
return DataType(BaseDataType.STRUCT_INSTANCE, null, null, null)
return simpletypes.getValue(dt)
}
fun arrayFor(elementDt: BaseDataType, splitwordarray: Boolean=true): DataType {
require(!elementDt.isPointer) { "use other array constructor for arrays of pointers" }
val actualElementDt = if(elementDt==BaseDataType.STR) BaseDataType.UWORD else elementDt // array of strings is actually just an array of UWORD pointers
return if(splitwordarray && actualElementDt.isWord)
DataType(BaseDataType.ARRAY_SPLITW, actualElementDt, null)
else {
if(actualElementDt.isNumericOrBool)
DataType(BaseDataType.ARRAY, actualElementDt, null)
else
throw NoSuchElementException("invalid basic element dt $elementDt")
}
fun arrayFor(elementDt: BaseDataType, memsizer: IMemSizer): DataType {
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, null)
else
throw NoSuchElementException("invalid basic element dt $elementDt")
}
fun arrayOfPointersTo(sub: BaseDataType): DataType = DataType(BaseDataType.ARRAY_POINTER, sub, null)
fun arrayOfPointersTo(structType: ISubType?): DataType = DataType(BaseDataType.ARRAY_POINTER, null, structType)
fun arrayOfPointersFromAntlrTo(sub: BaseDataType?, identifier: List<String>?): DataType =
DataType(BaseDataType.ARRAY_POINTER, sub, null, identifier)
fun splitWordArrayFor(elementDt: BaseDataType): DataType {
require(elementDt.isWord) { "split word array element type must be word" }
return DataType(BaseDataType.ARRAY_SPLITW, elementDt, null, null)
}
fun pointer(base: BaseDataType): DataType = DataType(BaseDataType.POINTER, base, null)
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, 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, null, structType)
fun arrayOfStructsFromAntlr(struct: List<String>): DataType =
DataType(BaseDataType.ARRAY, BaseDataType.STRUCT_INSTANCE, null, null, subTypeFromAntlr = struct)
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)
}
@@ -217,53 +250,85 @@ class DataType private constructor(
// DataType Methods
// ============================================================================
fun elementToArray(splitwords: Boolean = true): DataType {
return if (base == BaseDataType.UWORD || base == BaseDataType.WORD || base == BaseDataType.STR) arrayFor(base, splitwords)
else if(base.isPointer) {
if(subType!=null) arrayOfPointersTo(subType)
else if(subTypeFromAntlr!=null) arrayOfPointersFromAntlrTo(sub, subTypeFromAntlr)
else arrayOfPointersTo(sub!!)
fun elementToArray(target: ICompilationTarget, splitwords: Boolean = true): DataType {
fun pointerArray(): DataType =
if (subType != null)
arrayOfPointersTo(subType)
else if (subTypeFromAntlr != null)
arrayOfPointersFromAntlrTo(sub, subTypeFromAntlr)
else
arrayOfPointersTo(sub!!)
if(base == BaseDataType.STRUCT_INSTANCE) {
val st = subType
return if(st != null)
arrayOfStructs(st)
else
arrayOfStructsFromAntlr(subTypeFromAntlr!!)
}
else arrayFor(base, false)
if(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
arrayFor(base, target)
}
fun elementType(): DataType =
when {
isPointerArray -> DataType(BaseDataType.POINTER, sub, subType)
base.isArray || base==BaseDataType.STR -> forDt(sub!!)
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, null, subType)
val dt = forDt(sub!!)
if (subType != null) DataType(BaseDataType.POINTER, null, null, subType) else dt
}
else -> throw IllegalArgumentException("not an array")
}
fun typeForAddressOf(msb: Boolean): DataType {
fun typeForUntypedAddressOf(msb: Boolean, mem: IMemSizer): DataType {
val untypedPointerType = if(mem.memorySize(BaseDataType.POINTER)>2) LONG else UWORD
if (isUndefined)
return if(msb) pointer(BaseDataType.UBYTE) else UWORD
return if(msb) pointer(BaseDataType.UBYTE) else untypedPointerType
else {
if (isBasic)
return pointer(base)
if (isString)
return pointer(BaseDataType.UBYTE)
if (isPointer)
return UWORD
return untypedPointerType
if (isArray) {
if (msb || isSplitWordArray)
val isSplit = base == BaseDataType.ARRAY_SPLITW ||
(isPointerArray && mem.memorySize(BaseDataType.POINTER) <= 2)
if (msb || isSplit)
return pointer(BaseDataType.UBYTE)
val elementDt = elementType()
require(elementDt.isBasic)
return pointer(elementDt)
if (elementDt.isBasic)
return pointer(elementDt)
return untypedPointerType
}
if (subType != null)
return pointer(this)
return UWORD
return untypedPointerType
}
}
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")
}
}
@@ -278,6 +343,13 @@ 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")
}
}
@@ -291,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"
}
@@ -325,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["
@@ -342,6 +403,13 @@ 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")
}
}
@@ -364,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.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) || (targetType.isPointer && targetType.sub==BaseDataType.UBYTE)
BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW -> targetType.base in setOf(BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW) && targetType.sub == sub
BaseDataType.STR -> targetType.base in setOf(BaseDataType.STR, BaseDataType.UWORD, BaseDataType.LONG) || (targetType.isPointer && targetType.sub==BaseDataType.UBYTE)
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
@@ -378,25 +454,24 @@ class DataType private constructor(
}
}
BaseDataType.STRUCT_INSTANCE -> false // we cannot deal with actual struct instances yet in any shape or form (only getting fields from it)
BaseDataType.ARRAY_POINTER -> false
BaseDataType.UNDEFINED -> false
}
fun largerSizeThan(other: DataType): Boolean = base.largerSizeThan(other.base)
fun equalsSize(other: DataType): Boolean = base.equalsSize(other.base)
fun equalsSize(other: DataType, memsizer: IMemSizer): Boolean = base.equalsSize(other.base, memsizer)
/**
* Returns the memory size in bytes.
* 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
@@ -425,24 +500,34 @@ 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
val isLong = base == BaseDataType.LONG
val isStringly = base == BaseDataType.STR || base == BaseDataType.UWORD || (base == BaseDataType.ARRAY && (sub == BaseDataType.UBYTE || sub == BaseDataType.BYTE))
val isSplitWordArray = base.isSplitWordArray
val isSplitUnsignedWordArray = base.isSplitWordArray && !base.isPointerArray && sub == BaseDataType.UWORD
val isSplitSignedWordArray = base.isSplitWordArray && !base.isPointerArray && sub == BaseDataType.WORD
fun isStringly(memsizer: IMemSizer): Boolean =
base == BaseDataType.STR ||
(memsizer.POINTER_MEM_SIZE == 2u && base == BaseDataType.UWORD) ||
(memsizer.POINTER_MEM_SIZE > 2u && base == BaseDataType.LONG) ||
(base == BaseDataType.ARRAY && (sub == BaseDataType.UBYTE || sub == BaseDataType.BYTE))
fun isSplitWordArray(memsizer: IMemSizer): Boolean {
return 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
val isIterable = base.isIterable
val isPassByRef = base.isPassByRef
val isPassByValue = base.isPassByValue
+45 -2
View File
@@ -4,7 +4,11 @@ package prog8.code.core
enum class CpuRegister {
A,
X,
Y
Y,
// m68k:
D0, D1, D2, D3, D4, D5, D6, D7,
A0, A1, A2, A3, A4, A5, A6,
FP0, FP1, FP2, FP3, FP4, FP5, FP6, FP7
}
enum class RegisterOrPair {
@@ -20,7 +24,12 @@ enum class RegisterOrPair {
R0, R1, R2, R3, R4, R5, R6, R7,
R8, R9, R10, R11, R12, R13, R14, R15,
// combined virtual registers to store 32 bits longs:
R0R1, R2R3, R4R5, R6R7, R8R9, R10R11, R12R13, R14R15;
R0R1, R2R3, R4R5, R6R7, R8R9, R10R11, R12R13, R14R15,
// m68k data & address registers:
D0, D1, D2, D3, D4, D5, D6, D7,
A0, A1, A2, A3, A4, A5, A6,
// m68k FPU registers:
FP0, FP1, FP2, FP3, FP4, FP5, FP6, FP7;
companion object {
val names: Set<String> = entries.map { it.toString() }.toSet()
@@ -29,6 +38,29 @@ enum class RegisterOrPair {
CpuRegister.A -> A
CpuRegister.X -> X
CpuRegister.Y -> Y
CpuRegister.D0 -> D0
CpuRegister.D1 -> D1
CpuRegister.D2 -> D2
CpuRegister.D3 -> D3
CpuRegister.D4 -> D4
CpuRegister.D5 -> D5
CpuRegister.D6 -> D6
CpuRegister.D7 -> D7
CpuRegister.A0 -> A0
CpuRegister.A1 -> A1
CpuRegister.A2 -> A2
CpuRegister.A3 -> A3
CpuRegister.A4 -> A4
CpuRegister.A5 -> A5
CpuRegister.A6 -> A6
CpuRegister.FP0 -> FP0
CpuRegister.FP1 -> FP1
CpuRegister.FP2 -> FP2
CpuRegister.FP3 -> FP3
CpuRegister.FP4 -> FP4
CpuRegister.FP5 -> FP5
CpuRegister.FP6 -> FP6
CpuRegister.FP7 -> FP7
}
}
}
@@ -118,6 +150,15 @@ val CombinedLongRegisters = setOf(
RegisterOrPair.R14R15
)
val M68kRegisters = setOf(
RegisterOrPair.D0, RegisterOrPair.D1, RegisterOrPair.D2, RegisterOrPair.D3,
RegisterOrPair.D4, RegisterOrPair.D5, RegisterOrPair.D6, RegisterOrPair.D7,
RegisterOrPair.A0, RegisterOrPair.A1, RegisterOrPair.A2, RegisterOrPair.A3,
RegisterOrPair.A4, RegisterOrPair.A5, RegisterOrPair.A6,
RegisterOrPair.FP0, RegisterOrPair.FP1, RegisterOrPair.FP2, RegisterOrPair.FP3,
RegisterOrPair.FP4, RegisterOrPair.FP5, RegisterOrPair.FP6, RegisterOrPair.FP7
)
val CpuRegisters = setOf(
RegisterOrPair.A, RegisterOrPair.X, RegisterOrPair.Y,
RegisterOrPair.AX, RegisterOrPair.AY, RegisterOrPair.XY
@@ -128,6 +169,8 @@ enum class OutputType {
RAW,
PRG,
XEX,
ELF,
AMIGAHUNK,
LIBRARY
}
@@ -35,6 +35,12 @@ enum class CpuType(val statusBitsOnMultiByteOps: Boolean) {
/** 8-bit CMOS 65C02. Same status-bit issue as plain 6502. */
CPU65C02(false),
/** 16/32 bit Motorola 68000. has single instructions that set status bits when dealing with multi byte operands */
M68000(true),
/** 32 bit Motorola 68020. has single instructions that set status bits when dealing with multi byte operands */
M68020(true),
/**
* Virtual machine target. We use `false` here so the IR generator always
@@ -49,6 +55,14 @@ enum class CpuType(val statusBitsOnMultiByteOps: Boolean) {
/** True for the 6502-family targets (plain 6502 and 65C02). */
val is6502: Boolean
get() = this == CPU6502 || this == CPU65C02
val is68k: Boolean
get() = this == M68000 || this == M68020
/** True for targets that use the m68k-style hardware-register calling convention (D0-D7, A0-A6, FP0-FP7). */
val usesM68kConvention: Boolean
get() = is68k || this == VIRTUAL
/** True for big-endian targets. The 6502 family and the VM are little-endian. */
val isBigEndian: Boolean
get() = this == M68000 || this == M68020
}
interface ICompilationTarget: IStringEncoding, IMemSizer {
@@ -56,7 +70,7 @@ interface ICompilationTarget: IStringEncoding, IMemSizer {
val FLOAT_MAX_NEGATIVE: Double
val FLOAT_MAX_POSITIVE: Double
val FLOAT_MEM_SIZE: UInt
val ARRAY_SIZE_LIMIT: UInt
val PROGRAM_LOAD_ADDRESS : UInt
val PROGRAM_MEMTOP_ADDRESS: UInt
val BSSHIGHRAM_START: UInt
@@ -73,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>
+14 -1
View File
@@ -1,15 +1,28 @@
package prog8.code.core
interface IMemSizer {
val FLOAT_MEM_SIZE: UInt
val POINTER_MEM_SIZE: UInt
val pointerType: DataType
get() = if(POINTER_MEM_SIZE > 2u) DataType.LONG else DataType.UWORD
val pointerBaseType: BaseDataType
get() = if(POINTER_MEM_SIZE > 2u) BaseDataType.LONG else BaseDataType.UWORD
fun memorySize(dt: DataType, numElements: Int?): Int
fun memorySize(dt: BaseDataType): Int {
if(dt.isPassByRef)
return memorySize(DataType.UWORD, null) // a pointer size
return POINTER_MEM_SIZE.toInt()
try {
return memorySize(DataType.forDt(dt), null)
} catch (x: NoSuchElementException) {
throw IllegalArgumentException(x.message)
}
}
fun isSplitWordArray(dt: DataType): Boolean {
return dt.isSplitWordArray(this)
}
}
+22 -5
View File
@@ -33,6 +33,26 @@ abstract class Zeropage(options: CompilationOptions): MemoryAllocator(options) {
val allocatedVariables = mutableMapOf<String, VarAllocation>()
val free = mutableSetOf<UInt>() // subclasses must set this to the appropriate free locations.
/**
* Report a warning for every scratch byte that is excluded from allocation by a %zpreserved
* or %zpallowed directive. The compiler's code generators always use these fixed scratch
* locations internally (P8ZP_SCRATCH_*), so a %zpreserved/%zpallowed range that excludes one
* of them would let the compiler overwrite memory the user explicitly wants to protect.
*/
fun checkScratchConflicts(errors: IErrorReporter) {
if(options.zeropage== ZeropageType.DONTUSE)
return
val scratchBytes = listOf(SCRATCH_B1, SCRATCH_REG, SCRATCH_W1, SCRATCH_W1 + 1u, SCRATCH_W2, SCRATCH_W2 + 1u, SCRATCH_PTR, SCRATCH_PTR + 1u)
for(byte in scratchBytes) {
val preserved = options.zpReserved.any { byte in it }
val allowed = options.zpAllowed.any { byte in it }
if(preserved || !allowed)
errors.warn("zero-page address $${byte.toString(16)} is a fixed scratch location used by the compiler (P8ZP_SCRATCH_*), but it is excluded by a %zpreserved or %zpallowed directive", Position.DUMMY)
}
}
fun removeReservedFromFreePool() {
for (reserved in options.zpReserved)
reserve(reserved)
@@ -46,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
}
@@ -0,0 +1,86 @@
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
class Amiga1200Target: ICompilationTarget,
IStringEncoding by Encoder(false),
IMemSizer by NormalMemSizer(4u, 4u) {
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>()
override val additionalAssemblerOptions = emptyList<String>()
override val defaultOutputType = OutputType.AMIGAHUNK
override val defaultLauncherType = CbmPrgLauncherType.NONE
companion object {
const val NAME = "amiga1200"
/** needed to map bank numbers to libraries in extsub/CALLFAR library calls */
val LibraryNumbers = Amiga500Target.LibraryNumbers
}
override val cpu = CpuType.M68020
override val FLOAT_MAX_POSITIVE = Float.MAX_VALUE.toDouble()
override val FLOAT_MAX_NEGATIVE = -Float.MAX_VALUE.toDouble()
override val FLOAT_MEM_SIZE = 4u
override val POINTER_MEM_SIZE = 4u
override val ARRAY_SIZE_LIMIT = 32768u
override val PROGRAM_LOAD_ADDRESS = 0u // amiga executables are relocatable
override val PROGRAM_MEMTOP_ADDRESS = 0xfffffffeu // not actually used
override val BSSHIGHRAM_START = 0u // not used
override val BSSHIGHRAM_END = 0u // not used
override val BSSGOLDENRAM_START = 0u // not used
override val BSSGOLDENRAM_END = 0u // not used
override lateinit var zeropage: Zeropage
override fun getFloatAsmBytes(num: Number): String {
TODO("float asm bytes")
}
override fun convertFloatToBytes(num: Double): List<UByte> {
TODO("convert float to bytes")
}
override fun convertBytesToFloat(bytes: List<UByte>): Double {
require(bytes.size==4) { "need 4 bytes" }
TODO("convert bytes to float")
}
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
if (selectedEmulator == 1) {
CopperlineEmulator.launch(programFile, quiet, "A1200", "68020", listOf("--fast", "2M", "--fpu"))
return
}
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")
}
VamosEmulator.launch(programFile, quiet, cpuStr)
return
}
override fun isIOAddress(address: UInt): Boolean =
address in 0xbfd000u..0xbfdfffu || // CIA-B
address in 0xbfe000u..0xbfefffu || // CIA-A
address in 0xdff000u..0xdff1ffu || // Custom chipset (Paula, Alice, Lisa, Denise)
address in 0xe80000u..0xefffffu // Zorro II autoconfig space
override fun initializeMemoryAreas(compilerOptions: CompilationOptions) {
zeropage = M68kZeropage(compilerOptions)
}
}
@@ -0,0 +1,126 @@
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
class Amiga500Target: ICompilationTarget,
IStringEncoding by Encoder(false),
IMemSizer by NormalMemSizer(4u, 4u) {
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>()
override val additionalAssemblerOptions = emptyList<String>()
override val defaultOutputType = OutputType.AMIGAHUNK
override val defaultLauncherType = CbmPrgLauncherType.NONE
companion object {
const val NAME = "amiga500"
/** needed to map bank numbers to libraries in extsub/CALLFAR library calls */
val LibraryNumbers = mapOf(
1 to "Sys", // exec
2 to "DOS",
3 to "Gfx",
4 to "Intuition",
5 to "GadTools",
6 to "Layers",
7 to "Asl",
8 to "ConsoleDevice",
9 to "Utility",
10 to "Expansion",
11 to "Icon",
12 to "Workbench",
13 to "Diskfont",
14 to "IFFParse",
15 to "Input",
16 to "Keymap",
17 to "Locale",
18 to "Timer",
19 to "LowLevel",
20 to "Math",
21 to "MathIeeeSingBas",
22 to "MathIeeeSingTrans",
23 to "MathIeeeDoubBas",
24 to "MathIeeeDoubTrans",
25 to "MathTrans",
26 to "NV",
27 to "RealTime",
28 to "Translator",
29 to "RexxSys",
30 to "Cx",
31 to "DataTypes",
32 to "Disk",
33 to "AmigaGuide",
34 to "ARexx",
35 to "BattClock",
36 to "BattMem",
// 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)
)
}
override val cpu = CpuType.M68000
override val FLOAT_MAX_POSITIVE = Float.MAX_VALUE.toDouble()
override val FLOAT_MAX_NEGATIVE = -Float.MAX_VALUE.toDouble()
override val FLOAT_MEM_SIZE = 4u
override val POINTER_MEM_SIZE = 4u
override val ARRAY_SIZE_LIMIT = 32768u
override val PROGRAM_LOAD_ADDRESS = 0u // amiga executables are relocatable
override val PROGRAM_MEMTOP_ADDRESS = 0xfffffffeu // not actually used
override val BSSHIGHRAM_START = 0u // not used
override val BSSHIGHRAM_END = 0u // not used
override val BSSGOLDENRAM_START = 0u // not used
override val BSSGOLDENRAM_END = 0u // not used
override lateinit var zeropage: Zeropage
override fun getFloatAsmBytes(num: Number): String {
TODO("float asm bytes")
}
override fun convertFloatToBytes(num: Double): List<UByte> {
TODO("convert float to bytes")
}
override fun convertBytesToFloat(bytes: List<UByte>): Double {
require(bytes.size==4) { "need 4 bytes" }
TODO("convert bytes to float")
}
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
if (selectedEmulator == 1) {
CopperlineEmulator.launch(programFile, quiet, "A500", "68000", listOf("--slow", "512K"))
return
}
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")
}
VamosEmulator.launch(programFile, quiet, cpuStr)
return
}
override fun isIOAddress(address: UInt): Boolean =
address in 0xbfd000u..0xbfdfffu || // CIA-B
address in 0xbfe000u..0xbfefffu || // CIA-A
address in 0xdff000u..0xdff1ffu || // Custom chipset (Paula, Alice, Lisa, Denise)
address in 0xe80000u..0xefffffu // Zorro II autoconfig space
override fun initializeMemoryAreas(compilerOptions: CompilationOptions) {
zeropage = M68kZeropage(compilerOptions)
}
}
+9 -6
View File
@@ -8,10 +8,11 @@ import java.nio.file.Path
class C128Target: ICompilationTarget,
IStringEncoding by Encoder(true),
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE) {
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE, 2u) {
override val name = NAME
override val supportsBankedCalls = true
override val supportsStackFrames = false
override val defaultEncoding = Encoding.PETSCII
override val libraryPath = null
override val customLauncher = emptyList<String>()
@@ -28,7 +29,9 @@ 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
override val PROGRAM_MEMTOP_ADDRESS = 0xc000u
@@ -52,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
@@ -61,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()
+15 -8
View File
@@ -9,10 +9,11 @@ import java.nio.file.Path
class C64Target: ICompilationTarget,
IStringEncoding by Encoder(true),
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE) {
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE, 2u) {
override val name = NAME
override val supportsBankedCalls = true
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"
}
@@ -31,7 +30,9 @@ class C64Target: ICompilationTarget,
override val FLOAT_MAX_POSITIVE = Mflpt5.FLOAT_MAX_POSITIVE
override val FLOAT_MAX_NEGATIVE = Mflpt5.FLOAT_MAX_NEGATIVE
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE.toUInt()
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE
override val POINTER_MEM_SIZE = 2u
override val ARRAY_SIZE_LIMIT = 256u
override val PROGRAM_LOAD_ADDRESS = 0x0801u
override val PROGRAM_MEMTOP_ADDRESS = 0xcfe0u // $a000 if floats are used
// note that at $cfe0-$cfff are the 16 'virtual registers' R0-R15
@@ -56,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
@@ -66,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()
@@ -97,6 +98,9 @@ val CompilationTargets = listOf(
C128Target.NAME,
Cx16Target.NAME,
PETTarget.NAME,
Qemu68kTarget.NAME,
Amiga500Target.NAME,
Amiga1200Target.NAME,
VMTarget.NAME
)
@@ -105,6 +109,9 @@ fun getCompilationTargetByName(name: String) = when(name.lowercase()) {
C128Target.NAME -> C128Target()
Cx16Target.NAME -> Cx16Target()
PETTarget.NAME -> PETTarget()
Qemu68kTarget.NAME -> Qemu68kTarget()
Amiga500Target.NAME -> Amiga500Target()
Amiga1200Target.NAME -> Amiga1200Target()
VMTarget.NAME -> VMTarget()
else -> throw IllegalArgumentException("invalid compilation target")
}
@@ -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,
@@ -29,6 +31,7 @@ class ConfigFileTarget(
override val libraryPath: Path,
override val customLauncher: List<String>,
override val additionalAssemblerOptions: List<String>,
val varsAddress: UInt? = null,
val ioAddresses: List<UIntRange>,
val zpScratchB1: UInt,
val zpScratchReg: UInt,
@@ -38,8 +41,13 @@ class ConfigFileTarget(
val virtualregistersStart: UInt,
val zpFullsafe: List<UIntRange>,
val zpKernalsafe: List<UIntRange>,
val zpBasicsafe: List<UIntRange>
): ICompilationTarget, IStringEncoding by Encoder(true), IMemSizer by NormalMemSizer(8) {
val zpBasicsafe: List<UIntRange>,
val pointerSize: Int = 2
): ICompilationTarget, IStringEncoding by Encoder(true), IMemSizer by NormalMemSizer(8u, pointerSize.toUInt()) {
init {
require(pointerSize == 2 || pointerSize == 4) { "unsupported pointer size: $pointerSize" }
}
companion object {
@@ -66,6 +74,12 @@ class ConfigFileTarget(
return value.toUInt()
}
private fun parsePointerSize(props: Properties): Int {
val pointerSize = parseInt(props.getProperty("pointer_size", "2")).toInt()
require(pointerSize == 2 || pointerSize == 4) { "unsupported pointer size: $pointerSize" }
return pointerSize
}
private fun parseAddressRanges(key: String, props: Properties): List<UIntRange> {
val rangesStr = props.getString(key)
if(rangesStr.isBlank())
@@ -97,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())
@@ -111,6 +128,7 @@ class ConfigFileTarget(
(customLauncherStr+"\n").lines().map { it.trimEnd() }
else emptyList()
val assemblerOptionsStr = props.getProperty("assembler_options", "").trim()
val pointerSize = parsePointerSize(props)
val outputTypeString = props.getProperty("output_type", "PRG")
val defaultOutputType = OutputType.valueOf(outputTypeString.uppercase())
val launcherString = props.getProperty("launcher", "BASIC")
@@ -120,11 +138,14 @@ class ConfigFileTarget(
CbmPrgLauncherType.BASIC
}
val varsAddress = props.getProperty("vars_address")?.let { parseInt(it) }
return ConfigFileTarget(
configfile.nameWithoutExtension,
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"),
@@ -136,16 +157,18 @@ class ConfigFileTarget(
libraryPath,
customLauncher,
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,
pointerSize,
)
}
}
@@ -154,20 +177,22 @@ class ConfigFileTarget(
override val FLOAT_MAX_POSITIVE = 9.999999999e97
override val FLOAT_MAX_NEGATIVE = -9.999999999e97
override val FLOAT_MEM_SIZE = 8u
override val POINTER_MEM_SIZE = pointerSize.toUInt()
override val ARRAY_SIZE_LIMIT = 256u
override lateinit var zeropage: Zeropage
override fun getFloatAsmBytes(num: Number) = TODO("floats")
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/")
}
}
}
+8 -5
View File
@@ -8,10 +8,11 @@ import java.nio.file.Path
class Cx16Target: ICompilationTarget,
IStringEncoding by Encoder(true),
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE) {
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE, 2u) {
override val name = NAME
override val supportsBankedCalls = true
override val supportsStackFrames = false
override val defaultEncoding = Encoding.PETSCII
override val libraryPath = null
override val customLauncher = emptyList<String>()
@@ -28,7 +29,9 @@ 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
override val PROGRAM_MEMTOP_ADDRESS = 0x9f00u
@@ -52,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>
@@ -63,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.")
@@ -74,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/")
}
}
}
+2 -2
View File
@@ -9,7 +9,7 @@ data class Mflpt5(val b0: UByte, val b1: UByte, val b2: UByte, val b3: UByte, va
companion object {
const val FLOAT_MAX_POSITIVE = 1.7014118345e+38 // bytes: 255,127,255,255,255
const val FLOAT_MAX_NEGATIVE = -1.7014118345e+38 // bytes: 255,255,255,255,255
const val FLOAT_MEM_SIZE = 5
const val FLOAT_MEM_SIZE = 5u
val zero = Mflpt5(0u, 0u, 0u, 0u, 0u)
fun fromNumber(num: Number): Mflpt5 {
@@ -73,4 +73,4 @@ data class Mflpt5(val b0: UByte, val b1: UByte, val b2: UByte, val b3: UByte, va
val b4 = "$" + b4.toString(16).padStart(2, '0')
return "$b0, $b1, $b2, $b3, $b4"
}
}
}
@@ -4,35 +4,41 @@ import prog8.code.core.BaseDataType
import prog8.code.core.DataType
import prog8.code.core.IMemSizer
internal class NormalMemSizer(val floatsize: Int): IMemSizer {
internal class NormalMemSizer(override val FLOAT_MEM_SIZE: UInt, override val POINTER_MEM_SIZE: UInt): IMemSizer {
private val pointerSize = POINTER_MEM_SIZE.toInt()
private val floatSize = FLOAT_MEM_SIZE.toInt()
override fun memorySize(dt: DataType, numElements: Int?): Int {
if(dt.isPointerArray)
return 2 * numElements!! // array of pointers is just array of uwords
return pointerSize * numElements!! // array of pointers
else if(dt.isArray) {
if(numElements==null) return 2 // treat it as a pointer size
if(numElements==null) return pointerSize // treat it as a pointer size
return when(dt.sub) {
BaseDataType.BOOL, BaseDataType.UBYTE, BaseDataType.BYTE -> numElements
BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.STR -> numElements * 2
BaseDataType.UWORD, BaseDataType.WORD -> numElements * 2
BaseDataType.STR, BaseDataType.POINTER -> numElements * pointerSize
BaseDataType.LONG -> numElements * 4
BaseDataType.FLOAT-> numElements * floatsize
BaseDataType.FLOAT-> numElements * floatSize
BaseDataType.STRUCT_INSTANCE -> numElements * (dt.subType?.memsize(this) ?: throw IllegalArgumentException("struct instance array has unresolved subtype"))
BaseDataType.UNDEFINED -> throw IllegalArgumentException("undefined has no memory size")
else -> throw IllegalArgumentException("invalid sub type")
}
}
else if (dt.isString) {
return numElements // treat it as the size of the given string with the length
?: 2 // treat it as the size to store a string pointer
?: pointerSize // treat it as the size to store a string pointer
}
return when {
dt.isByteOrBool -> 1 * (numElements ?: 1)
dt.isFloat -> floatsize * (numElements ?: 1)
dt.isFloat -> floatSize * (numElements ?: 1)
dt.isLong -> 4 * (numElements ?: 1)
dt.isPointer -> 2 // pointer is just a uword
dt.isStructInstance -> dt.subType?.memsize(this) ?: 0
dt.isWord -> 2 * (numElements ?: 1)
dt.isPointer -> pointerSize * (numElements ?: 1)
dt.isStructInstance -> dt.subType?.memsize(this) ?: throw IllegalArgumentException("struct instance has unresolved subtype")
dt.isUndefined -> throw IllegalArgumentException("undefined has no memory size")
else -> 2 * (numElements ?: 1)
else -> throw IllegalArgumentException("invalid dt $dt")
}
}
+9 -6
View File
@@ -8,10 +8,11 @@ import java.nio.file.Path
class PETTarget: ICompilationTarget,
IStringEncoding by Encoder(true),
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE) {
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE, 2u) {
override val name = NAME
override val supportsBankedCalls = false
override val supportsStackFrames = false
override val defaultEncoding = Encoding.PETSCII
override val libraryPath = null
override val customLauncher = emptyList<String>()
@@ -27,7 +28,9 @@ 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
override val PROGRAM_MEMTOP_ADDRESS = 0x8000u
@@ -51,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
@@ -60,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()
@@ -0,0 +1,99 @@
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
/**
* For now target a m68020 cpu
* Eventually the goal is to be able to create programs for the Amiga A1200 (68020 cpu) or A500 (68000 cpu)
*/
class Qemu68kTarget: ICompilationTarget,
IStringEncoding by Encoder(false),
IMemSizer by NormalMemSizer(4u, 4u) {
override val name = NAME
override val supportsBankedCalls = false
override val supportsStackFrames = true
override val defaultEncoding = Encoding.ISO
override val libraryPath = null
override val customLauncher = emptyList<String>()
override val additionalAssemblerOptions = emptyList<String>()
override val defaultOutputType = OutputType.ELF
override val defaultLauncherType = CbmPrgLauncherType.NONE
companion object {
const val NAME = "qemu68k"
}
override val cpu = CpuType.M68020
override val FLOAT_MAX_POSITIVE = Float.MAX_VALUE.toDouble()
override val FLOAT_MAX_NEGATIVE = -Float.MAX_VALUE.toDouble()
override val FLOAT_MEM_SIZE = 4u
override val POINTER_MEM_SIZE = 4u
override val ARRAY_SIZE_LIMIT = 32768u
override val PROGRAM_LOAD_ADDRESS = 0x10000u
override val PROGRAM_MEMTOP_ADDRESS = 0x00100000u // TODO hardcoded at 1 Mb of RAM for now... it starts at $0
override val BSSHIGHRAM_START = 0u // not actually used
override val BSSHIGHRAM_END = 0u // not actually used
override val BSSGOLDENRAM_START = 0u // not actually used
override val BSSGOLDENRAM_END = 0u // not actually used
override lateinit var zeropage: Zeropage // not actually used
override fun getFloatAsmBytes(num: Number): String {
TODO("float asm bytes")
}
override fun convertFloatToBytes(num: Double): List<UByte> {
TODO("convert float to bytes")
}
override fun convertBytesToFloat(bytes: List<UByte>): Double {
require(bytes.size==4) { "need 4 bytes" }
TODO("convert bytes to float")
}
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
if(selectedEmulator!=1) {
System.err.println("The qemu68k target only supports the main emulator (Qemu).")
return
}
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) {
println("Launching QEMU (press Ctrl-A X to exit)...")
}
val pb = ProcessBuilder(cmd).inheritIO()
try {
pb.start().waitFor()
} catch (_: java.io.IOException) {
System.err.println("Cannot launch qemu-system-m68k. Install it via your package manager, e.g.:")
System.err.println(" sudo apt install qemu-system-m68k # Debian/Ubuntu")
System.err.println(" sudo pacman -S qemu-system-m68k # Arch Linux")
System.err.println("or build it from source: https://www.qemu.org/download/")
}
}
override fun isIOAddress(address: UInt): Boolean = address>=0xff000000u
override fun initializeMemoryAreas(compilerOptions: CompilationOptions) {
zeropage = M68kZeropage(compilerOptions)
}
}
+19 -14
View File
@@ -3,17 +3,16 @@ 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,
IStringEncoding by Encoder(false),
IMemSizer by NormalMemSizer(FLOAT_MEM_SIZE) {
IMemSizer by NormalMemSizer(8u, 4u) {
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>()
@@ -23,16 +22,19 @@ class VMTarget: ICompilationTarget,
companion object {
const val NAME = "virtual"
const val FLOAT_MEM_SIZE = 8 // 64-bits double
const val RAM_SIZE = 16 * 1024 * 1024 // 16 MB
}
override val cpu = CpuType.VIRTUAL
override val FLOAT_MAX_POSITIVE = Double.MAX_VALUE
override val FLOAT_MAX_NEGATIVE = -Double.MAX_VALUE
override val FLOAT_MEM_SIZE = VMTarget.FLOAT_MEM_SIZE.toUInt()
override val PROGRAM_LOAD_ADDRESS = 0u // not actually used
override val PROGRAM_MEMTOP_ADDRESS = 0xffffu // not actually used
override val FLOAT_MEM_SIZE = 8u
override val POINTER_MEM_SIZE = 4u
override val ARRAY_SIZE_LIMIT = 65535u
override val PROGRAM_LOAD_ADDRESS = 0u
override val PROGRAM_MEMTOP_ADDRESS = 0xffffffu
override val BSSHIGHRAM_START = 0u // not actually used
override val BSSHIGHRAM_END = 0u // not actually used
@@ -68,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")
}
@@ -8,6 +8,9 @@ import prog8.code.core.*
* Returns standard memory sizes for all data types.
*/
object DummyMemsizer : IMemSizer {
override val FLOAT_MEM_SIZE: UInt = 4u
override val POINTER_MEM_SIZE: UInt = 2u
override fun memorySize(dt: DataType, numElements: Int?): Int {
if (dt.isPointerArray)
return 2 * numElements!!
@@ -18,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")
}
}
@@ -0,0 +1,66 @@
package prog8tests.codecore
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import prog8.code.core.BaseDataType
import prog8.code.target.ConfigFileTarget
import java.nio.file.Files
import kotlin.io.path.writeText
class TestConfigFileTarget: FunSpec({
fun loadTarget(extraProperties: String = ""): ConfigFileTarget {
val directory = Files.createTempDirectory("prog8-config-target")
val library = Files.createDirectories(directory.resolve("library"))
val config = directory.resolve("target.properties")
config.writeText(
"""
cpu = 65C02
encoding = iso
load_address = 2048
memtop = 65535
bss_highram_start = 0
bss_highram_end = 0
bss_goldenram_start = 0
bss_goldenram_end = 0
zp_scratch_b1 = 240
zp_scratch_reg = 241
zp_scratch_w1 = 242
zp_scratch_w2 = 244
zp_scratch_ptr = 246
virtual_registers = 2
zp_fullsafe = 34-239
zp_kernalsafe = 34-239
zp_basicsafe = 34-239
io_regions =
""".trimIndent() + "\nlibrary = $library\n$extraProperties"
)
return try {
ConfigFileTarget.fromConfigFile(config)
} finally {
directory.toFile().deleteRecursively()
}
}
test("legacy config defaults to 2-byte pointers and 256 element arrays") {
val target = loadTarget()
target.POINTER_MEM_SIZE shouldBe 2u
target.memorySize(BaseDataType.POINTER) shouldBe 2
target.ARRAY_SIZE_LIMIT shouldBe 256u
}
test("config supports 4-byte pointers") {
val target = loadTarget("pointer_size = 4")
target.POINTER_MEM_SIZE shouldBe 4u
target.memorySize(BaseDataType.POINTER) shouldBe 4
target.ARRAY_SIZE_LIMIT shouldBe 256u
}
test("config rejects unsupported pointer sizes") {
shouldThrow<IllegalArgumentException> {
loadTarget("pointer_size = 3")
}
}
})
@@ -23,31 +23,31 @@ class TestConversions: FunSpec({
}
test("toHex for 16-255 (hex with $)") {
16.toHex() shouldBe "\$10"
255.toHex() shouldBe "\$ff"
16.toHex() shouldBe $$"$10"
255.toHex() shouldBe $$"$ff"
}
test("toHex for 256-65535 (4 digit hex)") {
256.toHex() shouldBe "\$0100"
65535.toHex() shouldBe "\$ffff"
256.toHex() shouldBe $$"$0100"
65535.toHex() shouldBe $$"$ffff"
}
test("toHex for larger numbers (8 digit hex)") {
65536.toHex() shouldBe "\$00010000"
0x12345678.toHex() shouldBe "\$12345678"
65536.toHex() shouldBe $$"$00010000"
0x12345678.toHex() shouldBe $$"$12345678"
}
test("toHex for negative numbers") {
(-1).toHex() shouldBe "-1"
(-255).toHex() shouldBe "-\$ff"
Int.MIN_VALUE.toHex() shouldBe "\$80000000"
(-255).toHex() shouldBe $$"-$ff"
Int.MIN_VALUE.toHex() shouldBe $$"$80000000"
}
test("UInt.toHex") {
0u.toHex() shouldBe "0"
15u.toHex() shouldBe "15"
255u.toHex() shouldBe "\$ff"
65535u.toHex() shouldBe "\$ffff"
255u.toHex() shouldBe $$"$ff"
65535u.toHex() shouldBe $$"$ffff"
}
// ============================================================================
+127 -106
View File
@@ -2,28 +2,36 @@ package prog8tests.codecore
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.booleans.shouldBeFalse
import io.kotest.matchers.booleans.shouldBeTrue
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import prog8.code.core.*
import prog8.code.target.Amiga500Target
import prog8.code.target.Cx16Target
import prog8.code.target.VMTarget
/**
* Unit tests for DataType and BaseDataType classes in codeCore.
* Focus on type predicates, relationships, and key operations.
*/
class TestDataType: FunSpec({
val tgt = Cx16Target()
val dummyMemSizer = object : IMemSizer {
override val FLOAT_MEM_SIZE: UInt = 4u
override val POINTER_MEM_SIZE: UInt = 2u
override fun memorySize(dt: DataType, numElements: Int?): Int {
if (dt.isPointerArray) return 2 * numElements!!
if (dt.isArray || dt.isSplitWordArray) {
if (dt.isArray || dt.isSplitWordArray(this)) {
require(numElements != null)
return when (dt.sub) {
BaseDataType.BOOL, BaseDataType.BYTE, BaseDataType.UBYTE -> numElements
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")
}
}
@@ -45,61 +53,74 @@ class TestDataType: FunSpec({
// ============================================================================
test("isByte extension") {
BaseDataType.UBYTE.isByte.shouldBeTrue()
BaseDataType.BYTE.isByte.shouldBeTrue()
BaseDataType.UWORD.isByte.shouldBeFalse()
BaseDataType.WORD.isByte.shouldBeFalse()
BaseDataType.UBYTE.isByte shouldBe true
BaseDataType.BYTE.isByte shouldBe true
BaseDataType.UWORD.isByte shouldBe false
BaseDataType.WORD.isByte shouldBe false
}
test("isByteOrBool extension") {
BaseDataType.UBYTE.isByteOrBool.shouldBeTrue()
BaseDataType.BYTE.isByteOrBool.shouldBeTrue()
BaseDataType.BOOL.isByteOrBool.shouldBeTrue()
BaseDataType.UWORD.isByteOrBool.shouldBeFalse()
BaseDataType.UBYTE.isByteOrBool shouldBe true
BaseDataType.BYTE.isByteOrBool shouldBe true
BaseDataType.BOOL.isByteOrBool shouldBe true
BaseDataType.UWORD.isByteOrBool shouldBe false
}
test("isWord extension") {
BaseDataType.UWORD.isWord.shouldBeTrue()
BaseDataType.WORD.isWord.shouldBeTrue()
BaseDataType.UBYTE.isWord.shouldBeFalse()
BaseDataType.UWORD.isWord shouldBe true
BaseDataType.WORD.isWord shouldBe true
BaseDataType.UBYTE.isWord shouldBe false
}
test("isInteger extension") {
BaseDataType.UBYTE.isInteger.shouldBeTrue()
BaseDataType.WORD.isInteger.shouldBeTrue()
BaseDataType.LONG.isInteger.shouldBeTrue()
BaseDataType.FLOAT.isInteger.shouldBeFalse()
BaseDataType.UBYTE.isInteger shouldBe true
BaseDataType.WORD.isInteger shouldBe true
BaseDataType.LONG.isInteger shouldBe true
BaseDataType.FLOAT.isInteger shouldBe false
}
test("isNumeric extension") {
BaseDataType.UBYTE.isNumeric.shouldBeTrue()
BaseDataType.FLOAT.isNumeric.shouldBeTrue()
BaseDataType.BOOL.isNumeric.shouldBeFalse()
BaseDataType.UBYTE.isNumeric shouldBe true
BaseDataType.FLOAT.isNumeric shouldBe true
BaseDataType.BOOL.isNumeric shouldBe false
}
test("isSigned extension") {
BaseDataType.BYTE.isSigned.shouldBeTrue()
BaseDataType.WORD.isSigned.shouldBeTrue()
BaseDataType.LONG.isSigned.shouldBeTrue()
BaseDataType.UBYTE.isSigned.shouldBeFalse()
BaseDataType.BYTE.isSigned shouldBe true
BaseDataType.WORD.isSigned shouldBe true
BaseDataType.LONG.isSigned shouldBe true
BaseDataType.UBYTE.isSigned shouldBe false
}
test("isArray extension") {
BaseDataType.ARRAY.isArray.shouldBeTrue()
BaseDataType.ARRAY_SPLITW.isArray.shouldBeTrue()
BaseDataType.ARRAY_POINTER.isArray.shouldBeTrue()
BaseDataType.UBYTE.isArray.shouldBeFalse()
BaseDataType.ARRAY.isArray shouldBe true
BaseDataType.ARRAY_SPLITW.isArray shouldBe true
BaseDataType.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.shouldBeTrue()
BaseDataType.UBYTE.isPointer.shouldBeFalse()
BaseDataType.POINTER.isPointer shouldBe true
BaseDataType.UBYTE.isPointer shouldBe false
}
test("isSplitWordArray extension") {
BaseDataType.ARRAY_SPLITW.isSplitWordArray.shouldBeTrue()
BaseDataType.ARRAY_POINTER.isSplitWordArray.shouldBeTrue()
BaseDataType.ARRAY.isSplitWordArray.shouldBeFalse()
DataType.splitWordArrayFor(BaseDataType.UWORD).isSplitWordArray(Cx16Target()) shouldBe true
DataType.splitWordArrayFor(BaseDataType.UWORD).isSplitWordArray(VMTarget()) shouldBe true
DataType.splitWordArrayFor(BaseDataType.UWORD).isSplitWordArray(Amiga500Target()) shouldBe true
DataType.arrayOfPointersTo(BaseDataType.UWORD).isSplitWordArray(Cx16Target()) shouldBe true
DataType.arrayOfPointersTo(BaseDataType.UWORD).isSplitWordArray(VMTarget()) shouldBe false
DataType.arrayOfPointersTo(BaseDataType.UWORD).isSplitWordArray(Amiga500Target()) shouldBe false
DataType.arrayFor(BaseDataType.UWORD, Cx16Target()).isSplitWordArray(Cx16Target()) shouldBe false
DataType.arrayFor(BaseDataType.UWORD, VMTarget()).isSplitWordArray(VMTarget()) shouldBe false
DataType.arrayFor(BaseDataType.UWORD, Amiga500Target()).isSplitWordArray(Amiga500Target()) shouldBe false
}
// ============================================================================
@@ -113,20 +134,20 @@ class TestDataType: FunSpec({
DataType.forDt(BaseDataType.FLOAT) shouldBe DataType.FLOAT
}
test("DataType.forDt throws for struct instance") {
shouldThrow<NotImplementedError> {
DataType.forDt(BaseDataType.STRUCT_INSTANCE)
}
test("DataType.forDt supports struct instance") {
val dt = DataType.forDt(BaseDataType.STRUCT_INSTANCE)
dt.base shouldBe BaseDataType.STRUCT_INSTANCE
dt.isStructInstance shouldBe true
}
test("DataType.arrayFor creates array types") {
val ubyteArray = DataType.arrayFor(BaseDataType.UBYTE)
val ubyteArray = DataType.arrayFor(BaseDataType.UBYTE, tgt)
ubyteArray.base shouldBe BaseDataType.ARRAY
ubyteArray.sub shouldBe BaseDataType.UBYTE
val wordArraySplit = DataType.arrayFor(BaseDataType.WORD)
wordArraySplit.base shouldBe BaseDataType.ARRAY_SPLITW
wordArraySplit.sub shouldBe BaseDataType.WORD
val wordArray = DataType.arrayFor(BaseDataType.WORD, tgt)
wordArray.base shouldBe BaseDataType.ARRAY
wordArray.sub shouldBe BaseDataType.WORD
}
test("DataType.pointer creates pointer types") {
@@ -140,45 +161,45 @@ class TestDataType: FunSpec({
// ============================================================================
test("DataType.isBasic property") {
DataType.UBYTE.isBasic.shouldBeTrue()
DataType.WORD.isBasic.shouldBeTrue()
DataType.STR.isBasic.shouldBeFalse()
DataType.arrayFor(BaseDataType.UBYTE).isBasic.shouldBeFalse()
DataType.UBYTE.isBasic shouldBe true
DataType.WORD.isBasic shouldBe true
DataType.STR.isBasic shouldBe false
DataType.arrayFor(BaseDataType.UBYTE, tgt).isBasic shouldBe false
}
test("DataType.isByte property") {
DataType.UBYTE.isByte.shouldBeTrue()
DataType.BYTE.isByte.shouldBeTrue()
DataType.WORD.isByte.shouldBeFalse()
DataType.UBYTE.isByte shouldBe true
DataType.BYTE.isByte shouldBe true
DataType.WORD.isByte shouldBe false
}
test("DataType.isNumeric property") {
DataType.UBYTE.isNumeric.shouldBeTrue()
DataType.FLOAT.isNumeric.shouldBeTrue()
DataType.BOOL.isNumeric.shouldBeFalse()
DataType.STR.isNumeric.shouldBeFalse()
DataType.UBYTE.isNumeric shouldBe true
DataType.FLOAT.isNumeric shouldBe true
DataType.BOOL.isNumeric shouldBe false
DataType.STR.isNumeric shouldBe false
}
test("DataType.isArray property") {
DataType.arrayFor(BaseDataType.UBYTE).isArray.shouldBeTrue()
DataType.UBYTE.isArray.shouldBeFalse()
DataType.arrayFor(BaseDataType.UBYTE, tgt).isArray shouldBe true
DataType.UBYTE.isArray shouldBe false
}
test("DataType.isPointer property") {
DataType.pointer(BaseDataType.UBYTE).isPointer.shouldBeTrue()
DataType.UBYTE.isPointer.shouldBeFalse()
DataType.pointer(BaseDataType.UBYTE).isPointer shouldBe true
DataType.UBYTE.isPointer shouldBe false
}
test("DataType.isString property") {
DataType.STR.isString.shouldBeTrue()
DataType.UBYTE.isString.shouldBeFalse()
DataType.STR.isString shouldBe true
DataType.UBYTE.isString shouldBe false
}
test("DataType.isPassByRef property") {
DataType.STR.isPassByRef.shouldBeTrue()
DataType.arrayFor(BaseDataType.UBYTE).isPassByRef.shouldBeTrue()
DataType.UBYTE.isPassByRef.shouldBeFalse()
DataType.pointer(BaseDataType.UBYTE).isPassByRef.shouldBeFalse()
DataType.STR.isPassByRef shouldBe true
DataType.arrayFor(BaseDataType.UBYTE, tgt).isPassByRef shouldBe true
DataType.UBYTE.isPassByRef shouldBe false
DataType.pointer(BaseDataType.UBYTE).isPassByRef shouldBe false
}
// ============================================================================
@@ -186,8 +207,8 @@ class TestDataType: FunSpec({
// ============================================================================
test("DataType.elementType returns element type for arrays") {
DataType.arrayFor(BaseDataType.UBYTE).elementType() shouldBe DataType.UBYTE
DataType.arrayFor(BaseDataType.WORD).elementType() shouldBe DataType.WORD
DataType.arrayFor(BaseDataType.UBYTE, tgt).elementType() shouldBe DataType.UBYTE
DataType.arrayFor(BaseDataType.WORD, tgt).elementType() shouldBe DataType.WORD
DataType.STR.elementType() shouldBe DataType.UBYTE
}
@@ -226,31 +247,31 @@ class TestDataType: FunSpec({
}
test("BaseDataType.largerSizeThan") {
BaseDataType.WORD.largerSizeThan(BaseDataType.UBYTE).shouldBeTrue()
BaseDataType.LONG.largerSizeThan(BaseDataType.WORD).shouldBeTrue()
BaseDataType.UBYTE.largerSizeThan(BaseDataType.WORD).shouldBeFalse()
BaseDataType.STR.largerSizeThan(BaseDataType.UBYTE).shouldBeTrue()
BaseDataType.STR.largerSizeThan(BaseDataType.BOOL).shouldBeTrue()
BaseDataType.STR.largerSizeThan(BaseDataType.UWORD).shouldBeFalse()
BaseDataType.STR.largerSizeThan(BaseDataType.WORD).shouldBeFalse()
BaseDataType.STR.largerSizeThan(BaseDataType.POINTER).shouldBeFalse()
BaseDataType.STR.largerSizeThan(BaseDataType.LONG).shouldBeFalse()
BaseDataType.POINTER.largerSizeThan(BaseDataType.UBYTE).shouldBeTrue()
BaseDataType.POINTER.largerSizeThan(BaseDataType.BOOL).shouldBeTrue()
BaseDataType.POINTER.largerSizeThan(BaseDataType.UWORD).shouldBeFalse()
BaseDataType.POINTER.largerSizeThan(BaseDataType.STR).shouldBeFalse()
BaseDataType.WORD.largerSizeThan(BaseDataType.UBYTE) shouldBe true
BaseDataType.LONG.largerSizeThan(BaseDataType.WORD) shouldBe true
BaseDataType.UBYTE.largerSizeThan(BaseDataType.WORD) shouldBe false
BaseDataType.STR.largerSizeThan(BaseDataType.UBYTE) shouldBe true
BaseDataType.STR.largerSizeThan(BaseDataType.BOOL) shouldBe true
BaseDataType.STR.largerSizeThan(BaseDataType.UWORD) shouldBe false
BaseDataType.STR.largerSizeThan(BaseDataType.WORD) shouldBe false
BaseDataType.STR.largerSizeThan(BaseDataType.POINTER) shouldBe false
BaseDataType.STR.largerSizeThan(BaseDataType.LONG) shouldBe false
BaseDataType.POINTER.largerSizeThan(BaseDataType.UBYTE) shouldBe true
BaseDataType.POINTER.largerSizeThan(BaseDataType.BOOL) shouldBe true
BaseDataType.POINTER.largerSizeThan(BaseDataType.UWORD) shouldBe false
BaseDataType.POINTER.largerSizeThan(BaseDataType.STR) shouldBe false
}
test("BaseDataType.equalsSize") {
BaseDataType.UBYTE.equalsSize(BaseDataType.UBYTE).shouldBeTrue()
BaseDataType.UBYTE.equalsSize(BaseDataType.BYTE).shouldBeTrue()
BaseDataType.WORD.equalsSize(BaseDataType.UWORD).shouldBeTrue()
BaseDataType.UBYTE.equalsSize(BaseDataType.WORD).shouldBeFalse()
BaseDataType.STR.equalsSize(BaseDataType.STR).shouldBeTrue()
BaseDataType.STR.equalsSize(BaseDataType.UWORD).shouldBeTrue()
BaseDataType.STR.equalsSize(BaseDataType.BYTE).shouldBeFalse()
BaseDataType.POINTER.equalsSize(BaseDataType.UWORD).shouldBeTrue()
BaseDataType.POINTER.equalsSize(BaseDataType.STR).shouldBeFalse()
BaseDataType.UBYTE.equalsSize(BaseDataType.UBYTE) shouldBe true
BaseDataType.UBYTE.equalsSize(BaseDataType.BYTE) shouldBe true
BaseDataType.WORD.equalsSize(BaseDataType.UWORD) shouldBe true
BaseDataType.UBYTE.equalsSize(BaseDataType.WORD) shouldBe false
BaseDataType.STR.equalsSize(BaseDataType.STR) shouldBe true
BaseDataType.STR.equalsSize(BaseDataType.UWORD) shouldBe true
BaseDataType.STR.equalsSize(BaseDataType.BYTE) shouldBe false
BaseDataType.POINTER.equalsSize(BaseDataType.UWORD) shouldBe true
BaseDataType.POINTER.equalsSize(BaseDataType.STR) shouldBe false
}
// ============================================================================
@@ -258,39 +279,39 @@ class TestDataType: FunSpec({
// ============================================================================
test("bool assignability") {
DataType.BOOL.isAssignableTo(DataType.BOOL).shouldBeTrue()
DataType.BOOL.isAssignableTo(DataType.UBYTE).shouldBeFalse()
DataType.BOOL.isAssignableTo(DataType.BOOL) shouldBe true
DataType.BOOL.isAssignableTo(DataType.UBYTE) shouldBe false
}
test("ubyte assignability") {
DataType.UBYTE.isAssignableTo(DataType.UBYTE).shouldBeTrue()
DataType.UBYTE.isAssignableTo(DataType.UWORD).shouldBeTrue()
DataType.UBYTE.isAssignableTo(DataType.LONG).shouldBeTrue()
DataType.UBYTE.isAssignableTo(DataType.FLOAT).shouldBeTrue()
DataType.UBYTE.isAssignableTo(DataType.UBYTE) shouldBe true
DataType.UBYTE.isAssignableTo(DataType.UWORD) shouldBe true
DataType.UBYTE.isAssignableTo(DataType.LONG) shouldBe true
DataType.UBYTE.isAssignableTo(DataType.FLOAT) shouldBe true
}
test("byte assignability") {
DataType.BYTE.isAssignableTo(DataType.BYTE).shouldBeTrue()
DataType.BYTE.isAssignableTo(DataType.WORD).shouldBeTrue()
DataType.BYTE.isAssignableTo(DataType.LONG).shouldBeTrue()
DataType.BYTE.isAssignableTo(DataType.BYTE) shouldBe true
DataType.BYTE.isAssignableTo(DataType.WORD) shouldBe true
DataType.BYTE.isAssignableTo(DataType.LONG) shouldBe true
}
test("uword assignability") {
DataType.UWORD.isAssignableTo(DataType.UWORD).shouldBeTrue()
DataType.UWORD.isAssignableTo(DataType.LONG).shouldBeTrue()
DataType.UWORD.isAssignableTo(DataType.FLOAT).shouldBeTrue()
DataType.UWORD.isAssignableTo(DataType.UWORD) shouldBe true
DataType.UWORD.isAssignableTo(DataType.LONG) shouldBe true
DataType.UWORD.isAssignableTo(DataType.FLOAT) shouldBe true
}
test("string assignability") {
DataType.STR.isAssignableTo(DataType.STR).shouldBeTrue()
DataType.STR.isAssignableTo(DataType.UWORD).shouldBeTrue()
DataType.STR.isAssignableTo(DataType.STR) shouldBe true
DataType.STR.isAssignableTo(DataType.UWORD) shouldBe true
}
test("pointer assignability") {
val ptr = DataType.pointer(BaseDataType.UBYTE)
ptr.isAssignableTo(DataType.UWORD).shouldBeTrue()
ptr.isAssignableTo(DataType.LONG).shouldBeTrue()
ptr.isAssignableTo(DataType.pointer(BaseDataType.UBYTE)).shouldBeTrue()
ptr.isAssignableTo(DataType.UWORD) shouldBe true
ptr.isAssignableTo(DataType.LONG) shouldBe true
ptr.isAssignableTo(DataType.pointer(BaseDataType.UBYTE)) shouldBe true
}
// ============================================================================
@@ -305,7 +326,7 @@ class TestDataType: FunSpec({
}
test("DataType.toString for arrays") {
DataType.arrayFor(BaseDataType.UBYTE).toString() shouldBe "ubyte[]"
DataType.arrayFor(BaseDataType.WORD).toString() shouldBe "word[] (split)"
DataType.arrayFor(BaseDataType.UBYTE, tgt).toString() shouldBe "ubyte[]"
DataType.arrayFor(BaseDataType.WORD, tgt).toString() shouldBe "word[]"
}
})
@@ -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
@@ -2,8 +2,12 @@ package prog8.codegen.cpu6502
import com.github.michaelbull.result.fold
import prog8.code.*
import prog8.code.assembly.AssemblyProgram6502
import prog8.code.assembly.IAssemblyProgram
import prog8.code.ast.*
import prog8.code.core.*
import prog8.code.cpu6502.byteShiftAddExpansion
import prog8.code.cpu6502.wordShiftAddExpansion
import prog8.code.source.ImportFileSystem
import prog8.code.source.SourceCode
import prog8.code.target.Cx16Target
@@ -15,8 +19,7 @@ import kotlin.io.path.writeLines
internal const val subroutineFloatEvalResultVar1 = "prog8_float_eval_result1"
internal const val subroutineFloatEvalResultVar2 = "prog8_float_eval_result2"
class AsmGen6502(val prefixSymbols: Boolean,
private val lastGeneratedLabelSequenceNr: Int,
class AsmGen6502(private val lastGeneratedLabelSequenceNr: Int,
private val preassignedCallSiteIds: Map<PtAsmSub, UByte> = emptyMap()
): ICodeGeneratorBackend {
override fun generate(
@@ -25,7 +28,7 @@ class AsmGen6502(val prefixSymbols: Boolean,
options: CompilationOptions,
errors: IErrorReporter,
): IAssemblyProgram? {
val st = if(prefixSymbols) prefixSymbols(program, options, symbolTable) else symbolTable
val st = prefixSymbols(program, options, symbolTable)
val asmgen = AsmGen6502Internal(program, st, options, errors, lastGeneratedLabelSequenceNr, preassignedCallSiteIds)
return asmgen.compileToAssembly()
}
@@ -44,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)
@@ -79,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)
@@ -99,7 +130,7 @@ class AsmGen6502Internal (
if(options.dumpVariables)
dumpVariables()
return AssemblyProgram(program.name, options.outputDir, options.compTarget)
return AssemblyProgram6502(program.name, options, options.compTarget)
} else {
errors.report()
return null
@@ -286,16 +317,16 @@ 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!!)
if (e.identifier!!.type.isSplitWordArray) {
if (e.identifier!!.type.isSplitWordArray(options.compTarget)) {
symbol = if (e.isMsbForSplitArray) symbol + "_msb" else symbol + "_lsb"
}
return Pair("#<$symbol", "#>$symbol")
@@ -441,6 +472,7 @@ class AsmGen6502Internal (
out(" tya | pha")
}
}
else -> throw AssemblyError("register not available on this target")
}
}
@@ -469,6 +501,7 @@ class AsmGen6502Internal (
out(" pla | tay")
}
}
else -> throw AssemblyError("register not available on this target")
}
}
@@ -549,6 +582,7 @@ class AsmGen6502Internal (
CpuRegister.A -> {}
CpuRegister.X -> out(" tax")
CpuRegister.Y -> out(" tay")
else -> throw AssemblyError("register not available on this target")
}
}
expr.type.isLong -> {
@@ -558,6 +592,7 @@ class AsmGen6502Internal (
CpuRegister.A -> {}
CpuRegister.X -> out(" tax")
CpuRegister.Y -> out(" tay")
else -> throw AssemblyError("register not available on this target")
}
}
expr.type.isFloat -> {
@@ -575,6 +610,7 @@ class AsmGen6502Internal (
CpuRegister.A -> {}
CpuRegister.X -> out(" tax")
CpuRegister.Y -> out(" tay")
else -> throw AssemblyError("register not available on this target")
}
}
else -> throw AssemblyError("weird dt")
@@ -701,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
@@ -714,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")
}
}
}
@@ -1403,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) {
@@ -1937,7 +1985,7 @@ $repeatLabel""")
private fun translate(ret: PtReturn) {
val returnvalue = ret.children.singleOrNull() as? PtExpression // could be a multi-value returning functioncall
val sub = ret.definingSub()!!
val returnRegs = sub.returnsWhatWhere()
val returnRegs = sub.returnsWhatWhere(options.compTarget)
if(returnvalue!=null) {
@@ -1948,9 +1996,8 @@ $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) {
is StSub -> (calledSub.astNode!! as IPtSubroutine).returnsWhatWhere()
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}")
}
@@ -1976,13 +2023,23 @@ $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)
}
else {
// all else take its address and assign that also to AY register pair
val addrOfDt = returnvalue.type.typeForAddressOf(false)
val addrOfDt = returnvalue.type.typeForUntypedAddressOf(false, program.memsizer)
val addrofValue = PtAddressOf(addrOfDt, false, returnvalue.position)
addrofValue.add(returnvalue)
addrofValue.parent = ret.parent
@@ -2010,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)
}
}
@@ -2042,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")
}
@@ -2161,6 +2229,13 @@ $repeatLabel""")
internal fun jmp(asmLabel: String, indirect: Boolean=false, indexedX: Boolean=false) {
if(indirect) {
// NOTE: `jmp (ptr)` / `jmp (ptr,x)` have the 6502 page-wrap bug
// on plain 6502: if the pointer's address ends in $FF, the
// high byte of the target is read from the same page instead
// of the next. 65C02 is fine. Latent: only misbehaves when the
// pointer variable (the operand, e.g. a computed-goto target)
// is placed at a $xxFF address. The new6502 codegen has the
// same hazard for its JUMPI/CALLI emitters.
if(indexedX)
out(" jmp ($asmLabel,x)")
else
@@ -2216,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
@@ -2238,12 +2311,22 @@ $repeatLabel""")
val constOffset = (ptrAndIndex.second as? PtNumber)?.number?.toInt()
if(addrOf!=null && constOffset!=null) {
if(addrOf.isFromArrayElement) {
val arrayId = addrOf.identifier
val arrayDt = arrayId?.type
if(arrayDt?.sub == BaseDataType.STRUCT_INSTANCE) {
val base = asmSymbolName(arrayId)
out(" pha")
assignmentAsmGen.computeStructArrayOffsetY(arrayDt, addrOf.arrayIndexExpr!!, constOffset)
out(" pla")
out(" sta $base,y")
return true
}
TODO("address-of array element ${addrOf.position}")
} else if(addrOf.dereference!=null) {
throw AssemblyError("write &dereference, makes no sense at ${addrOf.position}")
} else {
var symbolName = asmSymbolName(addrOf.identifier!!)
if(addrOf.identifier!!.type.isSplitWordArray) {
if(addrOf.identifier!!.type.isSplitWordArray(options.compTarget)) {
symbolName = if(addrOf.isMsbForSplitArray) symbolName+"_msb" else symbolName+"_lsb"
}
out(" sta $symbolName+${constOffset}")
@@ -2260,18 +2343,18 @@ $repeatLabel""")
out(" sta (${asmSymbolName(pointervar)}),y")
} else {
// copy the pointer var to zp first
val saveA = evalBytevalueWillClobberA(ptrAndIndex.first) || evalBytevalueWillClobberA(ptrAndIndex.second)
if(saveA) out(" pha")
// assignExpressionToVariable always clobbers A, so always save it
out(" pha")
if(ptrAndIndex.second.isSimple()) {
assignExpressionToVariable(ptrAndIndex.first, "P8ZP_SCRATCH_W2", DataType.UWORD)
assignExpressionToRegister(ptrAndIndex.second, RegisterOrPair.Y)
if(saveA) out(" pla")
out(" pla")
out(" sta (P8ZP_SCRATCH_W2),y")
} else {
pushCpuStack(BaseDataType.UBYTE, ptrAndIndex.second)
assignExpressionToVariable(ptrAndIndex.first, "P8ZP_SCRATCH_W2", DataType.UWORD)
restoreRegisterStack(CpuRegister.Y, true)
if(saveA) out(" pla")
out(" pla")
out(" sta (P8ZP_SCRATCH_W2),y")
}
}
@@ -2284,13 +2367,21 @@ $repeatLabel""")
val constOffset = (ptrAndIndex.second as? PtNumber)?.number?.toInt()
if(addrOf!=null && constOffset!=null) {
if(addrOf.isFromArrayElement) {
val arrayId = addrOf.identifier
val arrayDt = arrayId?.type
if(arrayDt?.sub == BaseDataType.STRUCT_INSTANCE) {
val base = asmSymbolName(arrayId)
assignmentAsmGen.computeStructArrayOffsetY(arrayDt, addrOf.arrayIndexExpr!!, constOffset)
out(" lda $base,y")
return true
}
TODO("address-of array element ${addrOf.position}")
} else if(addrOf.dereference!=null) {
if(pointerGen.readByteByAddressOfDereference(addrOf, constOffset))
return true
} else {
var symbolName = asmSymbolName(addrOf.identifier!!)
if(addrOf.identifier!!.type.isSplitWordArray) {
if(addrOf.identifier!!.type.isSplitWordArray(options.compTarget)) {
symbolName = if(addrOf.isMsbForSplitArray) symbolName+"_msb" else symbolName+"_lsb"
}
out(" lda $symbolName+${constOffset}")
@@ -2344,7 +2435,7 @@ $repeatLabel""")
throw AssemblyError("write &dereference, makes no sense at ${addrOf.position}")
} else {
var symbolName = asmSymbolName(addrOf.identifier!!)
if(addrOf.identifier!!.type.isSplitWordArray) {
if(addrOf.identifier!!.type.isSplitWordArray(options.compTarget)) {
symbolName = if(addrOf.isMsbForSplitArray) symbolName+"_msb" else symbolName+"_lsb"
}
out(" sta $symbolName-${constOffset}")
@@ -2381,7 +2472,7 @@ $repeatLabel""")
TODO("read &dereference ${addrOf.position}")
} else {
var symbolName = asmSymbolName(addrOf.identifier!!)
if(addrOf.identifier!!.type.isSplitWordArray) {
if(addrOf.identifier!!.type.isSplitWordArray(options.compTarget)) {
symbolName = if(addrOf.isMsbForSplitArray) symbolName+"_msb" else symbolName+"_lsb"
}
out(" lda $symbolName-${constOffset}")
@@ -2609,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) {
@@ -2991,6 +3081,7 @@ $repeatLabel""")
CpuRegister.A -> {}
CpuRegister.X -> out(" txa")
CpuRegister.Y -> out(" tya")
else -> throw AssemblyError("register not available on this target")
}
signExtendAXlsb(if(signed) BaseDataType.BYTE else BaseDataType.UBYTE)
out("""
@@ -3007,6 +3098,7 @@ $repeatLabel""")
CpuRegister.A -> out(" ldy #$offset | sta ($zpPtrVar),y")
CpuRegister.X -> out(" txa | ldy #$offset | sta ($zpPtrVar),y")
CpuRegister.Y -> out(" tya | ldy #$offset | sta ($zpPtrVar),y")
else -> throw AssemblyError("register not available on this target")
}
return
}
@@ -3023,6 +3115,7 @@ $repeatLabel""")
out(" tya")
if(isTargetCpu(CpuType.CPU65C02)) out(" sta ($zpPtrVar)") else out(" ldy #0 | sta ($zpPtrVar),y")
}
else -> throw AssemblyError("register not available on this target")
}
}
@@ -1,9 +1,6 @@
package prog8.codegen.cpu6502
import prog8.code.GENERATED_LABEL_PREFIX
import prog8.code.StConstant
import prog8.code.StMemVar
import prog8.code.SymbolTable
import prog8.code.*
import prog8.code.core.ICompilationTarget
// note: see https://wiki.nesdev.org/w/index.php/6502_assembly_optimisations
@@ -41,9 +38,18 @@ internal fun optimizeAssembly(lines: MutableList<String>, machine: ICompilationT
linesByFour = runPass(optimizeUnneededTempvarInAdd(linesByFour), 4, linesByFour)
linesByFour = runPass(optimizeTSBtoRegularOr(linesByFour), 4, linesByFour)
var linesByFive = getLinesBy(pretrimmed, lines, 5)
linesByFive = runPass(optimizeFoldIndexOffset(linesByFive, symbolTable), 5, linesByFive)
var linesBySix = getLinesBy(pretrimmed, lines, 6)
linesBySix = runPass(optimizeFoldIndexOffsetSplitWord(linesBySix, symbolTable), 6, linesBySix)
var linesBySeven = getLinesBy(pretrimmed, lines, 7)
linesBySeven = runPass(optimizeFoldIndexOffsetNonSplitWord(linesBySeven, symbolTable), 7, linesBySeven)
var linesByFourteen = getLinesBy(pretrimmed, lines, 14)
linesByFourteen = runPass(optimizeSameAssignments(linesByFourteen, machine, symbolTable), 14, linesByFourteen)
linesByFourteen = runPass(optimizeSamePointerIndexingAndUselessBeq(linesByFourteen), 14, linesByFourteen)
linesByFourteen = runPass(optimizeSamePointerIndexingAndUselessBeq(linesByFourteen, machine, symbolTable), 14, linesByFourteen)
linesByFourteen = runPass(optimizeAddWordToSameVariableOrExtraRegisterLoadInWordStore(linesByFourteen), 14, linesByFourteen)
if(!modified)
@@ -88,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)
@@ -406,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:
@@ -420,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 +
@@ -631,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> {
@@ -889,6 +999,205 @@ private fun optimizeUnneededTempvarInAdd(linesByFour: Sequence<List<TrimmedLine>
return mods
}
private fun withLabel(line: TrimmedLine, newInstruction: String): String {
val label = keeplabelPretrimmed(line.value)
return if (label.isEmpty()) newInstruction else "$label ${newInstruction.trimStart()}"
}
private fun scaleConstant(constStr: String, scale: Int): String {
val value = if (constStr.startsWith("$"))
constStr.substring(1).toInt(16)
else
constStr.toInt()
val scaled = value * scale
return if (constStr.startsWith("$"))
"$${scaled.toString(16).padStart(2, '0')}"
else
scaled.toString()
}
private fun isArrayByteSizedAndSmall(name: String, symbolTable: SymbolTable): Boolean {
return when (val symbol = symbolTable.flat[name]) {
is StStaticVariable -> {
val len = symbol.length
(symbol.dt.isByteArray || symbol.dt.isString) && len != null && len <= 256u
}
is StMemVar -> {
val len = symbol.length
symbol.dt.isByteArray && len != null && len <= 256u
}
else -> false
}
}
private fun isArrayWordSizedAndSmall(name: String, symbolTable: SymbolTable): Boolean {
return when (val symbol = symbolTable.flat[name]) {
is StStaticVariable -> {
val len = symbol.length
symbol.dt.isWordArray && len != null && len <= 256u
}
is StMemVar -> {
val len = symbol.length
symbol.dt.isWordArray && len != null && len <= 256u
}
else -> false
}
}
private fun optimizeFoldIndexOffset(
linesByFive: Sequence<List<TrimmedLine>>,
symbolTable: SymbolTable
): List<Modification> {
val mods = mutableListOf<Modification>()
for (lines in linesByFive) {
val first = lines[0].instruction
val second = lines[1].instruction
val third = lines[2].instruction
val fourth = lines[3].instruction
val fifth = lines[4].instruction
if (!first.startsWith("lda ") || second != "clc" || !third.startsWith("adc "))
continue
if (fourth != "tay" && fourth != "tax")
continue
val reg = fourth[2]
val constOperand = third.extractOperandTrimmed()
if (!constOperand.startsWith("#"))
continue
val constValue = constOperand.substring(1)
val fifthOperand = fifth.extractOperandTrimmed()
if (!fifthOperand.endsWith(",$reg") || fifthOperand.startsWith("("))
continue
val addrPart = fifthOperand.dropLast(2)
if (!isArrayByteSizedAndSmall(addrPart, symbolTable)) continue
val mnemonic = fifth.take(3)
val varName = first.extractOperandTrimmed()
mods.add(Modification(lines[0].index, false, withLabel(lines[0], " ld$reg $varName")))
mods.add(Modification(lines[1].index, true, null))
mods.add(Modification(lines[2].index, true, null))
mods.add(Modification(lines[3].index, true, null))
mods.add(Modification(lines[4].index, false, withLabel(lines[4], " $mnemonic $addrPart+$constValue,$reg")))
}
return mods
}
private fun optimizeFoldIndexOffsetSplitWord(
linesBySix: Sequence<List<TrimmedLine>>,
symbolTable: SymbolTable
): List<Modification> {
val mods = mutableListOf<Modification>()
for (lines in linesBySix) {
val first = lines[0].instruction
val second = lines[1].instruction
val third = lines[2].instruction
val fourth = lines[3].instruction
val fifth = lines[4].instruction
val sixth = lines[5].instruction
if (!first.startsWith("lda ") || second != "clc" || !third.startsWith("adc "))
continue
if (fourth != "tay" && fourth != "tax")
continue
val reg = fourth[2]
val constOperand = third.extractOperandTrimmed()
if (!constOperand.startsWith("#"))
continue
val constValue = constOperand.substring(1)
val fifthOp = fifth.extractOperandTrimmed()
val sixthOp = sixth.extractOperandTrimmed()
if (!fifthOp.endsWith(",$reg") || fifthOp.startsWith("("))
continue
if (!sixthOp.endsWith(",$reg") || sixthOp.startsWith("("))
continue
val lsbAddr = fifthOp.dropLast(2)
val msbAddr = sixthOp.dropLast(2)
if (!lsbAddr.endsWith("_lsb") || !msbAddr.endsWith("_msb"))
continue
val baseName = lsbAddr.dropLast(4)
if (msbAddr != baseName + "_msb")
continue
if (!isArrayWordSizedAndSmall(baseName, symbolTable))
continue
val lsbMnemonic = fifth.take(3)
val msbMnemonic = sixth.take(3)
val varName = first.extractOperandTrimmed()
mods.add(Modification(lines[0].index, false, withLabel(lines[0], " ld$reg $varName")))
mods.add(Modification(lines[1].index, true, null))
mods.add(Modification(lines[2].index, true, null))
mods.add(Modification(lines[3].index, true, null))
mods.add(Modification(lines[4].index, false, withLabel(lines[4], " $lsbMnemonic $lsbAddr+$constValue,$reg")))
mods.add(Modification(lines[5].index, false, withLabel(lines[5], " $msbMnemonic $msbAddr+$constValue,$reg")))
}
return mods
}
private fun optimizeFoldIndexOffsetNonSplitWord(
linesBySeven: Sequence<List<TrimmedLine>>,
symbolTable: SymbolTable
): List<Modification> {
val mods = mutableListOf<Modification>()
for (lines in linesBySeven) {
val first = lines[0].instruction
val second = lines[1].instruction
val third = lines[2].instruction
val fourth = lines[3].instruction
val fifth = lines[4].instruction
val sixth = lines[5].instruction
val seventh = lines[6].instruction
if (!first.startsWith("lda ") || second != "clc" || !third.startsWith("adc "))
continue
if (fourth != "asl a" || (fifth != "tay" && fifth != "tax"))
continue
val reg = fifth[2]
val constOperand = third.extractOperandTrimmed()
if (!constOperand.startsWith("#"))
continue
val constValue = constOperand.substring(1)
val scaledConst = scaleConstant(constValue, 2)
val sixthOp = sixth.extractOperandTrimmed()
val seventhOp = seventh.extractOperandTrimmed()
if (!sixthOp.endsWith(",$reg") || sixthOp.startsWith("("))
continue
if (!seventhOp.endsWith(",$reg") || seventhOp.startsWith("("))
continue
val baseAddr = sixthOp.dropLast(2)
if (seventhOp != "$baseAddr+1,$reg")
continue
if (!isArrayWordSizedAndSmall(baseAddr, symbolTable))
continue
val lowMnemonic = sixth.take(3)
val highMnemonic = seventh.take(3)
mods.add(Modification(lines[1].index, true, null))
mods.add(Modification(lines[2].index, true, null))
mods.add(Modification(lines[5].index, false, withLabel(lines[5], " $lowMnemonic $baseAddr+$scaledConst,$reg")))
mods.add(Modification(lines[6].index, false, withLabel(lines[6], " $highMnemonic $baseAddr+1+$scaledConst,$reg")))
}
return mods
}
private fun optimizeAddWordToSameVariableOrExtraRegisterLoadInWordStore(linesByFourteen: Sequence<List<TrimmedLine>>): List<Modification> {
/*
; FIRST SEQUYENCE: P8ZP_SCRATCH_PTR += AY :
@@ -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)
}
@@ -376,6 +399,11 @@ import prog8.codegen.cpu6502.assignment.*
val identifier = fcall.args[0] as? PtIdentifier
if(identifier!=null) {
// NOTE: `jmp (ptr)` below has the 6502 page-wrap bug on plain
// 6502 if the pointer (the identifier, a variable holding a
// routine address) lands at $xxFF. 65C02 is fine. Latent; only
// misbehaves for variables placed at a $xxFF address. The
// new6502 codegen's CALLI has the same hazard.
asmgen.out("""
; push a return address so the jmp becomes indirect jsr
lda #>((+)-1)
@@ -387,6 +415,9 @@ import prog8.codegen.cpu6502.assignment.*
return arrayOf(RegisterOrPair.AY)
}
// NOTE: same 6502 page-wrap pitfall as above: the pointer is
// staged through P8ZP_SCRATCH_W2 before the indirect jmp, so the
// scratch word must not land at $xxFF on plain 6502. 65C02 is fine.
asmgen.assignExpressionToVariable(fcall.args[0], asmgen.asmVariableName("P8ZP_SCRATCH_W2"), DataType.UWORD) // jump address
asmgen.out("""
; push a return address so the jmp becomes indirect jsr
@@ -401,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"
@@ -418,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")
@@ -475,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")
@@ -1137,7 +1167,7 @@ import prog8.codegen.cpu6502.assignment.*
val msbAdd: Int
if(indexer.splitWords) {
val arrayVariable = indexer.variable ?: TODO("ptr indexing ${indexer.position}")
indexer.setChild(0, PtIdentifier(arrayVariable.name + (if(msb) "_msb" else "_lsb"), DataType.arrayFor(BaseDataType.UBYTE, false), arrayVariable.position))
indexer.setChild(0, PtIdentifier(arrayVariable.name + (if(msb) "_msb" else "_lsb"), DataType.arrayFor(BaseDataType.UBYTE, asmgen.options.compTarget), arrayVariable.position))
elementSize = 1
msbAdd = 0
} else {
@@ -1432,7 +1462,7 @@ import prog8.codegen.cpu6502.assignment.*
return emptyArray()
} else if(addressOfIdentifier!=null && (addressOfIdentifier.type.isWord || addressOfIdentifier.type.isPointer || addressOfIdentifier.type.isByteArray)) {
var varname = asmgen.asmVariableName(addressOfIdentifier)
if(addressOfIdentifier.type.isSplitWordArray) {
if(addressOfIdentifier.type.isSplitWordArray(asmgen.options.compTarget)) {
val addrOf = result.first as PtAddressOf
varname = if(addrOf.isMsbForSplitArray) varname+"_msb" else varname+"_lsb"
}
@@ -1814,7 +1844,7 @@ import prog8.codegen.cpu6502.assignment.*
}
} else if(addressOfIdentifier!=null && (addressOfIdentifier.type.isWord || addressOfIdentifier.type.isPointer || addressOfIdentifier.type.isByteArray)) {
var varname = asmgen.asmVariableName(addressOfIdentifier)
if(addressOfIdentifier.type.isSplitWordArray) {
if(addressOfIdentifier.type.isSplitWordArray(asmgen.options.compTarget)) {
val addrOf = result.first as PtAddressOf
varname = if(addrOf.isMsbForSplitArray) varname+"_msb" else varname+"_lsb"
}
@@ -1973,11 +2003,252 @@ import prog8.codegen.cpu6502.assignment.*
return arrayOf(RegisterOrPair.R14R15)
}
private fun inlineClampByte(value: PtExpression, minVal: Int, maxVal: Int, signed: Boolean) {
val typeMin = if (signed) -128 else 0
val typeMax = if (signed) 127 else 255
assignAsmGen.assignExpressionToRegister(value, RegisterOrPair.A, signed)
when {
minVal == typeMin && maxVal == typeMax -> {
// no-op: the value in A is already the clamped result
}
minVal == typeMin -> {
// upper clamp only: min(x, maxVal)
emitByteUpperClamp(maxVal, signed)
}
maxVal == typeMax -> {
// lower clamp only: max(x, minVal)
emitByteLowerClamp(minVal, signed)
}
else -> {
// two-sided clamp: upper then lower, sequentially on the running value in A
emitByteUpperClamp(maxVal, signed)
emitByteLowerClamp(minVal, signed)
}
}
}
private fun emitByteUpperClamp(maxVal: Int, signed: Boolean) {
// result = min(x, maxVal); x is in A on entry
// mirrors the proven func_clamp_byte sequence (sec/sbc + bmi means "x < max")
val imm = "#${(maxVal and 0xff).toHex()}"
if (signed) {
val keep = asmgen.makeLabel("clampkeep")
val done = asmgen.makeLabel("clampdone")
asmgen.out(listOf(
" tay",
" sec",
" sbc $imm",
" bvc +",
" eor #128",
"+ bmi $keep",
" lda $imm",
" jmp $done",
"$keep tya",
done
).joinToString("\n"))
} else {
val keep = asmgen.makeLabel("clampkeep")
asmgen.out(listOf(
" cmp $imm",
" bcc $keep",
" lda $imm",
keep
).joinToString("\n"))
}
}
private fun emitByteLowerClamp(minVal: Int, signed: Boolean) {
// result = max(x, minVal); x is in A on entry
// mirrors the proven func_clamp_byte sequence (sec/sbc + bpl means "x >= min")
val imm = "#${(minVal and 0xff).toHex()}"
if (signed) {
val keep = asmgen.makeLabel("clampkeep")
val done = asmgen.makeLabel("clampdone")
asmgen.out(listOf(
" tay",
" sec",
" sbc $imm",
" bvc +",
" eor #128",
"+ bpl $keep",
" lda $imm",
" jmp $done",
"$keep tya",
done
).joinToString("\n"))
} else {
val keep = asmgen.makeLabel("clampkeep")
asmgen.out(listOf(
" cmp $imm",
" bcs $keep",
" lda $imm",
keep
).joinToString("\n"))
}
}
private fun 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) }
when {
fcall.type.isByte -> {
if (!signed && fcall.args[1] is PtNumber && fcall.args[2] is PtNumber) {
// unsigned (ubyte) clamp with constant bounds: emit a small inline clamp.
// Only worthwhile for ubyte: the signed inline sequence is larger than the
// shared jsr func_clamp_byte subroutine, so signed clamps keep using that.
val minVal = (fcall.args[1] as PtNumber).number.toInt()
val maxVal = (fcall.args[2] as PtNumber).number.toInt()
inlineClampByte(fcall.args[0], minVal, maxVal, signed)
return arrayOf(RegisterOrPair.A)
}
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck) && fcall.args[2].isSimple(zpCheck)) {
// simple values (number/variable) do not clobber W1, so min/max can be evaluated first
assignAsmGen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W1", fcall.args[1].type) // minimum
@@ -1998,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
@@ -2292,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) {
@@ -2669,7 +2949,7 @@ import prog8.codegen.cpu6502.assignment.*
private fun translateArguments(call: PtFunctionCall, funcname: String?, scope: IPtSubroutine?) {
val signature = BuiltinFunctions.getValue(call.name)
val callConv = signature.callConvention(call.args.map {
val callConv = signature.callConventionFor6502(call.args.map {
require(it.type.isNumericOrBool)
it.type.base
})
@@ -2722,7 +3002,7 @@ import prog8.codegen.cpu6502.assignment.*
conv.dt==BaseDataType.LONG -> getSourceForLong(value)
conv.dt.isPassByRef -> {
// put the address of the argument in AY
val addr = PtAddressOf(DataType.forDt(conv.dt).typeForAddressOf(false), false, value.position)
val addr = PtAddressOf(DataType.forDt(conv.dt).typeForUntypedAddressOf(false, program.memsizer), false, value.position)
addr.add(value)
addr.parent = call
AsmAssignSource.fromAstSource(addr, program, asmgen)
@@ -2741,7 +3021,7 @@ import prog8.codegen.cpu6502.assignment.*
conv.dt==BaseDataType.LONG -> AsmAssignSource.fromAstSource(value, program, asmgen)
conv.dt.isPassByRef -> {
// put the address of the argument in AY
val addr = PtAddressOf(DataType.forDt(conv.dt).typeForAddressOf(false), false,value.position)
val addr = PtAddressOf(DataType.forDt(conv.dt).typeForUntypedAddressOf(false, program.memsizer), false,value.position)
addr.add(value)
addr.parent = call
AsmAssignSource.fromAstSource(addr, program, asmgen)
@@ -5,6 +5,7 @@ import prog8.code.StMemVar
import prog8.code.StStaticVariable
import prog8.code.ast.PtForLoop
import prog8.code.ast.PtIdentifier
import prog8.code.ast.PtNumber
import prog8.code.ast.PtRange
import prog8.code.core.*
import kotlin.math.absoluteValue
@@ -33,15 +34,21 @@ internal class ForLoopsAsmGen(
}
private fun translateForOverNonconstRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
if(range.step.asConstInteger()!! < -1) {
val constStep = range.step.asConstInteger()
if(constStep != null && constStep < -1) {
val limit = range.to.asConstInteger()
if(limit==0)
throw AssemblyError("for unsigned loop variable it's not possible to count down with step != -1 from a non-const value to exactly zero due to value wrapping")
}
if(range.step !is PtNumber) {
translateForOverNonconstStepRange(stmt, iterableDt, range)
return
}
when {
iterableDt.isByteArray -> forOverNonconstByteRange(stmt, iterableDt, range)
iterableDt.isWordArray && !iterableDt.isSplitWordArray -> forOverNonconstWordRange(stmt, iterableDt, range)
iterableDt.isWordArray && !iterableDt.isSplitWordArray(asmgen.options.compTarget) -> forOverNonconstWordRange(stmt, iterableDt, range)
iterableDt.isLongArray -> forOverNonconstLongRange(stmt, iterableDt, range)
else -> throw AssemblyError("range expression can only be byte, word or long")
}
@@ -49,6 +56,369 @@ internal class ForLoopsAsmGen(
asmgen.loopEndLabels.removeLast()
}
private fun translateForOverNonconstStepRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
when {
iterableDt.isByteArray -> forOverNonconstStepByteRange(stmt, iterableDt, range)
iterableDt.isWordArray && !iterableDt.isSplitWordArray(asmgen.options.compTarget) -> forOverNonconstStepWordRange(stmt, iterableDt, range)
iterableDt.isLongArray -> throw AssemblyError("variable-step for loops over long ranges are not supported by the legacy 6502 code generator at ${stmt.position}")
else -> throw AssemblyError("range expression can only be byte, word or long")
}
asmgen.loopEndLabels.removeLast()
}
private fun forOverNonconstStepByteRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
val loopDt = iterableDt.elementType()
val loopvar = asmgen.asmVariableName(stmt.variable)
val toValueVar = asmgen.createTempVarReused(loopDt.base, false, range)
val stepVar = asmgen.createTempVarReused(range.step.type.base, false, range)
val nextVar = asmgen.createTempVarReused(loopDt.base, false, range)
val loopLabel = asmgen.makeLabel("for_loop")
val descendingLabel = if(range.step.type.isSigned) asmgen.makeLabel("for_desc") else null
val endLabel = asmgen.makeLabel("for_end")
asmgen.loopEndLabels.add(endLabel)
// Keep range expression evaluation in source order and preserve each value across the next one.
asmgen.assignExpressionToVariable(range.from, loopvar, loopDt)
asmgen.assignExpressionToVariable(range.to, toValueVar, loopDt)
asmgen.assignExpressionToVariable(range.step, stepVar, range.step.type)
if (range.step.type.isSigned)
asmgen.out(" lda $stepVar | beq $endLabel | bmi $descendingLabel")
else
asmgen.out(" lda $stepVar | beq $endLabel")
emitByteRangePrecheck(loopvar, toValueVar, loopLabel, endLabel, loopDt.isSigned, true)
asmgen.out(loopLabel)
asmgen.translate(stmt.statements)
emitByteRangeUpdate(loopvar, toValueVar, stepVar, nextVar, endLabel, loopLabel, loopDt.isSigned, range.step.type.isSigned)
if(descendingLabel != null) {
asmgen.out(descendingLabel)
emitByteRangePrecheck(loopvar, toValueVar, loopLabel, endLabel, loopDt.isSigned, false)
asmgen.out(" jmp $loopLabel")
}
asmgen.out(endLabel)
}
private fun emitByteRangePrecheck(loopvar: String, toValueVar: String, loopLabel: String, endLabel: String, signed: Boolean, ascending: Boolean) {
val notEqualLabel = asmgen.makeLabel("for_not_equal")
val compareDoneLabel = asmgen.makeLabel("for_compare_done")
if (signed) {
asmgen.out("""
lda $loopvar
cmp $toValueVar
bne $notEqualLabel
jmp $loopLabel
$notEqualLabel
sec
lda $loopvar
sbc $toValueVar
bvc $compareDoneLabel
eor #$80
$compareDoneLabel
${if (ascending) "bpl" else "bmi"} $endLabel
""")
} else if (ascending) {
asmgen.out("""
lda $toValueVar
cmp $loopvar
bcc $endLabel
jmp $loopLabel
""")
} else {
asmgen.out("""
lda $loopvar
cmp $toValueVar
bcc $endLabel
""")
}
}
private fun emitByteRangeUpdate(
loopvar: String,
toValueVar: String,
stepVar: String,
nextVar: String,
endLabel: String,
loopLabel: String,
loopvarSigned: Boolean,
stepSigned: Boolean
) {
val notLastLabel = asmgen.makeLabel("for_not_last")
val ascendingCompareDoneLabel = asmgen.makeLabel("for_next_compare_done")
val descendingCompareDoneLabel = if(stepSigned) asmgen.makeLabel("for_desc_compare_done") else null
val descendingUpdateLabel = if(stepSigned) asmgen.makeLabel("for_desc_update") else null
val storeNextLabel = asmgen.makeLabel("for_store_next")
val ascendingWrapBranch = if (loopvarSigned) "bvs" else "bcs"
val descendingWrapBranch = if (loopvarSigned) "bvs" else "bcc"
asmgen.out("""
lda $loopvar
cmp $toValueVar
bne $notLastLabel
jmp $endLabel
$notLastLabel
""")
if (stepSigned)
asmgen.out(" lda $stepVar | bmi $descendingUpdateLabel")
asmgen.out("""
clc
lda $loopvar
adc $stepVar
$ascendingWrapBranch $endLabel
sta $nextVar
""")
asmgen.out("""
lda $nextVar
cmp $toValueVar
beq $storeNextLabel
sec
lda $nextVar
sbc $toValueVar
bvc $ascendingCompareDoneLabel
eor #$80
$ascendingCompareDoneLabel
""")
if (loopvarSigned)
asmgen.out(" bpl $endLabel")
else
asmgen.out(" bcs $endLabel")
if(stepSigned) {
asmgen.out("""
jmp $storeNextLabel
$descendingUpdateLabel
lda $loopvar
cmp $toValueVar
beq $endLabel
clc
lda $loopvar
adc $stepVar
$descendingWrapBranch $endLabel
sta $nextVar
""")
if (loopvarSigned)
asmgen.out("""
sec
lda $nextVar
sbc $toValueVar
bvc $descendingCompareDoneLabel
eor #$80
$descendingCompareDoneLabel
bmi $endLabel
""")
else
asmgen.out("""
lda $nextVar
cmp $toValueVar
bcc $endLabel
""")
}
asmgen.out("""
jmp $storeNextLabel
$storeNextLabel
lda $nextVar
sta $loopvar
jmp $loopLabel
""")
}
private fun forOverNonconstStepWordRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
val loopDt = iterableDt.elementType()
val loopvar = asmgen.asmVariableName(stmt.variable)
val toValueVar = asmgen.createTempVarReused(loopDt.base, false, range)
val stepVar = asmgen.createTempVarReused(range.step.type.base, false, range)
val nextVar = asmgen.createTempVarReused(loopDt.base, false, range)
val loopLabel = asmgen.makeLabel("for_loop")
val descendingLabel = if(range.step.type.isSigned) asmgen.makeLabel("for_desc") else null
val endLabel = asmgen.makeLabel("for_end")
asmgen.loopEndLabels.add(endLabel)
// Keep range expression evaluation in source order and preserve each value across the next one.
asmgen.assignExpressionToVariable(range.from, loopvar, loopDt)
asmgen.assignExpressionToVariable(range.to, toValueVar, loopDt)
asmgen.assignExpressionToVariable(range.step, stepVar, range.step.type)
if (range.step.type.isSigned)
asmgen.out(" lda $stepVar | ora ${stepVar}+1 | beq $endLabel | lda ${stepVar}+1 | bmi $descendingLabel")
else
asmgen.out(" lda $stepVar | ora ${stepVar}+1 | beq $endLabel")
emitWordRangePrecheck(loopvar, toValueVar, loopLabel, endLabel, loopDt.isSigned, true)
asmgen.out(loopLabel)
asmgen.translate(stmt.statements)
emitWordRangeUpdate(loopvar, toValueVar, stepVar, nextVar, endLabel, loopLabel, loopDt.isSigned, range.step.type.isSigned)
if(descendingLabel != null) {
asmgen.out(descendingLabel)
emitWordRangePrecheck(loopvar, toValueVar, loopLabel, endLabel, loopDt.isSigned, false)
asmgen.out(" jmp $loopLabel")
}
asmgen.out(endLabel)
}
private fun emitWordRangePrecheck(loopvar: String, toValueVar: String, loopLabel: String, endLabel: String, signed: Boolean, ascending: Boolean) {
val notEqualLabel = asmgen.makeLabel("for_not_equal")
val compareDoneLabel = asmgen.makeLabel("for_compare_done")
if (signed) {
asmgen.out("""
lda $loopvar+1
cmp $toValueVar+1
bne $notEqualLabel
lda $loopvar
cmp $toValueVar
bne $notEqualLabel
jmp $loopLabel
$notEqualLabel
sec
lda $loopvar
sbc $toValueVar
lda $loopvar+1
sbc $toValueVar+1
bvc $compareDoneLabel
eor #$80
$compareDoneLabel
${if (ascending) "bpl" else "bmi"} $endLabel
""")
} else if (ascending) {
asmgen.out("""
lda $toValueVar+1
cmp $loopvar+1
bcc $endLabel
bne $loopLabel
lda $toValueVar
cmp $loopvar
bcc $endLabel
jmp $loopLabel
""")
} else {
asmgen.out("""
lda $loopvar+1
cmp $toValueVar+1
bcc $endLabel
bne $loopLabel
lda $loopvar
cmp $toValueVar
bcc $endLabel
jmp $loopLabel
""")
}
}
private fun emitWordRangeUpdate(
loopvar: String,
toValueVar: String,
stepVar: String,
nextVar: String,
endLabel: String,
loopLabel: String,
loopvarSigned: Boolean,
stepSigned: Boolean
) {
val notLastLabel = asmgen.makeLabel("for_not_last")
val ascendingCompareLabel = asmgen.makeLabel("for_next_compare")
val ascendingCompareDoneLabel = asmgen.makeLabel("for_next_compare_done")
val descendingCompareDoneLabel = if(stepSigned) asmgen.makeLabel("for_desc_compare_done") else null
val descendingUpdateLabel = if(stepSigned) asmgen.makeLabel("for_desc_update") else null
val descendingNotLastLabel = if(stepSigned) asmgen.makeLabel("for_desc_not_last") else null
val storeNextLabel = asmgen.makeLabel("for_store_next")
val ascendingWrapBranch = if (loopvarSigned) "bvs" else "bcs"
val descendingWrapBranch = if (loopvarSigned) "bvs" else "bcc"
asmgen.out("""
lda $loopvar+1
cmp $toValueVar+1
bne $notLastLabel
lda $loopvar
cmp $toValueVar
beq $endLabel
$notLastLabel
""")
if (stepSigned)
asmgen.out(" lda ${stepVar}+1 | bmi $descendingUpdateLabel")
asmgen.out("""
clc
lda $loopvar
adc $stepVar
sta $nextVar
lda $loopvar+1
adc $stepVar+1
$ascendingWrapBranch $endLabel
sta $nextVar+1
""")
asmgen.out("""
lda $nextVar+1
cmp $toValueVar+1
bne $ascendingCompareLabel
lda $nextVar
cmp $toValueVar
beq $storeNextLabel
$ascendingCompareLabel
sec
lda $nextVar
sbc $toValueVar
lda $nextVar+1
sbc $toValueVar+1
bvc $ascendingCompareDoneLabel
eor #$80
$ascendingCompareDoneLabel
""")
if (loopvarSigned)
asmgen.out(" bpl $endLabel")
else
asmgen.out(" bcs $endLabel")
if(stepSigned) {
asmgen.out("""
jmp $storeNextLabel
$descendingUpdateLabel
lda $loopvar+1
cmp $toValueVar+1
bne $descendingNotLastLabel
lda $loopvar
cmp $toValueVar
beq $endLabel
$descendingNotLastLabel
clc
lda $loopvar
adc $stepVar
sta $nextVar
lda $loopvar+1
adc $stepVar+1
$descendingWrapBranch $endLabel
sta $nextVar+1
""")
if (loopvarSigned)
asmgen.out("""
sec
lda $nextVar
sbc $toValueVar
lda $nextVar+1
sbc $toValueVar+1
bvc $descendingCompareDoneLabel
eor #$80
$descendingCompareDoneLabel
bmi $endLabel
""")
else
asmgen.out("""
lda $nextVar+1
cmp $toValueVar+1
bcc $endLabel
bne $storeNextLabel
lda $nextVar
cmp $toValueVar
bcc $endLabel
""")
}
else asmgen.out(" jmp $storeNextLabel")
asmgen.out("""
$storeNextLabel
lda $nextVar
sta $loopvar
lda $nextVar+1
sta $loopvar+1
jmp $loopLabel
""")
}
private fun forOverNonconstLongRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
val stepsize = range.step.asConstInteger()!!
val loopLabel = asmgen.makeLabel("for_loop")
@@ -157,12 +527,7 @@ internal class ForLoopsAsmGen(
}
private fun forOverLongsRangeStepGreaterOne(range: PtRange, varname: String, iterableDt: DataType, loopLabel: String, endLabel: String, forloop: PtForLoop) {
// TODO: implement 32-bit loop
asmgen.romableError("for loops over long ranges with step != 1 not yet fully implemented", forloop.position)
asmgen.out(loopLabel)
asmgen.translate(forloop.statements)
asmgen.jmp(loopLabel)
asmgen.out(endLabel)
TODO("legacy 6502 codegen does not support long FOR ranges with step other than 1 or -1 at ${forloop.position}")
}
private fun forOverNonconstByteRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
@@ -822,7 +1187,7 @@ $loopLabel sty $indexVar
when {
iterableDt.isString -> iterateStrings()
iterableDt.isByteArray || iterableDt.isBoolArray -> iterateBytes()
iterableDt.isSplitWordArray -> iterateSplitWords()
iterableDt.isSplitWordArray(asmgen.options.compTarget) -> iterateSplitWords()
iterableDt.isWordArray -> iterateWords()
iterableDt.isLongArray -> iterateWords(true)
iterableDt.isFloatArray -> throw AssemblyError("for loop with floating point variables is not supported")
@@ -920,7 +1285,7 @@ $loopLabel""")
}
asmgen.out(endLabel)
}
iterableDt.isWordArray && !iterableDt.isSplitWordArray -> {
iterableDt.isWordArray && !iterableDt.isSplitWordArray(asmgen.options.compTarget) -> {
// loop over word range via loopvar, step >= 2 or <= -2
when (range.step) {
1, -1 -> {
@@ -1091,16 +1456,15 @@ $loopLabel""")
$loopLabel""")
asmgen.translate(stmt.statements)
asmgen.out("""
lda $varname
cmp #<${range.last}
inc $varname
bne +
lda $varname+1
cmp #>${range.last}
beq $endLabel
+ inc $varname
inc $varname+1
+ lda $varname
cmp #<${range.last+1}
bne $loopLabel
inc $varname+1""")
asmgen.jmp(loopLabel)
lda $varname+1
cmp #>${range.last+1}
bne $loopLabel""")
asmgen.out(endLabel)
asmgen.loopEndLabels.removeLast()
}
@@ -1121,24 +1485,21 @@ $loopLabel""")
if(range.last==0) {
asmgen.out("""
lda $varname
bne ++
lda $varname+1
beq $endLabel""")
} else {
asmgen.out("""
lda $varname
cmp #<${range.last}
bne +
lda $varname+1
cmp #>${range.last}
beq $endLabel""")
}
asmgen.out("""
+ lda $varname
bne +
dec $varname+1
+ dec $varname""")
asmgen.jmp(loopLabel)
+ dec $varname
lda $varname
cmp #<${range.last-1}
bne $loopLabel
lda $varname+1
cmp #>${range.last-1}
bne $loopLabel""")
asmgen.out(endLabel)
asmgen.loopEndLabels.removeLast()
}
@@ -1150,7 +1511,7 @@ $loopLabel""")
asmgen.loopEndLabels.add(endLabel)
val varname = asmgen.asmVariableName(stmt.variable)
val from = range.first.toLongHex()
val to = range.last.toLongHex()
val toPlus1 = (range.last + 1).toLongHex()
asmgen.out("""
lda #$${from.substring(6, 8)}
sta $varname
@@ -1163,26 +1524,25 @@ $loopLabel""")
$loopLabel""")
asmgen.translate(stmt.statements)
asmgen.out("""
lda $varname
cmp #$${to.substring(6, 8)}
inc $varname
bne +
lda $varname+1
cmp #$${to.substring(4, 6)}
bne +
lda $varname+2
cmp #$${to.substring(2, 4)}
bne +
lda $varname+3
cmp #$${to.take(2)}
beq $endLabel
+ inc $varname
bne $loopLabel
inc $varname+1
bne $loopLabel
bne +
inc $varname+2
bne +
inc $varname+3
+ lda $varname
cmp #$${toPlus1.substring(6, 8)}
bne $loopLabel
inc $varname+3""")
asmgen.jmp(loopLabel)
lda $varname+1
cmp #$${toPlus1.substring(4, 6)}
bne $loopLabel
lda $varname+2
cmp #$${toPlus1.substring(2, 4)}
bne $loopLabel
lda $varname+3
cmp #$${toPlus1.take(2)}
bne $loopLabel""")
asmgen.out(endLabel)
asmgen.loopEndLabels.removeLast()
}
@@ -1194,7 +1554,7 @@ $loopLabel""")
asmgen.loopEndLabels.add(endLabel)
val varname = asmgen.asmVariableName(stmt.variable)
val from = range.first.toLongHex()
val to = range.last.toLongHex()
val toMinus1 = (range.last - 1).toLongHex()
asmgen.out("""
lda #$${from.substring(6, 8)}
sta $varname
@@ -1216,20 +1576,6 @@ $loopLabel""")
bne ++
lda $varname+3
beq $endLabel""")
} else {
asmgen.out("""
lda $varname
cmp #$${to.substring(6, 8)}
bne +
lda $varname+1
cmp #$${to.substring(4, 6)}
bne +
lda $varname+2
cmp #$${to.substring(2, 4)}
bne +
lda $varname+3
cmp #$${to.take(2)}
beq $endLabel""")
}
asmgen.out("""
+ lda $varname
@@ -1241,8 +1587,19 @@ $loopLabel""")
dec $varname+3
+ dec $varname+2
+ dec $varname+1
+ dec $varname""")
asmgen.jmp(loopLabel)
+ dec $varname
lda $varname
cmp #$${toMinus1.substring(6, 8)}
bne $loopLabel
lda $varname+1
cmp #$${toMinus1.substring(4, 6)}
bne $loopLabel
lda $varname+2
cmp #$${toMinus1.substring(2, 4)}
bne $loopLabel
lda $varname+3
cmp #$${toMinus1.take(2)}
bne $loopLabel""")
asmgen.out(endLabel)
asmgen.loopEndLabels.removeLast()
}
@@ -388,7 +388,7 @@ $label .byte 0 ; modified"""
}
val src = if(value.type.isPassByRef) {
if(value is PtIdentifier || value is PtPointerDeref) {
val addr = PtAddressOf(value.type.typeForAddressOf(false), false, value.position)
val addr = PtAddressOf(value.type.typeForUntypedAddressOf(false, program.memsizer), false, value.position)
addr.add(value)
addr.parent = scope as PtNode
AsmAssignSource.fromAstSource(addr, program, asmgen).adjustSignedUnsigned(target)
@@ -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,19 +928,17 @@ 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}")
val baseVarname = asmgen.asmVariableName(e.variable!!)
val constIndex = e.index.asConstInteger()
if (constIndex != null) {
val idx = constIndex
if (e.splitWords) {
require(idx < 256)
require(constIndex < 256)
val suffix = if (long) "_3" else "_msb"
asmgen.out(" lda $baseVarname$suffix+$idx")
asmgen.out(" lda $baseVarname$suffix+$constIndex")
return
}
}
@@ -964,7 +958,7 @@ internal class IfElseAsmGen(private val program: PtProgram,
asmgen.out(" cpy #0")
} else {
var varname = asmgen.asmVariableName(e.identifier!!)
if(e.identifier!!.type.isSplitWordArray) {
if(e.identifier!!.type.isSplitWordArray(asmgen.options.compTarget)) {
varname += if(e.isMsbForSplitArray) "_msb" else "_lsb"
}
asmgen.out(" lda #>$varname")
@@ -1831,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) {
val identifier = e.identifier!!
val varname = if (identifier.type.isSplitWordArray) {
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 {
@@ -723,7 +722,7 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
is PtAddressOf -> {
val identifier = e.identifier!!
var varname = asmgen.asmVariableName(identifier)
if (identifier.type.isSplitWordArray) {
if (identifier.type.isSplitWordArray(asmgen.options.compTarget)) {
varname += if (e.isMsbForSplitArray) "_msb" else "_lsb"
}
asmgen.out(" lda #<$varname | ora #>$varname | bne $falseLabel")
@@ -774,7 +773,7 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
is PtAddressOf -> {
val identifier = e.identifier!!
var varname = asmgen.asmVariableName(identifier)
if (identifier.type.isSplitWordArray) {
if (identifier.type.isSplitWordArray(asmgen.options.compTarget)) {
varname += if (e.isMsbForSplitArray) "_msb" else "_lsb"
}
asmgen.out(" lda #<$varname | ora #>$varname | beq $falseLabel")
@@ -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,19 +1089,17 @@ 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}")
val baseVarname = asmgen.asmVariableName(e.variable!!)
val constIndex = e.index.asConstInteger()
if (constIndex != null) {
val idx = constIndex
if (e.splitWords) {
require(idx < 256)
require(constIndex < 256)
val suffix = if (long) "_3" else "_msb"
asmgen.out(" lda $baseVarname$suffix+$idx")
asmgen.out(" lda $baseVarname$suffix+$constIndex")
return
}
}
@@ -1124,7 +1119,7 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
asmgen.out(" cpy #0")
} else {
var varname = asmgen.asmVariableName(e.identifier!!)
if(e.identifier!!.type.isSplitWordArray) {
if(e.identifier!!.type.isSplitWordArray(asmgen.options.compTarget)) {
varname += if(e.isMsbForSplitArray) "_msb" else "_lsb"
}
asmgen.out(" lda #>$varname")
@@ -1150,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) {
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")
}
}
}
@@ -173,6 +173,7 @@ internal class ProgramAndVarsGen(
asmgen.out(" jsr p8_sys_startup.init_system")
asmgen.out(" jsr p8_sys_startup.init_system_phase2")
}
else -> error("Unsupported output type: ${compTarget.defaultOutputType}")
}
if (options.zeropage !in arrayOf(ZeropageType.BASICSAFE, ZeropageType.DONTUSE)) {
@@ -223,20 +224,23 @@ internal class ProgramAndVarsGen(
private fun footer() {
asmgen.out(" .dsection STRUCTINSTANCES\n")
var relocateBssVars = false
var relocateBssSlabs = false
var relocatedBssStart = 0u
var relocatedBssEnd = 0u
var relocateVars = false
var relocatedStart = 0u
var relocatedEnd = 0u
if(options.varsGolden) {
if(options.varsAddress != null) {
relocateVars = true
relocatedStart = options.varsAddress!!
relocatedEnd = if(options.memtopAddress != 0u) options.memtopAddress else 0xFFFFu
} else if(options.varsGolden) {
if(options.compTarget.BSSGOLDENRAM_START == 0u ||
options.compTarget.BSSGOLDENRAM_END == 0u ||
options.compTarget.BSSGOLDENRAM_END <= options.compTarget.BSSGOLDENRAM_START) {
throw AssemblyError("current compilation target hasn't got the golden ram area properly defined or it is simply not available")
}
relocateBssVars = true
relocatedBssStart = options.compTarget.BSSGOLDENRAM_START
relocatedBssEnd = options.compTarget.BSSGOLDENRAM_END
relocateVars = true
relocatedStart = options.compTarget.BSSGOLDENRAM_START
relocatedEnd = options.compTarget.BSSGOLDENRAM_END
}
else if(options.varsHighBank!=null) {
if(options.compTarget.BSSHIGHRAM_START == 0u ||
@@ -244,67 +248,34 @@ internal class ProgramAndVarsGen(
options.compTarget.BSSHIGHRAM_END <= options.compTarget.BSSHIGHRAM_START) {
throw AssemblyError("current compilation target hasn't got the high ram area properly defined or it is simply not available")
}
if(options.slabsHighBank!=null && options.varsHighBank!=options.slabsHighBank)
throw AssemblyError("slabs and vars high bank must be the same")
relocateBssVars = true
relocatedBssStart = options.compTarget.BSSHIGHRAM_START
relocatedBssEnd = options.compTarget.BSSHIGHRAM_END
}
if(options.slabsGolden) {
if(options.compTarget.BSSGOLDENRAM_START == 0u ||
options.compTarget.BSSGOLDENRAM_END == 0u ||
options.compTarget.BSSGOLDENRAM_END <= options.compTarget.BSSGOLDENRAM_START) {
throw AssemblyError("current compilation target hasn't got the golden ram area properly defined or it is simply not available")
}
relocateBssSlabs = true
relocatedBssStart = options.compTarget.BSSGOLDENRAM_START
relocatedBssEnd = options.compTarget.BSSGOLDENRAM_END
}
else if(options.slabsHighBank!=null) {
if(options.compTarget.BSSHIGHRAM_START == 0u ||
options.compTarget.BSSHIGHRAM_END == 0u ||
options.compTarget.BSSHIGHRAM_END <= options.compTarget.BSSHIGHRAM_START) {
throw AssemblyError("current compilation target hasn't got the high ram area properly defined or it is simply not available")
}
if(options.varsHighBank!=null && options.varsHighBank!=options.slabsHighBank)
throw AssemblyError("slabs and vars high bank must be the same")
relocateBssSlabs = true
relocatedBssStart = options.compTarget.BSSHIGHRAM_START
relocatedBssEnd = options.compTarget.BSSHIGHRAM_END
relocateVars = true
relocatedStart = options.compTarget.BSSHIGHRAM_START
relocatedEnd = options.compTarget.BSSHIGHRAM_END
}
asmgen.out("; bss sections")
asmgen.out("PROG8_VARSHIGH_RAMBANK = ${options.varsHighBank ?: 1}")
if(relocateBssVars) {
if(!relocateBssSlabs)
asmgen.out(" .dsection BSS_SLABS")
if(relocateVars) {
asmgen.out("prog8_program_end\t; end of program label for progend()")
asmgen.out(" * = ${relocatedBssStart.toHex()}")
asmgen.out(" * = ${relocatedStart.toHex()}")
asmgen.out(" .dsection BSS_NOCLEAR")
asmgen.out("prog8_bss_section_start")
asmgen.out(" .dsection BSS")
if(relocateBssSlabs)
asmgen.out(" .dsection BSS_SLABS")
asmgen.out(" .cerror * > ${relocatedBssEnd.toHex()}, \"too many variables/data for BSS section\"")
asmgen.out(" .dsection BSS_SLABS")
asmgen.out(" .cerror * > ${relocatedEnd.toHex()}, \"too many variables/data for BSS section\"")
asmgen.out("prog8_bss_section_size = * - prog8_bss_section_start")
} else {
asmgen.out(" .dsection BSS_NOCLEAR")
asmgen.out("prog8_bss_section_start")
asmgen.out(" .dsection BSS")
asmgen.out("prog8_bss_section_size = * - prog8_bss_section_start")
if(!relocateBssSlabs)
asmgen.out(" .dsection BSS_SLABS")
asmgen.out(" .dsection BSS_SLABS")
asmgen.out("prog8_program_end\t; end of program label for progend()")
if(relocateBssSlabs) {
asmgen.out(" * = ${relocatedBssStart.toHex()}")
asmgen.out(" .dsection BSS_SLABS")
asmgen.out(" .cerror * > ${relocatedBssEnd.toHex()}, \"too many data for BSS_SLABS section\"")
}
}
if(relocatedBssEnd >= options.memtopAddress)
options.memtopAddress = relocatedBssEnd+1u
val effectiveEnd = if(relocateVars) relocatedEnd else 0u
if(effectiveEnd >= options.memtopAddress && effectiveEnd != 0u)
options.memtopAddress = effectiveEnd+1u
asmgen.out(" ; memtop check")
asmgen.out(" .cerror * >= ${options.memtopAddress.toHex()}, \"Program too long by \", * - ${(options.memtopAddress-1u).toHex()}, \" bytes, memtop=${options.memtopAddress.toHex()}\"")
@@ -425,30 +396,33 @@ 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")
instancesNoInit.forEach {
val structtype: StStruct = symboltable.lookup(it.structName) as StStruct
asmgen.out("${it.name} .fill ${structtype.size}\n")
asmgen.out("${it.name} .fill ${structtype.size} ; struct ${it.structName}\n")
}
asmgen.out(" .endblock\n")
asmgen.out(" .send BSS\n")
@@ -483,6 +457,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.
@@ -644,7 +639,7 @@ internal class ProgramAndVarsGen(
arrayVarsWithInitInZp.forEach {
val size = it.alloc.size
val name = asmgen.asmVariableName(it.name)
if(it.alloc.dt.isSplitWordArray) {
if(it.alloc.dt.isSplitWordArray(compTarget)) {
val halfSize = size / 2
if(halfSize>0) {
asmgen.out("""
@@ -723,7 +718,7 @@ internal class ProgramAndVarsGen(
if (scopedName.startsWith("cx16.r"))
continue // The 16 virtual registers of the cx16 are not actual variables in zp, they're memory mapped
val variable = symboltable.flat.getValue(scopedName) as StStaticVariable
if(variable.dt.isSplitWordArray) {
if(variable.dt.isSplitWordArray(compTarget)) {
val lsbAddr = zpvar.address
val msbAddr = zpvar.address + (zpvar.size/2).toUInt()
asmgen.out("${scopedName.substringAfterLast('.')}_lsb \t= $lsbAddr \t; zp ${zpvar.dt} (lsbs)")
@@ -815,21 +810,19 @@ internal class ProgramAndVarsGen(
dt.isSignedWord -> asmgen.out("${variable.name}\t.sint ?")
dt.isLong -> asmgen.out("${variable.name}\t.dint ?")
dt.isFloat -> asmgen.out("${variable.name}\t.fill ${compTarget.FLOAT_MEM_SIZE}")
dt.isSplitWordArray -> {
dt.isSplitWordArray(compTarget) -> {
alignVar(variable.align)
val numbytesPerHalf = compTarget.memorySize(variable.dt, variable.length!!.toInt()) / 2
asmgen.out("${variable.name}_lsb\t.fill $numbytesPerHalf")
asmgen.out("${variable.name}_msb\t.fill $numbytesPerHalf")
asmgen.out("${variable.name}_lsb\t.fill $numbytesPerHalf ; split word array of ${variable.length} ${variable.dt.elementType()} (lsbs)")
asmgen.out("${variable.name}_msb\t.fill $numbytesPerHalf ; split word array of ${variable.length} ${variable.dt.elementType()} (msbs)")
}
dt.isArray -> {
alignVar(variable.align)
val numbytes = compTarget.memorySize(variable.dt, variable.length!!.toInt())
asmgen.out("${variable.name}\t.fill $numbytes")
val amount = variable.length!!.toInt()
val elementSize = compTarget.memorySize(variable.dt.elementType(), null)
asmgen.out("${variable.name}\t.fill $amount * $elementSize ; array of $amount ${variable.dt.elementType()} ")
}
dt.isPointer -> asmgen.out("${variable.name}\t.word ?") // a pointer is just an uword address
dt.isPointerArray -> {
TODO("pointers are not supported yet for uninitialized array ${variable.astNode?.position}")
}
else -> {
throw AssemblyError("weird dt")
}
@@ -906,7 +899,7 @@ internal class ProgramAndVarsGen(
asmgen.out(" .char " + chunk.joinToString())
}
}
dt.isSplitWordArray -> {
dt.isSplitWordArray(compTarget) -> {
if(dt.elementType().isUnsignedWord || dt.elementType().isPointer) {
val data = makeArrayFillDataUnsigned(dt, value, orNumberOfZeros)
asmgen.out("${varname}_words := ${data.joinToString()}")
@@ -960,6 +953,50 @@ internal class ProgramAndVarsGen(
for (f in array.zip(floatFills))
asmgen.out(" .byte ${f.second} ; float ${f.first}")
}
dt.base==BaseDataType.ARRAY && dt.sub==BaseDataType.STRUCT_INSTANCE -> {
val struct = dt.subType as? StStruct ?: throw AssemblyError("struct type not found for $varname")
val structSize = struct.size.toInt()
if(value==null) {
val totalBytes = (orNumberOfZeros ?: 0) * structSize
if(totalBytes==0) {
asmgen.out("$varname: .byte 0")
} else {
asmgen.out("$varname: .fill $totalBytes, 0")
}
} else {
// initialized struct array: value contains StructInstance elements
asmgen.out("$varname ; array of ${value.size} ${struct.name} structs")
// expand struct field types for per-element handling
val expandedFieldTypes = struct.fields.flatMap { field ->
val arraySz = field.arraySize
if(arraySz!=null) List(arraySz) { field.type } else listOf(field.type)
}
for(elt in value) {
val instanceName = (elt as? StArrayElement.StructInstance)?.name ?: throw AssemblyError("expected struct instance in array init for $varname")
val instance = symboltable.lookup(instanceName) as? StStructInstance ?: throw AssemblyError("struct instance $instanceName not found")
val fieldValues = instance.initialValues
require(fieldValues.size==expandedFieldTypes.size) { "field count mismatch for $instanceName" }
for((fieldType, fieldVal) in expandedFieldTypes.zip(fieldValues)) {
val num = when(fieldVal) {
is StArrayElement.Number -> fieldVal.value
is StArrayElement.BoolValue -> if(fieldVal.value) 1.0 else 0.0
else -> throw AssemblyError("struct field value must be number for $varname (got $fieldVal)")
}
when {
fieldType.isByteOrBool -> asmgen.out(" .byte ${num.toInt()}")
fieldType.isWord -> asmgen.out(" .word ${num.toInt()}")
fieldType.isLong -> asmgen.out(" .dword ${num.toInt()}")
fieldType.isFloat -> {
val bytes = compTarget.getFloatAsmBytes(num)
asmgen.out(" .byte $bytes ; float $num")
}
fieldType.isPointer -> asmgen.out(" .word ${num.toInt()} ; pointer")
else -> throw AssemblyError("unsupported struct field type $fieldType")
}
}
}
}
}
else -> throw AssemblyError("require array dt")
}
}
@@ -975,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?")
}
}
}
@@ -1031,7 +1068,7 @@ internal class ProgramAndVarsGen(
is StArrayElement.Number -> "$" + it.value.toInt().toString(16).padStart(4, '0')
is StArrayElement.AddressOf -> {
val symbol = symboltable.lookup(it.symbol)!!
if(symbol is StStaticVariable && symbol.dt.isSplitWordArray)
if(symbol is StStaticVariable && symbol.dt.isSplitWordArray(compTarget))
asmgen.asmSymbolName(it.symbol+"_lsb")
else
asmgen.asmSymbolName(it.symbol)
@@ -1072,7 +1109,7 @@ internal class ProgramAndVarsGen(
is StArrayElement.Number -> "$" + it.value.toInt().toString(16).padStart(4, '0')
is StArrayElement.AddressOf -> {
val symbol = symboltable.lookup(it.symbol)!!
if(symbol is StStaticVariable && symbol.dt.isSplitWordArray)
if(symbol is StStaticVariable && symbol.dt.isSplitWordArray(compTarget))
asmgen.asmSymbolName(it.symbol+"_lsb")
else
asmgen.asmSymbolName(it.symbol)
@@ -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
@@ -3,7 +3,6 @@ package prog8.codegen.cpu6502.assignment
import prog8.code.StConstant
import prog8.code.ast.*
import prog8.code.core.*
import prog8.code.core.BaseDataType
import prog8.codegen.cpu6502.AsmGen6502Internal
@@ -237,7 +236,7 @@ internal class AsmAssignSource(val kind: SourceStorageKind,
if(sub is PtSub && sub.signature.returns.size>1)
DataType.UNDEFINED // TODO list of types instead?
else
sub.returnsWhatWhere().firstOrNull { rr -> rr.first.registerOrPair != null || rr.first.statusflag!=null }?.second
sub.returnsWhatWhere(asmgen.options.compTarget).firstOrNull { rr -> rr.first.registerOrPair != null || rr.first.statusflag!=null }?.second
?: throw AssemblyError("can't translate zero return values in assignment")
AsmAssignSource(SourceStorageKind.EXPRESSION, program, asmgen, returnType, expression = value)
}
File diff suppressed because it is too large. Load diff
@@ -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,13 +703,47 @@ 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 */ }
}
}
internal fun inplacemodificationLongWithExpression(targetVar: String, operator: String, value: PtExpression) {
// Adding an unsigned byte to a long: skip zero-extending the byte into a
// full 4-byte long and just add it with carry propagation through the upper bytes.
// The byte shows up here as a (widened) long-typed cast of an ubyte expression,
// so detect it via the cast child type.
if(operator=="+" && value is PtTypeCast && value.value.type==DataType.UBYTE) {
assignmentAsmGen.assignExpressionToRegister(value.value, RegisterOrPair.A, false)
asmgen.out("""
clc
adc $targetVar+0
sta $targetVar+0
lda $targetVar+1
adc #0
sta $targetVar+1
lda $targetVar+2
adc #0
sta $targetVar+2
lda $targetVar+3
adc #0
sta $targetVar+3
""")
return
}
// it's not an expression so no need to preserve R14-R15
if(value.type.isSigned) {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R14R15, true)
@@ -988,8 +1114,12 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
}
else -> {
asmgen.out("""
lda #0
sta $variable
lda $variable+3
bpl +
lda #255
bne ++
+ lda #0
+ sta $variable
sta $variable+1
sta $variable+2
sta $variable+3""")
@@ -1727,7 +1857,7 @@ $shortcutLabel:""")
// use the already existing optimized codegen for regular assignments x += array[index]
val binexpr = PtBinaryExpression(operator, dt, value.position)
binexpr.add(PtIdentifier(name, dt, value.position))
val arrayValue = PtArrayIndexer(value.type, value.position)
val arrayValue = PtArrayIndexer(value.type, valueVar.type.isSplitWordArray(program.memsizer), value.position)
arrayValue.add(valueVar)
arrayValue.add(value.index)
binexpr.add(arrayValue)
@@ -2117,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")
}
@@ -2428,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
@@ -2443,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
File diff suppressed because it is too large. Load diff
@@ -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,23 +216,33 @@ 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("""
ldy #$offset
asmgen.out(
$$"""
ldy #$$offset
iny
lda ($zpPtrVar),y
eor #${'$'}80
sta ($zpPtrVar),y""")
lda ($$zpPtrVar),y
eor #$80
sta ($$zpPtrVar),y"""
)
}
/**
* 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)
@@ -285,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()
}
}
}
@@ -331,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
@@ -381,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)
@@ -944,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) {
@@ -975,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
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,860 @@
package prog8.codegen.cpu6502.assignment
import prog8.code.ast.*
import prog8.code.core.*
import prog8.codegen.cpu6502.AsmGen6502Internal
internal class TypeCastAssignmentsGen(
private val program: PtProgram,
private val asmgen: AsmGen6502Internal,
private val pointergen: PointerAssignmentsGen
) {
lateinit var assignmentAsmGen: AssignmentAsmGen
internal fun assignTypeCastedValue(target: AsmAssignTarget, targetDt: DataType, value: PtExpression, origTypeCastExpression: PtTypeCast) {
val valueDt = value.type
if(valueDt==targetDt)
throw AssemblyError("type cast to identical dt should have been removed: $valueDt at ${value.position}")
when(value) {
is PtIdentifier -> {
if(targetDt.isWord) {
if(valueDt.isUnsignedByte || valueDt.isBool) {
assignmentAsmGen.assignVariableUByteIntoWord(target, value)
return
}
if(valueDt.isSignedByte) {
assignmentAsmGen.assignVariableByteIntoWord(target, value)
return
}
}
}
is PtMemoryByte -> {
if(targetDt.isWord) {
fun assignViaExprEval(addressExpression: PtExpression) {
asmgen.assignExpressionToVariable(addressExpression, "P8ZP_SCRATCH_W2", DataType.UWORD)
asmgen.loadAFromZpPointerVar("P8ZP_SCRATCH_W2")
asmgen.out(" ldy #0")
assignmentAsmGen.assignRegisterpairWord(target, RegisterOrPair.AY)
}
when (value.address) {
is PtNumber -> {
val address = (value.address as PtNumber).number.toUInt()
assignmentAsmGen.assignMemoryByteIntoWord(target, address, null)
}
is PtIdentifier -> {
assignmentAsmGen.assignMemoryByteIntoWord(target, null, value.address as PtIdentifier)
}
is PtBinaryExpression -> {
val addrExpr = value.address as PtBinaryExpression
if(asmgen.tryOptimizedPointerAccessWithA(addrExpr, false)) {
asmgen.out(" ldy #0")
assignmentAsmGen.assignRegisterpairWord(target, RegisterOrPair.AY)
} else {
assignViaExprEval(value.address)
}
}
else -> {
assignViaExprEval(value.address)
}
}
return
}
}
is PtNumber -> {
if(targetDt.isPointer) {
// assign a number to a pointer type
require(valueDt.isInteger)
return assignmentAsmGen.assignConstantWord(target, value.number.toInt())
}
else
throw AssemblyError("literal value cast should have been const-folded away (target type=$targetDt)")
}
is PtBool -> throw AssemblyError("literal value cast should have been const-folded away (target type=$targetDt)")
is PtArrayIndexer -> {
if(targetDt.isByte && valueDt.isWord) {
// just assign the lsb from the array value
return assignCastWordViaLsbFunc(value, target)
} else if(targetDt.isBool && valueDt.isWord) {
return assignWordToBool(value, target)
}
}
else -> {}
}
// special case optimizations
if(target.kind == TargetStorageKind.VARIABLE) {
if(value is PtIdentifier && !valueDt.isUndefined)
return assignTypeCastedIdentifier(target.asmVarname, targetDt, asmgen.asmVariableName(value), valueDt)
when {
valueDt.isBool -> {
if(targetDt.isInteger) {
// optimization to assign boolean expression to integer target (just assign the 0 or 1 directly)
val assignDirect = AsmAssignment(
AsmAssignSource.fromAstSource(value, program, asmgen),
listOf(target),
program.memsizer,
target.position
)
assignmentAsmGen.assignExpression(assignDirect, target.scope)
} else {
TODO("assign bool to non-integer type $targetDt ${value.position}")
}
}
valueDt.isByte -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.A, valueDt.isSigned)
assignTypeCastedRegisters(target.asmVarname, targetDt.base, RegisterOrPair.A, valueDt.base, target.position)
}
valueDt.isLong -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R14R15, valueDt.isSigned)
assignTypeCastedRegisters(target.asmVarname, targetDt.base, RegisterOrPair.R14R15, valueDt.base, target.position)
}
valueDt.isWord || valueDt.isPointer -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.AY, valueDt.isSigned)
assignTypeCastedRegisters(target.asmVarname, targetDt.base, RegisterOrPair.AY, valueDt.base, target.position)
}
valueDt.isFloat -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.FAC1, true)
assignTypeCastedFloatFAC1(target.asmVarname, targetDt.base)
}
valueDt.isPassByRef -> {
// str/array value cast (most likely to UWORD, take address-of)
assignmentAsmGen.assignExpressionToVariable(value, target.asmVarname, targetDt)
}
else -> throw AssemblyError("strange dt in typecast assign to var: $valueDt --> $targetDt")
}
return
}
if(valueDt.isWord && origTypeCastExpression.type.isUnsignedByte) {
val parentTc = origTypeCastExpression.parent as? PtTypeCast
if(parentTc!=null && parentTc.type.isUnsignedWord) {
// typecast a word value to ubyte and directly back to uword
// generate code for lsb(value) here instead of the ubyte typecast
return assignCastWordViaLsbFunc(value, target)
}
}
if(valueDt.isByteOrBool) {
when(target.register) {
RegisterOrPair.A,
RegisterOrPair.X,
RegisterOrPair.Y -> {
// 'cast' an ubyte value to a byte register; no cast needed at all
return assignmentAsmGen.assignExpressionToRegister(value, target.register, valueDt.isSigned)
}
RegisterOrPair.AX,
RegisterOrPair.AY,
RegisterOrPair.XY,
in Cx16VirtualRegisters -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.A, false)
assignmentAsmGen.assignRegisterByte(target, CpuRegister.A, valueDt.isSigned, true)
return
}
in CombinedLongRegisters -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.A, false)
assignmentAsmGen.assignRegisterByte(target, CpuRegister.A, valueDt.isSigned, true)
return
}
else -> {}
}
} else if(valueDt.isWord) {
when(target.register) {
RegisterOrPair.A,
RegisterOrPair.X,
RegisterOrPair.Y -> {
return if(targetDt.isBool)
assignWordToBool(value, target)
else
// cast an uword to a byte register, do this via lsb(value)
// generate code for lsb(value) here instead of the ubyte typecast
assignCastWordViaLsbFunc(value, target)
}
RegisterOrPair.AX,
RegisterOrPair.AY,
RegisterOrPair.XY,
in Cx16VirtualRegisters -> {
// 'cast' uword into a 16 bits register, just assign it
return assignmentAsmGen.assignExpressionToRegister(value, target.register!!, targetDt.isSigned)
}
RegisterOrPair.R0R1 -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R0, false)
return asmgen.signExtendLongVariable("cx16.r0", valueDt.base)
}
RegisterOrPair.R2R3 -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R2, false)
return asmgen.signExtendLongVariable("cx16.r2", valueDt.base)
}
RegisterOrPair.R4R5 -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R4, false)
return asmgen.signExtendLongVariable("cx16.r4", valueDt.base)
}
RegisterOrPair.R6R7 -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R6, false)
return asmgen.signExtendLongVariable("cx16.r6", valueDt.base)
}
RegisterOrPair.R8R9 -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R8, false)
return asmgen.signExtendLongVariable("cx16.r8", valueDt.base)
}
RegisterOrPair.R10R11 -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R10, false)
return asmgen.signExtendLongVariable("cx16.r10", valueDt.base)
}
RegisterOrPair.R12R13 -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R12, false)
return asmgen.signExtendLongVariable("cx16.r12", valueDt.base)
}
RegisterOrPair.R14R15 -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R14, false)
return asmgen.signExtendLongVariable("cx16.r14", valueDt.base)
}
else -> {}
}
}
if(target.kind==TargetStorageKind.REGISTER) {
if(valueDt.isFloat && !target.datatype.isFloat) {
// have to typecast the float number on the fly down to an integer
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.FAC1, targetDt.isSigned)
assignTypeCastedFloatFAC1("P8ZP_SCRATCH_W1", targetDt.base)
assignmentAsmGen.assignVariableToRegister("P8ZP_SCRATCH_W1", target.register!!, targetDt.isSigned, origTypeCastExpression.definingISub(), target.position)
return
} else {
if(!(valueDt isAssignableTo targetDt)) {
return if(valueDt.isWord && targetDt.isByte) {
// word to byte, just take the lsb
assignCastWordViaLsbFunc(value, target)
} else if(valueDt.isWord && targetDt.isBool) {
// word to bool
assignWordToBool(value, target)
} else if(valueDt.isWord && targetDt.isWord) {
// word to word, just assign
assignmentAsmGen.assignExpressionToRegister(value, target.register!!, valueDt.isSigned)
} else if(valueDt.isByteOrBool && targetDt.isByte) {
// byte to byte, just assign
assignmentAsmGen.assignExpressionToRegister(value, target.register!!, valueDt.isSigned)
} else if(valueDt.isByteOrBool && targetDt.isWord) {
// byte to word, just assign
assignmentAsmGen.assignExpressionToRegister(value, target.register!!, valueDt.isSigned)
} else if(valueDt.isLong && targetDt.isByte) {
// long to byte, just take the lsb
assignCastLongToByte(value, target)
} else if(valueDt.isLong && targetDt.isWord) {
// long to word, just take the lsw
assignCastViaLswFunc(value, target)
} else if(valueDt.isPointer && targetDt.isPointer) {
// pointer type A to pointer type B, just assign
assignmentAsmGen.assignExpressionToRegister(value, target.register!!, valueDt.isSigned)
} else
throw AssemblyError("can't cast $valueDt to $targetDt, this should have been checked in the astchecker ${value.position}")
}
}
}
if(targetDt.isInteger && valueDt.isByteOrBool && valueDt.isAssignableTo(targetDt)) {
require(targetDt.isWord || targetDt.isLong) {
"should be byte to word or long assignment ${origTypeCastExpression.position}"
}
when(target.kind) {
// TargetStorageKind.VARIABLE -> {
// This has been handled already earlier on line 961.
// // byte to word, just assign to registers first, then assign to variable
// assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.AY, targetDt==BaseDataType.WORD)
// assignTypeCastedRegisters(target.asmVarname, targetDt, RegisterOrPair.AY, targetDt)
// return
// }
TargetStorageKind.ARRAY -> {
// byte to word, just assign to registers first, then assign into array
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.AY, targetDt.isSigned)
val deref = target.array!!.pointerderef
if(deref!=null)
pointergen.assignWordReg(IndexedPtrTarget(target), RegisterOrPair.AY)
else
assignmentAsmGen.assignRegisterpairWord(target, RegisterOrPair.AY)
return
}
TargetStorageKind.REGISTER -> {
// byte to word or long, just assign to registers
assignmentAsmGen.assignExpressionToRegister(value, target.register!!, targetDt.isSigned)
return
}
TargetStorageKind.POINTER -> {
pointergen.assignByteToWord(PtrTarget(target), value)
return
}
else -> throw AssemblyError("weird target at ${target.position}")
}
}
if(targetDt.isFloat && (target.register==RegisterOrPair.FAC1 || target.register==RegisterOrPair.FAC2)) {
if(target.register==RegisterOrPair.FAC2)
asmgen.pushFAC1()
when {
valueDt.isUnsignedByte -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.Y, false)
asmgen.out(" jsr floats.FREADUY")
}
valueDt.isSignedByte -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.A, true)
asmgen.out(" jsr floats.FREADSA")
}
valueDt.isUnsignedWord -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.AY, false)
asmgen.out(" jsr floats.GIVUAYFAY")
}
valueDt.isSignedWord -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.AY, true)
asmgen.out(" jsr floats.GIVAYFAY")
}
valueDt.isLong -> {
when(value) {
is PtIdentifier -> {
val varname = asmgen.asmVariableName(value)
asmgen.out("""
lda #<$varname
ldy #>$varname
jsr floats.internal_long_AY_to_FAC""")
}
else -> {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R14R15, true)
asmgen.out("""
lda #<cx16.r14
ldy #>cx16.r14
jsr floats.internal_long_AY_to_FAC""")
}
}
}
else -> throw AssemblyError("invalid dt at ${target.position}")
}
if(target.register==RegisterOrPair.FAC2) {
asmgen.out(" jsr floats.MOVEF")
asmgen.popFAC1()
}
return
}
if(targetDt.isUnsignedWord && valueDt.isPointer) {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.AY, false)
assignmentAsmGen.assignRegisterpairWord(target, RegisterOrPair.AY)
return
}
// No more special optimized cases yet. Do the rest via more complex evaluation
// note: cannot use assignTypeCastedValue because that is ourselves :P
// NOTE: THIS MAY TURN INTO A STACK OVERFLOW ERROR IF IT CAN'T SIMPLIFY THE TYPECAST..... :-/
asmgen.assignExpressionTo(origTypeCastExpression, target)
}
internal fun assignCastLongToByte(value: PtExpression, target: AsmAssignTarget) {
// long to byte, can't use lsb() because that only works on words
when(value) {
is PtIdentifier -> {
val longvar = asmgen.asmVariableName(value)
asmgen.out(" lda $longvar")
assignmentAsmGen.assignRegisterByte(target, CpuRegister.A, true, false)
}
is PtNumber -> throw AssemblyError("casting a long number to byte should have been const-folded away ${value.position}")
else -> {
asmgen.pushLongRegisters(RegisterOrPair.R14R15, 1)
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R14R15, true)
asmgen.out(" lda cx16.r14")
assignmentAsmGen.assignRegisterByte(target, CpuRegister.A, true, false)
asmgen.popLongRegisters(RegisterOrPair.R14R15, 1)
}
}
}
internal fun assignCastWordViaLsbFunc(value: PtExpression, target: AsmAssignTarget) {
val lsb = PtFunctionCall("lsb", true, true,arrayOf(DataType.UBYTE), value.position)
lsb.parent = value.parent
lsb.add(value)
val src = AsmAssignSource(SourceStorageKind.EXPRESSION, program, asmgen, DataType.UBYTE, expression = lsb)
val assign = AsmAssignment(src, listOf(target), program.memsizer, value.position)
assignmentAsmGen.translateNormalAssignment(assign, value.definingISub())
}
internal fun assignCastViaLswFunc(value: PtExpression, target: AsmAssignTarget) {
val lsb = PtFunctionCall("lsw", true, true, arrayOf(DataType.UWORD), value.position)
lsb.parent = value.parent
lsb.add(value)
val src = AsmAssignSource(SourceStorageKind.EXPRESSION, program, asmgen, DataType.UWORD, expression = lsb)
val assign = AsmAssignment(src, listOf(target), program.memsizer, value.position)
assignmentAsmGen.translateNormalAssignment(assign, value.definingISub())
}
internal fun assignWordToBool(value: PtExpression, target: AsmAssignTarget) {
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.AY, false)
asmgen.out("""
sty P8ZP_SCRATCH_REG
ora P8ZP_SCRATCH_REG
beq +
lda #1
+""")
assignmentAsmGen.assignRegisterByte(target, CpuRegister.A, false, false)
}
internal fun assignTypeCastedFloatFAC1(targetAsmVarName: String, targetDt: BaseDataType) {
if(targetDt==BaseDataType.FLOAT)
throw AssemblyError("typecast to identical type")
when(targetDt) {
BaseDataType.BOOL -> asmgen.out(" jsr floats.cast_FAC1_as_bool_into_a | sta $targetAsmVarName")
BaseDataType.UBYTE -> asmgen.out(" jsr floats.cast_FAC1_as_uw_into_ya | sty $targetAsmVarName")
BaseDataType.BYTE -> asmgen.out(" jsr floats.cast_FAC1_as_w_into_ay | sta $targetAsmVarName")
BaseDataType.UWORD -> asmgen.out(" jsr floats.cast_FAC1_as_uw_into_ya | sty $targetAsmVarName | sta $targetAsmVarName+1")
BaseDataType.WORD -> asmgen.out(" jsr floats.cast_FAC1_as_w_into_ay | sta $targetAsmVarName | sty $targetAsmVarName+1")
BaseDataType.LONG -> {
asmgen.out("""
jsr floats.QINT
lda floats.FAC_ADDR+4
sta $targetAsmVarName
lda floats.FAC_ADDR+3
sta $targetAsmVarName+1
lda floats.FAC_ADDR+2
sta $targetAsmVarName+2
lda floats.FAC_ADDR+1
sta $targetAsmVarName+3
""".trimIndent())
}
else -> throw AssemblyError("weird type $targetDt")
}
}
internal fun assignTypeCastedIdentifier(targetAsmVarName: String, targetDt: DataType,
sourceAsmVarName: String, sourceDt: DataType) {
if(sourceDt == targetDt)
throw AssemblyError("typecast to identical type")
// also see: PtExpressionAsmGen, fun translateExpression(typecast: PtTypeCast)
when {
sourceDt.isUnsignedByte || sourceDt.isBool -> {
when(targetDt.base) {
BaseDataType.BOOL -> {
asmgen.out("""
lda $sourceAsmVarName
beq +
lda #1
+ sta $targetAsmVarName""")
}
BaseDataType.UBYTE, BaseDataType.BYTE -> {
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName")
}
BaseDataType.UWORD, BaseDataType.WORD -> {
if(asmgen.isTargetCpu(CpuType.CPU65C02))
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName | stz $targetAsmVarName+1")
else
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName | lda #0 | sta $targetAsmVarName+1")
}
BaseDataType.POINTER -> TODO("byte should not be cast to pointer")
BaseDataType.LONG -> {
asmgen.out("""
lda $sourceAsmVarName
sta $targetAsmVarName
lda #0
sta $targetAsmVarName+1
sta $targetAsmVarName+2
sta $targetAsmVarName+3""")
}
BaseDataType.FLOAT -> {
asmgen.out("""
lda #<$targetAsmVarName
ldy #>$targetAsmVarName
sta P8ZP_SCRATCH_W2
sty P8ZP_SCRATCH_W2+1
ldy $sourceAsmVarName
jsr floats.cast_from_ub""")
}
else -> throw AssemblyError("weird type $targetDt")
}
}
sourceDt.isSignedByte -> {
when(targetDt.base) {
BaseDataType.UBYTE, BaseDataType.BOOL -> {
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName")
}
BaseDataType.UWORD -> {
if(asmgen.isTargetCpu(CpuType.CPU65C02))
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName | stz $targetAsmVarName+1")
else
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName | lda #0 | sta $targetAsmVarName+1")
}
BaseDataType.WORD -> {
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName")
asmgen.signExtendVariableLsb(targetAsmVarName, BaseDataType.BYTE)
}
BaseDataType.POINTER -> TODO("byte should not be cast to pointer")
BaseDataType.LONG -> {
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName")
asmgen.signExtendVariableLsb(targetAsmVarName, BaseDataType.BYTE)
asmgen.signExtendLongVariable(targetAsmVarName, BaseDataType.WORD)
}
BaseDataType.FLOAT -> {
asmgen.out("""
lda #<$targetAsmVarName
ldy #>$targetAsmVarName
sta P8ZP_SCRATCH_W2
sty P8ZP_SCRATCH_W2+1
lda $sourceAsmVarName
jsr floats.cast_from_b""")
}
else -> throw AssemblyError("weird type")
}
}
sourceDt.isUnsignedWord -> {
when(targetDt.base) {
BaseDataType.BOOL -> {
asmgen.out("""
lda $sourceAsmVarName
ora $sourceAsmVarName+1
beq +
lda #1
+ sta $targetAsmVarName""")
}
BaseDataType.BYTE, BaseDataType.UBYTE -> {
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName")
}
BaseDataType.WORD -> {
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName | lda $sourceAsmVarName+1 | sta $targetAsmVarName+1")
}
BaseDataType.POINTER -> asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName | lda $sourceAsmVarName+1 | sta $targetAsmVarName+1")
BaseDataType.LONG -> {
asmgen.out("""
lda $sourceAsmVarName
sta $targetAsmVarName
lda $sourceAsmVarName+1
sta $targetAsmVarName+1
lda #0
sta $targetAsmVarName+2
sta $targetAsmVarName+3""")
}
BaseDataType.FLOAT -> {
asmgen.out("""
lda #<$targetAsmVarName
ldy #>$targetAsmVarName
sta P8ZP_SCRATCH_W2
sty P8ZP_SCRATCH_W2+1
lda $sourceAsmVarName
ldy $sourceAsmVarName+1
jsr floats.cast_from_uw""")
}
else -> throw AssemblyError("weird type")
}
}
sourceDt.isSignedWord || sourceDt.isPointer -> {
when(targetDt.base) {
BaseDataType.BOOL -> {
asmgen.out("""
lda $sourceAsmVarName
ora $sourceAsmVarName+1
beq +
lda #1
+ sta $targetAsmVarName""")
}
BaseDataType.BYTE, BaseDataType.UBYTE -> {
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName")
}
BaseDataType.UWORD, BaseDataType.POINTER -> {
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName | lda $sourceAsmVarName+1 | sta $targetAsmVarName+1")
}
BaseDataType.LONG -> {
asmgen.out("""
lda $sourceAsmVarName
sta $targetAsmVarName
lda $sourceAsmVarName+1
sta $targetAsmVarName+1""")
asmgen.signExtendLongVariable(targetAsmVarName, BaseDataType.WORD)
}
BaseDataType.FLOAT -> {
asmgen.out("""
lda #<$targetAsmVarName
ldy #>$targetAsmVarName
sta P8ZP_SCRATCH_W2
sty P8ZP_SCRATCH_W2+1
lda $sourceAsmVarName
ldy $sourceAsmVarName+1
jsr floats.cast_from_w""")
}
else -> throw AssemblyError("weird type")
}
}
sourceDt.isFloat -> {
asmgen.out(" lda #<$sourceAsmVarName | ldy #>$sourceAsmVarName")
when(targetDt.base) {
BaseDataType.BOOL -> asmgen.out(" jsr floats.cast_as_bool_into_a | sta $targetAsmVarName")
BaseDataType.UBYTE -> asmgen.out(" jsr floats.cast_as_uw_into_ya | sty $targetAsmVarName")
BaseDataType.BYTE -> asmgen.out(" jsr floats.cast_as_w_into_ay | sta $targetAsmVarName")
BaseDataType.UWORD -> asmgen.out(" jsr floats.cast_as_uw_into_ya | sty $targetAsmVarName | sta $targetAsmVarName+1")
BaseDataType.WORD -> asmgen.out(" jsr floats.cast_as_w_into_ay | sta $targetAsmVarName | sty $targetAsmVarName+1")
BaseDataType.LONG -> {
asmgen.out("""
sta cx16.r0L
sty cx16.r0H
lda #<$targetAsmVarName
ldy #>$targetAsmVarName
jsr floats.cast_as_long""")
}
else -> throw AssemblyError("weird type")
}
}
sourceDt.isString -> throw AssemblyError("cannot typecast a string value")
sourceDt.isLong -> {
if(targetDt.isByteOrBool) {
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName")
} else if(targetDt.isWord || targetDt.isPointer) {
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName | lda $sourceAsmVarName+1 | sta $targetAsmVarName+1")
} else if(targetDt.isFloat) {
asmgen.out("""
lda #<$sourceAsmVarName
ldy #>$sourceAsmVarName
sta cx16.r1L
sty cx16.r1H
lda #<$targetAsmVarName
ldy #>$targetAsmVarName
jsr floats.internal_long_R1_to_float_AY""")
} else
throw AssemblyError("weird type")
}
else -> throw AssemblyError("weird type")
}
}
internal fun assignTypeCastedRegisters(targetAsmVarName: String, targetDt: BaseDataType,
regs: RegisterOrPair, sourceDt: BaseDataType, position: Position) {
if(sourceDt == targetDt && sourceDt != BaseDataType.POINTER)
throw AssemblyError("typecast to identical type")
// also see: PtExpressionAsmGen, fun translateExpression(typecast: PtTypeCast)
when(sourceDt) {
BaseDataType.BOOL -> {
if (targetDt.isByteOrBool) asmgen.out(" st${regs.toString().lowercase()} $targetAsmVarName")
else throw AssemblyError("assign bool to non-byte variable")
}
BaseDataType.UBYTE -> {
when(targetDt) {
BaseDataType.BOOL -> {
val compare = if(regs==RegisterOrPair.A) "cmp" else "cp${regs.toString().lowercase()}"
asmgen.out("""
$compare #0
beq +
ld${regs.toString().lowercase()} #1
+ st${regs.toString().lowercase()} $targetAsmVarName""")
}
BaseDataType.BYTE -> {
asmgen.out(" st${regs.toString().lowercase()} $targetAsmVarName")
}
BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.POINTER -> {
if(asmgen.isTargetCpu(CpuType.CPU65C02))
asmgen.out(" st${regs.toString().lowercase()} $targetAsmVarName | stz $targetAsmVarName+1")
else
asmgen.out(" st${regs.toString().lowercase()} $targetAsmVarName | lda #0 | sta $targetAsmVarName+1")
}
BaseDataType.LONG -> {
asmgen.out("""
st${regs.toString().lowercase()} $targetAsmVarName
lda #0
sta $targetAsmVarName+1
sta $targetAsmVarName+2
sta $targetAsmVarName+3""")
}
BaseDataType.FLOAT -> {
when(regs) {
RegisterOrPair.A -> asmgen.out(" tay")
RegisterOrPair.X -> asmgen.out(" txa | tay")
RegisterOrPair.Y -> {}
else -> throw AssemblyError("non-byte regs")
}
asmgen.out("""
lda #<$targetAsmVarName
sta P8ZP_SCRATCH_W2
lda #>$targetAsmVarName
sta P8ZP_SCRATCH_W2+1
jsr floats.cast_from_ub""")
}
else -> throw AssemblyError("weird type")
}
}
BaseDataType.BYTE -> {
when(targetDt) {
BaseDataType.BOOL -> TODO("assign byte to bool $position")
BaseDataType.UBYTE -> {
asmgen.out(" st${regs.toString().lowercase()} $targetAsmVarName")
}
BaseDataType.UWORD -> {
if(asmgen.isTargetCpu(CpuType.CPU65C02))
asmgen.out(" st${regs.toString().lowercase()} $targetAsmVarName | stz $targetAsmVarName+1")
else
asmgen.out(" st${regs.toString().lowercase()} $targetAsmVarName | lda #0 | sta $targetAsmVarName+1")
}
BaseDataType.WORD, BaseDataType.POINTER -> {
when(regs) {
RegisterOrPair.A -> {}
RegisterOrPair.X -> asmgen.out(" txa")
RegisterOrPair.Y -> asmgen.out(" tya")
else -> throw AssemblyError("non-byte regs")
}
asmgen.signExtendAYlsb(sourceDt)
asmgen.out(" sta $targetAsmVarName | sty $targetAsmVarName+1")
}
BaseDataType.LONG -> {
asmgen.out(" st${regs.toString().lowercase()} $targetAsmVarName")
asmgen.signExtendLongVariable(targetAsmVarName, sourceDt)
}
BaseDataType.FLOAT -> {
when(regs) {
RegisterOrPair.A -> {}
RegisterOrPair.X -> asmgen.out(" txa")
RegisterOrPair.Y -> asmgen.out(" tya")
else -> throw AssemblyError("non-byte regs")
}
asmgen.out("""
ldy #<$targetAsmVarName
sty P8ZP_SCRATCH_W2
ldy #>$targetAsmVarName
sty P8ZP_SCRATCH_W2+1
jsr floats.cast_from_b""")
}
else -> throw AssemblyError("weird type")
}
}
BaseDataType.UWORD -> {
when(targetDt) {
BaseDataType.BOOL -> TODO("assign uword to bool $position")
BaseDataType.BYTE, BaseDataType.UBYTE -> {
asmgen.out(" st${regs.toString().lowercase().first()} $targetAsmVarName")
}
BaseDataType.WORD, BaseDataType.POINTER -> {
when(regs) {
RegisterOrPair.AX -> asmgen.out(" sta $targetAsmVarName | stx $targetAsmVarName+1")
RegisterOrPair.AY -> asmgen.out(" sta $targetAsmVarName | sty $targetAsmVarName+1")
RegisterOrPair.XY -> asmgen.out(" stx $targetAsmVarName | sty $targetAsmVarName+1")
else -> throw AssemblyError("non-word regs")
}
}
BaseDataType.LONG -> {
when(regs) {
RegisterOrPair.AX -> asmgen.out(" sta $targetAsmVarName | stx $targetAsmVarName+1")
RegisterOrPair.AY -> asmgen.out(" sta $targetAsmVarName | sty $targetAsmVarName+1")
RegisterOrPair.XY -> asmgen.out(" stx $targetAsmVarName | sty $targetAsmVarName+1")
else -> throw AssemblyError("non-word regs")
}
asmgen.out(" lda #0 | sta $targetAsmVarName+2 | sta $targetAsmVarName+3")
}
BaseDataType.FLOAT -> {
if(regs!=RegisterOrPair.AY)
throw AssemblyError("only supports AY here")
asmgen.out("""
tax
lda #<$targetAsmVarName
sta P8ZP_SCRATCH_W2
lda #>$targetAsmVarName
sta P8ZP_SCRATCH_W2+1
txa
jsr floats.cast_from_uw""")
}
else -> throw AssemblyError("weird type")
}
}
BaseDataType.WORD -> {
when(targetDt) {
BaseDataType.BOOL -> TODO("assign word to bool $position")
BaseDataType.BYTE, BaseDataType.UBYTE -> {
asmgen.out(" st${regs.toString().lowercase().first()} $targetAsmVarName")
}
BaseDataType.UWORD, BaseDataType.POINTER -> {
when(regs) {
RegisterOrPair.AX -> asmgen.out(" sta $targetAsmVarName | stx $targetAsmVarName+1")
RegisterOrPair.AY -> asmgen.out(" sta $targetAsmVarName | sty $targetAsmVarName+1")
RegisterOrPair.XY -> asmgen.out(" stx $targetAsmVarName | sty $targetAsmVarName+1")
else -> throw AssemblyError("non-word regs")
}
}
BaseDataType.LONG -> {
when(regs) {
RegisterOrPair.AX -> asmgen.out(" sta $targetAsmVarName | stx $targetAsmVarName+1")
RegisterOrPair.AY -> asmgen.out(" sta $targetAsmVarName | sty $targetAsmVarName+1")
RegisterOrPair.XY -> asmgen.out(" stx $targetAsmVarName | sty $targetAsmVarName+1")
else -> throw AssemblyError("non-word regs")
}
asmgen.signExtendLongVariable(targetAsmVarName, BaseDataType.WORD)
}
BaseDataType.FLOAT -> {
if(regs!=RegisterOrPair.AY)
throw AssemblyError("only supports AY here")
asmgen.out("""
tax
lda #<$targetAsmVarName
sta P8ZP_SCRATCH_W2
lda #>$targetAsmVarName
sta P8ZP_SCRATCH_W2+1
txa
jsr floats.cast_from_w""")
}
else -> throw AssemblyError("weird type")
}
}
BaseDataType.STR -> throw AssemblyError("cannot typecast a string value")
BaseDataType.POINTER -> {
if(targetDt.isWord || targetDt.isPointer) {
when(regs) {
RegisterOrPair.AX -> asmgen.out(" sta $targetAsmVarName | stx $targetAsmVarName+1")
RegisterOrPair.AY -> asmgen.out(" sta $targetAsmVarName | sty $targetAsmVarName+1")
RegisterOrPair.XY -> asmgen.out(" stx $targetAsmVarName | sty $targetAsmVarName+1")
else -> throw AssemblyError("non-word regs")
}
} else if(targetDt.isLong) {
when(regs) {
RegisterOrPair.AX -> asmgen.out(" sta $targetAsmVarName | stx $targetAsmVarName+1")
RegisterOrPair.AY -> asmgen.out(" sta $targetAsmVarName | sty $targetAsmVarName+1")
RegisterOrPair.XY -> asmgen.out(" stx $targetAsmVarName | sty $targetAsmVarName+1")
else -> throw AssemblyError("non-word regs")
}
asmgen.out(" lda #0 | sta $targetAsmVarName+2 | sta $targetAsmVarName+3")
} else {
throw AssemblyError("cannot assign pointer to $targetDt")
}
}
BaseDataType.LONG -> {
val startreg = regs.startregname()
if(targetDt.isByteOrBool) {
asmgen.out(" lda cx16.$startreg | sta $targetAsmVarName")
} else if(targetDt.isWord || targetDt.isPointer) {
asmgen.out(" lda cx16.$startreg | sta $targetAsmVarName | lda cx16.$startreg+1 | sta $targetAsmVarName+1")
} else if(targetDt.isFloat) {
if(regs==RegisterOrPair.R0R1)
throw AssemblyError("cannot assign long in R0R1 to float because r1 is used as a parameter $position")
asmgen.out("""
lda #<cx16.$startreg
ldy #>cx16.$startreg
sta cx16.r1L
sty cx16.r1H
lda #<$targetAsmVarName
ldy #>$targetAsmVarName
jsr floats.internal_long_R1_to_float_AY""")
} else
throw AssemblyError("weird type $targetDt")
}
else -> throw AssemblyError("weird type $sourceDt")
}
}
/**
* Compute 8-bit offset Y = idx*structSize + fieldOffset for a struct-array element.
* Assumes totalBytes <=256 so offset fits in Y. Uses A/Y and multiply_bytes when needed.
*/
}
+296 -12
View File
@@ -5,30 +5,41 @@ import io.kotest.matchers.shouldBe
import prog8.code.StMemVar
import prog8.code.SymbolTable
import prog8.code.ast.PtProgram
import prog8.code.core.BaseDataType
import prog8.code.core.DataType
import prog8.code.target.C64Target
import prog8.codegen.cpu6502.*
import prog8tests.helpers.DummyMemsizer
import prog8tests.helpers.DummyStringEncoder
import prog8.code.target.VMTarget
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({
val machine = C64Target()
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val symbolTable = SymbolTable(program)
// Add test variables with known non-IO addresses so the optimizer can resolve them
// Non-IO addresses in typical C64 RAM range
symbolTable.add(StMemVar("myvar", DataType.UBYTE, 0x0300u, null, null))
symbolTable.add(StMemVar("var", DataType.UBYTE, 0x0301u, null, null))
symbolTable.add(StMemVar("var1", DataType.UWORD, 0x0302u, null, null))
symbolTable.add(StMemVar("var2", DataType.UWORD, 0x0304u, null, null))
symbolTable.add(StMemVar("ptr", DataType.UWORD, 0x0306u, null, null))
symbolTable.add(StMemVar("array", DataType.arrayFor(BaseDataType.UBYTE, machine), 0x0302u, 16u, null))
symbolTable.add(StMemVar("wordarray", DataType.arrayFor(BaseDataType.UWORD, machine), 0x0310u, 32u, null))
symbolTable.add(StMemVar("nswordarray", DataType.arrayFor(BaseDataType.UWORD, machine), 0x0320u, 32u, null))
symbolTable.add(StMemVar("var1", DataType.UWORD, 0x0400u, null, null))
symbolTable.add(StMemVar("var2", DataType.UWORD, 0x0402u, null, null))
symbolTable.add(StMemVar("ptr", DataType.UWORD, 0x0404u, null, null))
symbolTable.add(StMemVar("P8ZP_SCRATCH_PTR", DataType.UWORD, 0x02u, null, null))
symbolTable.add(StMemVar("P8ZP_SCRATCH_W1", DataType.UWORD, 0x04u, null, null))
symbolTable.add(StMemVar("A1", DataType.UWORD, 0x0310u, null, null))
symbolTable.add(StMemVar("A2", DataType.UWORD, 0x0312u, null, null))
symbolTable.add(StMemVar("A1", DataType.UWORD, 0x0410u, null, null))
symbolTable.add(StMemVar("A2", DataType.UWORD, 0x0412u, null, null))
fun optimize(lines: MutableList<String>) {
optimizeAssembly(lines, machine, symbolTable)
@@ -161,13 +172,13 @@ class TestAsmOptimizer: FunSpec({
}
test("optimizeJsrRts: three consecutive jsr only tail jsr+rts becomes jmp") {
val lines = mutableListOf(" jsr \$4000", " jsr \$5000", " jsr \$6000", " rts", " nop", " nop")
val lines = mutableListOf($$" jsr $4000", $$" jsr $5000", $$" jsr $6000", " rts", " nop", " nop")
optimize(lines)
lines shouldBe listOf(" jsr \$4000", " jsr \$5000", " jmp \$6000", " nop", " nop")
lines shouldBe listOf($$" jsr $4000", $$" jsr $5000", $$" jmp $6000", " nop", " nop")
}
test("optimizeJsrRts: single line with embedded newlines only tail converted defensively") {
val lines = mutableListOf(" jsr \$4000\n jsr \$5000\n jsr \$6000", " rts", " nop", " nop")
val lines = mutableListOf($$" jsr $4000\n jsr $5000\n jsr $6000", " rts", " nop", " nop")
optimize(lines)
// Before fix this became jmp/jmp/jmp via replace("jsr","jmp"); after fix only one jmp via replaceMnemonic
val jmpCount = lines.joinToString("\n").split("jmp").size - 1
@@ -259,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") {
@@ -284,6 +395,179 @@ class TestAsmOptimizer: FunSpec({
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop")
}
// --- optimizeFoldIndexOffset (byte arrays) ---
test("optimizeFoldIndexOffset: folds byte array lda+clc+adc+tay+lda to ldy+lda") {
val lines = mutableListOf(
" lda myvar", " clc", " adc #3", " tay", " lda array,y",
" rts", " nop", " nop", " nop", " nop"
)
optimize(lines)
lines[0] shouldBe " ldy myvar"
lines[1] shouldBe " lda array+3,y"
lines.size shouldBe 7
}
test("optimizeFoldIndexOffset: folds with x register") {
val lines = mutableListOf(
" lda var", " clc", " adc #5", " tax", " lda array,x",
" rts", " nop", " nop", " nop", " nop"
)
optimize(lines)
lines[0] shouldBe " ldx var"
lines[1] shouldBe " lda array+5,x"
lines.size shouldBe 7
}
test("optimizeFoldIndexOffset: works with sta instead of lda") {
val lines = mutableListOf(
" lda myvar", " clc", " adc #1", " tay", " sta array,y",
" rts", " nop", " nop", " nop", " nop"
)
optimize(lines)
lines[0] shouldBe " ldy myvar"
lines[1] shouldBe " sta array+1,y"
lines.size shouldBe 7
}
test("optimizeFoldIndexOffset: does not fold indirect indexed (ptr),y") {
val lines = mutableListOf(
" lda myvar", " clc", " adc #3", " tay", " lda (ptr),y",
" rts", " nop", " nop", " nop", " nop"
)
optimize(lines)
lines.size shouldBe 10
}
test("optimizeFoldIndexOffset: preserves hex constant format") {
val lines = mutableListOf(
" lda myvar", " clc", " adc #$0f", " tay", " lda array,y",
" rts", " nop", " nop", " nop", " nop"
)
optimize(lines)
lines[0] shouldBe " ldy myvar"
lines[1] shouldBe " lda array+$0f,y"
lines.size shouldBe 7
}
test("optimizeFoldIndexOffset: does not fold when 5th op is not absolute indexed") {
val lines = mutableListOf(
" lda myvar", " clc", " adc #3", " tay", " nop",
" rts", " nop", " nop", " nop", " nop"
)
optimize(lines)
lines.size shouldBe 10
}
test("optimizeFoldIndexOffset: works with cmp instruction") {
val lines = mutableListOf(
" lda myvar", " clc", " adc #7", " tay", " cmp array,y",
" rts", " nop", " nop", " nop", " nop"
)
optimize(lines)
lines[0] shouldBe " ldy myvar"
lines[1] shouldBe " cmp array+7,y"
lines.size shouldBe 7
}
test("optimizeFoldIndexOffset: preserves label on first instruction") {
val lines = mutableListOf(
"mylabel: lda myvar", " clc", " adc #3", " tay", " lda array,y",
" rts", " nop", " nop", " nop", " nop"
)
optimize(lines)
lines[0] shouldBe "mylabel: ldy myvar"
lines[1] shouldBe " lda array+3,y"
lines.size shouldBe 7
}
// --- optimizeFoldIndexOffsetSplitWord (split word arrays) ---
test("optimizeFoldIndexOffsetSplitWord: folds split word array access with y") {
val lines = mutableListOf(
" lda var", " clc", " adc #3", " tay",
" lda wordarray_lsb,y", " ldx wordarray_msb,y",
" rts", " nop", " nop", " nop"
)
optimize(lines)
lines[0] shouldBe " ldy var"
lines[1] shouldBe " lda wordarray_lsb+3,y"
lines[2] shouldBe " ldx wordarray_msb+3,y"
lines.size shouldBe 7
}
test("optimizeFoldIndexOffsetSplitWord: folds split word array access with x") {
val lines = mutableListOf(
" lda var", " clc", " adc #3", " tax",
" lda wordarray_lsb,x", " ldx wordarray_msb,x",
" rts", " nop", " nop", " nop"
)
optimize(lines)
lines[0] shouldBe " ldx var"
lines[1] shouldBe " lda wordarray_lsb+3,x"
lines[2] shouldBe " ldx wordarray_msb+3,x"
lines.size shouldBe 7
}
test("optimizeFoldIndexOffsetSplitWord: does not fold mismatched registers") {
val lines = mutableListOf(
" lda var", " clc", " adc #3", " tay",
" lda wordarray_lsb,y", " ldx wordarray_msb,x",
" rts", " nop", " nop", " nop"
)
optimize(lines)
lines.size shouldBe 10
}
test("optimizeFoldIndexOffsetSplitWord: does not fold non-lsb-msb pair") {
val lines = mutableListOf(
" lda var", " clc", " adc #3", " tay",
" lda wordarray_lsb,y", " ldx other,y",
" rts", " nop", " nop", " nop"
)
optimize(lines)
lines.size shouldBe 10
}
// --- optimizeFoldIndexOffsetNonSplitWord (non-split word arrays) ---
test("optimizeFoldIndexOffsetNonSplitWord: folds non-split word array access") {
val lines = mutableListOf(
" lda var", " clc", " adc #3", " asl a", " tay",
" lda nswordarray,y", " ldx nswordarray+1,y",
" rts", " nop"
)
optimize(lines)
lines[0] shouldBe " lda var"
lines[1] shouldBe " asl a"
lines[2] shouldBe " tay"
lines[3] shouldBe " lda nswordarray+6,y"
lines[4] shouldBe " ldx nswordarray+1+6,y"
lines.size shouldBe 7
}
test("optimizeFoldIndexOffsetNonSplitWord: scales hex constants") {
val lines = mutableListOf(
" lda var", " clc", " adc #$02", " asl a", " tay",
" lda nswordarray,y", " ldx nswordarray+1,y",
" rts", " nop"
)
optimize(lines)
lines[3] shouldBe " lda nswordarray+$04,y"
lines[4] shouldBe " ldx nswordarray+1+$04,y"
lines.size shouldBe 7
}
test("optimizeFoldIndexOffsetNonSplitWord: does not fold without asl a") {
val lines = mutableListOf(
" lda var", " clc", " adc #3", " tay",
" lda nswordarray,y", " ldx nswordarray+1,y",
" rts", " nop", " nop"
)
optimize(lines)
lines.size shouldBe 9
}
// --- Edge cases ---
test("handles empty input") {
+15 -11
View File
@@ -12,10 +12,9 @@ import prog8.code.ast.*
import prog8.code.core.*
import prog8.code.source.SourceCode
import prog8.code.target.C64Target
import prog8.code.target.VMTarget
import prog8.codegen.cpu6502.AsmGen6502
import prog8.codegen.cpu6502.VariableAllocator
import prog8tests.helpers.DummyMemsizer
import prog8tests.helpers.DummyStringEncoder
import prog8tests.helpers.ErrorReporterForTests
import java.nio.file.Files
import kotlin.io.path.Path
@@ -46,13 +45,15 @@ class TestCodegen: FunSpec({
// xx += cx16.r0
// }
//}
val codegen = AsmGen6502(prefixSymbols = false, 0)
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val compTarget = C64Target()
val codegen = AsmGen6502(0)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main",false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
sub.add(PtVariable(
"pi",
DataType.UBYTE,
false,
ZeropageWish.DONTCARE,
0u,
false,
@@ -62,7 +63,8 @@ class TestCodegen: FunSpec({
))
sub.add(PtVariable(
"particleX",
DataType.arrayFor(BaseDataType.UBYTE),
DataType.arrayFor(BaseDataType.UBYTE, compTarget),
false,
ZeropageWish.DONTCARE,
0u,
false,
@@ -72,7 +74,8 @@ class TestCodegen: FunSpec({
))
sub.add(PtVariable(
"particleDX",
DataType.arrayFor(BaseDataType.UBYTE),
DataType.arrayFor(BaseDataType.UBYTE, compTarget),
false,
ZeropageWish.DONTCARE,
0u,
false,
@@ -83,6 +86,7 @@ class TestCodegen: FunSpec({
sub.add(PtVariable(
"xx",
DataType.WORD,
false,
ZeropageWish.DONTCARE,
0u,
false,
@@ -93,14 +97,14 @@ class TestCodegen: FunSpec({
val assign = PtAugmentedAssign("+=", Position.DUMMY)
val target = PtAssignTarget(false, Position.DUMMY).also {
val targetIdx = PtArrayIndexer(DataType.UBYTE, Position.DUMMY).also { idx ->
idx.add(PtIdentifier("main.start.particleX", DataType.arrayFor(BaseDataType.UBYTE), Position.DUMMY))
val targetIdx = PtArrayIndexer(DataType.UBYTE, false, Position.DUMMY).also { idx ->
idx.add(PtIdentifier("main.start.particleX", DataType.arrayFor(BaseDataType.UBYTE, compTarget), Position.DUMMY))
idx.add(PtNumber(BaseDataType.UBYTE, 2.0, Position.DUMMY))
}
it.add(targetIdx)
}
val value = PtArrayIndexer(DataType.UBYTE, Position.DUMMY)
value.add(PtIdentifier("main.start.particleDX", DataType.arrayFor(BaseDataType.UBYTE), Position.DUMMY))
val value = PtArrayIndexer(DataType.UBYTE, false, Position.DUMMY)
value.add(PtIdentifier("main.start.particleDX", DataType.arrayFor(BaseDataType.UBYTE, compTarget), Position.DUMMY))
value.add(PtNumber(BaseDataType.UBYTE, 2.0, Position.DUMMY))
assign.add(target)
assign.add(value)
@@ -166,7 +170,7 @@ class TestCodegen: FunSpec({
}
test("memory mapped zp var is correctly considered to be zp var") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val st = SymbolTable(program)
st.add(StMemVar("zpmemvar", DataType.WORD, 0x20u, null, null))
st.add(StMemVar("normalmemvar", DataType.WORD, 0x9000u, null, null))
File diff suppressed because it is too large. Load diff
@@ -81,7 +81,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val result = mutableListOf<IRCodeChunkBase>()
val tr = exprGen.translateExpression(call.args[0])
addToResult(result, tr, tr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.PUSH, IRDataType.BYTE, reg1=tr.resultReg), null)
addInstr(result, IRInstructions.push(IRDataType.BYTE, tr.resultReg), null)
return ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
}
@@ -89,7 +89,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val result = mutableListOf<IRCodeChunkBase>()
val tr = exprGen.translateExpression(call.args[0])
addToResult(result, tr, tr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.PUSH, IRDataType.WORD, reg1=tr.resultReg), null)
addInstr(result, IRInstructions.push(IRDataType.WORD, tr.resultReg), null)
return ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
}
@@ -97,7 +97,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val result = mutableListOf<IRCodeChunkBase>()
val tr = exprGen.translateExpression(call.args[0])
addToResult(result, tr, tr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.PUSH, IRDataType.LONG, reg1=tr.resultReg), null)
addInstr(result, IRInstructions.push(IRDataType.LONG, tr.resultReg), null)
return ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
}
@@ -105,36 +105,36 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val result = mutableListOf<IRCodeChunkBase>()
val tr = exprGen.translateExpression(call.args[0])
addToResult(result, tr, -1, tr.resultFpReg)
addInstr(result, IRInstruction(Opcode.PUSH, IRDataType.FLOAT, fpReg1 = RegisterNum(tr.resultFpReg)), null)
addInstr(result, IRInstructions.push(IRDataType.FLOAT, tr.resultFpReg), null)
return ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
}
private fun funcPop(): ExpressionCodeResult {
val result = mutableListOf<IRCodeChunkBase>()
val resultRegister = codeGen.registers.next(IRDataType.BYTE)
addInstr(result, IRInstruction(Opcode.POP, IRDataType.BYTE, reg1=resultRegister), null)
addInstr(result, IRInstructions.pop(IRDataType.BYTE, resultRegister), null)
return ExpressionCodeResult(result, IRDataType.BYTE, resultRegister, -1)
}
private fun funcPopW(): ExpressionCodeResult {
val result = mutableListOf<IRCodeChunkBase>()
val resultRegister = codeGen.registers.next(IRDataType.WORD)
addInstr(result, IRInstruction(Opcode.POP, IRDataType.WORD, reg1=resultRegister), null)
return ExpressionCodeResult(result, IRDataType.BYTE, resultRegister, -1)
addInstr(result, IRInstructions.pop(IRDataType.WORD, resultRegister), null)
return ExpressionCodeResult(result, IRDataType.WORD, resultRegister, -1)
}
private fun funcPopL(): ExpressionCodeResult {
val result = mutableListOf<IRCodeChunkBase>()
val resultRegister = codeGen.registers.next(IRDataType.LONG)
addInstr(result, IRInstruction(Opcode.POP, IRDataType.LONG, reg1=resultRegister), null)
return ExpressionCodeResult(result, IRDataType.BYTE, resultRegister, -1)
addInstr(result, IRInstructions.pop(IRDataType.LONG, resultRegister), null)
return ExpressionCodeResult(result, IRDataType.LONG, resultRegister, -1)
}
private fun funcPopF(): ExpressionCodeResult {
val result = mutableListOf<IRCodeChunkBase>()
val resultRegister = codeGen.registers.next(IRDataType.FLOAT)
addInstr(result, IRInstruction(Opcode.POP, IRDataType.FLOAT, fpReg1 = RegisterNum(resultRegister)), null)
return ExpressionCodeResult(result, IRDataType.BYTE, -1, resultRegister)
addInstr(result, IRInstructions.pop(IRDataType.FLOAT, resultRegister), null)
return ExpressionCodeResult(result, IRDataType.FLOAT, -1, resultRegister)
}
private fun funcCopyFromPointer1ToPointer2(call: PtFunctionCall, type: IRDataType): ExpressionCodeResult {
@@ -147,15 +147,15 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
IRDataType.LONG -> {
val intermediateReg = codeGen.registers.next(IRDataType.LONG)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOADI, type, reg1=intermediateReg, reg2=trSourceAddr.resultReg, immediate = 0)
it += IRInstruction(Opcode.STOREI, type, reg1=intermediateReg, reg2=trTargetAddr.resultReg, immediate = 0)
it += IRInstructions.loadMemory(Opcode.LOADI, type, intermediateReg, IRMemory.indirect(trSourceAddr.resultReg))
it += IRInstructions.storeMemory(Opcode.STOREI, type, intermediateReg, IRMemory.indirect(trTargetAddr.resultReg))
}
}
IRDataType.FLOAT -> {
val intermediateReg = codeGen.registers.next(IRDataType.FLOAT)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOADI, type, fpReg1=RegisterNum(intermediateReg), reg1=trSourceAddr.resultReg, immediate = 0)
it += IRInstruction(Opcode.STOREI, type, fpReg1=RegisterNum(intermediateReg), reg1=trTargetAddr.resultReg, immediate = 0)
it += IRInstructions.loadMemory(Opcode.LOADI, type, intermediateReg, IRMemory.indirect(trSourceAddr.resultReg))
it += IRInstructions.storeMemory(Opcode.STOREI, type, intermediateReg, IRMemory.indirect(trTargetAddr.resultReg))
}
}
else -> throw AssemblyError("invalid type $type")
@@ -169,12 +169,12 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
addToResult(result, valueTr, valueTr.resultReg, valueTr.resultFpReg)
return if(resultType==IRDataType.FLOAT) {
val resultFpReg = codeGen.registers.next(IRDataType.FLOAT)
addInstr(result, IRInstruction(Opcode.SQUARE, resultType, fpReg1 = RegisterNum(resultFpReg), fpReg2 = RegisterNum(valueTr.resultFpReg)), null)
addInstr(result, IRInstructions.binary(Opcode.SQUARE, resultType, resultFpReg, valueTr.resultFpReg), null)
ExpressionCodeResult(result, resultType, -1, resultFpReg)
}
else {
val resultReg = codeGen.registers.next(resultType)
addInstr(result, IRInstruction(Opcode.SQUARE, resultType, reg1 = resultReg, reg2 = valueTr.resultReg), null)
addInstr(result, IRInstructions.binary(Opcode.SQUARE, resultType, resultReg, valueTr.resultReg), null)
ExpressionCodeResult(result, resultType, resultReg, -1)
}
}
@@ -183,12 +183,13 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val result = mutableListOf<IRCodeChunkBase>()
val addressTr = exprGen.translateExpression(call.args[0])
addToResult(result, addressTr, addressTr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.CALLI, reg1 = addressTr.resultReg), null)
addInstr(result, IRInstructions.call(Opcode.CALLI, CallSite(CallTarget.Direct(codeIndirect(addressTr.resultReg)))), null)
return if(call.void)
ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
else {
val resultvalueReg = codeGen.registers.next(IRDataType.WORD)
addInstr(result, IRInstruction(Opcode.LOADHR, IRDataType.WORD, reg1=resultvalueReg, immediate=4), null)
val slot = if(codeGen.options.compTarget.cpu.usesM68kConvention) 10 else 4
addInstr(result, IRInstructions.hardwareLoad(IRDataType.WORD, resultvalueReg, CallingConventionSlot(slot)), null)
ExpressionCodeResult(result, IRDataType.WORD, resultvalueReg, -1)
}
}
@@ -201,8 +202,9 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
addToResult(result, bankTr, bankTr.resultReg, -1)
addToResult(result, addressTr, addressTr.resultReg, -1)
addToResult(result, argumentwordTr, argumentwordTr.resultReg, -1)
result += codeGen.makeSyscall(IMSyscall.CALLFAR, listOf(IRDataType.BYTE to bankTr.resultReg, IRDataType.WORD to addressTr.resultReg, IRDataType.WORD to argumentwordTr.resultReg), IRDataType.WORD to argumentwordTr.resultReg)
return ExpressionCodeResult(result, IRDataType.WORD, argumentwordTr.resultReg, -1)
val resultReg = codeGen.registers.next(IRDataType.WORD)
result += codeGen.makeSyscall(IMSyscall.CALLFAR, listOf(IRDataType.BYTE to bankTr.resultReg, codeGen.addressDt to addressTr.resultReg, IRDataType.WORD to argumentwordTr.resultReg), IRDataType.WORD to resultReg)
return ExpressionCodeResult(result, IRDataType.WORD, resultReg, -1)
}
private fun funcCallfar2(call: PtFunctionCall): ExpressionCodeResult {
@@ -219,15 +221,20 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
addToResult(result, argumentX, argumentX.resultReg, -1)
addToResult(result, argumentY, argumentY.resultReg, -1)
addToResult(result, argumentCarry, argumentCarry.resultReg, -1)
result += codeGen.makeSyscall(IMSyscall.CALLFAR2, listOf(IRDataType.BYTE to bankTr.resultReg, IRDataType.WORD to addressTr.resultReg,
val resultReg = codeGen.registers.next(IRDataType.WORD)
result += codeGen.makeSyscall(IMSyscall.CALLFAR2, listOf(IRDataType.BYTE to bankTr.resultReg, codeGen.addressDt to addressTr.resultReg,
IRDataType.BYTE to argumentA.resultReg,
IRDataType.BYTE to argumentX.resultReg,
IRDataType.BYTE to argumentY.resultReg,
IRDataType.BYTE to argumentCarry.resultReg), IRDataType.WORD to addressTr.resultReg)
return ExpressionCodeResult(result, IRDataType.WORD, addressTr.resultReg, -1)
IRDataType.BYTE to argumentCarry.resultReg), IRDataType.WORD to resultReg)
return ExpressionCodeResult(result, IRDataType.WORD, resultReg, -1)
}
private fun funcDivmod(call: PtFunctionCall, type: IRDataType, signed: Boolean): ExpressionCodeResult {
if(signed && codeGen.options.compTarget.cpu.is6502) {
codeGen.errors.err("expected all ubyte or all uword arguments (no signed divmod support on 6502 yet)", call.position)
return ExpressionCodeResult(mutableListOf(), type, -1, -1)
}
val result = mutableListOf<IRCodeChunkBase>()
val number = call.args[0]
val divident = call.args[1]
@@ -238,33 +245,56 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
if(divident is PtNumber) {
val tr = exprGen.translateExpression(number)
addToResult(result, tr, tr.resultReg, -1)
addInstr(result, IRInstruction(divmodOpcode, type, reg1 = tr.resultReg, immediate = divident.number.toInt()), null)
divisionReg = tr.resultReg
remainderReg = codeGen.registers.next(type)
addInstr(result, IRInstructions.divmodImmediate(divmodOpcode, type, tr.resultReg, remainderReg, divident.number.toInt()), null)
divisionReg = tr.resultReg
} else {
val numTr = exprGen.translateExpression(number)
addToResult(result, numTr, numTr.resultReg, -1)
val dividentTr = exprGen.translateExpression(divident)
addToResult(result, dividentTr, dividentTr.resultReg, -1)
addInstr(result, IRInstruction(divmodrOpcode, type, reg1 = numTr.resultReg, reg2=dividentTr.resultReg), null)
divisionReg = numTr.resultReg
remainderReg = dividentTr.resultReg
addInstr(result, IRInstructions.divmodRegister(divmodrOpcode, type, numTr.resultReg, dividentTr.resultReg), null)
divisionReg = numTr.resultReg
}
// DIVMOD instruction result convention: on value stack, division and remainder on top.
addInstr(result, IRInstruction(Opcode.POP, type, reg1=remainderReg), null)
addInstr(result, IRInstruction(Opcode.POP, type, reg1=divisionReg), null)
// return the 2 result values: division and remainder division in AY, remainder in R15
if(type==IRDataType.BYTE) {
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.STOREHR, IRDataType.BYTE, reg1=divisionReg, immediate=0)
it += IRInstruction(Opcode.STOREM, IRDataType.BYTE, reg1=remainderReg, labelSymbol = "cx16.r15")
// DIVMOD result convention: quotient in reg1, remainder in reg2.
// Route to the return value locations expected by the caller (via returnsWhatWhere).
if(codeGen.options.compTarget.cpu.usesM68kConvention) {
// m68k: first return value in D0 (slot 10), second in D1 (slot 11)
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))
}
}
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 if(type==IRDataType.WORD) {
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.STOREHR, IRDataType.WORD, reg1=divisionReg, immediate=4)
it += IRInstruction(Opcode.STOREM, IRDataType.WORD, reg1=remainderReg, labelSymbol = "cx16.r15")
} else {
// 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.
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))
}
}
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)
}
@@ -275,7 +305,8 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
addToResult(result, left, left.resultReg, -1)
addToResult(result, right, right.resultReg, -1)
val resultReg = codeGen.registers.next(IRDataType.BYTE)
result += codeGen.makeSyscall(IMSyscall.COMPARE_STRINGS, listOf(IRDataType.WORD to left.resultReg, IRDataType.WORD to right.resultReg), IRDataType.BYTE to resultReg)
val addressDt = codeGen.addressDt
result += codeGen.makeSyscall(IMSyscall.COMPARE_STRINGS, listOf(addressDt to left.resultReg, addressDt to right.resultReg), IRDataType.BYTE to resultReg)
return ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
}
@@ -285,9 +316,9 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
addToResult(result, leftTr, leftTr.resultReg, -1)
val rightTr = exprGen.translateExpression(call.args[1])
addToResult(result, rightTr, rightTr.resultReg, -1)
val dt = irType(call.args[0].type)
val dt = codeGen.irType(call.args[0].type)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.CMP, dt, reg1=leftTr.resultReg, reg2=rightTr.resultReg)
it += IRInstructions.compare(dt, leftTr.resultReg, rightTr.resultReg)
}
return ExpressionCodeResult(result, dt, leftTr.resultReg, -1)
}
@@ -306,11 +337,11 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val notNegativeLabel = codeGen.createLabelName()
val compareReg = codeGen.registers.next(IRDataType.BYTE)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOADR, IRDataType.BYTE, reg1=compareReg, reg2=tr.resultReg)
it += IRInstructions.move(IRDataType.BYTE, compareReg, tr.resultReg)
if(needsExplicitCmpi)
it += IRInstruction(Opcode.CMPI, IRDataType.BYTE, reg1=compareReg, immediate = 0)
it += IRInstruction(Opcode.BSTPOS, labelSymbol = notNegativeLabel)
it += IRInstruction(Opcode.NEG, IRDataType.BYTE, reg1=tr.resultReg)
it += IRInstructions.compareImmediate(IRDataType.BYTE, compareReg, 0)
it += IRInstructions.branch(Opcode.BSTPOS, codeLabel(notNegativeLabel))
it += IRInstructions.unary(Opcode.NEG, IRDataType.BYTE, tr.resultReg)
}
result += IRCodeChunk(notNegativeLabel, null)
return ExpressionCodeResult(result, IRDataType.BYTE, tr.resultReg, -1)
@@ -319,11 +350,11 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val notNegativeLabel = codeGen.createLabelName()
val compareReg = codeGen.registers.next(IRDataType.WORD)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOADR, IRDataType.WORD, reg1=compareReg, reg2=tr.resultReg)
it += IRInstructions.move(IRDataType.WORD, compareReg, tr.resultReg)
if(needsExplicitCmpi)
it += IRInstruction(Opcode.CMPI, IRDataType.WORD, reg1=compareReg, immediate = 0)
it += IRInstruction(Opcode.BSTPOS, labelSymbol = notNegativeLabel)
it += IRInstruction(Opcode.NEG, IRDataType.WORD, reg1=tr.resultReg)
it += IRInstructions.compareImmediate(IRDataType.WORD, compareReg, 0)
it += IRInstructions.branch(Opcode.BSTPOS, codeLabel(notNegativeLabel))
it += IRInstructions.unary(Opcode.NEG, IRDataType.WORD, tr.resultReg)
}
result += IRCodeChunk(notNegativeLabel, null)
return ExpressionCodeResult(result, IRDataType.WORD, tr.resultReg, -1)
@@ -332,20 +363,24 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val notNegativeLabel = codeGen.createLabelName()
val compareReg = codeGen.registers.next(IRDataType.LONG)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOADR, IRDataType.LONG, reg1=compareReg, reg2=tr.resultReg)
it += IRInstructions.move(IRDataType.LONG, compareReg, tr.resultReg)
if(needsExplicitCmpi)
it += IRInstruction(Opcode.CMPI, IRDataType.LONG, reg1=compareReg, immediate = 0)
it += IRInstruction(Opcode.BSTPOS, labelSymbol = notNegativeLabel)
it += IRInstruction(Opcode.NEG, IRDataType.LONG, reg1=tr.resultReg)
it += IRInstructions.compareImmediate(IRDataType.LONG, compareReg, 0)
it += IRInstructions.branch(Opcode.BSTPOS, codeLabel(notNegativeLabel))
it += IRInstructions.unary(Opcode.NEG, IRDataType.LONG, tr.resultReg)
}
result += IRCodeChunk(notNegativeLabel, null)
return ExpressionCodeResult(result, IRDataType.LONG, tr.resultReg, -1)
}
BaseDataType.FLOAT -> {
val resultFpReg = codeGen.registers.next(IRDataType.FLOAT)
addInstr(result, IRInstruction(Opcode.FABS, IRDataType.FLOAT, fpReg1 = RegisterNum(resultFpReg), fpReg2 = RegisterNum(tr.resultFpReg)), null)
addInstr(result, IRInstructions.binary(Opcode.FABS, IRDataType.FLOAT, resultFpReg, tr.resultFpReg), null)
return ExpressionCodeResult(result, IRDataType.FLOAT, -1, resultFpReg)
}
BaseDataType.POINTER -> {
// pointer is unsigned, abs has no effect
return ExpressionCodeResult(result, IRDataType.POINTER, tr.resultReg, -1)
}
else -> throw AssemblyError("weird dt")
}
}
@@ -358,12 +393,12 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
if(tr.dt==IRDataType.FLOAT) {
addToResult(result, tr, -1, tr.resultFpReg)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.SGN, tr.dt, reg1 = resultReg, fpReg1 = RegisterNum(tr.resultFpReg))
it += IRInstructions.binary(Opcode.SGN, tr.dt, resultReg, tr.resultFpReg)
}
} else {
addToResult(result, tr, tr.resultReg, -1)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.SGN, tr.dt, reg1 = resultReg, reg2 = tr.resultReg)
it += IRInstructions.binary(Opcode.SGN, tr.dt, resultReg, tr.resultReg)
}
}
return ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
@@ -378,7 +413,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
addToResult(result, tr, tr.resultReg, -1)
val resultReg = codeGen.registers.next(IRDataType.BYTE)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.SQRT, IRDataType.BYTE, reg1=resultReg, reg2=tr.resultReg)
it += IRInstructions.binary(Opcode.SQRT, IRDataType.BYTE, resultReg, tr.resultReg)
}
return ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
}
@@ -386,7 +421,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
addToResult(result, tr, tr.resultReg, -1)
val resultReg = codeGen.registers.next(IRDataType.BYTE) // sqrt of a word still produces just a byte result
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.SQRT, IRDataType.WORD, reg1=resultReg, reg2=tr.resultReg)
it += IRInstructions.binary(Opcode.SQRT, IRDataType.WORD, resultReg, tr.resultReg)
}
return ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
}
@@ -394,7 +429,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
addToResult(result, tr, tr.resultReg, -1)
val resultReg = codeGen.registers.next(IRDataType.WORD) // sqrt of a long still produces just a word result
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.SQRT, IRDataType.LONG, reg1=resultReg, reg2=tr.resultReg)
it += IRInstructions.binary(Opcode.SQRT, IRDataType.LONG, resultReg, tr.resultReg)
}
return ExpressionCodeResult(result, IRDataType.WORD, resultReg, -1)
}
@@ -402,7 +437,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
addToResult(result, tr, -1, tr.resultFpReg)
val resultFpReg = codeGen.registers.next(IRDataType.FLOAT)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.SQRT, IRDataType.FLOAT, fpReg1 = RegisterNum(resultFpReg), fpReg2 = RegisterNum(tr.resultFpReg))
it += IRInstructions.binary(Opcode.SQRT, IRDataType.FLOAT, resultFpReg, tr.resultFpReg)
}
return ExpressionCodeResult(result, IRDataType.FLOAT, -1, resultFpReg)
}
@@ -417,13 +452,13 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
// msb is 0, use EXT
val lsbTr = exprGen.translateExpression(call.args[1])
addToResult(result, lsbTr, lsbTr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.EXT, IRDataType.BYTE, reg1=resultReg, reg2 = lsbTr.resultReg), null)
addInstr(result, IRInstructions.binary(Opcode.EXT, IRDataType.BYTE, resultReg, lsbTr.resultReg), null)
} else {
val msbTr = exprGen.translateExpression(call.args[0])
addToResult(result, msbTr, msbTr.resultReg, -1)
val lsbTr = exprGen.translateExpression(call.args[1])
addToResult(result, lsbTr, lsbTr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.CONCAT, IRDataType.BYTE, reg1=resultReg, reg2 = msbTr.resultReg, reg3 = lsbTr.resultReg), null)
addInstr(result, IRInstructions.concat(IRDataType.BYTE, resultReg, msbTr.resultReg, lsbTr.resultReg), null)
}
return ExpressionCodeResult(result, IRDataType.WORD, resultReg, -1)
}
@@ -437,23 +472,21 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
// msw is 0, use EXT
val lswTr = exprGen.translateExpression(call.args[1])
addToResult(result, lswTr, lswTr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.EXT, IRDataType.WORD, reg1=resultReg, reg2 = lswTr.resultReg), null)
addInstr(result, IRInstructions.binary(Opcode.EXT, IRDataType.WORD, resultReg, lswTr.resultReg), null)
} else {
val mswTr = exprGen.translateExpression(call.args[0])
addToResult(result, mswTr, mswTr.resultReg, -1)
val lswTr = exprGen.translateExpression(call.args[1])
addToResult(result, lswTr, lswTr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.CONCAT, IRDataType.WORD, reg1=resultReg, reg2 = mswTr.resultReg, reg3 = lswTr.resultReg), null)
addInstr(result, IRInstructions.concat(IRDataType.WORD, resultReg, mswTr.resultReg, lswTr.resultReg), null)
}
} else {
// mklong(msb, b3, b2, lsb)
if((call.args[0] as? PtNumber)?.number == 0.0 && (call.args[1] as? PtNumber)?.number == 0.0 && (call.args[2] as? PtNumber)?.number == 0.0) {
// use EXT.b + EXT.w
// use EXTL.b: zero-extend byte directly to long
val lsbTr = exprGen.translateExpression(call.args[3])
addToResult(result, lsbTr, lsbTr.resultReg, -1)
val wordReg = codeGen.registers.next(IRDataType.WORD)
addInstr(result, IRInstruction(Opcode.EXT, IRDataType.BYTE, reg1=wordReg, reg2 = lsbTr.resultReg), null)
addInstr(result, IRInstruction(Opcode.EXT, IRDataType.WORD, reg1=resultReg, reg2 = wordReg), null)
addInstr(result, IRInstructions.binary(Opcode.EXTL, IRDataType.BYTE, resultReg, lsbTr.resultReg), null)
} else {
val msbTr = exprGen.translateExpression(call.args[0])
val b2Tr = exprGen.translateExpression(call.args[1])
@@ -465,9 +498,9 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
addToResult(result, lsbTr, lsbTr.resultReg, -1)
val lswReg = codeGen.registers.next(IRDataType.WORD)
val mswReg = codeGen.registers.next(IRDataType.WORD)
addInstr(result, IRInstruction(Opcode.CONCAT, IRDataType.BYTE, reg1=mswReg, reg2 = msbTr.resultReg, reg3 = b2Tr.resultReg), null)
addInstr(result, IRInstruction(Opcode.CONCAT, IRDataType.BYTE, reg1=lswReg, reg2 = b1Tr.resultReg, reg3 = lsbTr.resultReg), null)
addInstr(result, IRInstruction(Opcode.CONCAT, IRDataType.WORD, reg1=resultReg, reg2 = mswReg, reg3 = lswReg), null)
addInstr(result, IRInstructions.concat(IRDataType.BYTE, mswReg, msbTr.resultReg, b2Tr.resultReg), null)
addInstr(result, IRInstructions.concat(IRDataType.BYTE, lswReg, b1Tr.resultReg, lsbTr.resultReg), null)
addInstr(result, IRInstructions.concat(IRDataType.WORD, resultReg, mswReg, lswReg), null)
}
}
@@ -475,7 +508,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
}
private fun funcClamp(call: PtFunctionCall): ExpressionCodeResult {
val type = irType(call.type)
val type = codeGen.irType(call.type)
require(type != IRDataType.FLOAT)
val result = mutableListOf<IRCodeChunkBase>()
val valueTr = exprGen.translateExpression(call.args[0])
@@ -492,17 +525,18 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
BaseDataType.LONG -> IMSyscall.CLAMP_LONG
else -> throw AssemblyError("invalid dt")
}
val resultReg = codeGen.registers.next(type)
result += codeGen.makeSyscall(syscall, listOf(
valueTr.dt to valueTr.resultReg,
minimumTr.dt to minimumTr.resultReg,
maximumTr.dt to maximumTr.resultReg,
), type to valueTr.resultReg
), type to resultReg
)
return ExpressionCodeResult(result, type, valueTr.resultReg, -1)
return ExpressionCodeResult(result, type, resultReg, -1)
}
private fun funcMin(call: PtFunctionCall): ExpressionCodeResult {
val type = irType(call.type)
val type = codeGen.irType(call.type)
val result = mutableListOf<IRCodeChunkBase>()
val leftTr = exprGen.translateExpression(call.args[0])
addToResult(result, leftTr, leftTr.resultReg, -1)
@@ -511,17 +545,17 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val comparisonOpcode = if(call.type.isSigned) Opcode.BGTSR else Opcode.BGTR
val after = codeGen.createLabelName()
result += IRCodeChunk(null, null).also {
it += IRInstruction(comparisonOpcode, type, reg1 = rightTr.resultReg, reg2 = leftTr.resultReg, labelSymbol = after)
it += IRInstructions.branchRegister(comparisonOpcode, type, rightTr.resultReg, leftTr.resultReg, codeLabel(after))
// right <= left, take right
it += IRInstruction(Opcode.LOADR, type, reg1=leftTr.resultReg, reg2=rightTr.resultReg)
it += IRInstruction(Opcode.JUMP, labelSymbol = after)
it += IRInstructions.move(type, leftTr.resultReg, rightTr.resultReg)
it += IRInstructions.jump(codeLabel(after))
}
result += IRCodeChunk(after, null)
return ExpressionCodeResult(result, type, leftTr.resultReg, -1)
}
private fun funcMax(call: PtFunctionCall): ExpressionCodeResult {
val type = irType(call.type)
val type = codeGen.irType(call.type)
val result = mutableListOf<IRCodeChunkBase>()
val leftTr = exprGen.translateExpression(call.args[0])
addToResult(result, leftTr, leftTr.resultReg, -1)
@@ -530,33 +564,35 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val comparisonOpcode = if(call.type.isSigned) Opcode.BGTSR else Opcode.BGTR
val after = codeGen.createLabelName()
result += IRCodeChunk(null, null).also {
it += IRInstruction(comparisonOpcode, type, reg1 = leftTr.resultReg, reg2 = rightTr.resultReg, labelSymbol = after)
it += IRInstructions.branchRegister(comparisonOpcode, type, leftTr.resultReg, rightTr.resultReg, codeLabel(after))
// right >= left, take right
it += IRInstruction(Opcode.LOADR, type, reg1=leftTr.resultReg, reg2=rightTr.resultReg)
it += IRInstruction(Opcode.JUMP, labelSymbol = after)
it += IRInstructions.move(type, leftTr.resultReg, rightTr.resultReg)
it += IRInstructions.jump(codeLabel(after))
}
result += IRCodeChunk(after, null)
return ExpressionCodeResult(result, type, leftTr.resultReg, -1)
}
private fun funcPoke(call: PtFunctionCall, dt: IRDataType): ExpressionCodeResult {
// Try struct-array field folding: pokew(&arr[idx]+fieldOff, value) -> STOREX arr+fieldOff,S=structSize
tryFoldStructArrayPoke(call, dt)?.let { return it }
val result = mutableListOf<IRCodeChunkBase>()
if(codeGen.isZero(call.args[1])) {
if (call.args[0] is PtNumber) {
val address = (call.args[0] as PtNumber).number.toUInt()
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.STOREZM, dt, address = address.toAddress())
it += IRInstructions.storeZero(Opcode.STOREZM, dt, IRMemory.direct(address.toAddress()))
}
} else {
val (address, offset) = exprGen.getAddressAndOffset(call.args[0])
if(address!=null) {
val tr = exprGen.translateExpression(address)
addToResult(result, tr, tr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.STOREZI, dt, reg1 = tr.resultReg, immediate = offset!!), null)
addInstr(result, IRInstructions.storeZero(Opcode.STOREZI, dt, IRMemory.indirect(tr.resultReg, offset!!)), null)
} else {
val tr = exprGen.translateExpression(call.args[0])
addToResult(result, tr, tr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.STOREZI, dt, reg1 = tr.resultReg, immediate = 0), null)
addInstr(result, IRInstructions.storeZero(Opcode.STOREZI, dt, IRMemory.indirect(tr.resultReg)), null)
}
}
} else {
@@ -565,9 +601,9 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val address = (call.args[0] as PtNumber).number.toUInt()
addToResult(result, valueTr, valueTr.resultReg, valueTr.resultFpReg)
if(dt==IRDataType.FLOAT) {
addInstr(result, IRInstruction(Opcode.STOREM, dt, fpReg1 = RegisterNum(valueTr.resultFpReg), address = address.toAddress()), null)
addInstr(result, IRInstructions.storeMemory(Opcode.STOREM, dt, valueTr.resultFpReg, IRMemory.direct(address.toAddress())), null)
} else {
addInstr(result, IRInstruction(Opcode.STOREM, dt, reg1 = valueTr.resultReg, address = address.toAddress()), null)
addInstr(result, IRInstructions.storeMemory(Opcode.STOREM, dt, valueTr.resultReg, IRMemory.direct(address.toAddress())), null)
}
} else {
val (address, offset) = exprGen.getAddressAndOffset(call.args[0])
@@ -576,18 +612,18 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
addToResult(result, valueTr, valueTr.resultReg, valueTr.resultFpReg)
addToResult(result, addressTr, addressTr.resultReg, -1)
if(dt==IRDataType.FLOAT) {
addInstr(result, IRInstruction(Opcode.STOREI, IRDataType.FLOAT, reg1 = addressTr.resultReg, fpReg1 = RegisterNum(valueTr.resultFpReg), immediate = offset!!), null)
addInstr(result, IRInstructions.storeMemory(Opcode.STOREI, IRDataType.FLOAT, valueTr.resultFpReg, IRMemory.indirect(addressTr.resultReg, offset!!)), null)
} else {
addInstr(result, IRInstruction(Opcode.STOREI, dt, reg1 = valueTr.resultReg, reg2 = addressTr.resultReg, immediate = offset!!), null)
addInstr(result, IRInstructions.storeMemory(Opcode.STOREI, dt, valueTr.resultReg, IRMemory.indirect(addressTr.resultReg, offset!!)), null)
}
} else {
val addressTr = exprGen.translateExpression(call.args[0])
addToResult(result, valueTr, valueTr.resultReg, valueTr.resultFpReg)
addToResult(result, addressTr, addressTr.resultReg, -1)
if(dt==IRDataType.FLOAT) {
addInstr(result, IRInstruction(Opcode.STOREI, IRDataType.FLOAT, reg1 = addressTr.resultReg, fpReg1 = RegisterNum(valueTr.resultFpReg), immediate = 0), null)
addInstr(result, IRInstructions.storeMemory(Opcode.STOREI, IRDataType.FLOAT, valueTr.resultFpReg, IRMemory.indirect(addressTr.resultReg)), null)
} else {
addInstr(result, IRInstruction(Opcode.STOREI, dt, reg1 = valueTr.resultReg, reg2 = addressTr.resultReg, immediate = 0), null)
addInstr(result, IRInstructions.storeMemory(Opcode.STOREI, dt, valueTr.resultReg, IRMemory.indirect(addressTr.resultReg)), null)
}
}
}
@@ -601,26 +637,26 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
if(call.args[0] is PtNumber) {
val resultFpRegister = codeGen.registers.next(IRDataType.FLOAT)
val address = (call.args[0] as PtNumber).number.toUInt()
addInstr(result, IRInstruction(Opcode.LOADM, IRDataType.FLOAT, fpReg1 = RegisterNum(resultFpRegister), address = address.toAddress()), null)
addInstr(result, IRInstructions.loadMemory(Opcode.LOADM, IRDataType.FLOAT, resultFpRegister, IRMemory.direct(address.toAddress())), null)
ExpressionCodeResult(result, IRDataType.FLOAT, -1, resultFpRegister)
} else {
val tr = exprGen.translateExpression(call.args.single())
addToResult(result, tr, tr.resultReg, -1)
val resultFpReg = codeGen.registers.next(IRDataType.FLOAT)
addInstr(result, IRInstruction(Opcode.LOADI, IRDataType.FLOAT, reg1 = tr.resultReg, fpReg1 = RegisterNum(resultFpReg), immediate = 0), null)
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, IRDataType.FLOAT, resultFpReg, IRMemory.indirect(tr.resultReg)), null)
ExpressionCodeResult(result, IRDataType.FLOAT, -1, resultFpReg)
}
} else {
if (call.args[0] is PtNumber) {
val resultRegister = codeGen.registers.next(dt)
val address = (call.args[0] as PtNumber).number.toUInt()
addInstr(result, IRInstruction(Opcode.LOADM, dt, reg1 = resultRegister, address = address.toAddress()), null)
addInstr(result, IRInstructions.loadMemory(Opcode.LOADM, dt, resultRegister, IRMemory.direct(address.toAddress())), null)
ExpressionCodeResult(result, dt, resultRegister, -1)
} else {
val tr = exprGen.translateExpression(call.args.single())
addToResult(result, tr, tr.resultReg, -1)
val resultReg = codeGen.registers.next(dt)
addInstr(result, IRInstruction(Opcode.LOADI, dt, reg1 = resultReg, reg2 = tr.resultReg, immediate = 0), null)
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, dt, resultReg, IRMemory.indirect(tr.resultReg)), null)
ExpressionCodeResult(result, dt, resultReg, -1)
}
}
@@ -637,11 +673,11 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val addressNum = address.number.toUInt()
val resultRegister = codeGen.registers.next(IRDataType.BYTE)
if (newValueTr != null) addToResult(result, newValueTr, newValueTr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.LOADM, IRDataType.BYTE, reg1 = resultRegister, address = addressNum.toAddress()), null)
addInstr(result, IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, resultRegister, IRMemory.direct(addressNum.toAddress())), null)
if (newValueTr == null) {
addInstr(result, IRInstruction(Opcode.STOREZM, IRDataType.BYTE, address = addressNum.toAddress()), null)
addInstr(result, IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.direct(addressNum.toAddress())), null)
} else {
addInstr(result, IRInstruction(Opcode.STOREM, IRDataType.BYTE, reg1 = newValueTr.resultReg, address = addressNum.toAddress()), null)
addInstr(result, IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, newValueTr.resultReg, IRMemory.direct(addressNum.toAddress())), null)
}
ExpressionCodeResult(result, IRDataType.BYTE, resultRegister, -1)
} else {
@@ -654,11 +690,11 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
if (newValueTr != null) addToResult(result, newValueTr, newValueTr.resultReg, -1)
addToResult(result, addressTr, addressTr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.LOADI, IRDataType.BYTE, reg1 = resultReg, reg2 = addressTr.resultReg, immediate = finalOffset), null)
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, IRDataType.BYTE, resultReg, IRMemory.indirect(addressTr.resultReg, finalOffset)), null)
if (newValueTr == null) {
addInstr(result, IRInstruction(Opcode.STOREZI, IRDataType.BYTE, reg1 = addressTr.resultReg, immediate = finalOffset), null)
addInstr(result, IRInstructions.storeZero(Opcode.STOREZI, IRDataType.BYTE, IRMemory.indirect(addressTr.resultReg, finalOffset)), null)
} else {
addInstr(result, IRInstruction(Opcode.STOREI, IRDataType.BYTE, reg1 = newValueTr.resultReg, reg2 = addressTr.resultReg, immediate = finalOffset), null)
addInstr(result, IRInstructions.storeMemory(Opcode.STOREI, IRDataType.BYTE, newValueTr.resultReg, IRMemory.indirect(addressTr.resultReg, finalOffset)), null)
}
ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
}
@@ -667,10 +703,10 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
private fun funcStructAlloc(call: PtFunctionCall): ExpressionCodeResult {
val code = IRCodeChunk(null, null)
val resultReg = codeGen.registers.next(IRDataType.WORD)
val resultReg = codeGen.registers.next(IRDataType.POINTER)
val labelname = SymbolTable.labelnameForStructInstance(call)
code += IRInstruction(Opcode.LOAD, IRDataType.WORD, reg1=resultReg, labelSymbol = "${StStructInstanceBlockName}.$labelname")
return ExpressionCodeResult(code, IRDataType.WORD, resultReg, -1)
code += IRInstructions.loadAddress(IRDataType.POINTER, resultReg, "${StStructInstanceBlockName}.$labelname")
return ExpressionCodeResult(code, IRDataType.POINTER, resultReg, -1)
}
private fun funcLsb(call: PtFunctionCall, fromLong: Boolean): ExpressionCodeResult {
@@ -679,9 +715,9 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
addToResult(result, tr, tr.resultReg, -1)
val resultReg = codeGen.registers.next(IRDataType.BYTE)
if(fromLong)
addInstr(result, IRInstruction(Opcode.LSIGB, IRDataType.LONG, reg1 = resultReg, reg2 = tr.resultReg), null)
addInstr(result, IRInstructions.binary(Opcode.LSIGB, IRDataType.LONG, resultReg, tr.resultReg), null)
else
addInstr(result, IRInstruction(Opcode.LSIGB, IRDataType.WORD, reg1 = resultReg, reg2 = tr.resultReg), null)
addInstr(result, IRInstructions.binary(Opcode.LSIGB, IRDataType.WORD, resultReg, tr.resultReg), null)
// note: if a word result is needed, the upper byte is cleared by the typecast that follows. No need to do it here.
return ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
}
@@ -691,14 +727,25 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
// TODO this can be more optimal if the argument is a variable or memory address
val tr = exprGen.translateExpression(call.args.single())
addToResult(result, tr, tr.resultReg, -1)
// low byte returned in A, mid in R15, high (bank) in R14
val byteReg = codeGen.registers.next(IRDataType.BYTE)
addInstr(result, IRInstruction(Opcode.BSIGB, IRDataType.LONG, reg1 = byteReg, reg2 = tr.resultReg), null)
addInstr(result, IRInstruction(Opcode.STOREM, IRDataType.BYTE, reg1 = byteReg, labelSymbol = "cx16.r14"), null)
addInstr(result, IRInstruction(Opcode.MIDB, IRDataType.LONG, reg1 = byteReg, reg2 = tr.resultReg), null)
addInstr(result, IRInstruction(Opcode.STOREM, IRDataType.BYTE, reg1 = byteReg, labelSymbol = "cx16.r15"), null)
addInstr(result, IRInstruction(Opcode.LSIGB, IRDataType.LONG, reg1 = byteReg, reg2 = tr.resultReg), null)
addInstr(result, IRInstruction(Opcode.STOREHR, IRDataType.BYTE, reg1 = byteReg, immediate = 0), null)
if(codeGen.options.compTarget.cpu.usesM68kConvention) {
// m68k: 3 return values in D0 (slot 10), D1 (slot 11), D2 (slot 12)
// store high byte first, then mid, then low (to avoid clobbering)
addInstr(result, IRInstructions.binary(Opcode.BSIGB, IRDataType.LONG, byteReg, tr.resultReg), null)
addInstr(result, IRInstructions.hardwareStore(IRDataType.BYTE, byteReg, CallingConventionSlot(12)), null)
addInstr(result, IRInstructions.binary(Opcode.MIDB, IRDataType.LONG, byteReg, tr.resultReg), null)
addInstr(result, IRInstructions.hardwareStore(IRDataType.BYTE, byteReg, CallingConventionSlot(11)), null)
addInstr(result, IRInstructions.binary(Opcode.LSIGB, IRDataType.LONG, byteReg, tr.resultReg), null)
addInstr(result, IRInstructions.hardwareStore(IRDataType.BYTE, byteReg, CallingConventionSlot(10)), null)
} else {
// 6502/cx16: low byte in A (slot 0), mid in cx16.r15, high (bank) in cx16.r14
addInstr(result, IRInstructions.binary(Opcode.BSIGB, IRDataType.LONG, byteReg, tr.resultReg), null)
addInstr(result, IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, byteReg, IRMemory.direct("cx16.r14")), null)
addInstr(result, IRInstructions.binary(Opcode.MIDB, IRDataType.LONG, byteReg, tr.resultReg), null)
addInstr(result, IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, byteReg, IRMemory.direct("cx16.r15")), null)
addInstr(result, IRInstructions.binary(Opcode.LSIGB, IRDataType.LONG, byteReg, tr.resultReg), null)
addInstr(result, IRInstructions.hardwareStore(IRDataType.BYTE, byteReg, CallingConventionSlot(0)), null)
}
return ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
}
@@ -707,7 +754,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val tr = exprGen.translateExpression(call.args.single())
addToResult(result, tr, tr.resultReg, -1)
val resultReg = codeGen.registers.next(IRDataType.WORD)
addInstr(result, IRInstruction(Opcode.LSIGW, IRDataType.LONG, reg1 = resultReg, reg2 = tr.resultReg), null)
addInstr(result, IRInstructions.binary(Opcode.LSIGW, IRDataType.LONG, resultReg, tr.resultReg), null)
return ExpressionCodeResult(result, IRDataType.WORD, resultReg, -1)
}
@@ -717,9 +764,9 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
addToResult(result, tr, tr.resultReg, -1)
val resultReg = codeGen.registers.next(IRDataType.BYTE)
if(fromLong)
addInstr(result, IRInstruction(Opcode.MSIGB, IRDataType.LONG, reg1 = resultReg, reg2 = tr.resultReg), null)
addInstr(result, IRInstructions.binary(Opcode.MSIGB, IRDataType.LONG, resultReg, tr.resultReg), null)
else
addInstr(result, IRInstruction(Opcode.MSIGB, IRDataType.WORD, reg1 = resultReg, reg2 = tr.resultReg), null)
addInstr(result, IRInstructions.binary(Opcode.MSIGB, IRDataType.WORD, resultReg, tr.resultReg), null)
// note: if a word result is needed, the upper byte is cleared by the typecast that follows. No need to do it here.
return ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
}
@@ -729,14 +776,14 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val tr = exprGen.translateExpression(call.args.single())
addToResult(result, tr, tr.resultReg, -1)
val resultReg = codeGen.registers.next(IRDataType.WORD)
addInstr(result, IRInstruction(Opcode.MSIGW, IRDataType.LONG, reg1 = resultReg, reg2 = tr.resultReg), null)
addInstr(result, IRInstructions.binary(Opcode.MSIGW, IRDataType.LONG, resultReg, tr.resultReg), null)
return ExpressionCodeResult(result, IRDataType.WORD, resultReg, -1)
}
private fun funcRolRor(call: PtFunctionCall): ExpressionCodeResult {
val result = mutableListOf<IRCodeChunkBase>()
val arg = call.args[0]
val vmDt = irType(arg.type)
val vmDt = codeGen.irType(arg.type)
val opcodeMemAndReg = when(call.name) {
"rol" -> Opcode.ROXLM to Opcode.ROXL
"ror" -> Opcode.ROXRM to Opcode.ROXR
@@ -747,13 +794,13 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val ident = arg as? PtIdentifier
if(ident!=null) {
addInstr(result, IRInstruction(opcodeMemAndReg.first, vmDt, labelSymbol = ident.name), null)
addInstr(result, IRInstructions.memoryOp(opcodeMemAndReg.first, vmDt, IRMemory.direct(ident.name)), null)
return ExpressionCodeResult(result, vmDt, -1, -1)
}
val memAddr: UInt? = (arg as? PtMemoryByte)?.address?.asConstInteger()?.toUInt()
if(memAddr!=null) {
addInstr(result, IRInstruction(opcodeMemAndReg.first, vmDt, address = memAddr.toAddress()), null)
addInstr(result, IRInstructions.memoryOp(opcodeMemAndReg.first, vmDt, IRMemory.direct(memAddr.toAddress())), null)
return ExpressionCodeResult(result, vmDt, -1, -1)
}
@@ -765,19 +812,19 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
result += IRCodeChunk(null, null).also {
when(opcodeMemAndReg.first) {
Opcode.ROXRM, Opcode.RORM -> {
it += IRInstruction(opcodeMemAndReg.first, IRDataType.BYTE, labelSymbol = "${variable}_msb", symbolOffset = index)
it += IRInstruction(opcodeMemAndReg.first, IRDataType.BYTE, labelSymbol = "${variable}_lsb", symbolOffset = index)
it += IRInstructions.memoryOp(opcodeMemAndReg.first, IRDataType.BYTE, IRMemory.direct("${variable}_msb", index))
it += IRInstructions.memoryOp(opcodeMemAndReg.first, IRDataType.BYTE, IRMemory.direct("${variable}_lsb", index))
}
Opcode.ROXLM, Opcode.ROLM -> {
it += IRInstruction(opcodeMemAndReg.first, IRDataType.BYTE, labelSymbol = "${variable}_lsb", symbolOffset = index)
it += IRInstruction(opcodeMemAndReg.first, IRDataType.BYTE, labelSymbol = "${variable}_msb", symbolOffset = index)
it += IRInstructions.memoryOp(opcodeMemAndReg.first, IRDataType.BYTE, IRMemory.direct("${variable}_lsb", index))
it += IRInstructions.memoryOp(opcodeMemAndReg.first, IRDataType.BYTE, IRMemory.direct("${variable}_msb", index))
}
else -> throw AssemblyError("wrong rol/ror opcode")
}
}
} else {
val offset = codeGen.program.memsizer.memorySize(arr.type, index)
addInstr(result, IRInstruction(opcodeMemAndReg.first, vmDt, labelSymbol = variable, symbolOffset = offset), null)
addInstr(result, IRInstructions.memoryOp(opcodeMemAndReg.first, vmDt, IRMemory.direct(variable, offset)), null)
}
return ExpressionCodeResult(result, vmDt, -1, -1)
}
@@ -785,17 +832,17 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val opcode = opcodeMemAndReg.second
val saveCarry = opcode in OpcodesThatDependOnCarry && !arg.isSimple()
if(saveCarry)
addInstr(result, IRInstruction(Opcode.PUSHST), null) // save Carry
addInstr(result, IRInstructions.simple(Opcode.PUSHST), null) // save Carry
val tr = exprGen.translateExpression(arg)
addToResult(result, tr, tr.resultReg, -1)
if(saveCarry)
addInstr(result, IRInstruction(Opcode.POPST), null)
addInstr(result, IRInstruction(opcode, vmDt, reg1 = tr.resultReg), null)
addInstr(result, IRInstructions.simple(Opcode.POPST), null)
addInstr(result, IRInstructions.unary(opcode, vmDt, tr.resultReg), null)
if(saveCarry)
addInstr(result, IRInstruction(Opcode.PUSHST), null) // save Carry
addInstr(result, IRInstructions.simple(Opcode.PUSHST), null) // save Carry
result += codeGen.assignRegisterTo(arg, tr.resultReg)
if(saveCarry)
addInstr(result, IRInstruction(Opcode.POPST), null)
addInstr(result, IRInstructions.simple(Opcode.POPST), null)
return ExpressionCodeResult(result, vmDt, -1, -1)
}
@@ -803,22 +850,24 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val result = mutableListOf<IRCodeChunkBase>()
val target = call.args[0]
val isConstZeroValue = call.args[1].asConstInteger()==0
val isBigEndian = codeGen.options.compTarget.cpu.isBigEndian
fun byteOffset(elementSize: Int) = if(msb != isBigEndian) elementSize - 1 else 0
when(target) {
is PtIdentifier -> {
val offset = if (msb) { if(target.type.isLong) 3 else 1 } else 0
val offset = byteOffset(if(target.type.isLong) 4 else 2)
if(isConstZeroValue) {
result += IRCodeChunk(null, null).also {
val pointerReg = codeGen.registers.next(IRDataType.WORD)
it += IRInstruction(Opcode.LOAD, IRDataType.WORD, reg1 = pointerReg, labelSymbol = target.name)
it += IRInstruction(Opcode.STOREZI, IRDataType.BYTE, reg1 = pointerReg, immediate = offset)
val pointerReg = codeGen.registers.next(IRDataType.POINTER)
it += IRInstructions.loadAddress(IRDataType.POINTER, pointerReg, target.name)
it += IRInstructions.storeZero(Opcode.STOREZI, IRDataType.BYTE, IRMemory.indirect(pointerReg, offset))
}
} else {
val valueTr = exprGen.translateExpression(call.args[1])
addToResult(result, valueTr, valueTr.resultReg, -1)
result += IRCodeChunk(null, null).also {
val pointerReg = codeGen.registers.next(IRDataType.WORD)
it += IRInstruction(Opcode.LOAD, IRDataType.WORD, reg1 = pointerReg, labelSymbol = target.name)
it += IRInstruction(Opcode.STOREI, IRDataType.BYTE, reg1 = valueTr.resultReg, reg2 = pointerReg, immediate = offset)
val pointerReg = codeGen.registers.next(IRDataType.POINTER)
it += IRInstructions.loadAddress(IRDataType.POINTER, pointerReg, target.name)
it += IRInstructions.storeMemory(Opcode.STOREI, IRDataType.BYTE, valueTr.resultReg, IRMemory.indirect(pointerReg, offset))
}
}
}
@@ -829,23 +878,26 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val pointerTr = exprGen.translateExpression(target.pointerderef!!)
addToResult(result, pointerTr, pointerTr.resultReg, -1)
val constIndex = target.index.asConstInteger()
val elementByteOffset = byteOffset(eltSize)
if(constIndex != null) {
val offset = eltSize * constIndex + if(msb) eltSize - 1 else 0
val offset = eltSize * constIndex + elementByteOffset
if(offset > 0)
addInstr(result, IRInstruction(Opcode.ADD, IRDataType.WORD, reg1 = pointerTr.resultReg, immediate = offset), null)
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerTr.resultReg, offset), null)
} else {
val (code, indexWordReg) = codeGen.loadIndexReg(target.index, eltSize, true, false)
val (code, indexWordReg) = codeGen.loadIndexReg(target.index, true)
result += code
addInstr(result, IRInstruction(Opcode.ADDR, IRDataType.WORD, reg1 = pointerTr.resultReg, reg2 = indexWordReg), null)
if(msb)
addInstr(result, IRInstruction(Opcode.ADD, IRDataType.WORD, reg1 = pointerTr.resultReg, immediate = eltSize - 1), null)
if(eltSize!=1)
result += codeGen.multiplyByConst(DataType.UWORD, indexWordReg, eltSize)
addInstr(result, IRInstructions.binary(Opcode.ADDR, IRDataType.POINTER, pointerTr.resultReg, indexWordReg), null)
if(elementByteOffset > 0)
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerTr.resultReg, elementByteOffset), null)
}
if(isConstZeroValue) {
addInstr(result, IRInstruction(Opcode.STOREZI, IRDataType.BYTE, reg1 = pointerTr.resultReg, immediate = 0), null)
addInstr(result, IRInstructions.storeZero(Opcode.STOREZI, IRDataType.BYTE, IRMemory.indirect(pointerTr.resultReg)), null)
} else {
val valueTr = exprGen.translateExpression(call.args[1])
addToResult(result, valueTr, valueTr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.STOREI, IRDataType.BYTE, reg1 = valueTr.resultReg, reg2 = pointerTr.resultReg, immediate = 0), null)
addInstr(result, IRInstructions.storeMemory(Opcode.STOREI, IRDataType.BYTE, valueTr.resultReg, IRMemory.indirect(pointerTr.resultReg)), null)
}
}
else if(target.splitWords) {
@@ -854,32 +906,36 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val varName = target.variable!!.name + if(msb) "_msb" else "_lsb"
if(isConstZeroValue) {
if(constIndex!=null) {
val offsetReg = codeGen.registers.next(IRDataType.BYTE)
val idxType = codeGen.options.compTarget.indexRegType
val offsetReg = codeGen.registers.next(idxType)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=offsetReg, immediate = constIndex)
it += IRInstruction(Opcode.STOREZX, IRDataType.BYTE, reg1=offsetReg, labelSymbol = varName)
it += IRInstructions.load(idxType, offsetReg, constIndex)
it += IRInstructions.storeZero(Opcode.STOREZX, IRDataType.BYTE, IRMemory.indexed(varName, offsetReg, codeGen.options.compTarget.indexRegType))
}
} else {
val indexTr = exprGen.translateExpression(target.index)
addToResult(result, indexTr, indexTr.resultReg, -1)
val indexReg = codeGen.canonicalizeIndexReg(result, indexTr)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.STOREZX, IRDataType.BYTE, reg1=indexTr.resultReg, labelSymbol = varName)
it += IRInstructions.storeZero(Opcode.STOREZX, IRDataType.BYTE, IRMemory.indexed(varName, indexReg, codeGen.options.compTarget.indexRegType))
}
}
} else {
val valueTr = exprGen.translateExpression(call.args[1])
addToResult(result, valueTr, valueTr.resultReg, -1)
if(constIndex!=null) {
val offsetReg = codeGen.registers.next(IRDataType.BYTE)
val idxType = codeGen.options.compTarget.indexRegType
val offsetReg = codeGen.registers.next(idxType)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=offsetReg, immediate = constIndex)
it += IRInstruction(Opcode.STOREX, IRDataType.BYTE, reg1=valueTr.resultReg, reg2=offsetReg, labelSymbol = varName)
it += IRInstructions.load(idxType, offsetReg, constIndex)
it += IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, valueTr.resultReg, IRMemory.indexed(varName, offsetReg, codeGen.options.compTarget.indexRegType))
}
} else {
val indexTr = exprGen.translateExpression(target.index)
addToResult(result, indexTr, indexTr.resultReg, -1)
val indexReg = codeGen.canonicalizeIndexReg(result, indexTr)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.STOREX, IRDataType.BYTE, reg1=valueTr.resultReg, reg2=indexTr.resultReg, labelSymbol = varName)
it += IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, valueTr.resultReg, IRMemory.indexed(varName, indexReg, codeGen.options.compTarget.indexRegType))
}
}
}
@@ -891,50 +947,36 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val constIndex = target.index.asConstInteger()
if(isConstZeroValue) {
if(constIndex!=null) {
val offsetReg = codeGen.registers.next(IRDataType.BYTE)
val offset = eltSize*constIndex + if(msb) eltSize-1 else 0
val offsetReg = codeGen.registers.next(if(codeGen.wordArrayIndex) IRDataType.WORD else IRDataType.BYTE)
val offset = eltSize*constIndex + byteOffset(eltSize)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=offsetReg, immediate = offset)
it += IRInstruction(Opcode.STOREZX, IRDataType.BYTE, reg1=offsetReg, labelSymbol = targetVariable.name)
it += IRInstructions.load(if(codeGen.wordArrayIndex) IRDataType.WORD else IRDataType.BYTE, offsetReg, offset)
it += IRInstructions.storeZero(Opcode.STOREZX, IRDataType.BYTE, IRMemory.indexed(targetVariable.name, offsetReg, codeGen.options.compTarget.indexRegType))
}
} else {
val indexTr = exprGen.translateExpression(target.index)
addToResult(result, indexTr, indexTr.resultReg, -1)
val (code, indexReg) = codeGen.loadIndexReg(target.index, codeGen.wordArrayIndex)
result += code
result += IRCodeChunk(null, null).also {
if(eltSize>1)
it += codeGen.multiplyByConst(DataType.UBYTE, indexTr.resultReg, eltSize)
if(msb) {
it += if(target.type.isLong)
IRInstruction(Opcode.ADD, IRDataType.BYTE, reg1 = indexTr.resultReg, immediate = 3)
else
IRInstruction(Opcode.INC, IRDataType.BYTE, reg1 = indexTr.resultReg)
}
it += IRInstruction(Opcode.STOREZX, IRDataType.BYTE, reg1=indexTr.resultReg, labelSymbol = targetVariable.name)
val offset = byteOffset(eltSize)
it += IRInstructions.storeZero(Opcode.STOREZX, IRDataType.BYTE, IRMemory.indexed(targetVariable.name, indexReg, codeGen.options.compTarget.indexRegType, scale = eltSize, displacement = offset))
}
}
} else {
val valueTr = exprGen.translateExpression(call.args[1])
addToResult(result, valueTr, valueTr.resultReg, -1)
if(constIndex!=null) {
val offsetReg = codeGen.registers.next(IRDataType.BYTE)
val offset = eltSize*constIndex + if(msb) eltSize-1 else 0
val offsetReg = codeGen.registers.next(if(codeGen.wordArrayIndex) IRDataType.WORD else IRDataType.BYTE)
val offset = eltSize*constIndex + byteOffset(eltSize)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=offsetReg, immediate = offset)
it += IRInstruction(Opcode.STOREX, IRDataType.BYTE, reg1=valueTr.resultReg, reg2=offsetReg, labelSymbol = targetVariable.name)
it += IRInstructions.load(if(codeGen.wordArrayIndex) IRDataType.WORD else IRDataType.BYTE, offsetReg, offset)
it += IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, valueTr.resultReg, IRMemory.indexed(targetVariable.name, offsetReg, codeGen.options.compTarget.indexRegType))
}
} else {
val indexTr = exprGen.translateExpression(target.index)
addToResult(result, indexTr, indexTr.resultReg, -1)
val (code, indexReg) = codeGen.loadIndexReg(target.index, codeGen.wordArrayIndex)
result += code
result += IRCodeChunk(null, null).also {
if(eltSize>1)
it += codeGen.multiplyByConst(DataType.UBYTE, indexTr.resultReg, eltSize)
if(msb) {
it += if(target.type.isLong)
IRInstruction(Opcode.ADD, IRDataType.BYTE, reg1 = indexTr.resultReg, immediate = 3)
else
IRInstruction(Opcode.INC, IRDataType.BYTE, reg1 = indexTr.resultReg)
}
it += IRInstruction(Opcode.STOREX, IRDataType.BYTE, reg1=valueTr.resultReg, reg2=indexTr.resultReg, labelSymbol = targetVariable.name)
val offset = byteOffset(eltSize)
it += IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, valueTr.resultReg, IRMemory.indexed(targetVariable.name, indexReg, codeGen.options.compTarget.indexRegType, scale = eltSize, displacement = offset))
}
}
}
@@ -944,4 +986,31 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
}
return ExpressionCodeResult(result, IRDataType.WORD, -1, -1)
}
private fun tryFoldStructArrayPoke(call: PtFunctionCall, dt: IRDataType): ExpressionCodeResult? {
// Detect pokew(&arr[idx]+fieldOff, value) where arr is struct array with variable idx
// and fold to single STOREX/STOREZX with scale=structSize.
val info = extractStructArrayIndexInfo(call.args[0], codeGen) ?: return null
val isZero = codeGen.isZero(call.args[1])
val result = mutableListOf<IRCodeChunkBase>()
val (idxCode, indexReg) = codeGen.loadIndexReg(info.idxExpr, codeGen.wordArrayIndex)
result += idxCode
return if(isZero) {
result += IRCodeChunk(null, null).also {
it += IRInstructions.storeZero(Opcode.STOREZX, dt, IRMemory.indexed(info.arrayName, indexReg, codeGen.options.compTarget.indexRegType, scale=info.structSize, displacement=info.fieldOffset))
}
ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
} else {
val valueTr = exprGen.translateExpression(call.args[1])
addToResult(result, valueTr, valueTr.resultReg, valueTr.resultFpReg)
result += IRCodeChunk(null, null).also {
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 {
IRInstructions.storeMemory(Opcode.STOREX, dt, valueTr.resultReg, IRMemory.indexed(info.arrayName, indexReg, codeGen.options.compTarget.indexRegType, scale=info.structSize, displacement=info.fieldOffset))
}
}
ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
}
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,36 @@
package prog8.codegen.intermediate
import prog8.intermediate.*
/*
Construction helpers for the few IR opcodes that have no dedicated IRInstructions factory,
plus a small operand-inspection helper used by the optimizers.
The structured IRInstructions factory covers almost every opcode, but three opcodes have an
operand shape that no named factory produces (a lone source register, or a source register plus
an immediate without a destination). They are built here from schema-derived operands, so this
module never constructs IRInstruction directly.
*/
/** STOREHFACZERO / STOREHFACONE: a single source float register (no dedicated factory exists). */
internal fun storeHwFac(opcode: Opcode, fpRegister: Int): IRInstruction =
IRInstructions.push(IRDataType.FLOAT, fpRegister).copy(opcode = opcode)
/** BITTST: a source register plus a byte immediate bit number, with no destination register. */
internal fun bitTest(type: IRDataType, register: Int, bit: Int): IRInstruction =
IRInstructions.simple(Opcode.NOP).copy(
opcode = Opcode.BITTST,
type = type,
srcA = IRInstructions.operandFor(Opcode.BITTST, type, InstructionSlot.SRC_A, register),
immediate = IRInstructions.immediateFor(Opcode.BITTST, type, bit)
)
/**
* The name of the single symbol this instruction refers to, if any.
* That is either the symbol a memory reference is based on, the symbol whose address is taken
* in an immediate operand, or the label a branch/jump/call transfers control to.
*/
internal fun IRInstruction.referencedSymbol(): String? =
memory?.symbolName
?: (immediate as? ImmediateOperand.SymbolAddress)?.symbol
?: labelTarget
File diff suppressed because it is too large. Load diff
@@ -8,12 +8,28 @@ 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()
// remove empty blocks
// remove empty blocks (but keep blocks with %option force_output,
// and also blocks that contain labels -- labels are addressable symbols,
// and blocks that still have variables in the symbol table -- a block with variables is not truly empty)
irprog.blocks.reversed().forEach { block ->
if(block.isEmpty()) {
val hasLabels = block.children.any { it.label != null }
val hasVariables = irprog.st.allVariables().any { it.name.startsWith(block.label + ".") }
if(!hasLabels && !hasVariables && block.isEmpty() && !block.options.forceOutput) {
irprog.blocks.remove(block)
pruneSymboltable(block.label)
numRemoved++
@@ -33,7 +49,7 @@ class IRUnusedCodeRemover(
blockVars.forEach { stVar ->
irprog.allSubs().flatMap { it.chunks }.forEach { chunk ->
chunk.instructions.forEach { ins ->
if(ins.labelSymbol == stVar.name) {
if(ins.referencedSymbol() == stVar.name) {
return // symbol occurs in an instruction
}
}
@@ -48,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) }
@@ -70,41 +94,26 @@ class IRUnusedCodeRemover(
private fun removeBlockInits(code: IRProgram, blockLabel: String) {
val instructions = code.globalInits.instructions
instructions.toTypedArray().forEach {ins ->
if(ins.labelSymbol?.startsWith(blockLabel)==true) {
if(ins.referencedSymbol()?.startsWith(blockLabel)==true) {
instructions.remove(ins)
}
}
// remove stray loads
val readRegs = mutableSetOf<Int>()
val readFpRegs = mutableSetOf<Int>()
val readRegs = mutableSetOf<VirtualRegister>()
instructions.forEach { ins ->
val readRegsCounts = mutableMapOf<RegisterNum, Int>()
val readFpRegsCounts = mutableMapOf<RegisterNum, Int>()
val writeRegsCounts = mutableMapOf<RegisterNum, Int>()
val writeFpRegsCounts = mutableMapOf<RegisterNum, Int>()
val regsTypes = mutableMapOf<RegisterNum, IRDataType>()
ins.addUsedRegistersCounts(readRegsCounts, writeRegsCounts, readFpRegsCounts, writeFpRegsCounts, regsTypes, null)
readRegs.addAll(readRegsCounts.keys.map { it.value })
readFpRegs.addAll(readFpRegsCounts.keys.map { it.value })
val readRegsCounts = mutableMapOf<VirtualRegister, Int>()
val writeRegsCounts = mutableMapOf<VirtualRegister, Int>()
val regsTypes = mutableMapOf<VirtualRegister, IRDataType>()
ins.addUsedRegistersCounts(readRegsCounts, writeRegsCounts, regsTypes, null)
readRegs.addAll(readRegsCounts.keys)
}
instructions.toTypedArray().forEach { ins ->
if(ins.opcode in arrayOf(Opcode.LOAD, Opcode.LOADR, Opcode.LOADM)) {
val reg1 = ins.reg1
val fpReg1 = ins.fpReg1
if(reg1!=null) {
if(reg1 !in readRegs) {
if(ins.labelSymbol!=null)
code.st.removeIfExists(ins.labelSymbol!!)
instructions.remove(ins)
}
}
else if(fpReg1!=null) {
if(fpReg1.value !in readFpRegs) {
if(ins.labelSymbol!=null)
code.st.removeIfExists(ins.labelSymbol!!)
instructions.remove(ins)
}
val dest = ins.dest
if(dest != null && dest.register !in readRegs) {
ins.referencedSymbol()?.let { code.st.removeIfExists(it) }
instructions.remove(ins)
}
}
}
@@ -187,8 +196,8 @@ class IRUnusedCodeRemover(
// check if asmsub is linked or called from another regular subroutine
irprog.foreachCodeChunk { chunk ->
chunk.instructions.forEach {
it.labelSymbol?.let { label -> allSubs[label]?.let { cc -> linkedAsmSubs += cc } }
// note: branchTarget can't yet point to another IRAsmSubroutine, so do nothing when it's set
it.referencedSymbol()?.let { label -> allSubs[label]?.let { cc -> linkedAsmSubs += cc } }
// note: codeTarget can't yet point to another IRAsmSubroutine, so do nothing when it's set
}
}
@@ -232,21 +241,35 @@ 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 {
it.next?.let { next -> new += next }
if (it is IRLoopChunk) {
// A reachable counted loop makes every body chunk reachable too.
// The body is no longer represented by next pointers in the outer list.
new += it.body
}
it.instructions.forEach { instr ->
if (instr.branchTarget == null)
instr.labelSymbol?.let { label ->
val codeTarget = instr.codeTarget
if (codeTarget == null) {
instr.referencedSymbol()?.let { label ->
val chunk = allLabeledChunks[label] ?: allLabeledChunks[label.substringBeforeLast('.')]
if(chunk!=null)
new+=chunk
else
allLabeledChunks[label]?.let { c -> new += c }
}
else
new += instr.branchTarget!!
}
else {
val chunk = irprog.resolveCodeTarget(codeTarget)
?: codeTarget.labelName?.let { label -> allLabeledChunks[label] }
if(chunk!=null)
new += chunk
}
}
}
reachable += new
@@ -279,13 +302,22 @@ 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 {
if(it.branchTarget==null) {
it.labelSymbol?.let { label -> allLabeledChunks[label]?.let { cc -> linkedChunks += cc } }
val codeTarget = it.codeTarget
if(codeTarget==null) {
it.referencedSymbol()?.let { label -> allLabeledChunks[label]?.let { cc -> linkedChunks += cc } }
} else {
linkedChunks += it.branchTarget!!
val target = irprog.resolveCodeTarget(codeTarget)
?: codeTarget.labelName?.let { label -> allLabeledChunks[label] }
if(target!=null)
linkedChunks += target
}
}
if (chunk.label in entrypointNames)
@@ -295,8 +327,8 @@ class IRUnusedCodeRemover(
// make sure that chunks that are only used as a prefix of a label, are also marked as linked
linkedChunks.toList().forEach { chunk ->
chunk.instructions.forEach {
if(it.labelSymbol!=null) {
val chunkName = it.labelSymbol!!.substringBeforeLast('.')
it.referencedSymbol()?.let { symbol ->
val chunkName = symbol.substringBeforeLast('.')
allLabeledChunks[chunkName]?.let { c -> linkedChunks += c }
}
}
@@ -312,7 +344,7 @@ class IRUnusedCodeRemover(
// don't remove stuff in library modules or with %forceoutput
// TODO this is needed to keep the subroutine body of sqrt_long from being wiped !??! Sounds like the bad solution to the problem: why is the *body* being cleared , making an empty sqrt_long subrotuine end up in the p8ir output (and eventually in the .asm, which crashes the resulting program)
// TODO this causes other stuff to end up in the output IR that is not used......
// note: this wasn't a problem on the virtual target, but it is a problem when using the expericodegen target (because on the virtual target, the VM simply executes the SQRT instruction directly without needing a library routine)
// note: this wasn't a problem on the virtual target, but it is a problem when using the newcodegen targets (because on the virtual target, the VM simply executes the SQRT instruction directly without needing a library routine)
val block = irprog.blocks.first { b -> b.children.any { it === sub } }
if (block.library || block.options.forceOutput)
return@foreachSub
@@ -328,6 +360,9 @@ class IRUnusedCodeRemover(
numRemoved++
}
}
is IRLoopChunk -> {
// keep loop at subroutine entry; don't remove
}
is IRInlineAsmChunk, is IRInlineBinaryChunk -> {
sub.chunks[index] = IRCodeChunk(chunk.label, chunk.next)
numRemoved++
@@ -0,0 +1,465 @@
package prog8.codegen.intermediate
import prog8.intermediate.*
/*
* ============================================================================
* PROTOTYPE - NOT FOR PRODUCTION USE
* ============================================================================
*
* This is a PROTOTYPE memory-slot packer and the intended reusable foundation
* for a future hardware-register allocator targeting the m68k backend. It is
* NOT a complete or production-ready register allocator in its current form.
* It is DISABLED in production: its only call site in IRCodeGen.generate() is
* commented out, and it currently runs only under TestRegisterPacker.
*
* What this prototype demonstrates:
* - A working graph-coloring register allocator skeleton: CFG construction,
* intraprocedural liveness analysis (gen/kill fixed-point), live interval
* derivation, conflict-graph construction, greedy coloring, and IR rewrite.
* - Coalescing of virtual registers into shared memory slots in the flat
* `p8_regfile` BSS block (vregs with non-overlapping live ranges share a
* slot).
*
* What this prototype DOES NOT do (and why it is not production-ready):
* - No hardware-register allocation: slots are memory addresses in a flat
* BSS regfile, not physical D/A/FP registers.
* - No CALL clobbering: a callee packed into the same slot as a caller's
* live value silently overwrites it (see bug 3 below).
* - No register classes: no distinction between DATA, ADDRESS, and FPU
* registers (required by the m68k backend).
* - No spilling, prologue/epilogue emission, or CALL-aware interference.
* - Known-unsound liveness for complex control flow (nested loops,
* conditionals, early returns) - see bug 2 below.
*
* Intended reuse path (per ideas/m68k-register-allocation.md):
* - Stage 1: Make the m68k backend's instruction selection location-agnostic
* (operand() indirection returning "d3" if allocated, "p8_regfile+N" if
* spilled) - prerequisite bulk work, no behavior change.
* - Stage 2: Replace this packer's interference model with a correct one
* (CALL-aware, class-constrained) and point the coloring step at real
* D2-D6 / A2-A4 / FP2-FP7 registers. Six open design decisions remain
* in Stage 2 (see design doc section 7.1).
* - Stage 3: Delete subsumed peephole optimizations, update test assertions.
*/
/*
* Known bugs / unsoundness (carried over from the removed register-packing.md):
*
* 1. Disjoint-interval value clobbering (observed on cx16 Fibonacci).
* A register with multiple DISJOINT live ranges (e.g. a loop counter written
* at loop entry and read at loop exit, with no uses in between) was split
* into separate intervals; the packer allowed another register to share the
* same slot during the gap, overwriting the value before its next read.
* Fix applied (see packSubroutine interval merging): always merge ALL
* intervals of the same register into one contiguous range, regardless of
* overlap, so the value persists across gaps.
*
* 2. Complex control flow (observed on m68k TextElite).
* The iterative gen/kill fixed-point liveness (computeLiveness) produced
* incorrect live ranges for subroutines with complex CFGs (nested
* conditionals, loops, early returns). Two registers with genuinely
* overlapping live ranges were assigned the same slot, so one clobbered the
* other at runtime (symptom: infinite loop printing spaces — galaxy map
* data read from the wrong memory location). Root cause: the dataflow does
* not converge to the true meet-over-all-paths solution for all loop /
* conditional structures. The liveness MUST be validated against nested
* loops, conditionals, early returns, and switch-like dispatch before reuse.
*
* 3. Cross-subroutine slot collision (fundamental design issue; see header
* comment above). Per-subroutine packing into ONE flat shared regfile is
* unsound: a callee packed into the same slot as a caller's live value
* clobbers it. The packer does not model CALL clobbering. Soundness would
* require the full-program call tree (call-graph-aware allocation, depth
* ranges, or save/restore around calls) — which is exactly what the m68k
* allocator with a calling convention avoids by allocating per-subroutine.
*
* 4. Interval-merge type loss / silent skips. When merging a register's
* intervals, the merged interval keeps only the FIRST interval's type; if a
* later interval has an incompatible type, the register is dropped via
* skipRegs (and its original number is preserved via the startSlot =
* maxReg + 1 invariant). When reusing this code, merged intervals must carry
* the WIDEST type, not just the first, to avoid under-sized slots.
*/
object RegisterPacker {
data class Interval(val register: VirtualRegister, val start: Int, val end: Int, val type: IRDataType)
fun pack(irProg: IRProgram) {
val allRegTypes = mutableMapOf<VirtualRegister, IRDataType>()
irProg.forEachInstruction { instr ->
for (access in instr.registerAccesses)
allRegTypes.putIfAbsent(access.register, access.type)
}
val beforeCount = allRegTypes.size
// Start packing slots after the highest original register number to avoid
// collisions between packed slot numbers and original (non-packed) register numbers.
val maxReg = allRegTypes.keys.maxOfOrNull { it.num } ?: 0
val startSlot = maxReg + 1
val globalSlotTypes = mutableMapOf<VirtualRegister, IRDataType>()
irProg.foreachSub { sub -> packSubroutine(irProg, sub, allRegTypes, globalSlotTypes, startSlot) }
val afterTypes = rebuildTypeMap(irProg)
val afterCount = afterTypes.size
if (!irProg.options.quiet)
println("Register packing: $beforeCount -> $afterCount registers")
}
// Rebuild the register type map after packing, using the same type determination as the packer.
// This avoids the strict per-instruction validation in usedRegisters() that POINTER↔LONG↔WORD
// cross-type packing can trigger.
fun rebuildTypeMap(irProg: IRProgram): Map<VirtualRegister, IRDataType> {
val newTypes = mutableMapOf<VirtualRegister, IRDataType>()
irProg.forEachInstruction { instr ->
for (access in instr.registerAccesses)
newTypes.putIfAbsent(access.register, access.type)
}
return newTypes
}
private fun packSubroutine(irProg: IRProgram, sub: IRSubroutine, allRegTypes: Map<VirtualRegister, IRDataType>, globalSlotTypes: MutableMap<VirtualRegister, IRDataType>, startSlot: Int) {
if (sub.chunks.isEmpty())
return
val successors = buildCFG(sub, irProg)
val (_, liveOut) = computeLiveness(sub, successors)
val codeChunks = sub.chunks.filterIsInstance<IRCodeChunk>()
if (codeChunks.isEmpty())
return
val chunkRanges = mutableMapOf<IRCodeChunk, IntRange>()
var globalIdx = 0
for (chunk in codeChunks) {
val start = globalIdx
val end = globalIdx + chunk.instructions.size - 1
chunkRanges[chunk] = start..end
globalIdx += chunk.instructions.size
}
val registerIntervals = mutableMapOf<VirtualRegister, MutableList<Interval>>()
val registerTypes = mutableMapOf<VirtualRegister, IRDataType>()
for (chunk in codeChunks) {
val range = chunkRanges[chunk]!!
val chunkStart = range.first
if (chunk.instructions.isEmpty())
continue
val liveSet = liveOut[chunk]?.toMutableSet() ?: mutableSetOf()
val lastUse = mutableMapOf<VirtualRegister, Int>()
// Scan backward through instructions
for (i in chunk.instructions.indices.reversed()) {
val instr = chunk.instructions[i]
val globalI = chunkStart + i
val accesses = instr.registerAccesses
// Process READ (USE / USE_DEF) first so types are set before WRITE processing
for (access in accesses) {
if (access.direction != OperandDirection.DEF) {
val r = access.register
registerTypes.putIfAbsent(r, access.type)
if (r !in liveSet) {
lastUse[r] = globalI
liveSet.add(r)
}
}
}
// Process WRITTEN (DEF / USE_DEF) registers: end their live range (start of interval going forward)
for (access in accesses) {
if (access.direction != OperandDirection.USE) {
val r = access.register
if (r in liveSet) {
val end = lastUse.getOrElse(r) { globalI }
registerIntervals.getOrPut(r) { mutableListOf() }
.add(Interval(r, globalI, end, access.type))
liveSet.remove(r)
lastUse.remove(r)
}
}
}
}
// Registers still live at start of chunk are live-in
for (r in liveSet) {
val end = lastUse.getOrElse(r) { chunkStart }
registerIntervals.getOrPut(r) { mutableListOf() }
.add(Interval(r, chunkStart, end, registerTypes.getOrElse(r) { IRDataType.BYTE }))
}
}
if (registerIntervals.isEmpty())
return
// Merge adjacent/overlapping intervals for each register.
// Also merge non-overlapping intervals of the same register into a single
// contiguous range, because the value in the slot must persist across the
// gap to the next use (otherwise another register packed to the same slot
// would clobber it between intervals).
val mergedIntervals = mutableListOf<Interval>()
val skipRegs = mutableSetOf<VirtualRegister>()
for ((reg, intervals) in registerIntervals) {
val sorted = intervals.sortedBy { it.start }
var current = sorted.first()
for (next in sorted.drop(1)) {
if (current.type != next.type && !typesCompatible(current.type, next.type)) {
skipRegs.add(reg)
}
// Always merge: same register's intervals are merged into a single
// contiguous range to preserve the value across gaps.
current = Interval(reg, current.start, maxOf(current.end, next.end), current.type)
}
mergedIntervals.add(current)
}
if (skipRegs.isNotEmpty()) {
mergedIntervals.removeAll { it.register in skipRegs }
}
val conflictGraph = buildConflictGraph(sub)
val packing = greedyColor(mergedIntervals, conflictGraph, allRegTypes, globalSlotTypes, startSlot)
if (packing.isNotEmpty())
rewrite(sub, packing)
}
private fun buildConflictGraph(sub: IRSubroutine): Map<VirtualRegister, Set<VirtualRegister>> {
val conflicts = mutableMapOf<VirtualRegister, MutableSet<VirtualRegister>>()
for (chunk in sub.chunks.filterIsInstance<IRCodeChunk>()) {
val typeRegs = mutableMapOf<IRDataType, MutableSet<VirtualRegister>>()
for (instr in chunk.instructions) {
val accesses = instr.registerAccesses
for (access in accesses)
typeRegs.getOrPut(access.type) { mutableSetOf() }.add(access.register)
// Registers accessed together within a single instruction must not share a slot
// (generalizes the old reg1/reg2-only conflict rule to every register access of
// the instruction, now that they are all enumerable via registerAccesses).
val regsInInstr = accesses.map { it.register }.distinct()
for (i in regsInInstr.indices) {
for (j in i + 1 until regsInInstr.size) {
val r1 = regsInInstr[i]
val r2 = regsInInstr[j]
conflicts.getOrPut(r1) { mutableSetOf() }.add(r2)
conflicts.getOrPut(r2) { mutableSetOf() }.add(r1)
}
}
}
// Add conflicts between registers with incompatible types in the same chunk
val typeList = typeRegs.entries.toList()
for (i in typeList.indices) {
for (j in i + 1 until typeList.size) {
val (t1, regs1) = typeList[i]
val (t2, regs2) = typeList[j]
if (t1 != t2 && !typesCompatible(t1, t2)) {
for (r1 in regs1) {
for (r2 in regs2) {
if (r1 != r2) {
conflicts.getOrPut(r1) { mutableSetOf() }.add(r2)
conflicts.getOrPut(r2) { mutableSetOf() }.add(r1)
}
}
}
}
}
}
}
return conflicts
}
private fun buildCFG(sub: IRSubroutine, irProg: IRProgram): Map<IRCodeChunkBase, List<IRCodeChunkBase>> {
val successors = mutableMapOf<IRCodeChunkBase, MutableList<IRCodeChunkBase>>()
val conditionals = setOf(
Opcode.BSTCC, Opcode.BSTCS, Opcode.BSTEQ, Opcode.BSTNE,
Opcode.BSTNEG, Opcode.BSTPOS, Opcode.BSTVC, Opcode.BSTVS,
Opcode.BGTR, Opcode.BGT, Opcode.BLT,
Opcode.BGTSR, Opcode.BGTS, Opcode.BLTS,
Opcode.BGER, Opcode.BGE, Opcode.BLE,
Opcode.BGESR, Opcode.BGES, Opcode.BLES
)
for (chunk in sub.chunks) {
val succ = mutableListOf<IRCodeChunkBase>()
if (chunk is IRCodeChunk) {
val lastInstr = chunk.instructions.lastOrNull()
val endsWithUnconditional = lastInstr != null && lastInstr.opcode in OpcodesThatBranchUnconditionally
val endsWithConditional = lastInstr != null && lastInstr.opcode in conditionals
if (!endsWithUnconditional) {
chunk.next?.let { succ.add(it) }
}
if (endsWithConditional) {
lastInstr.codeTarget?.let { irProg.resolveCodeTarget(it) }?.let { target ->
if (!succ.contains(target))
succ.add(target)
}
} else if (lastInstr?.opcode == Opcode.JUMP) {
succ.clear()
lastInstr.codeTarget?.let { irProg.resolveCodeTarget(it) }?.let { succ.add(it) }
}
} else {
chunk.next?.let { succ.add(it) }
}
successors[chunk] = succ
}
return successors
}
private fun computeLiveness(
sub: IRSubroutine,
successors: Map<IRCodeChunkBase, List<IRCodeChunkBase>>
): Pair<Map<IRCodeChunk, Set<VirtualRegister>>, Map<IRCodeChunk, Set<VirtualRegister>>> {
val liveIn = mutableMapOf<IRCodeChunk, MutableSet<VirtualRegister>>()
val liveOut = mutableMapOf<IRCodeChunk, MutableSet<VirtualRegister>>()
val gen = mutableMapOf<IRCodeChunk, MutableSet<VirtualRegister>>()
val kill = mutableMapOf<IRCodeChunk, MutableSet<VirtualRegister>>()
for (chunk in sub.chunks) {
if (chunk !is IRCodeChunk)
continue
val genSet = mutableSetOf<VirtualRegister>()
val killSet = mutableSetOf<VirtualRegister>()
for (instr in chunk.instructions) {
val written = instr.definitions
val read = instr.uses
for (r in read) {
if (r !in killSet)
genSet.add(r)
}
for (r in written) {
killSet.add(r)
}
}
gen[chunk] = genSet
kill[chunk] = killSet
liveIn[chunk] = mutableSetOf()
liveOut[chunk] = mutableSetOf()
}
var changed = true
while (changed) {
changed = false
for (chunk in sub.chunks.reversed()) {
if (chunk !is IRCodeChunk)
continue
val newLiveOut = mutableSetOf<VirtualRegister>()
for (succ in successors[chunk].orEmpty()) {
if (succ is IRCodeChunk)
newLiveOut.addAll(liveIn[succ].orEmpty())
}
if (newLiveOut != liveOut[chunk]) {
liveOut[chunk] = newLiveOut
changed = true
}
val newLiveIn = mutableSetOf<VirtualRegister>()
newLiveIn.addAll(gen[chunk].orEmpty())
newLiveIn.addAll(liveOut[chunk].orEmpty() - kill[chunk].orEmpty())
if (newLiveIn != liveIn[chunk]) {
liveIn[chunk] = newLiveIn
changed = true
}
}
}
return liveIn to liveOut
}
private fun greedyColor(
intervals: List<Interval>,
conflictGraph: Map<VirtualRegister, Set<VirtualRegister>>,
allRegTypes: Map<VirtualRegister, IRDataType>,
slotTypes: MutableMap<VirtualRegister, IRDataType>,
startSlot: Int
): Map<VirtualRegister, VirtualRegister> {
if (intervals.isEmpty())
return emptyMap()
val packing = mutableMapOf<VirtualRegister, VirtualRegister>()
// Reserve slots for non-packed registers
val packedRegs = intervals.map { it.register }.toSet()
for ((reg, type) in allRegTypes) {
if (reg !in packedRegs)
slotTypes.putIfAbsent(reg, type)
}
val sorted = intervals.sortedWith(compareBy<Interval> { it.type.ordinal }.thenBy { it.start })
val activeSlots = mutableMapOf<VirtualRegister, Pair<VirtualRegister, Int>>()
for (interval in sorted) {
activeSlots.entries.removeAll { it.value.second < interval.start }
var slotNum = startSlot
var slot: VirtualRegister
while (true) {
// a packed slot stays in the same register file (int/float) as the register it replaces
slot = if (interval.register is VirtualRegister.FloatReg) VirtualRegister.float(slotNum) else VirtualRegister.int(slotNum)
if (slot !in activeSlots) {
val existingType = slotTypes[slot]
if (existingType == null || existingType == interval.type || typesCompatible(existingType, interval.type)) {
if (!conflictsWithRegister(slot, interval.register, conflictGraph, packing))
break
}
}
slotNum++
}
packing[interval.register] = slot
slotTypes.putIfAbsent(slot, interval.type) // keep the first-assigned type to prevent type narrowing via typesCompatible chain
activeSlots[slot] = Pair(interval.register, interval.end)
}
return packing
}
private fun typesCompatible(t1: IRDataType, t2: IRDataType): Boolean {
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(
slot: VirtualRegister,
register: VirtualRegister,
conflictGraph: Map<VirtualRegister, Set<VirtualRegister>>,
packing: Map<VirtualRegister, VirtualRegister>
): Boolean {
val conflictingRegs = conflictGraph[register] ?: return false
for (cr in conflictingRegs) {
val crSlot = packing[cr]
if (crSlot != null && crSlot == slot)
return true
if (cr !in packing && cr == slot)
return true
}
return false
}
private fun rewrite(sub: IRSubroutine, packing: Map<VirtualRegister, VirtualRegister>) {
for (chunk in sub.chunks) {
if (chunk !is IRCodeChunk)
continue
for (i in chunk.instructions.indices) {
val instr = chunk.instructions[i]
val newInstr = instr.mapRegisters { vr -> packing[vr] ?: vr }
if (newInstr != instr)
chunk.instructions[i] = newInstr
}
}
}
}
@@ -14,6 +14,11 @@ internal class RegisterPool {
private val registerTypes: MutableMap<RegisterNum, IRDataType> = mutableMapOf()
fun getTypes(): Map<RegisterNum, IRDataType> = registerTypes
fun resetTypes(types: Map<RegisterNum, IRDataType>) {
// called after register packing
registerTypes.clear()
registerTypes.putAll(types)
}
init {
for(i in 99000..99099)
@@ -2,8 +2,8 @@ package prog8.codegen.intermediate
import prog8.code.*
import prog8.code.ast.PtStructField
import prog8.code.core.Position
import prog8.intermediate.*
import kotlin.collections.flatMap
/**
* Converter object for transforming SymbolTable nodes to IR symbol table nodes.
@@ -11,7 +11,7 @@ import kotlin.collections.flatMap
*/
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) }
@@ -77,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 {
@@ -112,13 +112,14 @@ private class StToIrConverter(val romable: Boolean) {
constant.name
}
}
val noPrefix = constant.astNode?.definingBlock()?.options?.noSymbolPrefixing == true
if(constant.value != null)
return IRStConstant(scopedName, constant.dt, constant.value)
return IRStConstant(scopedName, constant.dt, constant.value, noPrefix = noPrefix)
else if(constant.memorySlab != null) {
// For memory() constants, store reference to slab name
// The actual address will be resolved during code generation
val slabName = constant.memorySlab!!.name
return IRStConstant(scopedName, constant.dt, null, slabName)
return IRStConstant(scopedName, constant.dt, null, slabName, noPrefix)
}
else
TODO("constant without value or memory slab: $constant")
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,54 @@
package prog8.codegen.intermediate
import prog8.code.ast.*
internal data class StructArrayIndexInfo(
val arrayName: String,
val idxExpr: PtExpression,
val structSize: Int,
val fieldOffset: Int
)
internal fun extractStructArrayIndexInfo(
addressExpr: PtExpression,
codeGen: IRCodeGen
): StructArrayIndexInfo? {
fun unwrapCast(expr: PtExpression): PtExpression {
var cur = expr
while(cur is PtTypeCast) cur = cur.value
return cur
}
fun asAddressOf(expr: PtExpression): PtAddressOf? {
return unwrapCast(expr) as? PtAddressOf
}
val baseAddrOf: PtAddressOf
val fieldOffset: Int
val unwrapped = unwrapCast(addressExpr)
when (unwrapped) {
is PtAddressOf -> {
baseAddrOf = unwrapped
fieldOffset = 0
}
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
val arrayIdent = baseAddrOf.identifier ?: return null
val idxExpr = baseAddrOf.arrayIndexExpr ?: return null
if(idxExpr is PtNumber) return null
val arrayType = arrayIdent.type
if(!arrayType.isArray) return null
val elemType = arrayType.elementType()
if(!elemType.isStructInstance) return null
val structSize = codeGen.program.memsizer.memorySize(elemType, null)
if(structSize !in 1..65535) return null
if(fieldOffset !in 0..65535) return null
return StructArrayIndexInfo(arrayIdent.name, idxExpr, structSize, fieldOffset)
}
@@ -2,7 +2,10 @@ package prog8.codegen.intermediate
import prog8.code.*
import prog8.code.ast.*
import prog8.code.core.*
import prog8.code.core.AssemblyError
import prog8.code.core.CompilationOptions
import prog8.code.core.DataType
import prog8.code.core.ISubType
/**
@@ -76,7 +79,7 @@ fun prefixSymbols(program: PtProgram, options: CompilationOptions, st: SymbolTab
}
}
var lookupName = node.name
if(node.type.isSplitWordArray && (lookupName.endsWith("_lsb") || lookupName.endsWith("_msb"))) {
if(node.type.isSplitWordArray(program.memsizer) && (lookupName.endsWith("_lsb") || lookupName.endsWith("_msb"))) {
lookupName = lookupName.dropLast(4)
}
val stNode = st.lookup(lookupName) ?:
@@ -120,7 +123,7 @@ fun prefixSymbols(program: PtProgram, options: CompilationOptions, st: SymbolTab
}
else if (node is PtIdentifier) {
var lookupName = node.name
if(node.type.isSplitWordArray && (lookupName.endsWith("_lsb") || lookupName.endsWith("_msb"))) {
if(node.type.isSplitWordArray(program.memsizer) && (lookupName.endsWith("_lsb") || lookupName.endsWith("_msb"))) {
lookupName = lookupName.dropLast(4)
}
val stNode = st.lookup(lookupName) ?: throw AssemblyError("unknown identifier $node")
@@ -193,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
@@ -203,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(".")
}
@@ -226,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
@@ -253,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, zeropage, align, dirty, newValue, arraySize, position)
val result = PtVariable(name, type, isSplitWordArray, zeropage, align, dirty, newValue, arraySize, position, shared)
result.parent = parent
result
}
@@ -287,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
@@ -1,14 +1,14 @@
package prog8.codegen.vm
import prog8.code.IAssemblyProgram
import prog8.code.ICodeGeneratorBackend
import prog8.code.SymbolTable
import prog8.code.assembly.IAssemblyProgram
import prog8.code.ast.PtProgram
import prog8.code.core.CompilationOptions
import prog8.code.core.IErrorReporter
import prog8.code.core.OutputFiles
import prog8.codegen.intermediate.IRCodeGen
import prog8.intermediate.IRFileWriter
import prog8.intermediate.IRProgram
import prog8.intermediate.*
class VmCodeGen(val retainSSA: Boolean,
private val preassignedCallSiteIds: Map<String, UByte> = emptyMap()
@@ -22,7 +22,14 @@ class VmCodeGen(val retainSSA: Boolean,
val irCodeGen = IRCodeGen(program, symbolTable, options, errors, retainSSA, preassignedCallSiteIds)
val irProgram = irCodeGen.generate()
irProgram.verifyRegisterTypes(irCodeGen.registerTypes())
val virtualRegisterTypes = irCodeGen.registerTypes().entries.associate { (num, type) ->
val register: VirtualRegister = if (type == IRDataType.FLOAT) VirtualRegister.float(num.value) else VirtualRegister.int(num.value)
register to type
}
irProgram.verifyRegisterTypes(virtualRegisterTypes)
if (options.dumpVariables)
dumpVariables(irProgram)
return VmAssemblyProgram(irProgram.name, irProgram)
}
@@ -44,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
}
}
+483 -131
View File
@@ -10,6 +10,8 @@ import prog8.intermediate.*
import prog8tests.helpers.ErrorReporterForTests
class TestIRPeepholeOpt: FunSpec({
val indexRegType = VMTarget().indexRegType
fun makeIRProgram(chunks: List<IRCodeChunkBase>): IRProgram {
require(chunks.first().label=="p8b_main.p8s_start")
val block = IRBlock("p8b_main", false, IRBlock.Options(), Position.DUMMY)
@@ -40,9 +42,9 @@ class TestIRPeepholeOpt: FunSpec({
test("remove nops") {
val irProg = makeIRProgram(listOf(
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=1, immediate=42),
IRInstruction(Opcode.NOP),
IRInstruction(Opcode.NOP)
IRInstructions.load(IRDataType.BYTE, 1, 42),
IRInstructions.simple(Opcode.NOP),
IRInstructions.simple(Opcode.NOP)
))
irProg.chunks().single().instructions.size shouldBe 3
val opt = IRPeepholeOptimizer(irProg, false)
@@ -52,13 +54,13 @@ class TestIRPeepholeOpt: FunSpec({
test("remove jmp to label below but keep labels") {
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
c1 += IRInstruction(Opcode.JUMP, labelSymbol = "label")
c1 += IRInstructions.jump(codeLabel("label"))
val c2 = IRCodeChunk("label", null)
c2 += IRInstruction(Opcode.JUMP, labelSymbol = "label2")
c2 += IRInstruction(Opcode.NOP) // removed
c2 += IRInstructions.jump(codeLabel("label2"))
c2 += IRInstructions.simple(Opcode.NOP) // removed
val c3 = IRCodeChunk("label2", null)
c3 += IRInstruction(Opcode.JUMP, labelSymbol = "label3")
c3 += IRInstruction(Opcode.INC, IRDataType.BYTE, reg1=1)
c3 += IRInstructions.jump(codeLabel("label3"))
c3 += IRInstructions.unary(Opcode.INC, IRDataType.BYTE, 1)
val c4 = IRCodeChunk("label3", null)
val irProg = makeIRProgram(listOf(c1, c2, c3, c4))
@@ -84,18 +86,18 @@ class TestIRPeepholeOpt: FunSpec({
test("remove double sec/clc/sei/cli") {
val irProg = makeIRProgram(listOf(
IRInstruction(Opcode.SEC),
IRInstruction(Opcode.SEC),
IRInstruction(Opcode.SEC),
IRInstruction(Opcode.CLC),
IRInstruction(Opcode.CLC),
IRInstruction(Opcode.CLC),
IRInstruction(Opcode.SEI),
IRInstruction(Opcode.SEI),
IRInstruction(Opcode.SEI),
IRInstruction(Opcode.CLI),
IRInstruction(Opcode.CLI),
IRInstruction(Opcode.CLI),
IRInstructions.simple(Opcode.SEC),
IRInstructions.simple(Opcode.SEC),
IRInstructions.simple(Opcode.SEC),
IRInstructions.simple(Opcode.CLC),
IRInstructions.simple(Opcode.CLC),
IRInstructions.simple(Opcode.CLC),
IRInstructions.simple(Opcode.SEI),
IRInstructions.simple(Opcode.SEI),
IRInstructions.simple(Opcode.SEI),
IRInstructions.simple(Opcode.CLI),
IRInstructions.simple(Opcode.CLI),
IRInstructions.simple(Opcode.CLI),
))
irProg.chunks().single().instructions.size shouldBe 12
val opt = IRPeepholeOptimizer(irProg, false)
@@ -108,18 +110,18 @@ class TestIRPeepholeOpt: FunSpec({
test("remove double sec/clc/sei/cli reversed") {
val irProg = makeIRProgram(listOf(
IRInstruction(Opcode.CLC),
IRInstruction(Opcode.CLC),
IRInstruction(Opcode.CLC),
IRInstruction(Opcode.SEC),
IRInstruction(Opcode.SEC),
IRInstruction(Opcode.SEC),
IRInstruction(Opcode.CLI),
IRInstruction(Opcode.CLI),
IRInstruction(Opcode.CLI),
IRInstruction(Opcode.SEI),
IRInstruction(Opcode.SEI),
IRInstruction(Opcode.SEI),
IRInstructions.simple(Opcode.CLC),
IRInstructions.simple(Opcode.CLC),
IRInstructions.simple(Opcode.CLC),
IRInstructions.simple(Opcode.SEC),
IRInstructions.simple(Opcode.SEC),
IRInstructions.simple(Opcode.SEC),
IRInstructions.simple(Opcode.CLI),
IRInstructions.simple(Opcode.CLI),
IRInstructions.simple(Opcode.CLI),
IRInstructions.simple(Opcode.SEI),
IRInstructions.simple(Opcode.SEI),
IRInstructions.simple(Opcode.SEI),
))
irProg.chunks().single().instructions.size shouldBe 12
val opt = IRPeepholeOptimizer(irProg, false)
@@ -132,10 +134,10 @@ class TestIRPeepholeOpt: FunSpec({
test("push followed by pop") {
val irProg = makeIRProgram(listOf(
IRInstruction(Opcode.PUSH, IRDataType.BYTE, reg1=42),
IRInstruction(Opcode.POP, IRDataType.BYTE, reg1=42),
IRInstruction(Opcode.PUSH, IRDataType.BYTE, reg1=99),
IRInstruction(Opcode.POP, IRDataType.BYTE, reg1=222)
IRInstructions.push(IRDataType.BYTE, 42),
IRInstructions.pop(IRDataType.BYTE, 42),
IRInstructions.push(IRDataType.BYTE, 99),
IRInstructions.pop(IRDataType.BYTE, 222)
))
irProg.chunks().single().instructions.size shouldBe 4
val opt = IRPeepholeOptimizer(irProg, false)
@@ -143,23 +145,23 @@ class TestIRPeepholeOpt: FunSpec({
val instr = irProg.chunks().single().instructions
instr.size shouldBe 1
instr[0].opcode shouldBe Opcode.LOADR
instr[0].reg1 shouldBe 222
instr[0].reg2 shouldBe 99
instr[0].requireDest().register shouldBe VirtualRegister.int(222)
instr[0].requireSrcA().register shouldBe VirtualRegister.int(99)
}
test("remove useless div/mul, add/sub") {
// Use different registers for each test case to avoid dead store elimination removing them
val irProg = makeIRProgram(listOf(
IRInstruction(Opcode.DIV, IRDataType.BYTE, reg1=1, immediate = 1),
IRInstruction(Opcode.DIVS, IRDataType.BYTE, reg1=2, immediate = 1),
IRInstruction(Opcode.MUL, IRDataType.BYTE, reg1=3, immediate = 1),
IRInstruction(Opcode.MOD, IRDataType.BYTE, reg1=4, immediate = 1),
IRInstruction(Opcode.DIV, IRDataType.BYTE, reg1=5, immediate = 2),
IRInstruction(Opcode.DIVS, IRDataType.BYTE, reg1=6, immediate = 2),
IRInstruction(Opcode.MUL, IRDataType.BYTE, reg1=7, immediate = 2),
IRInstruction(Opcode.MOD, IRDataType.BYTE, reg1=8, immediate = 2),
IRInstruction(Opcode.ADD, IRDataType.BYTE, reg1=9, immediate = 0),
IRInstruction(Opcode.SUB, IRDataType.BYTE, reg1=10, immediate = 0)
IRInstructions.binaryImmediate(Opcode.DIV, IRDataType.BYTE, 1, 1),
IRInstructions.binaryImmediate(Opcode.DIVS, IRDataType.BYTE, 2, 1),
IRInstructions.binaryImmediate(Opcode.MUL, IRDataType.BYTE, 3, 1),
IRInstructions.binaryImmediate(Opcode.MOD, IRDataType.BYTE, 4, 1),
IRInstructions.binaryImmediate(Opcode.DIV, IRDataType.BYTE, 5, 2),
IRInstructions.binaryImmediate(Opcode.DIVS, IRDataType.BYTE, 6, 2),
IRInstructions.binaryImmediate(Opcode.MUL, IRDataType.BYTE, 7, 2),
IRInstructions.binaryImmediate(Opcode.MOD, IRDataType.BYTE, 8, 2),
IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.BYTE, 9, 0),
IRInstructions.binaryImmediate(Opcode.SUB, IRDataType.BYTE, 10, 0)
))
irProg.chunks().single().instructions.size shouldBe 10
val opt = IRPeepholeOptimizer(irProg, false)
@@ -168,11 +170,66 @@ class TestIRPeepholeOpt: FunSpec({
irProg.chunks().single().instructions.size shouldBe 4
}
test("replace integer multiply by zero with load") {
val irProg = makeIRProgram(listOf(
IRInstructions.binaryImmediate(Opcode.MUL, IRDataType.BYTE, 1, 0),
IRInstructions.binaryImmediate(Opcode.MULS, IRDataType.WORD, 2, 0),
IRInstructions.binaryImmediate(Opcode.MULS, IRDataType.LONG, 3, 0),
IRInstructions.binaryImmediateFloat(Opcode.MUL, 4, 0.0),
IRInstructions.returnVoid()
))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.count { it.opcode == Opcode.LOAD } shouldBe 3
instr.count { it.opcode == Opcode.MUL } shouldBe 1
}
test("fold adjacent integer immediate multiplications") {
val irProg = makeIRProgram(listOf(
IRInstructions.binaryImmediate(Opcode.MUL, IRDataType.BYTE, 1, 7),
IRInstructions.binaryImmediate(Opcode.MUL, IRDataType.BYTE, 1, 9),
IRInstructions.binaryImmediate(Opcode.MULS, IRDataType.WORD, 2, -3),
IRInstructions.binaryImmediate(Opcode.MULS, IRDataType.WORD, 2, 4),
IRInstructions.binaryImmediate(Opcode.MULS, IRDataType.LONG, 3, 100000),
IRInstructions.binaryImmediate(Opcode.MULS, IRDataType.LONG, 3, 30000),
IRInstructions.returnVoid()
))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.size shouldBe 4
instr[0].requireImmediateInt() shouldBe 63
instr[1].requireImmediateInt() shouldBe 65524
instr[2].requireImmediateInt() shouldBe 3000000000L.toInt()
}
test("collapse adjacent integer and float loadr chains") {
val irProg = makeIRProgram(listOf(
IRInstructions.move(IRDataType.BYTE, 2, 1),
IRInstructions.move(IRDataType.BYTE, 3, 2),
IRInstructions.move(IRDataType.FLOAT, 5, 4),
IRInstructions.move(IRDataType.FLOAT, 6, 5),
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 3, IRMemory.direct(MemoryAddress(100u))),
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.FLOAT, 6, IRMemory.direct(MemoryAddress(200u)))
))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.map { it.opcode } shouldBe listOf(Opcode.LOADR, Opcode.LOADR, Opcode.STOREM, Opcode.STOREM)
instr[0].requireDest().register shouldBe VirtualRegister.int(3)
instr[0].requireSrcA().register shouldBe VirtualRegister.int(1)
instr[1].requireDest().register shouldBe VirtualRegister.float(6)
instr[1].requireSrcA().register shouldBe VirtualRegister.float(4)
instr[2].requireSrcA().register shouldBe VirtualRegister.int(3)
instr[3].requireSrcA().register shouldBe VirtualRegister.float(6)
}
test("replace add/sub 1 by inc/dec") {
// Use different registers for each test case to avoid dead store elimination removing them
val irProg = makeIRProgram(listOf(
IRInstruction(Opcode.ADD, IRDataType.BYTE, reg1=1, immediate = 1),
IRInstruction(Opcode.SUB, IRDataType.BYTE, reg1=2, immediate = 1)
IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.BYTE, 1, 1),
IRInstructions.binaryImmediate(Opcode.SUB, IRDataType.BYTE, 2, 1)
))
irProg.chunks().single().instructions.size shouldBe 2
val opt = IRPeepholeOptimizer(irProg, false)
@@ -186,23 +243,23 @@ class TestIRPeepholeOpt: FunSpec({
test("remove useless and/or/xor") {
// Use different registers for each test case to avoid dead store elimination removing them
val irProg = makeIRProgram(listOf(
IRInstruction(Opcode.AND, IRDataType.BYTE, reg1=1, immediate = 0),
IRInstruction(Opcode.AND, IRDataType.WORD, reg1=2, immediate = 0),
IRInstruction(Opcode.AND, IRDataType.LONG, reg1=3, immediate = 0),
IRInstruction(Opcode.AND, IRDataType.BYTE, reg1=4, immediate = 255),
IRInstruction(Opcode.AND, IRDataType.WORD, reg1=5, immediate = 65535),
IRInstruction(Opcode.AND, IRDataType.LONG, reg1=6, immediate = 2147483647),
IRInstruction(Opcode.AND, IRDataType.LONG, reg1=7, immediate = -1),
IRInstruction(Opcode.OR, IRDataType.BYTE, reg1=8, immediate = 0),
IRInstruction(Opcode.OR, IRDataType.BYTE, reg1=9, immediate = 255),
IRInstruction(Opcode.OR, IRDataType.WORD, reg1=10, immediate = 65535),
IRInstruction(Opcode.OR, IRDataType.LONG, reg1=11, immediate = 2147483647),
IRInstruction(Opcode.OR, IRDataType.LONG, reg1=12, immediate = -1),
IRInstruction(Opcode.XOR, IRDataType.BYTE, reg1=13, immediate = 0),
IRInstruction(Opcode.AND, IRDataType.BYTE, reg1=14, immediate = 200),
IRInstruction(Opcode.AND, IRDataType.WORD, reg1=15, immediate = 60000),
IRInstruction(Opcode.OR, IRDataType.BYTE, reg1=16, immediate = 1),
IRInstruction(Opcode.XOR, IRDataType.BYTE, reg1=17, immediate = 1)
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.BYTE, 1, 0),
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.WORD, 2, 0),
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.LONG, 3, 0),
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.BYTE, 4, 255),
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.WORD, 5, 65535),
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.LONG, 6, 2147483647),
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.LONG, 7, -1),
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.BYTE, 8, 0),
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.BYTE, 9, 255),
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.WORD, 10, 65535),
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.LONG, 11, 2147483647),
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.LONG, 12, -1),
IRInstructions.binaryImmediate(Opcode.XOR, IRDataType.BYTE, 13, 0),
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.BYTE, 14, 200),
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.WORD, 15, 60000),
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.BYTE, 16, 1),
IRInstructions.binaryImmediate(Opcode.XOR, IRDataType.BYTE, 17, 1)
))
irProg.chunks().single().instructions.size shouldBe 17
val opt = IRPeepholeOptimizer(irProg, false)
@@ -223,40 +280,86 @@ class TestIRPeepholeOpt: FunSpec({
test("replace and/or/xor by constant number") {
// Use different registers for each test case to avoid dead store elimination removing them
val irProg = makeIRProgram(listOf(
IRInstruction(Opcode.AND, IRDataType.BYTE, reg1=1, immediate = 0),
IRInstruction(Opcode.AND, IRDataType.WORD, reg1=2, immediate = 0),
IRInstruction(Opcode.AND, IRDataType.LONG, reg1=3, immediate = 0),
IRInstruction(Opcode.OR, IRDataType.BYTE, reg1=4, immediate = 255),
IRInstruction(Opcode.OR, IRDataType.WORD, reg1=5, immediate = 65535),
IRInstruction(Opcode.OR, IRDataType.LONG, reg1=6, immediate = -1)
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.BYTE, 1, 0),
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.WORD, 2, 0),
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.LONG, 3, 0),
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.BYTE, 4, 255),
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.WORD, 5, 65535),
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.LONG, 6, -1)
))
irProg.chunks().single().instructions.size shouldBe 6
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.size shouldBe 6
instr.map { it.opcode } shouldBe List(6) { Opcode.LOAD }
instr.map { it.requireImmediateInt() } shouldBe listOf(0, 0, 0, 255, 65535, -1)
}
test("combine adjacent immediate and/or operations") {
val irProg = makeIRProgram(listOf(
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.BYTE, 1, 0xf0),
IRInstructions.binaryImmediate(Opcode.AND, IRDataType.BYTE, 1, 0xcc),
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.WORD, 2, 0x0010),
IRInstructions.binaryImmediate(Opcode.OR, IRDataType.WORD, 2, 0x0003),
))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.size shouldBe 2
instr[0].opcode shouldBe Opcode.AND
instr[0].requireImmediateInt() shouldBe 0xc0
instr[1].opcode shouldBe Opcode.OR
instr[1].requireImmediateInt() shouldBe 0x0013
}
test("cancel identical adjacent immediate xor operations") {
val irProg = makeIRProgram(listOf(
IRInstructions.binaryImmediate(Opcode.XOR, IRDataType.BYTE, 1, 3),
IRInstructions.binaryImmediate(Opcode.XOR, IRDataType.BYTE, 1, 3),
IRInstructions.load(IRDataType.BYTE, 2, 42)
))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.size shouldBe 1
instr[0].opcode shouldBe Opcode.LOAD
instr[1].opcode shouldBe Opcode.LOAD
instr[2].opcode shouldBe Opcode.LOAD
instr[3].opcode shouldBe Opcode.LOAD
instr[4].opcode shouldBe Opcode.LOAD
instr[5].opcode shouldBe Opcode.LOAD
instr[0].immediate shouldBe 0
instr[1].immediate shouldBe 0
instr[2].immediate shouldBe 0
instr[3].immediate shouldBe 255
instr[4].immediate shouldBe 65535
instr[5].immediate shouldBe -1
instr[0].requireImmediateInt() shouldBe 42
}
test("fold adjacent immediate arithmetic with inc/dec") {
val irProg = makeIRProgram(listOf(
IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.BYTE, 1, 4),
IRInstructions.unary(Opcode.INC, IRDataType.BYTE, 1),
IRInstructions.unary(Opcode.INC, IRDataType.BYTE, 2),
IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.BYTE, 2, 4),
IRInstructions.binaryImmediate(Opcode.SUB, IRDataType.BYTE, 3, 4),
IRInstructions.unary(Opcode.DEC, IRDataType.BYTE, 3),
IRInstructions.unary(Opcode.DEC, IRDataType.BYTE, 4),
IRInstructions.binaryImmediate(Opcode.SUB, IRDataType.BYTE, 4, 4)
))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.size shouldBe 4
instr[0].opcode shouldBe Opcode.ADD
instr[0].requireImmediateInt() shouldBe 5
instr[1].opcode shouldBe Opcode.ADD
instr[1].requireImmediateInt() shouldBe 5
instr[2].opcode shouldBe Opcode.SUB
instr[2].requireImmediateInt() shouldBe 5
instr[3].opcode shouldBe Opcode.SUB
instr[3].requireImmediateInt() shouldBe 5
}
test("replace register arithmetic by immediate operations") {
// Test the new optimization: LOAD + SUBR/DIVR/MODR → immediate operation
// Pattern: LOAD r1, #const followed by SUBR r2, r1 → SUB r2, #const
// SUBR optimization
val subrProg = makeIRProgram(listOf(
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=1, immediate = 5),
IRInstruction(Opcode.SUBR, IRDataType.BYTE, reg1=2, reg2=1)
IRInstructions.load(IRDataType.BYTE, 1, 5),
IRInstructions.binary(Opcode.SUBR, IRDataType.BYTE, 2, 1)
))
subrProg.chunks().single().instructions.size shouldBe 2
val opt1 = IRPeepholeOptimizer(subrProg, false)
@@ -264,12 +367,12 @@ class TestIRPeepholeOpt: FunSpec({
val subInstr = subrProg.chunks().single().instructions
subInstr.size shouldBe 1
subInstr[0].opcode shouldBe Opcode.SUB
subInstr[0].immediate shouldBe 5
subInstr[0].requireImmediateInt() shouldBe 5
// DIVR optimization
val divrProg = makeIRProgram(listOf(
IRInstruction(Opcode.LOAD, IRDataType.WORD, reg1=10, immediate = 100),
IRInstruction(Opcode.DIVR, IRDataType.WORD, reg1=20, reg2=10)
IRInstructions.load(IRDataType.WORD, 10, 100),
IRInstructions.binary(Opcode.DIVR, IRDataType.WORD, 20, 10)
))
divrProg.chunks().single().instructions.size shouldBe 2
val opt2 = IRPeepholeOptimizer(divrProg, false)
@@ -277,12 +380,12 @@ class TestIRPeepholeOpt: FunSpec({
val divInstr = divrProg.chunks().single().instructions
divInstr.size shouldBe 1
divInstr[0].opcode shouldBe Opcode.DIV
divInstr[0].immediate shouldBe 100
divInstr[0].requireImmediateInt() shouldBe 100
// MODR optimization
val modrProg = makeIRProgram(listOf(
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=30, immediate = 7),
IRInstruction(Opcode.MODR, IRDataType.BYTE, reg1=40, reg2=30)
IRInstructions.load(IRDataType.BYTE, 30, 7),
IRInstructions.binary(Opcode.MODR, IRDataType.BYTE, 40, 30)
))
modrProg.chunks().single().instructions.size shouldBe 2
val opt3 = IRPeepholeOptimizer(modrProg, false)
@@ -290,34 +393,42 @@ class TestIRPeepholeOpt: FunSpec({
val modInstr = modrProg.chunks().single().instructions
modInstr.size shouldBe 1
modInstr[0].opcode shouldBe Opcode.MOD
modInstr[0].immediate shouldBe 7
modInstr[0].requireImmediateInt() shouldBe 7
}
test("remove self identity operations") {
// LOADR with same fp register is allowed (constructor only blocks integer reg1==reg2)
// The IR is not SSA: a written register may also be a read-only source register.
// Self-identity LOADR/ANDR/ORR become NOP, self-identity XORR becomes load 0.
val irProg = makeIRProgram(listOf(
IRInstruction(Opcode.LOADR, IRDataType.FLOAT, fpReg1=RegisterNum(10), fpReg2=RegisterNum(10)),
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=99, immediate=42) // keep chunk non-empty
IRInstructions.move(IRDataType.FLOAT, 10, 10),
IRInstructions.move(IRDataType.BYTE, 11, 11),
IRInstructions.binary(Opcode.ANDR, IRDataType.BYTE, 12, 12),
IRInstructions.binary(Opcode.ORR, IRDataType.BYTE, 13, 13),
IRInstructions.binary(Opcode.XORR, IRDataType.BYTE, 14, 14),
IRInstructions.load(IRDataType.BYTE, 99, 42) // keep chunk non-empty
))
irProg.chunks().single().instructions.size shouldBe 2
irProg.chunks().single().instructions.size shouldBe 6
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.size shouldBe 1
val instr = irProg.chunks().single().instructions.filter { it.opcode != Opcode.NOP }
instr.size shouldBe 2
instr[0].opcode shouldBe Opcode.LOAD
instr[0].reg1 shouldBe 99
instr[0].immediate shouldBe 42
instr[0].requireDest().register shouldBe VirtualRegister.int(14)
instr[0].requireImmediateInt() shouldBe 0
instr[1].opcode shouldBe Opcode.LOAD
instr[1].requireDest().register shouldBe VirtualRegister.int(99)
instr[1].requireImmediateInt() shouldBe 42
}
test("remove shift by zero") {
val irProg = makeIRProgram(listOf(
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=10, immediate=0),
IRInstruction(Opcode.LSLN, IRDataType.BYTE, reg1=1, reg2=10),
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=11, immediate=0),
IRInstruction(Opcode.LSRN, IRDataType.BYTE, reg1=2, reg2=11),
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=12, immediate=0),
IRInstruction(Opcode.ASRN, IRDataType.BYTE, reg1=3, reg2=12),
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=99, immediate=42) // keep chunk non-empty
IRInstructions.load(IRDataType.BYTE, 10, 0),
IRInstructions.binary(Opcode.LSLN, IRDataType.BYTE, 1, 10),
IRInstructions.load(IRDataType.BYTE, 11, 0),
IRInstructions.binary(Opcode.LSRN, IRDataType.BYTE, 2, 11),
IRInstructions.load(IRDataType.BYTE, 12, 0),
IRInstructions.binary(Opcode.ASRN, IRDataType.BYTE, 3, 12),
IRInstructions.load(IRDataType.BYTE, 99, 42) // keep chunk non-empty
))
irProg.chunks().single().instructions.size shouldBe 7
val opt = IRPeepholeOptimizer(irProg, false)
@@ -325,25 +436,68 @@ class TestIRPeepholeOpt: FunSpec({
val instr = irProg.chunks().single().instructions
instr.size shouldBe 1
instr[0].opcode shouldBe Opcode.LOAD
instr[0].reg1 shouldBe 99
instr[0].immediate shouldBe 42
instr[0].requireDest().register shouldBe VirtualRegister.int(99)
instr[0].requireImmediateInt() shouldBe 42
}
test("replace concat with ext after harmless instruction") {
val irProg = makeIRProgram(listOf(
IRInstructions.load(IRDataType.BYTE, 10, 0),
IRInstructions.unary(Opcode.INC, IRDataType.BYTE, 20),
IRInstructions.concat(IRDataType.BYTE, 1, 10, 11),
IRInstructions.load(IRDataType.BYTE, 99, 42)
))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.any {
it.opcode == Opcode.EXT &&
it.requireDest().register == VirtualRegister.int(1) &&
it.requireSrcA().register == VirtualRegister.int(11)
} shouldBe true
instr.any { it.opcode == Opcode.CONCAT } shouldBe false
}
test("do not replace concat after intervening write") {
val irProg = makeIRProgram(listOf(
IRInstructions.load(IRDataType.BYTE, 10, 0),
IRInstructions.unary(Opcode.INC, IRDataType.BYTE, 10),
IRInstructions.concat(IRDataType.BYTE, 1, 10, 11),
IRInstructions.load(IRDataType.BYTE, 99, 42)
))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
irProg.chunks().single().instructions.any { it.opcode == Opcode.CONCAT } shouldBe true
}
test("do not replace concat across control flow") {
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
c1 += IRInstructions.load(IRDataType.BYTE, 10, 0)
c1 += IRInstructions.jump(codeLabel("after"))
c1 += IRInstructions.concat(IRDataType.BYTE, 1, 10, 11)
c1 += IRInstructions.load(IRDataType.BYTE, 99, 42)
val c2 = IRCodeChunk("after", null)
val irProg = makeIRProgram(listOf(c1, c2))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
irProg.chunks().flatMap { it.instructions }.any { it.opcode == Opcode.CONCAT } shouldBe true
}
test("cancel adjacent ops") {
val irProg = makeIRProgram(listOf(
IRInstruction(Opcode.INV, IRDataType.BYTE, reg1=1),
IRInstruction(Opcode.INV, IRDataType.BYTE, reg1=1),
IRInstruction(Opcode.NEG, IRDataType.BYTE, reg1=2),
IRInstruction(Opcode.NEG, IRDataType.BYTE, reg1=2),
IRInstruction(Opcode.EXT, IRDataType.BYTE, reg1=3, reg2=33),
IRInstruction(Opcode.EXT, IRDataType.BYTE, reg1=3, reg2=33),
IRInstruction(Opcode.EXTS, IRDataType.WORD, reg1=4, reg2=44),
IRInstruction(Opcode.EXTS, IRDataType.WORD, reg1=4, reg2=44),
IRInstruction(Opcode.INC, IRDataType.BYTE, reg1=5),
IRInstruction(Opcode.DEC, IRDataType.BYTE, reg1=5),
IRInstruction(Opcode.DEC, IRDataType.BYTE, reg1=6),
IRInstruction(Opcode.INC, IRDataType.BYTE, reg1=6),
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=99, immediate=42) // keep chunk non-empty
IRInstructions.unary(Opcode.INV, IRDataType.BYTE, 1),
IRInstructions.unary(Opcode.INV, IRDataType.BYTE, 1),
IRInstructions.unary(Opcode.NEG, IRDataType.BYTE, 2),
IRInstructions.unary(Opcode.NEG, IRDataType.BYTE, 2),
IRInstructions.binary(Opcode.EXT, IRDataType.BYTE, 3, 33),
IRInstructions.binary(Opcode.EXT, IRDataType.BYTE, 3, 33),
IRInstructions.binary(Opcode.EXTS, IRDataType.WORD, 4, 44),
IRInstructions.binary(Opcode.EXTS, IRDataType.WORD, 4, 44),
IRInstructions.unary(Opcode.INC, IRDataType.BYTE, 5),
IRInstructions.unary(Opcode.DEC, IRDataType.BYTE, 5),
IRInstructions.unary(Opcode.DEC, IRDataType.BYTE, 6),
IRInstructions.unary(Opcode.INC, IRDataType.BYTE, 6),
IRInstructions.load(IRDataType.BYTE, 99, 42) // keep chunk non-empty
))
irProg.chunks().single().instructions.size shouldBe 13
val opt = IRPeepholeOptimizer(irProg, false)
@@ -351,12 +505,210 @@ class TestIRPeepholeOpt: FunSpec({
val instr = irProg.chunks().single().instructions
instr.size shouldBe 3
instr[0].opcode shouldBe Opcode.EXT
instr[0].reg1 shouldBe 3
instr[0].reg2 shouldBe 33
instr[0].requireDest().register shouldBe VirtualRegister.int(3)
instr[0].requireSrcA().register shouldBe VirtualRegister.int(33)
instr[1].opcode shouldBe Opcode.EXTS
instr[1].reg1 shouldBe 4
instr[1].reg2 shouldBe 44
instr[1].requireDest().register shouldBe VirtualRegister.int(4)
instr[1].requireSrcA().register shouldBe VirtualRegister.int(44)
instr[2].opcode shouldBe Opcode.LOAD
instr[2].immediate shouldBe 42
instr[2].requireImmediateInt() shouldBe 42
}
test("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)),
IRInstructions.unary(Opcode.INC, IRDataType.BYTE, 99),
IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 10, indexRegType)),
IRInstructions.returnVoid()
))
irProg.chunks().single().instructions.count { it.opcode==Opcode.STOREX } shouldBe 1
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.count { it.opcode==Opcode.STOREX } shouldBe 0
instr.count { it.opcode==Opcode.LOADX } shouldBe 1
instr.size shouldBe 3
}
test("do not coalesce LOADX/STOREX when index differs") {
val irProg = makeIRProgram(listOf(
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 10, indexRegType)),
IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 11, indexRegType)),
IRInstructions.returnVoid()
))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.count { it.opcode==Opcode.LOADX } shouldBe 1
instr.count { it.opcode==Opcode.STOREX } shouldBe 1
}
test("do not coalesce LOADX/STOREX with intervening read of value") {
val irProg = makeIRProgram(listOf(
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 10, indexRegType)),
IRInstructions.binary(Opcode.ADDR, IRDataType.BYTE, 1, 2),
IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 10, indexRegType)),
IRInstructions.returnVoid()
))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.count { it.opcode==Opcode.STOREX } shouldBe 1
}
test("do not coalesce LOADX/STOREX across chunks single-chunk") {
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
c1 += IRInstructions.loadMemory(Opcode.LOADX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 10, indexRegType))
val c2 = IRCodeChunk("other", null)
c2 += IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 10, indexRegType))
c2 += IRInstructions.returnVoid()
val irProg = makeIRProgram(listOf(c1, c2))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val all = irProg.chunks().flatMap { it.instructions }
all.count { it.opcode==Opcode.LOADX } shouldBe 1
all.count { it.opcode==Opcode.STOREX } shouldBe 1
}
test("do not fold storem+loadm same symbol different offsets") {
val irProg = makeIRProgram(listOf(
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 5, IRMemory.direct("myVar", 0)),
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 6, IRMemory.direct("myVar", 1)),
IRInstructions.returnVoid()
))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.count { it.opcode==Opcode.STOREM } shouldBe 1
instr.count { it.opcode==Opcode.LOADM } shouldBe 1
instr.count { it.opcode==Opcode.LOADR } shouldBe 0
}
test("do not fold storem+loadm across chunks single-chunk") {
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
c1 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 5, IRMemory.direct("myVar"))
val c2 = IRCodeChunk("other", null)
c2 += IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 6, IRMemory.direct("myVar"))
c2 += IRInstructions.returnVoid()
val irProg = makeIRProgram(listOf(c1, c2))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val all = irProg.chunks().flatMap { it.instructions }
all.count { it.opcode==Opcode.STOREM } shouldBe 1
all.count { it.opcode==Opcode.LOADM } shouldBe 1
all.count { it.opcode==Opcode.LOADR } shouldBe 0
}
test("do not fold storem+loadm different types") {
val irProg = makeIRProgram(listOf(
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 5, IRMemory.direct("myVar")),
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.WORD, 6, IRMemory.direct("myVar")),
IRInstructions.returnVoid()
))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.count { it.opcode==Opcode.STOREM } shouldBe 1
instr.count { it.opcode==Opcode.LOADM } shouldBe 1
instr.count { it.opcode==Opcode.LOADR } shouldBe 0
}
test("dead store analysis sees reads via secondary operand slots") {
// r10 is only read as the INDEX register inside the LOADX memory reference. The dead
// store detector must see that read, otherwise the first LOAD r10 is wrongly removed
// and the array would be indexed with an uninitialized register.
val irProg = makeIRProgram(listOf(
IRInstructions.load(indexRegType, 10, 3),
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 10, indexRegType)),
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 1, IRMemory.direct("sym")),
IRInstructions.load(indexRegType, 10, 7),
IRInstructions.binaryImmediate(Opcode.ADD, indexRegType, 10, 1),
IRInstructions.storeMemory(Opcode.STOREM, indexRegType, 10, IRMemory.direct("sym2")),
IRInstructions.returnVoid()
))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.count { it.opcode == Opcode.LOADX } shouldBe 1
instr.any { it.opcode == Opcode.LOAD && it.requireImmediateInt() == 3 } shouldBe true
}
test("dead store to the same register is removed") {
val irProg = makeIRProgram(listOf(
IRInstructions.load(IRDataType.BYTE, 1, 5),
IRInstructions.load(IRDataType.BYTE, 1, 10),
IRInstructions.unary(Opcode.INC, IRDataType.BYTE, 1),
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 1, IRMemory.direct("sym")),
IRInstructions.returnVoid()
))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.any { it.opcode == Opcode.LOAD && it.requireImmediateInt() == 5 } shouldBe false
instr.any { it.opcode == Opcode.LOAD && it.requireImmediateInt() == 10 } shouldBe true
}
test("loadr forwarding does not delete live-out source") {
// r1 is live-out to next chunk, so LOAD r1 should be kept even though r1 has single use in first chunk.
// The forwarder is conservative: it keeps the source live when it may be needed in a successor chunk,
// preventing the previous bug where the source LOAD was deleted.
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
c1 += IRInstructions.load(IRDataType.BYTE, 1, 42)
c1 += IRInstructions.move(IRDataType.BYTE, 2, 1)
c1 += IRInstructions.jump(codeLabel("next"))
val c2 = IRCodeChunk("next", null)
c2 += IRInstructions.move(IRDataType.BYTE, 3, 1)
c2 += IRInstructions.returnVoid()
val irProg = makeIRProgram(listOf(c1, c2))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val all = irProg.chunks().flatMap { it.instructions }
// LOAD r1 must be kept because r1 is live-out; whether LOADR is forwarded to LOAD r2 is secondary.
all.count { it.opcode == Opcode.LOAD && it.dest?.register == VirtualRegister.int(1) } shouldBe 1
}
})
@@ -0,0 +1,537 @@
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldContain
import prog8.code.core.CompilationOptions
import prog8.code.core.OutputType
import prog8.code.core.Position
import prog8.code.core.ZeropageType
import prog8.code.target.VMTarget
import prog8.codegen.intermediate.RegisterPacker
import prog8.intermediate.*
class TestRegisterPacker: FunSpec({
fun makeIRProgram(chunks: List<IRCodeChunkBase>): IRProgram {
require(chunks.first().label=="p8b_main.p8s_start")
val block = IRBlock("p8b_main", false, IRBlock.Options(), Position.DUMMY)
val sub = IRSubroutine("p8b_main.p8s_start", emptyList(), emptyList(), Position.DUMMY)
chunks.forEach { sub += it }
block += sub
val target = VMTarget()
val options = CompilationOptions.builder(target)
.output(OutputType.RAW)
.zeropage(ZeropageType.DONTUSE)
.noSysInit(true)
.compilerVersion("99.99")
.build()
val prog = IRProgram("test", IRSymbolTable(), options, target)
prog.addBlock(block)
prog.linkChunks()
prog.validate()
return prog
}
fun makeIRProgram(instructions: List<IRInstruction>): IRProgram {
val chunk = IRCodeChunk("p8b_main.p8s_start", null)
instructions.forEach { chunk += it }
return makeIRProgram(listOf(chunk as IRCodeChunkBase))
}
fun IRProgram.chunks(): List<IRCodeChunkBase> = this.allSubs().flatMap { it.chunks }.toList()
fun IRProgram.instructions(): List<IRInstruction> = chunks().flatMap { it.instructions }
fun IRProgram.registerAccesses(): List<RegisterOperand> = instructions().flatMap { it.registerAccesses }
test("simple coalescing") {
// r1=[0,1], r2=[2,3] non-overlapping -> same packed slot
val irProg = makeIRProgram(listOf(
IRInstructions.load(IRDataType.BYTE, 1, 10),
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 1, IRMemory.direct("sym")),
IRInstructions.load(IRDataType.BYTE, 2, 20),
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 2, IRMemory.direct("sym2"))
))
RegisterPacker.pack(irProg)
val regs = irProg.registerAccesses().map { it.register }
// After packing, all register accesses should refer to the same register (both r1 and r2 mapped to 1 slot)
regs.toSet().size shouldBe 1
val used = irProg.registersUsed()
(used.readRegs.keys + used.writeRegs.keys).size shouldBe 1
}
test("no coalescing with overlapping intervals") {
// r1=[0,3], r2=[1,4] overlap -> 2 distinct slots needed
// LOADR uses both at same time -> destination must differ from source
val irProg = makeIRProgram(listOf(
IRInstructions.load(IRDataType.BYTE, 1, 10),
IRInstructions.load(IRDataType.BYTE, 2, 20),
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 1, IRMemory.direct("s")),
IRInstructions.move(IRDataType.BYTE, 1, 2)
))
RegisterPacker.pack(irProg)
val instrs = irProg.instructions()
val loadr = instrs.first { it.opcode == Opcode.LOADR }
loadr.requireDest().register shouldNotBe loadr.requireSrcA().register
}
test("cross-chunk liveness") {
// Chunk A: def r1, def r2, use r1
// r1=[0,2] (lives only in A), r2=defined but not used in A,
// r2 is live-out from A because it's used in B
// Chunk B: def r3, use r2, use r3
// r2=[3,4] (first read in B), r3=[3,5] (lives only in B)
// Non-overlapping: r1=[0,2] and r2=[3,4] -> can share
// Overlapping: r2=[3,4] and r3=[3,5] -> must differ
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
c1 += IRInstructions.load(IRDataType.BYTE, 1, 10)
c1 += IRInstructions.load(IRDataType.BYTE, 2, 20)
c1 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 1, IRMemory.direct("sym"))
val c2 = IRCodeChunk(null, null)
c2 += IRInstructions.load(IRDataType.BYTE, 3, 30)
c2 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 2, IRMemory.direct("sym2"))
c2 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 3, IRMemory.direct("sym3"))
val irProg = makeIRProgram(listOf(c1, c2))
RegisterPacker.pack(irProg)
val distinctRegs = irProg.registerAccesses().map { it.register }.toSet()
// r1 and r2 can share a slot (non-overlapping), r3 must differ from r2 (overlapping)
// Results in exactly 2 distinct slots used
distinctRegs.size shouldBe 2
}
test("different types must not share the same slot number") {
// BYTE R5=[0,1] and POINTER R1=[2,3] in same subroutine.
// Non-overlapping but different incompatible types.
// Packer must NOT assign both to slot 1 (would cause type conflict).
val irProg = makeIRProgram(listOf(
IRInstructions.load(IRDataType.BYTE, 5, 10),
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 5, IRMemory.direct("s")),
IRInstructions.load(IRDataType.POINTER, 1, 0x1000),
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.POINTER, 1, IRMemory.direct("s2"))
))
RegisterPacker.pack(irProg)
// After packing, usedRegisters() must not throw (internal type consistency)
// and all registers must have exactly one type each
val chunkTypes = irProg.chunks().first().usedRegisters().regsTypes
chunkTypes.size shouldBe 2 // exactly 2 distinct registers
// Each register should appear with exactly one type (no conflicts)
chunkTypes.values.toSet().size shouldBe 2 // BYTE and POINTER, each on different regs
}
test("unconditional branch only one successor") {
// Chunk A -> unconditional jump -> chunk C
// Chunk B is unreachable from A (but still a predecessor of C via fall-through)
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
c1 += IRInstructions.load(IRDataType.BYTE, 1, 10)
c1 += IRInstructions.jump(codeLabel("label_c"))
val c2 = IRCodeChunk("label_b", null)
c2 += IRInstructions.load(IRDataType.BYTE, 2, 20)
val c3 = IRCodeChunk("label_c", null)
c3 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 1, IRMemory.direct("sym"))
c3 += IRInstructions.load(IRDataType.BYTE, 2, 99)
c3 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 2, IRMemory.direct("sym2"))
val irProg = makeIRProgram(listOf(c1, c2, c3))
RegisterPacker.pack(irProg)
// r1 is live-out of A into C via JUMP; r2 is NOT live-in to C from A via the branch
// but IS live-in to C from B's fall-through -> conservative: r2 is live-in to C
// r1 and r3 both defined across non-overlapping chunks
// Just verify no crash and packing happened
val instrs = irProg.instructions()
instrs shouldNotBe emptyList<IRInstruction>()
}
test("conditional branch two successors") {
// Chunk A -> conditional branch to C, fall-through to B
// Liveness flows from A to both B and C
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
c1 += IRInstructions.compareImmediate(IRDataType.BYTE, 1, 0)
c1 += IRInstructions.branch(Opcode.BSTNE, codeLabel("label_c"))
val c2 = IRCodeChunk(null, null) // fall-through (B)
c2 += IRInstructions.load(IRDataType.BYTE, 2, 42)
c2 += IRInstructions.jump(codeLabel("label_end"))
val c3 = IRCodeChunk("label_c", null)
c3 += IRInstructions.load(IRDataType.BYTE, 2, 99)
val c4 = IRCodeChunk("label_end", null)
c4 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 2, IRMemory.direct("sym"))
val irProg = makeIRProgram(listOf(c1, c2, c3, c4))
RegisterPacker.pack(irProg)
val instrs = irProg.instructions()
instrs shouldNotBe emptyList<IRInstruction>()
}
test("multi-chunk register reduction") {
// 3 chunks with 8 registers, non-overlapping patterns
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
c1 += IRInstructions.load(IRDataType.BYTE, 1, 10)
c1 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 1, IRMemory.direct("sym"))
c1 += IRInstructions.load(IRDataType.BYTE, 2, 20)
c1 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 2, IRMemory.direct("sym2"))
c1 += IRInstructions.jump(codeLabel("cont"))
val c2 = IRCodeChunk("cont", null)
c2 += IRInstructions.load(IRDataType.BYTE, 3, 30)
c2 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 3, IRMemory.direct("sym3"))
c2 += IRInstructions.load(IRDataType.BYTE, 4, 40)
c2 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 4, IRMemory.direct("sym4"))
c2 += IRInstructions.jump(codeLabel("cont2"))
val c3 = IRCodeChunk("cont2", null)
c3 += IRInstructions.load(IRDataType.BYTE, 5, 50)
c3 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 5, IRMemory.direct("sym5"))
c3 += IRInstructions.load(IRDataType.BYTE, 6, 60)
c3 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 6, IRMemory.direct("sym6"))
val irProg = makeIRProgram(listOf(c1, c2, c3))
RegisterPacker.pack(irProg)
val distinctRegs = irProg.registerAccesses().map { it.register }.toSet()
// Each chunk defines and uses 2 regs. Within each chunk the regs are non-overlapping
// (def rX at i, use at i+1, then def rY at i+2). Across chunks they don't overlap either
// (sequential, jumps between chunks). So all 6 regs can share 1 slot.
distinctRegs.size shouldBe 1
}
test("floating point registers packed independently") {
val irProg = makeIRProgram(listOf(
IRInstructions.loadFloat(1, 1.0),
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.FLOAT, 1, IRMemory.direct("sym")),
IRInstructions.loadFloat(2, 2.0),
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.FLOAT, 2, IRMemory.direct("sym2"))
))
RegisterPacker.pack(irProg)
// Both fp regs have non-overlapping intervals -> same packed slot
val fpRegs = irProg.registerAccesses().filter { it.isFloat }.map { it.register }
fpRegs.distinct().size shouldBe 1
}
test("function call args are remapped") {
val callSite = CallSite(
CallTarget.SystemCall(0),
arguments = listOf(
Calls.argument(1, IRDataType.BYTE),
Calls.argument(2, IRDataType.BYTE)
),
results = listOf(Calls.result(3, IRDataType.BYTE))
)
val irProg = makeIRProgram(listOf(
IRInstructions.load(IRDataType.BYTE, 1, 10),
IRInstructions.load(IRDataType.BYTE, 2, 20),
// Non-overlapping: r1=[0,0], r2=[1,1], r3=live only as return from SYSCALL
IRInstructions.call(Opcode.SYSCALL, callSite)
))
RegisterPacker.pack(irProg)
val instrs = irProg.instructions()
val syscall = instrs.first { it.opcode == Opcode.SYSCALL }
val remapped = syscall.requireCallSite()
// Should have same packed register numbers
remapped.arguments.map { it.source.register }.toSet().size shouldBe 2
remapped.results.single().destination!!.registerNumber shouldBe 3
}
test("empty subroutine no crash") {
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
val irProg = makeIRProgram(listOf(c1))
// Should not throw
RegisterPacker.pack(irProg)
}
test("no registers used") {
val irProg = makeIRProgram(listOf(
IRInstructions.simple(Opcode.NOP),
IRInstructions.simple(Opcode.NOP)
))
RegisterPacker.pack(irProg)
irProg.instructions().size shouldBe 2
}
test("call arguments with mixed types must not share same slot") {
// Regression: call site registers with incompatible types (WORD vs LONG)
// in the same chunk must be assigned different packed slots.
// The conflict graph must track call site register types.
val callSite = CallSite(
CallTarget.SystemCall(0),
arguments = listOf(
Calls.argument(1, IRDataType.WORD),
Calls.argument(2, IRDataType.LONG)
)
)
val irProg = makeIRProgram(listOf(
IRInstructions.load(IRDataType.WORD, 1, 100),
IRInstructions.call(Opcode.SYSCALL, callSite)
))
RegisterPacker.pack(irProg)
// usedRegisters() must not throw (type consistency within chunk)
val used = irProg.registersUsed()
val regs = (used.readRegs.keys + used.writeRegs.keys).map { it.num }.toSet()
// Must have at least 2 distinct packed slots (WORD and LONG cannot share)
regs.size shouldBe 2
}
test("call results with mixed types must not share same slot") {
// Same as above but for return registers
val callSite = CallSite(
CallTarget.SystemCall(0),
results = listOf(
Calls.result(1, IRDataType.WORD),
Calls.result(2, IRDataType.LONG)
)
)
val irProg = makeIRProgram(listOf(
IRInstructions.call(Opcode.SYSCALL, callSite),
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.WORD, 1, IRMemory.direct("s")),
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.LONG, 2, IRMemory.direct("s2"))
))
RegisterPacker.pack(irProg)
// usedRegisters() must not throw
val used = irProg.registersUsed()
val regs = (used.readRegs.keys + used.writeRegs.keys).map { it.num }.toSet()
regs.size shouldBe 2
}
test("WORD and LONG non-overlapping in same chunk must not share slot") {
// Regression: two registers with incompatible types (WORD vs LONG) but
// non-overlapping live ranges in the same chunk must NOT be assigned
// the same packed slot, because usedRegisters() enforces a single type
// per register number within a chunk.
val irProg = makeIRProgram(listOf(
IRInstructions.load(IRDataType.WORD, 1, 100),
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.WORD, 1, IRMemory.direct("s")),
IRInstructions.load(IRDataType.LONG, 2, 0x10000),
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.LONG, 2, IRMemory.direct("s2"))
))
RegisterPacker.pack(irProg)
// usedRegisters() must not throw
val used = irProg.registersUsed()
val types = used.regsTypes
// Exactly 2 registers, one WORD and one LONG (different slots)
types.size shouldBe 2
types.values.toSet() shouldBe setOf(IRDataType.WORD, IRDataType.LONG)
}
test("cross-subroutine slot type is preserved via putIfAbsent") {
// Regression: when a slot is first assigned a type (e.g. POINTER),
// subsequent compatible assignments (WORD via typesCompatible) must not
// overwrite the slot's type. Otherwise a later LONG interval would be
// blocked (WORD vs LONG incompatible), forcing it to a different slot,
// and registersUsed() would see the same slot with both POINTER and
// WORD types across subroutines → should be compatible (POINTER bridges both).
fun makeMultiSubProgram(sub1Chunks: List<IRCodeChunkBase>, sub2Chunks: List<IRCodeChunkBase>): IRProgram {
val target = VMTarget()
val options = CompilationOptions.builder(target)
.output(OutputType.RAW)
.zeropage(ZeropageType.DONTUSE)
.noSysInit(true)
.compilerVersion("99.99")
.build()
val prog = IRProgram("test", IRSymbolTable(), options, target)
val block = IRBlock("p8b_main", false, IRBlock.Options(), Position.DUMMY)
val sub1 = IRSubroutine("p8b_main.sub1", emptyList(), emptyList(), Position.DUMMY)
sub1Chunks.forEach { sub1 += it }
val sub2 = IRSubroutine("p8b_main.sub2", emptyList(), emptyList(), Position.DUMMY)
sub2Chunks.forEach { sub2 += it }
block += sub1
block += sub2
prog.addBlock(block)
prog.linkChunks()
prog.validate()
return prog
}
// Sub 1: POINTER interval (gets slot ≥ startSlot), and then the same slot again with WORD
val c1 = IRCodeChunk("p8b_main.sub1", null)
c1 += IRInstructions.load(IRDataType.POINTER, 100, 0x1000)
c1 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.POINTER, 100, IRMemory.direct("sp"))
c1 += IRInstructions.load(IRDataType.WORD, 101, 200)
c1 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.WORD, 101, IRMemory.direct("sw"))
// Sub 2: LONG interval that should be able to share the same slot as sub 1's POINTER
val c2 = IRCodeChunk("p8b_main.sub2", null)
c2 += IRInstructions.load(IRDataType.LONG, 200, 0x100000)
c2 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.LONG, 200, IRMemory.direct("sl"))
val irProg = makeMultiSubProgram(listOf(c1), listOf(c2))
RegisterPacker.pack(irProg)
// registersUsed() must not throw: POINTER and WORD compatible,
// POINTER and LONG compatible, and putIfAbsent keeps the slot type
// as POINTER so WORD and LONG intervals can share the same slot.
val used = irProg.registersUsed()
used.regsTypes.size shouldBe 2 // POINTER (or POINTER+WORD+LONG sharing) + WORD
}
test("original register numbers must not collide with packed slots") {
// Regression: pre-existing low register numbers (e.g. r5) in the program
// must not collide with packed slot numbers. Packed slots start at
// maxReg+1 to avoid this.
val irProg = makeIRProgram(listOf(
// Register 5 is used directly (not packed? actually it HAS an interval and gets packed)
IRInstructions.load(IRDataType.BYTE, 5, 99),
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 5, IRMemory.direct("s")),
// Register 1 and 2 can be packed
IRInstructions.load(IRDataType.WORD, 1, 100),
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.WORD, 1, IRMemory.direct("s2")),
IRInstructions.load(IRDataType.WORD, 2, 200),
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.WORD, 2, IRMemory.direct("s3"))
))
RegisterPacker.pack(irProg)
// after packing:
// - all registers should have been packed to slots > 5 (since maxReg=5, startSlot=6)
// - no packed slot should equal an original register number
val used = irProg.registersUsed()
val allRegNums = (used.readRegs.keys + used.writeRegs.keys).map { it.num }.toSet()
// packed slots start at 6, so no register should be 5 anymore
(5 in allRegNums) shouldBe false
// all registers should be >= 6
for (rn in allRegNums)
rn shouldNotBe 5
}
test("strict registersUsed throws on incompatible types across subroutines") {
// Regression: registersUsed() in strict mode (no packing applied) must detect
// the same register number used with incompatible types in different subroutines.
val target = VMTarget()
val options = CompilationOptions.builder(target)
.output(OutputType.RAW)
.zeropage(ZeropageType.DONTUSE)
.noSysInit(true)
.compilerVersion("99.99")
.build()
val prog = IRProgram("test", IRSymbolTable(), options, target)
val block = IRBlock("p8b_main", false, IRBlock.Options(), Position.DUMMY)
// Sub 1: uses r37 as POINTER
val sub1 = IRSubroutine("p8b_main.sub1", emptyList(), emptyList(), Position.DUMMY)
val c1 = IRCodeChunk("p8b_main.sub1", null)
c1 += IRInstructions.load(IRDataType.POINTER, 37, 0x1000)
c1 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.POINTER, 37, IRMemory.direct("sp"))
sub1 += c1
// Sub 2: uses same r37 as BYTE (incompatible)
val sub2 = IRSubroutine("p8b_main.sub2", emptyList(), emptyList(), Position.DUMMY)
val c2 = IRCodeChunk("p8b_main.sub2", null)
c2 += IRInstructions.load(IRDataType.BYTE, 37, 42)
c2 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 37, IRMemory.direct("sb"))
sub2 += c2
block += sub1
block += sub2
prog.addBlock(block)
prog.linkChunks()
prog.validate()
// No packing applied - strict mode should detect the incompatibility
val ex = shouldThrow<IllegalArgumentException> {
prog.registersUsed()
}
ex.message shouldContain "register r37 given multiple types"
ex.message shouldContain "POINTER"
ex.message shouldContain "BYTE"
}
test("intra-subroutine cross-type register is skipped by packer") {
// When a register has non-overlapping intervals with incompatible types (e.g.
// BYTE and LONG in different chunks), the packer skips it entirely because its
// single-entry packing map can't handle a register with multiple types.
// After packing, the register retains its original number and is not rewritten.
// registersUsed() in permissive mode uses putIfAbsent and should not throw.
val target = VMTarget()
val options = CompilationOptions.builder(target)
.output(OutputType.RAW)
.zeropage(ZeropageType.DONTUSE)
.noSysInit(true)
.compilerVersion("99.99")
.build()
val prog = IRProgram("test", IRSymbolTable(), options, target)
val block = IRBlock("p8b_main", false, IRBlock.Options(), Position.DUMMY)
// Single subroutine with two chunks using same register with incompatible types
val sub = IRSubroutine("p8b_main.sub1", emptyList(), emptyList(), Position.DUMMY)
val c1 = IRCodeChunk("p8b_main.sub1", null)
c1 += IRInstructions.load(IRDataType.POINTER, 37, 0x1000)
c1 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.POINTER, 37, IRMemory.direct("sp"))
sub += c1
val c2 = IRCodeChunk("p8b_main.sub1.c2", c1)
c2 += IRInstructions.load(IRDataType.BYTE, 37, 42)
c2 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 37, IRMemory.direct("sb"))
sub += c2
block += sub
prog.addBlock(block)
prog.linkChunks()
prog.validate()
RegisterPacker.pack(prog)
prog.wasPackingApplied = true
// registersUsed() in permissive mode: putIfAbsent, should not throw
val used = prog.registersUsed()
// r37 should still appear (was skipped by packer)
used.regsTypes.containsKey(VirtualRegister.int(37)) shouldBe true
}
test("disjoint intervals of same register must be merged into one contiguous range") {
// Regression: if register r1 has disjoint intervals (written in chunk 1,
// read in chunk 3 with a gap in chunk 2), the packer must merge them into
// a single interval spanning the gap. Otherwise another register (r2) with
// an interval in chunk 2 could share the same slot and clobber r1's value.
val target = VMTarget()
val options = CompilationOptions.builder(target)
.output(OutputType.RAW)
.zeropage(ZeropageType.DONTUSE)
.noSysInit(true)
.compilerVersion("99.99")
.build()
val prog = IRProgram("test", IRSymbolTable(), options, target)
val block = IRBlock("p8b_main", false, IRBlock.Options(), Position.DUMMY)
val sub = IRSubroutine("p8b_main.sub1", emptyList(), emptyList(), Position.DUMMY)
// Chunk 1: write r1 (value should persist to chunk 3)
val c1 = IRCodeChunk("p8b_main.sub1", null)
c1 += IRInstructions.load(IRDataType.BYTE, 1, 42)
c1 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 1, IRMemory.direct("s1"))
sub += c1
// Chunk 2: write and read r2 (this interval falls in the gap between r1's uses)
val c2 = IRCodeChunk("p8b_main.sub1.c2", c1)
c2 += IRInstructions.load(IRDataType.BYTE, 2, 99)
c2 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 2, IRMemory.direct("s2"))
sub += c2
// Chunk 3: read r1 (uses the value from chunk 1)
val c3 = IRCodeChunk("p8b_main.sub1.c3", c2)
c3 += IRInstructions.move(IRDataType.BYTE, 3, 1)
c3 += IRInstructions.storeMemory(Opcode.STOREM, IRDataType.BYTE, 3, IRMemory.direct("s3"))
sub += c3
block += sub
prog.addBlock(block)
prog.linkChunks()
prog.validate()
RegisterPacker.pack(prog)
// After packing, r1 and r2 must be in DIFFERENT slots because r1's
// merged interval spans [0..?] covering the gap, preventing r2
// from sharing the same slot.
val instructions = prog.instructions()
// Find the register operands used for the STOREM to s1 (was r1) and s2 (was r2)
val s1Reg = instructions.first { it.memory?.symbolName=="s1" }.requireSrcA().register
val s2Reg = instructions.first { it.memory?.symbolName=="s2" }.requireSrcA().register
// They must be different (can't share a slot)
s1Reg shouldNotBe s2Reg
}
})
+30 -19
View File
@@ -2,15 +2,15 @@ import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import prog8.code.*
import prog8.code.StNodeType
import prog8.code.SymbolTableMaker
import prog8.code.ast.*
import prog8.code.core.*
import prog8.code.source.SourceCode
import prog8.code.target.VMTarget
import prog8.codegen.intermediate.prefixScopedName
import prog8.codegen.intermediate.prefixSymbols
import prog8.codegen.intermediate.typePrefixChar
import prog8tests.helpers.DummyMemsizer
import prog8tests.helpers.DummyStringEncoder
class TestSymbolPrefixer : FunSpec({
@@ -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
@@ -59,10 +70,10 @@ class TestSymbolPrefixer : FunSpec({
// --- full program prefixing tests ---
test("prefixSymbols prefixes a simple program") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
val varX = PtVariable("x", DataType.UBYTE, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY)
val varX = PtVariable("x", DataType.UBYTE, false, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY)
sub.add(varX)
block.add(sub)
program.add(block)
@@ -84,7 +95,7 @@ class TestSymbolPrefixer : FunSpec({
}
test("prefixSymbols handles subroutine parameters") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
val param = PtSubroutineParameter("arg", DataType.UBYTE, null, Position.DUMMY)
@@ -100,7 +111,7 @@ class TestSymbolPrefixer : FunSpec({
}
test("prefixSymbols handles constants") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val constant = PtConstant("SIZE", DataType.UWORD, 100.0, null, Position.DUMMY)
block.add(constant)
@@ -114,7 +125,7 @@ class TestSymbolPrefixer : FunSpec({
}
test("prefixSymbols handles labels") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
val label = PtLabel("mylabel", Position.DUMMY)
@@ -130,7 +141,7 @@ class TestSymbolPrefixer : FunSpec({
}
test("prefixSymbols handles generated labels without prefixing them") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
val genLabel = PtLabel("p8_label_gen_42", Position.DUMMY)
@@ -146,7 +157,7 @@ class TestSymbolPrefixer : FunSpec({
}
test("prefixSymbols handles struct declarations") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val struct = PtStructDecl("Point", listOf(
PtStructField(DataType.UBYTE, "x", null),
@@ -163,7 +174,7 @@ class TestSymbolPrefixer : FunSpec({
}
test("prefixSymbols handles asmsub") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val asmsub = PtAsmSub("routine", PtAsmSub.Address(null, null, 0x5000u), setOf(CpuRegister.Y), emptyList(), emptyList(), false, Position.DUMMY)
block.add(asmsub)
@@ -177,7 +188,7 @@ class TestSymbolPrefixer : FunSpec({
}
test("prefixSymbols handles memmapped variables") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val memvar = PtMemMapped("reg", DataType.UBYTE, 0xd020u, null, Position.DUMMY)
block.add(memvar)
@@ -193,7 +204,7 @@ class TestSymbolPrefixer : FunSpec({
// --- noSymbolPrefixing tests ---
test("noSymbolPrefixing blocks keep their names intact") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val libOptions = PtBlock.Options(noSymbolPrefixing = true)
val libBlock = PtBlock("lib", false, SourceCode.Generated("test"), libOptions, Position.DUMMY)
val libSub = PtSub("func", emptyList(), emptyList(), Position.DUMMY)
@@ -209,7 +220,7 @@ class TestSymbolPrefixer : FunSpec({
}
test("references from user block to noSymbolPrefixing block are not prefixed") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
// Library block with no symbol prefixing
val libOptions = PtBlock.Options(noSymbolPrefixing = true)
@@ -240,7 +251,7 @@ class TestSymbolPrefixer : FunSpec({
}
test("references from noSymbolPrefixing block to regular block get prefixed") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
// Regular user block
val userBlock = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
@@ -267,10 +278,10 @@ class TestSymbolPrefixer : FunSpec({
}
test("identifier references are prefixed according to their target type") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
val varX = PtVariable("x", DataType.UBYTE, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY)
val varX = PtVariable("x", DataType.UBYTE, false, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY)
sub.add(varX)
// Add an identifier reference to the variable
@@ -290,7 +301,7 @@ class TestSymbolPrefixer : FunSpec({
}
test("prefixSymbols exception for unknown identifier") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
val idRef = PtIdentifier("nonexistent", DataType.UBYTE, Position.DUMMY)
@@ -308,7 +319,7 @@ class TestSymbolPrefixer : FunSpec({
})
private fun basicTestOptions(): CompilationOptions {
val target = prog8.code.target.VMTarget()
val target = VMTarget()
return CompilationOptions.builder(target)
.output(OutputType.RAW)
.zeropage(ZeropageType.DONTUSE)
+76 -28
View File
@@ -5,14 +5,16 @@ import prog8.code.SymbolTableMaker
import prog8.code.ast.*
import prog8.code.core.*
import prog8.code.source.SourceCode
import prog8.code.target.Cx16Target
import prog8.code.target.VMTarget
import prog8.codegen.vm.VmAssemblyProgram
import prog8.codegen.vm.VmCodeGen
import prog8.intermediate.IRSubroutine
import prog8.intermediate.Opcode
import prog8.intermediate.codeAddress
import prog8.intermediate.requireCallSite
import prog8.intermediate.toAddress
import prog8tests.helpers.DummyMemsizer
import prog8tests.helpers.DummyStringEncoder
import prog8tests.helpers.ErrorReporterForTests
class TestVmCodeGen: FunSpec({
@@ -40,13 +42,15 @@ class TestVmCodeGen: FunSpec({
// xx += cx16.r0
// }
//}
val compTarget = VMTarget()
val codegen = VmCodeGen(false)
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
sub.add(PtVariable(
"pi",
DataType.UBYTE,
false,
ZeropageWish.DONTCARE,
0u,
false,
@@ -56,7 +60,8 @@ class TestVmCodeGen: FunSpec({
))
sub.add(PtVariable(
"particleX",
DataType.arrayFor(BaseDataType.UBYTE),
DataType.arrayFor(BaseDataType.UBYTE, compTarget),
false,
ZeropageWish.DONTCARE,
0u,
false,
@@ -66,7 +71,8 @@ class TestVmCodeGen: FunSpec({
))
sub.add(PtVariable(
"particleDX",
DataType.arrayFor(BaseDataType.UBYTE),
DataType.arrayFor(BaseDataType.UBYTE, compTarget),
false,
ZeropageWish.DONTCARE,
0u,
false,
@@ -77,6 +83,7 @@ class TestVmCodeGen: FunSpec({
sub.add(PtVariable(
"xx",
DataType.WORD,
false,
ZeropageWish.DONTCARE,
0u,
false,
@@ -87,17 +94,17 @@ class TestVmCodeGen: FunSpec({
val assign = PtAugmentedAssign("+=", Position.DUMMY)
val target = PtAssignTarget(false, Position.DUMMY).also {
val targetIdx = PtArrayIndexer(DataType.UBYTE, Position.DUMMY).also { idx ->
val targetIdx = PtArrayIndexer(DataType.UBYTE, false, Position.DUMMY).also { idx ->
idx.add(PtIdentifier("main.start.particleX",
DataType.arrayFor(BaseDataType.UBYTE),
DataType.arrayFor(BaseDataType.UBYTE, compTarget),
Position.DUMMY))
idx.add(PtNumber(BaseDataType.UBYTE, 2.0, Position.DUMMY))
}
it.add(targetIdx)
}
val value = PtArrayIndexer(DataType.UBYTE, Position.DUMMY)
val value = PtArrayIndexer(DataType.UBYTE, false, Position.DUMMY)
value.add(PtIdentifier("main.start.particleDX",
DataType.arrayFor(BaseDataType.UBYTE),
DataType.arrayFor(BaseDataType.UBYTE, compTarget),
Position.DUMMY))
value.add(PtNumber(BaseDataType.UBYTE, 2.0, Position.DUMMY))
assign.add(target)
@@ -152,11 +159,11 @@ class TestVmCodeGen: FunSpec({
// Pattern: @(ptr+offset) = @(ptr+offset) + val i.e. ptr[offset] += val
// Non-zero offsets are used to avoid potential optimization of the address expression.
val codegen = VmCodeGen(false)
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
sub.add(PtVariable("ptr", DataType.pointer(BaseDataType.UBYTE), ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY))
sub.add(PtVariable("val", DataType.UBYTE, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY))
sub.add(PtVariable("ptr", DataType.pointer(BaseDataType.UBYTE), false, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY))
sub.add(PtVariable("val", DataType.UBYTE, false, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY))
fun makeMemByteAssign(op: String, offset: Int): PtAugmentedAssign {
// Use non-zero offset to avoid optimization
@@ -203,12 +210,12 @@ class TestVmCodeGen: FunSpec({
andrCount shouldBe 1
xorrCount shouldBe 1
// Verify that the immediate-only variants are NOT used with reg2
val wrongAdd = instructions.any { it.opcode == Opcode.ADD && it.reg2 != null }
val wrongSub = instructions.any { it.opcode == Opcode.SUB && it.reg2 != null }
val wrongOr = instructions.any { it.opcode == Opcode.OR && it.reg2 != null }
val wrongAnd = instructions.any { it.opcode == Opcode.AND && it.reg2 != null }
val wrongXor = instructions.any { it.opcode == Opcode.XOR && it.reg2 != null }
// Verify that the immediate-only variants are NOT used with a register source
val wrongAdd = instructions.any { it.opcode == Opcode.ADD && it.srcA != null }
val wrongSub = instructions.any { it.opcode == Opcode.SUB && it.srcA != null }
val wrongOr = instructions.any { it.opcode == Opcode.OR && it.srcA != null }
val wrongAnd = instructions.any { it.opcode == Opcode.AND && it.srcA != null }
val wrongXor = instructions.any { it.opcode == Opcode.XOR && it.srcA != null }
(wrongAdd || wrongSub || wrongOr || wrongAnd || wrongXor) shouldBe false
}
@@ -229,12 +236,13 @@ class TestVmCodeGen: FunSpec({
// }
//}
val codegen = VmCodeGen(false)
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
sub.add(PtVariable(
"f1",
DataType.FLOAT,
false,
ZeropageWish.DONTCARE,
0u,
false,
@@ -301,12 +309,13 @@ class TestVmCodeGen: FunSpec({
// }
//}
val codegen = VmCodeGen(false)
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
sub.add(PtVariable(
"f1",
DataType.FLOAT,
false,
ZeropageWish.DONTCARE,
0u,
false,
@@ -369,12 +378,13 @@ class TestVmCodeGen: FunSpec({
// }
//}
val codegen = VmCodeGen(false)
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
sub.add(PtVariable(
"f1",
DataType.FLOAT,
false,
ZeropageWish.DONTCARE,
0u,
false,
@@ -425,12 +435,13 @@ class TestVmCodeGen: FunSpec({
// }
//}
val codegen = VmCodeGen(false)
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
sub.add(PtVariable(
"sb1",
DataType.BYTE,
false,
ZeropageWish.DONTCARE,
0u,
false,
@@ -497,12 +508,13 @@ class TestVmCodeGen: FunSpec({
// }
//}
val codegen = VmCodeGen(false)
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
sub.add(PtVariable(
"sb1",
DataType.BYTE,
false,
ZeropageWish.DONTCARE,
0u,
false,
@@ -565,12 +577,13 @@ class TestVmCodeGen: FunSpec({
// }
//}
val codegen = VmCodeGen(false)
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
sub.add(PtVariable(
"ub1",
DataType.BYTE,
false,
ZeropageWish.DONTCARE,
0u,
false,
@@ -614,7 +627,7 @@ class TestVmCodeGen: FunSpec({
// }
//}
val codegen = VmCodeGen(false)
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val extsub = PtAsmSub("routine", PtAsmSub.Address(null, null, 0x5000u), setOf(CpuRegister.Y), emptyList(), emptyList(), false, Position.DUMMY)
block.add(extsub)
@@ -634,15 +647,15 @@ class TestVmCodeGen: FunSpec({
irChunks[0].instructions.size shouldBe 1
val callInstr = irChunks[0].instructions[0]
callInstr.opcode shouldBe Opcode.CALL
callInstr.address shouldBe 0x5000u.toAddress()
callInstr.requireCallSite().codeReference shouldBe codeAddress(0x5000u.toAddress())
}
test("ir codegen for target 'virtual' produces no prefixed names") {
val codegen = VmCodeGen(false)
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
sub.add(PtVariable("x", DataType.UBYTE, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY))
sub.add(PtVariable("x", DataType.UBYTE, false, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY))
block.add(sub)
program.add(block)
@@ -664,4 +677,39 @@ class TestVmCodeGen: FunSpec({
val irVar = irProg.st.allVariables().single()
irVar.name shouldBe "main.start.x"
}
test("ir codegen for target 'cx16' via newcodegen produces prefixed names") {
val codegen = VmCodeGen(false)
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val sub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
sub.add(PtVariable("x", DataType.UBYTE, false, ZeropageWish.DONTCARE, 0u, false, null, null, Position.DUMMY))
block.add(sub)
program.add(block)
val target = Cx16Target()
val options = CompilationOptions.builder(target)
.output(OutputType.RAW)
.zeropage(ZeropageType.DONTUSE)
.floats(true)
.compilerVersion("99.99")
.newCodegen(true)
.build()
val st = SymbolTableMaker(program, options).make()
val errors = ErrorReporterForTests()
val result = codegen.generate(program, st, options, errors) as VmAssemblyProgram
val irProg = result.irProgram
// Block label should be prefixed
val irBlock = irProg.blocks.first()
irBlock.label shouldBe "p8b_main"
// Subroutine label should be fully scoped and prefixed
val irSub = irBlock.children.single() as IRSubroutine
irSub.label shouldBe "p8b_main.p8s_start"
// Variable in IR symbol table should be fully scoped and prefixed
val irVar = irProg.st.allVariables().single()
irVar.name shouldBe "p8b_main.p8s_start.p8v_x"
}
})
+97
View File
@@ -0,0 +1,97 @@
plugins {
kotlin("jvm")
id("application")
id("com.peterabeles.gversion") version "1.11.0"
}
dependencies {
implementation(project(":codeCore"))
implementation(project(":simpleAst"))
implementation(project(":intermediate"))
implementation(project(":codeGenIntermediate"))
implementation("com.michael-bull.kotlin-result:kotlin-result-jvm:2.3.1")
testImplementation(testFixtures(project(":codeCore")))
testImplementation("io.kotest:kotest-runner-junit5")
}
val debugPort = 8000
val debugArgs = "-agentlib:jdwp=transport=dt_socket,server=y,address=8000,suspend=n,quiet=y"
val serverMainClassName = "prog8.codegen.m68k.MainKt"
val applicationName = "prog8-m68kgen"
application {
mainClass.set(serverMainClassName)
description = "New codegen based on IR files"
applicationDistribution.into("bin") {
filePermissions {
user {
read=true
execute=true
write=true
}
other.execute = true
group.execute = true
}
}
}
repositories {
mavenCentral()
}
dependencies {
implementation(project(":codeCore"))
implementation(project(":intermediate"))
implementation("com.github.ajalt.clikt:clikt:5.1.0")
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")
}
configurations.forEach { config ->
config.resolutionStrategy {
preferProjectModules()
}
}
sourceSets {
main {
java.srcDir("src")
resources.srcDir("resources")
}
}
tasks.startScripts {
applicationName = "prog8-m68kgen"
}
tasks.register<Exec>("fixFilePermissions") {
description = "When running on macOS or Linux the start script needs executable permissions to run, this sets them"
onlyIf { !System.getProperty("os.name").lowercase().contains("windows") }
commandLine("chmod", "+x", "${tasks.installDist.get().destinationDir}/bin/prog8-m68kgen")
}
tasks.register<JavaExec>("debugRun") {
description = "run with remote debugging"
mainClass.set(serverMainClassName)
classpath(sourceSets.main.get().runtimeClasspath)
standardInput = System.`in`
jvmArgs(debugArgs)
doLast {
println("Using debug port $debugPort")
}
}
tasks.installDist {
finalizedBy("fixFilePermissions")
}
tasks.build {
finalizedBy("installDist")
}
+411
View File
@@ -0,0 +1,411 @@
; A working m68k kernel for use in QEMU `-M virt` m68k machine.
; It parses the bootinfo chunks, prints stuff to the uart output, and reads input as well.
section .text,code
xref _end_of_code
global _start
_start: move.l #$fffc,sp
bsr bootinfo_parse
move.l ram_top,d0
beq 1$
sub.l #256,d0
move.l d0,sp
1$:
movea.l #str_welcome,a0
bsr puts
2$: bsr getchar
move.b d0,d1
move.b d1,(a0)
bsr putchar
cmpi.b #13,d1
bne 3$
move.b #10,d0
bsr putchar
3$: cmpi.b #$1b,d1
bne 2$
bsr do_halt
bra 2$
; ============================================================
; bootinfo_parse -- iterate bootinfo records and print them
; ============================================================
bootinfo_parse:
move.l d2,-(sp)
move.l d4,-(sp)
moveq #0,d4
move.l #_end_of_code,d0
addq.l #1,d0
lsr.l #1,d0
lsl.l #1,d0
movea.l d0,a2
bi_loop:
clr.l d0
move.w (a2)+,d0
beq bi_done
move.l d0,d2
cmpi.w #2,d2
bne 1$
moveq #1,d4
1$:
clr.l d1
move.w (a2)+,d1
move.l a2,a3
lea (-4,a2,d1.l),a2
subq.l #4,d1
movea.l #str_indent,a0
bsr puts
move.l a3,d0
subq.l #4,d0
bsr puthex8
movea.l #str_colon_space,a0
bsr puts
move.l d2,d0
bsr bi_print_tag
bsr bi_newline
movea.l #str_pad,a0
bsr puts
move.l a3,-(sp)
move.l d1,-(sp)
move.l d2,-(sp)
bsr bi_print_data
add.w #12,sp
bsr bi_newline
bra bi_loop
bi_done:
tst.l d4
bne 1$
movea.l #str_indent,a0
bsr puts
movea.l #str_no_cputype,a0
bsr puts
bsr bi_newline
1$: move.l (sp)+,d4
move.l (sp)+,d2
rts
bi_newline:
movea.l #str_newline,a0
bra puts
; ============================================================
; bi_print_tag -- print tag name for value in d0
; ============================================================
bi_print_tag:
cmpi.w #1,d0
beq bi_tag_machtype
cmpi.w #2,d0
beq bi_tag_cputype
cmpi.w #3,d0
beq bi_tag_fputype
cmpi.w #4,d0
beq bi_tag_mmutype
cmpi.w #5,d0
beq bi_tag_memchunk
cmpi.w #6,d0
beq bi_tag_ramdisk
cmpi.w #7,d0
beq bi_tag_cmdline
cmpi.w #8,d0
beq bi_tag_rngseed
cmpi.w #$8000,d0
beq bi_tag_version
cmpi.w #$8001,d0
beq bi_tag_pic
cmpi.w #$8002,d0
beq bi_tag_rtc
cmpi.w #$8003,d0
beq bi_tag_tty
cmpi.w #$8004,d0
beq bi_tag_virtio
cmpi.w #$8005,d0
beq bi_tag_ctrl
movea.l #str_hex_prefix,a0
bsr puts
bsr puthex8
rts
bi_tag_machtype: movea.l #str_machtype,a0
bra puts
bi_tag_cputype: movea.l #str_cputype,a0
bra puts
bi_tag_fputype: movea.l #str_fputype,a0
bra puts
bi_tag_mmutype: movea.l #str_mmutype,a0
bra puts
bi_tag_memchunk: movea.l #str_memchunk,a0
bra puts
bi_tag_ramdisk: movea.l #str_ramdisk,a0
bra puts
bi_tag_cmdline: movea.l #str_cmdline,a0
bra puts
bi_tag_rngseed: movea.l #str_rngseed,a0
bra puts
bi_tag_version: movea.l #str_version,a0
bra puts
bi_tag_pic: movea.l #str_pic,a0
bra puts
bi_tag_rtc: movea.l #str_rtc,a0
bra puts
bi_tag_tty: movea.l #str_tty,a0
bra puts
bi_tag_ctrl: movea.l #str_ctrl,a0
bra puts
bi_tag_virtio: movea.l #str_virtio,a0
bra puts
; ============================================================
; bi_print_data -- print data based on tag type
; stack args (low to high): tag, size, data_ptr
; ============================================================
bi_print_data:
move.l 4(sp),d0
move.l 8(sp),d1
movea.l 12(sp),a3
cmpi.w #1,d0
beq bi_data_machtype
cmpi.w #2,d0
beq bi_data_cputype
cmpi.w #3,d0
beq bi_data_u32
cmpi.w #4,d0
beq bi_data_u32
cmpi.w #$8000,d0
beq bi_data_u32
cmpi.w #5,d0
beq bi_data_memchunk
cmpi.w #6,d0
beq bi_data_base_size
cmpi.w #7,d0
beq bi_data_str
cmpi.w #8,d0
beq bi_data_hex
cmpi.w #$8001,d0
beq bi_data_base_count
cmpi.w #$8002,d0
beq bi_data_base_count
cmpi.w #$8003,d0
beq bi_data_base_count
cmpi.w #$8004,d0
beq bi_data_base_count
cmpi.w #$8005,d0
beq bi_data_base_count
bi_data_hex:
move.l d1,d3
1$: move.b (a3)+,d0
bsr puthex2
subq.l #1,d3
beq 2$
movea.l #str_space,a0
bsr puts
bra 1$
2$: rts
bi_data_u32:
move.l (a3),d0
bra puthex8
bi_data_memchunk:
move.l (a3),-(sp)
movea.l #str_mem_at,a0
bsr puts
move.l (sp)+,d0
bsr puthex8
movea.l #str_to,a0
bsr puts
move.l 4(a3),d0
add.l (a3),d0
move.l d0,ram_top
bsr puthex8
movea.l #str_size,a0
bsr puts
move.l 4(a3),d0
bra puthex8
bi_data_base_size:
movea.l #str_base,a0
bsr puts
move.l (a3),d0
bsr puthex8
movea.l #str_size,a0
bsr puts
move.l 4(a3),d0
bra puthex8
bi_data_base_count:
movea.l #str_base,a0
bsr puts
move.l (a3),d0
bsr puthex8
movea.l #str_count,a0
bsr puts
move.l 4(a3),d0
bra puthex8
bi_data_str:
movea.l a3,a0
bra puts
bi_data_machtype:
move.l (a3),d0
cmpi.l #14,d0
beq 1$
bra puthex8
1$: movea.l #str_mach_virt,a0
bra puts
bi_data_cputype:
move.l (a3),d0
beq 6$
btst #0,d0
bne 1$
btst #1,d0
bne 2$
btst #2,d0
bne 3$
btst #3,d0
bne 4$
btst #4,d0
bne 5$
bra puthex8
1$: movea.l #str_cpu68020,a0
bra puts
2$: movea.l #str_cpu68030,a0
bra puts
3$: movea.l #str_cpu68040,a0
bra puts
4$: movea.l #str_cpu68060,a0
bra puts
5$: movea.l #str_coldfire,a0
bra puts
6$: movea.l #str_cpu68000,a0
bra puts
; ============================================================
puts: move.b (a0)+,d0
beq puts_rts
bsr putchar
bra puts
puts_rts:
rts
; ============================================================
puthex8:
move.l d2,-(sp)
move.l d3,-(sp)
move.l d0,d2
moveq #28,d3
1$: move.l d2,d0
lsr.l d3,d0
and.b #$f,d0
add.b #'0',d0
cmp.b #'9',d0
bls 2$
add.b #7,d0
2$: bsr putchar
subq.l #4,d3
bpl 1$
move.l (sp)+,d3
move.l (sp)+,d2
rts
; ============================================================
puthex2:
move.l d2,-(sp)
move.l d3,-(sp)
move.l d0,d2
moveq #4,d3
1$: move.l d2,d0
lsr.l d3,d0
and.b #$f,d0
add.b #'0',d0
cmp.b #'9',d0
bls 2$
add.b #7,d0
2$: bsr putchar
subq.l #4,d3
bpl 1$
move.l (sp)+,d3
move.l (sp)+,d2
rts
; ============================================================
putchar:
movea.l #$ff008000,a1
move.l d0,(a1)
rts
; ============================================================
getchar:
suba.l #16,sp
movea.l #$ff008004,a1
1$: move.l (a1),d0
tst.l d0
beq 1$
movea.l #$ff008010,a1
move.l sp,(a1)
movea.l #$ff008014,a1
move.l #1,(a1)
movea.l #$ff008008,a1
move.l #3,(a1)
move.b (sp),d0
adda.l #16,sp
rts
; ============================================================
do_halt:
movea.l #$ff009004,a1
move.l #2,(a1)
rts
; ============================================================
; Strings
; ============================================================
str_indent: dc.b " ",0
str_colon_space: dc.b ": ",0
str_pad: dc.b " ",0
str_newline: dc.b 10,0
str_base: dc.b "base=0x",0
str_size: dc.b " size=0x",0
str_count: dc.b " count=0x",0
str_mem_at: dc.b "mem at 0x",0
str_to: dc.b " - 0x",0
str_space: dc.b " ",0
str_hex_prefix: dc.b "TAG_0x",0
str_mach_virt: dc.b "MACH_VIRT",0
str_cpu68000: dc.b "CPU_68000",0
str_cpu68020: dc.b "CPU_68020",0
str_cpu68030: dc.b "CPU_68030",0
str_cpu68040: dc.b "CPU_68040",0
str_cpu68060: dc.b "CPU_68060",0
str_coldfire: dc.b "ColdFire",0
str_no_cputype: dc.b "(no BI_CPUTYPE - assuming CPU_68000)",0
str_machtype: dc.b "BI_MACHTYPE",0
str_cputype: dc.b "BI_CPUTYPE",0
str_fputype: dc.b "BI_FPUTYPE",0
str_mmutype: dc.b "BI_MMUTYPE",0
str_memchunk: dc.b "BI_MEMCHUNK",0
str_ramdisk: dc.b "BI_RAMDISK",0
str_cmdline: dc.b "BI_COMMAND_LINE",0
str_rngseed: dc.b "BI_RNG_SEED",0
str_version: dc.b "BI_VIRT_QEMU_VER",0
str_pic: dc.b "BI_VIRT_GF_PIC",0
str_rtc: dc.b "BI_VIRT_GF_RTC",0
str_tty: dc.b "BI_VIRT_GF_TTY",0
str_ctrl: dc.b "BI_VIRT_CTRL",0
str_virtio: dc.b "BI_VIRT_VIRTIO",0
str_welcome: dc.b "Hello from m68k! (ESC to halt)",10,0
align 4
ram_top: dc.l 0
+94
View File
@@ -0,0 +1,94 @@
# Linking the kernel
The kernel is linked using **vlink**, producing an ELF32 binary (`kernel.elf`) that qemu loads directly via its `-kernel` option.
## Toolchain
| Tool | Role |
|---|---|
| `vasmm68k_mot` | Assembles `kernel.S` → `kernel.o` |
| `vlink` | Links `kernel.o` → `kernel.elf` using `link.ld` |
The `Makefile` invocation:
```makefile
vlink -b elf32m68k -n -T link.ld -o kernel.elf kernel.o
```
## Command-line flags
### `-b elf32m68k` — output format
Tells vlink to produce a 32-bit big-endian m68k ELF binary.
vlink supports many output formats; without this flag it would default to something else. Note that vlink ignores the `OUTPUT_FORMAT()` directive inside linker scripts, so the format **must** be specified on the command line.
### `-n` — NMAGIC / no page alignment
Disables page-boundary alignment of ELF segments.
Without `-n`, vlink aligns the LOAD segment to a full page (0x2000 = 8 KB). The resulting ELF has a large gap of padding between the ELF header and the actual code:
```
Type Offset VirtAddr PhysAddr Align
LOAD 0x002000 0x00010000 0x00010000 0x2000
```
qemu's ELF loader gets confused by this layout and either fails to load the kernel or jumps to the wrong address.
With `-n`, segment alignment is reduced to the minimum (0x10) and the code starts right after the ELF header:
```
Type Offset VirtAddr PhysAddr Align
LOAD 0x000080 0x00010000 0x00010000 0x10
```
qemu loads this cleanly — the code is at file offset 0x80, maps to virtual address 0x10000, and execution starts at `_start` as expected.
`-n` is the vlink equivalent of GNU ld's `-N` / `--omagic` option.
### `-T link.ld` — linker script
Passes the linker script (see below).
## The linker script (`link.ld`)
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
{
. = 0x10000;
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) *(COMMON) }
_end_of_code = .;
}
```
### Line by line
| Line | What it does |
|---|---|
| `ENTRY(_start)` | Sets the ELF entry point to `_start` — the first instruction in `kernel.S`. qemu uses this to know where to jump after loading the kernel. |
| `SECTIONS { … }` | Opens the section layout block — the core of any linker script. Everything inside describes how to arrange the output binary. |
| `. = 0x10000;` | Sets the **load address** to `0x10000` (64 KB). This is where qemu places the kernel in memory and begins execution. The `.` is the *location counter* — it advances automatically as sections are placed. |
| `.text : { *(.text) }` | Collects all `.text` (code) sections from every input object file into one output `.text` section. |
| `.data : { *(.data) }` | Same for initialised data. |
| `.bss : { *(.bss) *(COMMON) }` | Same for uninitialised (zero-filled) data. `COMMON` catches any unresolved global variables that the assembler emits as common symbols. |
| `_end_of_code = .;` | Defines the symbol `_end_of_code` at the address immediately after the last section. `kernel.S` references this symbol to find where the bootinfo records begin in memory (qemu appends them just past the kernel image). |
### Why a linker script is required
vlink has no command-line flag equivalent to `. = 0x10000` (setting the load address). Attempting `-Ttext=0x10000` causes a fatal error because vlink treats the `=` as part of a filename. The correct syntax `-Ttext 0x10000` (space-separated) does set the address, but then `_end_of_code` is undefined because there is no linker script to define it.
The linker script is therefore the minimum required: it sets the load address and defines `_end_of_code`.
+456
View File
@@ -0,0 +1,456 @@
# QEMU m68k "virt" Machine -- IO Interfaces Reference
All MMIO addresses and register layouts for the QEMU `-M virt` m68k machine.
## Memory Map
| Device | Base Address | Size | CPU IRQ |
|---|---|---|---|
| Goldfish PIC (x6) | `0xFF000000` | 6 x 0x1000 | #1-#6 |
| Goldfish RTC (x2) | `0xFF006000` | 2 x 0x1000 | #6 (PIC#6, IRQ#1) |
| Goldfish TTY | `0xFF008000` | 0x1000 | #1 (PIC#1, IRQ#32) |
| Virt Ctrl | `0xFF009000` | 0x100 | #1 (PIC#1, IRQ#1) |
| Virtio-MMIO (x128) | `0xFF010000` | 128 x 0x200 | #2-#5 (PIC#2-#5) |
### IRQ Hierarchy
```
CPU IRQ #1 -> PIC #1 (goldfish_pic.0) -> IRQ#1 = virt-ctrl, IRQ#32 = tty
CPU IRQ #2 -> PIC #2 (goldfish_pic.1) -> IRQ#1..#32 = virtio-mmio #1..#32
CPU IRQ #3 -> PIC #3 (goldfish_pic.2) -> IRQ#1..#32 = virtio-mmio #33..#64
CPU IRQ #4 -> PIC #4 (goldfish_pic.3) -> IRQ#1..#32 = virtio-mmio #65..#96
CPU IRQ #5 -> PIC #5 (goldfish_pic.4) -> IRQ#1..#32 = virtio-mmio #97..#128
CPU IRQ #6 -> PIC #6 (goldfish_pic.5) -> IRQ#1 = RTC, IRQ#2..#32 = unused
CPU IRQ #7 -> NMI
```
All registers are **32 bits wide**. Accesses must be 4-byte aligned.
---
## Goldfish TTY -- `0xFF008000`
Primary console. All access via 32-bit loads/stores.
### Registers
| Offset | Name | Access | Description |
|---|---|---|---|
| `0x00` | `REG_PUT_CHAR` | WO | Write one character to serial output |
| `0x04` | `REG_BYTES_READY` | RO | Number of bytes available in RX FIFO |
| `0x08` | `REG_CMD` | WO | Issue command |
| `0x10` | `REG_DATA_PTR` | WO | DMA buffer address (low 32 bits) |
| `0x14` | `REG_DATA_LEN` | WO | DMA transfer byte count |
| `0x18` | `REG_DATA_PTR_HIGH` | WO | DMA buffer address (high 32 bits) |
| `0x20` | `REG_VERSION` | RO | Returns 1 |
### Commands (write to REG_CMD)
| Value | Name | Description |
|---|---|---|
| `0` | `CMD_INT_DISABLE` | Disable interrupts |
| `1` | `CMD_INT_ENABLE` | Enable interrupts |
| `2` | `CMD_WRITE_BUFFER` | Write N bytes from guest memory to chardev |
| `3` | `CMD_READ_BUFFER` | Read N bytes from chardev into guest memory |
### Usage: putchar (blocking, non-DMA)
Write the character directly to `REG_PUT_CHAR`. QEMU sends it immediately.
```asm
putchar:
movea.l #0xff008000, %a1
move.l %d0, (%a1)
rts
```
### Usage: getchar (blocking, polled, DMA)
1. Poll `REG_BYTES_READY` until non-zero
2. Set `REG_DATA_PTR` to buffer address
3. Set `REG_DATA_LEN` to number of bytes to read (e.g. 1)
4. Write `3` (`CMD_READ_BUFFER`) to `REG_CMD`
5. Read character(s) from buffer
```asm
getchar:
suba.l #16, %sp
movea.l #0xff008004, %a1 @ REG_BYTES_READY
1: move.l (%a1), %d0
tst.l %d0
beq 1b @ wait until data available
movea.l #0xff008010, %a1 @ REG_DATA_PTR
move.l %sp, (%a1)
movea.l #0xff008014, %a1 @ REG_DATA_LEN
move.l #1, (%a1)
movea.l #0xff008008, %a1 @ REG_CMD
move.l #3, (%a1) @ CMD_READ_BUFFER
move.b (%sp), %d0
adda.l #16, %sp
rts
```
To use interrupts instead of polling:
1. Write `1` (`CMD_INT_ENABLE`) to `REG_CMD` (offset `0x08`)
2. Enable IRQ in PIC (PIC#1 at `0xFF000000`, IRQ#32)
3. On interrupt, read `REG_BYTES_READY` then DMA data from buffer
---
## Virt Controller -- `0xFF009000`
Controls machine power state.
### Registers
| Offset | Name | Access | Description |
|---|---|---|---|
| `0x00` | `REG_FEATURES` | RO | Feature bitmask |
| `0x04` | `REG_CMD` | WO | Issue power/reset command |
### Feature Bits (REG_FEATURES)
| Bit | Name | Description |
|---|---|---|
| `0` | `FEAT_POWER_CTRL` | Power control is available |
### Commands (write to REG_CMD)
| Value | Name | Description |
|---|---|---|
| `0` | `CMD_NOOP` | No operation |
| `1` | `CMD_RESET` | Reset the VM |
| `2` | `CMD_HALT` | Shut down the VM |
| `3` | `CMD_PANIC` | Shut down with guest panic |
### Usage: halt
```asm
halt:
movea.l #0xff009004, %a1 @ REG_CMD
move.l #2, (%a1) @ CMD_HALT
rts
```
### Usage: reset
```asm
reset:
movea.l #0xff009004, %a1 @ REG_CMD
move.l #1, (%a1) @ CMD_RESET
rts
```
---
## Goldfish PIC -- `0xFF000000` (+ idx * 0x1000)
Programmable Interrupt Controller. Six PICs in the virt machine:
| Base Address | PIC # | CPU IRQ | Devices |
|---|---|---|---|
| `0xFF000000` | 1 | #1 | virt-ctrl (IRQ#1), tty (IRQ#32) |
| `0xFF001000` | 2 | #2 | virtio-mmio #1..#32 |
| `0xFF002000` | 3 | #3 | virtio-mmio #33..#64 |
| `0xFF003000` | 4 | #4 | virtio-mmio #65..#96 |
| `0xFF004000` | 5 | #5 | virtio-mmio #97..#128 |
| `0xFF005000` | 6 | #6 | RTC (IRQ#1) |
### Registers
| Offset | Name | Access | Description |
|---|---|---|---|
| `0x00` | `REG_STATUS` | RO | Count of pending+enabled IRQs (popcount) |
| `0x04` | `REG_IRQ_PENDING` | RO | Bitmask of pending AND enabled IRQs |
| `0x08` | `REG_IRQ_DISABLE_ALL` | WO | Disable ALL interrupts (clears pending too) |
| `0x0c` | `REG_DISABLE` | WO | Disable specific IRQs (bitmask) |
| `0x10` | `REG_ENABLE` | WO | Enable specific IRQs (bitmask) |
Interrupts are level-sensitive. Each PIC manages 32 IRQ inputs (IRQ#1 = bit 0, IRQ#32 = bit 31).
### Usage: enable IRQ in PIC
```asm
@ Enable IRQ#32 (tty) in PIC#1 (0xFF000000)
movea.l #0xff000010, %a1 @ REG_ENABLE of PIC#1
move.l #1 << 31, (%a1) @ bit 31 = IRQ#32
```
### Usage: read pending IRQs
```asm
movea.l #0xff000004, %a1 @ REG_IRQ_PENDING
move.l (%a1), %d0 @ d0 = bitmask of pending IRQs
btst #31, %d0 @ check if IRQ#32 (tty) is firing
```
### Usage: disable all interrupts (mask)
```asm
movea.l #0xff000008, %a1 @ REG_IRQ_DISABLE_ALL
move.l #0, (%a1) @ any value disables all
```
---
## Goldfish RTC -- `0xFF006000`
Real-time clock with a 64-bit nanosecond counter. **Big-endian** on m68k (unlike other goldfish devices which are little-endian).
### Registers
| Offset | Name | Access | Description |
|---|---|---|---|
| `0x00` | `RTC_TIME_LOW` | RW | Time value, lower 32 bits |
| `0x04` | `RTC_TIME_HIGH` | RW | Time value, upper 32 bits |
| `0x08` | `RTC_ALARM_LOW` | RW | Alarm value, lower 32 bits |
| `0x0c` | `RTC_ALARM_HIGH` | RW | Alarm value, upper 32 bits |
| `0x10` | `RTC_IRQ_ENABLED` | RW | IRQ enable (bit 0) |
| `0x14` | `RTC_CLEAR_ALARM` | WO | Clear the alarm |
| `0x18` | `RTC_ALARM_STATUS` | RO | 1 if alarm is set |
| `0x1c` | `RTC_CLEAR_INTERRUPT` | WO | Clear pending interrupt |
Reading `RTC_TIME_LOW` latches the high 32 bits; the subsequent read of `RTC_TIME_HIGH` returns that latched value. This ensures atomic 64-bit time reads.
Writing either `RTC_TIME_LOW` or `RTC_TIME_HIGH` adjusts the tick offset to set the time.
The RTC interrupt goes to PIC#6 (CPU IRQ #6), IRQ #1.
### Usage: read time
```asm
movea.l #0xff006000, %a1 @ RTC_TIME_LOW
move.l (%a1), %d0 @ d0 = low 32 bits (latches high)
movea.l #0xff006004, %a1 @ RTC_TIME_HIGH
move.l (%a1), %d1 @ d1 = high 32 bits
@ now d1:d0 = 64-bit nanosecond counter
```
---
## Virtio-MMIO -- `0xFF010000` (+ idx * 0x200)
128 virtio-mmio transport devices. Each occupies 0x200 bytes.
| Start | End | Slot Range |
|---|---|---|
| `0xFF010000` | `0xFF01FFFF` | 128 slots (0x200 each) |
### Identification Registers (RO)
| Offset | Name | Value | Description |
|---|---|---|---|
| `0x000` | `MAGIC_VALUE` | `0x74726976` ("virt") |
| `0x004` | `VERSION` | 2 (modern) | 1 = legacy |
| `0x008` | `DEVICE_ID` | varies | 0 = no device |
| `0x00c` | `VENDOR_ID` | `0x554D4551` ("QEMU") |
### Key Registers (32-bit)
| Offset | Name | Access | Description |
|---|---|---|---|
| `0x010` | `DEVICE_FEATURES` | RO | Features offered by host (low 32) |
| `0x014` | `DEVICE_FEATURES_SEL` | WO | Select feature bits (high/low) |
| `0x020` | `DRIVER_FEATURES` | WO | Features accepted by driver (low 32) |
| `0x024` | `DRIVER_FEATURES_SEL` | WO | Select driver feature bits |
| `0x030` | `QUEUE_SEL` | WO | Select virtqueue |
| `0x034` | `QUEUE_NUM_MAX` | RO | Max queue size |
| `0x038` | `QUEUE_NUM` | WO | Set queue size |
| `0x044` | `QUEUE_READY` | RW | Queue ready bit |
| `0x050` | `QUEUE_NOTIFY` | WO | Kick the virtqueue |
| `0x060` | `INTERRUPT_STATUS` | RO | Interrupt reason bits |
| `0x064` | `INTERRUPT_ACK` | WO | Acknowledge interrupt |
| `0x070` | `STATUS` | RW | Device status (driver negotiation) |
| `0x080` | `QUEUE_DESC_LOW` | WO | Descriptor table address (low) |
| `0x084` | `QUEUE_DESC_HIGH` | WO | Descriptor table address (high) |
| `0x090` | `QUEUE_AVAIL_LOW` | WO | Available ring address (low) |
| `0x094` | `QUEUE_AVAIL_HIGH` | WO | Available ring address (high) |
| `0x0a0` | `QUEUE_USED_LOW` | WO | Used ring address (low) |
| `0x0a4` | `QUEUE_USED_HIGH` | WO | Used ring address (high) |
| `0x0fc` | `CONFIG_GENERATION` | RO | Config space version |
| `0x100` | `CONFIG` | RW | Device-specific config space |
### Interrupt Status Bits
| Bit | Name | Description |
|---|---|---|
| `0` | `VIRTIO_MMIO_INT_VRING` | Used buffer notification |
| `1` | `VIRTIO_MMIO_INT_CONFIG` | Config change notification |
### Usage: detect virtio device
```asm
movea.l #0xff010000, %a1 @ first virtio slot
move.l 0x000(%a1), %d0 @ MAGIC_VALUE
cmp.l #0x74726976, %d0 @ "virt" ?
bne no_device
move.l 0x008(%a1), %d0 @ DEVICE_ID
tst.l %d0
beq no_device
@ valid virtio device found, d0 = device ID
```
---
## m68k Bootinfo Protocol
Kernels can query device base addresses, memory layout, and machine properties via the m68k bootinfo protocol. QEMU places a bootinfo blob immediately after the kernel's ELF LOAD segment in guest physical memory.
### Record Format
Every bootinfo record has a 4-byte header followed by variable-length data:
```
struct bi_record {
uint16 tag; // tag ID (big-endian)
uint16 size; // total record size in bytes, including 4-byte header (big-endian)
uint8 data[size - 4]; // tag-specific data
};
```
Records are contiguous (no padding between them). The list is terminated by a sentinel record with `tag = 0` (`BI_LAST`).
### Locating the Bootinfo Blob
QEMU loads the kernel ELF at `0x10000` (the `LOAD_ADDR`). The `_end` linker symbol marks the end of the LOAD segment. QEMU places the bootinfo at the next word-aligned address:
```c
parameters_base = (_end + 1) & ~1;
```
When writing a kernel in assembly, use:
```asm
move.l #_end, d0
addq.l #1, d0
lsr.l #1, d0
lsl.l #1, d0
movea.l d0, a2 ; a2 = bootinfo base
```
### Parsing Loop
```asm
movea.l d0, a2 ; a2 = bootinfo base
1$:
clr.l d0
move.w (a2)+, d0 ; tag (2 bytes)
beq 3$ ; BI_LAST sentinel
clr.l d1
move.w (a2)+, d1 ; size = total record size (2 bytes)
move.l a2, a3 ; a3 -> data portion
lea (-4, a2, d1.l), a2 ; advance to next record
subq.l #4, d1 ; d1 = data size (total - header)
; handle tag d0 with data at a3 (d1 bytes)
bra 1$
3$: ; done
```
**Important**: The `size` field stores the *total* record size (including the 4-byte header), not just the data size. To advance `a2` to the next record after reading the header, compute `a2 + size - 4`. To get the data payload size, compute `size - 4`.
### Tag Definitions
#### Standard Tags (Linux/m68k)
Defined in Linux `include/uapi/asm-generic/bootinfo.h`:
| Tag | Name | Data Size | Description |
|------|---------------------|-----------|--------------------------------------------|
| 0x0000 | `BI_LAST` | 0 | End-of-list sentinel |
| 0x0001 | `BI_MACHTYPE` | 4 | Machine type (enum, see below) |
| 0x0002 | `BI_CPUTYPE` | 4 | CPU type bitmask (see below) |
| 0x0003 | `BI_FPUTYPE` | 4 | FPU type bitmask |
| 0x0004 | `BI_MMUTYPE` | 4 | MMU type bitmask |
| 0x0005 | `BI_MEMCHUNK` | 8 | Memory chunk: `{uint32 base, uint32 size}` |
| 0x0006 | `BI_RAMDISK` | 8 | Ramdisk: `{uint32 addr, uint32 size}` |
| 0x0007 | `BI_COMMAND_LINE` | variable | Null-terminated kernel command line string |
| 0x0008 | `BI_RNG_SEED` | 32 | Random seed for RNG initialization |
**BI_MACHTYPE values:**
| Value | Machine |
|-------|------------|
| 1 | Amiga |
| 2 | Atari |
| 3 | Mac |
| 4 | Apollo |
| 5 | Sun3 |
| 6 | MVME147 |
| 7 | MVME16x |
| 8 | BVME6000 |
| 9 | HP300 |
| 10 | Q40 |
| 11 | Sun3x |
| 12 | M54xx |
| 13 | M5441x |
| 14 | **VIRT** |
**BI_CPUTYPE (bitmask):**
| Bit | CPU |
|-----|-----------|
| 0 | 68020 |
| 1 | 68030 |
| 2 | 68040 |
| 3 | 68060 |
| 4 | ColdFire |
#### Virt-Specific Tags (BI_VIRT_*)
Defined in `bootinfo-virt.h`:
| Tag | Name | Data Size | Description |
|--------|-------------------------|-----------|--------------------------------------------------|
| 0x8000 | `BI_VIRT_QEMU_VERSION` | 4 | QEMU version: `(major << 16) \| minor` |
| 0x8001 | `BI_VIRT_GF_PIC_BASE` | 8 | Goldfish PIC base address + IRQ count |
| 0x8002 | `BI_VIRT_GF_RTC_BASE` | 8 | Goldfish RTC base address + count |
| 0x8003 | `BI_VIRT_GF_TTY_BASE` | 8 | Goldfish TTY base address + count |
| 0x8004 | `BI_VIRT_VIRTIO_BASE` | 8 | Virtio-MMIO base address + mmio region size |
| 0x8005 | `BI_VIRT_CTRL_BASE` | 4 | Virt controller base address |
For tags 0x8001-0x8004, the data is two `uint32` values: `{base address, count_or_size}`.
### Example: QEMU 11.0.0 on `m68020`
Bootinfo blob (captured via QEMU monitor `xp/32x <addr>`):
```
00010490: 0x00010008 0x0000000e 0x00020008 0x00000001
000104a0: 0x0005000c 0x00000000 0x08000000 0x80000008
000104b0: 0x0b000000 0x8001000c 0xff000000 0x00000001
000104c0: 0x8002000c 0xff006000 0x000000a8 0x8003000c
000104d0: 0xff008000 0x00000027 0x8005000c 0xff009000
000104e0: 0x00000008 0x8004000c 0xff010000 0x00000028
000104f0: 0x00070008 0x00000000 0x00080028 0x0020ac0e
00010500: 0xd72bb388 0x3ec5e0e0 0x5a1534b3 0xe295bd9a
```
Decoded (from `xp/40x 0x10490` with QEMU 11.0.0, m68020, 128 MiB RAM):
| Offset | Tag | Total | Data | Meaning |
|--------|---------------------|-------|------------------------------------------|-----------------------------------|
| 0x10490 | BI_MACHTYPE | 8 | 0x0000000e (14) | MACH_VIRT |
| 0x10498 | BI_CPUTYPE | 8 | 0x00000001 (bit 0) | CPU_68020 |
| 0x104a0 | BI_MEMCHUNK | 12 | base=0x00000000, size=0x08000000 | 128 MiB RAM at 0x0 |
| 0x104ac | BI_VIRT_QEMU_VER | 8 | 0x0B000000 (major=11, minor=0) | QEMU 11.0 |
| 0x104b4 | BI_VIRT_GF_PIC | 12 | base=0xFF000000, count=1 | 1 PIC instance |
| 0x104c0 | BI_VIRT_GF_RTC | 12 | base=0xFF006000, count=168 | 168 RTC regs |
| 0x104cc | BI_VIRT_GF_TTY | 12 | base=0xFF008000, count=39 | 39 TTY channels |
| 0x104d8 | BI_VIRT_CTRL | 12 | base=0xFF009000, count=8 | CTRL regs + feature bits |
| 0x104e4 | BI_VIRT_VIRTIO | 12 | base=0xFF010000, count=40 | 40 VIRTIO slots |
| 0x104f0 | BI_COMMAND_LINE | 8 | (empty) | No kernel command line |
| 0x104f8 | BI_RNG_SEED | 40 | 36 bytes random seed | RNG initialization seed |
| 0x10520 | BI_LAST | 4 | -- | End-of-list sentinel |
Key observations:
- The `size` field is the **total** record size including the 4-byte header.
- Most virt device records use 12 bytes total (8 bytes data: `{base, count}`).
- Singletons (MACHTYPE, CPUTYPE, QEMU_VERSION) use 8 bytes total (4 bytes data).
- BI_COMMAND_LINE is always present (4 bytes of zero when empty).
- BI_RNG_SEED: total=40 (0x28), data=36 bytes — the last record before the sentinel.
- BI_LAST has `size=4` (a 4-byte all-zero header with tag=0).
### Fixed Addresses vs Bootinfo
Although the bootinfo protocol provides device addresses, the QEMU `-M virt` machine uses fixed addresses for all MMIO devices (see Memory Map above). A standalone kernel that skips bootinfo can hardcode these addresses directly.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,783 @@
package prog8.codegen.m68k
// M68k assembly-level peephole optimizer.
//
// It operates on a MutableList<String> of assembly lines. Only instructions that a peephole
// actually matches are modified or removed; every other line (including all comments) is left
// completely untouched. This keeps the generated .asm readable and comments intact.
//
// To add a new peephole:
// 1. Write a private function `optimizeXxx(window: Sequence<List<TrimmedLine>>): List<Modification>`
// that inspects a sliding window of consecutive instructions and returns the Modifications
// it wants to apply.
// 2. Register it in optimizeAssembly() by adding a line:
// byN = runPass(optimizeXxx(byN), N, byN)
// The optimizer re-runs all passes until a full pass makes no changes (so cascading optimizations
// are picked up too).
internal fun optimizeAssembly(lines: MutableList<String>) {
val pretrimmed = lines.map { it.trimStart() }.toMutableList()
while (true) {
var modified = false
/** Runs a single optimization pass: applies any modifications, then recomputes the line windows. */
fun runPass(
mods: List<Modification>,
windowSize: Int,
currentLines: Sequence<List<TrimmedLine>>
): Sequence<List<TrimmedLine>> {
if (mods.isNotEmpty()) {
applyModifications(mods, lines, pretrimmed)
modified = true
return getLinesBy(pretrimmed, lines, windowSize)
}
return currentLines
}
var by2 = getLinesBy(pretrimmed, lines, 2)
by2 = runPass(optimizeRedundantReload(by2), 2, by2) // REGFILE-DEPENDENT
by2 = runPass(optimizeBounceToGlobal(by2, pretrimmed), 2, by2) // REGFILE-DEPENDENT
by2 = runPass(optimizeJmpToNextLabel(by2), 2, by2)
by2 = runPass(optimizeTailCall(by2), 2, by2)
by2 = runPass(optimizeRedundantTst(by2), 2, by2)
by2 = runPass(optimizeMsigbSpill(by2, pretrimmed), 2, by2) // REGFILE-DEPENDENT
// DBRA peephole for repeat loops is now obsolete: counted loops are emitted as IRLoopChunk and directly translated to dbra.
// The old REGFILE-DEPENDENT pattern (move #N,p8_regfile+slot / label: body / subq #1,slot / bne label) no longer occurs.
if (!modified)
break
}
// M3 is a full-file scan (not a 2-window peephole) and is idempotent; run once after the fixed-point loop
// REGFILE-DEPENDENT
val clampMods = optimizeClampImmediate(pretrimmed, lines)
if (clampMods.isNotEmpty()) {
applyModifications(clampMods, lines, pretrimmed)
}
// M4: fuse LOADX+JUMPI - eliminate the register file store/reload between array load and indirect jump
// REGFILE-DEPENDENT
val fuseMods = optimizeFuseLoadxJumpi(pretrimmed, lines)
if (fuseMods.isNotEmpty()) {
applyModifications(fuseMods, lines, pretrimmed)
}
// 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?)
/** Pre-trimmed assembly line that also knows its original text and source line index. */
private class TrimmedLine(val value: String, val trimmed: String, val index: Int) {
val instruction: String = trimmed.instructionPart()
}
private fun getLinesBy(pretrimmed: MutableList<String>, originalLines: MutableList<String>, windowSize: Int): Sequence<List<TrimmedLine>> =
pretrimmed.asSequence()
.withIndex()
.filter { it.value.isNotBlank() && !it.value.startsWith(';') }
.map { TrimmedLine(originalLines[it.index], it.value, it.index) }
.windowed(windowSize, partialWindows = false)
private fun applyModifications(modifications: List<Modification>, lines: MutableList<String>, pretrimmed: MutableList<String>) {
for (modification in modifications.sortedBy { it.lineIndex }.reversed()) {
val idx = modification.lineIndex
if (modification.remove) {
val pretrim = pretrimmed.getOrNull(idx)
// If the line carries a label we must keep it (other code may jump to it);
// otherwise the line is dropped entirely.
if (pretrim.isNullOrBlank() || pretrim.startsWith(';')) {
lines.removeAt(idx)
} else if (hasLabel(pretrim)) {
val label = keepLabel(pretrim)
if (label.isNotEmpty())
lines[idx] = label
else
lines.removeAt(idx)
} else {
lines.removeAt(idx)
}
pretrimmed.removeAt(idx)
} else {
lines[idx] = modification.replacement!!
pretrimmed[idx] = modification.replacement.trimStart()
}
}
}
// === helpers ===
/** Strips a leading label (e.g. "mylabel: move.l ..." -> "move.l ..."). */
private fun String.instructionPart(): String {
val idx = indexOf(':')
return if (idx >= 0) substring(idx + 1).trimStart() else this
}
private fun hasLabel(line: String): Boolean =
line.length > 1 && !line.startsWith(';') && ':' in line
/** Returns just the "label:" part of a line, or "" if there is no label. */
private fun keepLabel(line: String): String {
val idx = line.indexOf(':')
return if (idx >= 0) line.substring(0, idx + 1) else ""
}
// Deliberately only recognizes move.b/w/l (data register moves), not movea or movem:
// movea always writes to an address register (rejected by isSafeMemoryOperand), and
// movem uses register-list operands (also rejected). So they can never participate in
// the patterns we optimize, and excluding them keeps moveOperands() simple.
private fun String.isMove(): Boolean =
startsWith("move.b") || startsWith("move.w") || startsWith("move.l")
/** Size suffix of a move instruction: 'b', 'w' or 'l'. */
private fun String.moveSize(): Char = substringAfter('.').firstOrNull() ?: ' '
/** Splits a move instruction "move.S src, dst" into (src, dst), or null if it isn't a move. */
private fun String.moveOperands(): Pair<String, String>? {
val sp = indexOf(' ')
if (sp < 0) return null
val rest = substring(sp + 1)
val commentIdx = rest.indexOf(';')
val ops = (if (commentIdx >= 0) rest.substring(0, commentIdx) else rest).split(',', limit = 2)
if (ops.size != 2) return null
return ops[0].trim() to ops[1].trim()
}
private fun String.isRegister(): Boolean =
this.matches(Regex("^(d[0-7]|a[0-7]|fp[0-9]+|sp|pc)$"))
/**
* A plain memory operand that can be safely read twice and assumed to hit the same location
* (e.g. "p8_regfile+268", "sys.DOSBase", "$1234", "symbol"). Excludes data/address registers
* and any indirect/indexed addressing ((a0), (a0)+, ($1234,a0), ...) whose effective address may
* change between the two accesses.
*/
private fun String.isSafeMemoryOperand(): Boolean {
if (isEmpty()) return false
if (isRegister()) return false
if ('(' in this || ')' in this) return false
return true
}
// === peephole passes ===
private fun optimizeRedundantReload(linesBy: Sequence<List<TrimmedLine>>): List<Modification> {
// REGFILE-DEPENDENT: this pattern is produced by the current regfile-based
// codegen (store to p8_regfile then immediately reload to d0). If values are
// kept in registers instead of spilled to p8_regfile, this peephole will
// rarely fire and can be removed or generalized.
// move.S d0, MEM
// move.S MEM, d0 -> remove the second line
//
// The reload is redundant: d0 already holds the value (it was just stored from d0 and is
// unchanged between the two instructions), so dropping the reload leaves d0 intact and the
// store to MEM stays for any later readers.
// MEM must be a plain memory operand (see isSafeMemoryOperand) so both accesses hit the
// exact same location.
val mods = mutableListOf<Modification>()
for (lines in linesBy) {
val first = lines[0].instruction
val second = lines[1].instruction
// Never remove an instruction that is a jump target: arriving via the label, d0 is
// not guaranteed to hold the stored value, so the reload would be needed there.
if (hasLabel(lines[1].trimmed))
continue
if (first.isMove() && second.isMove() && first.moveSize() == second.moveSize()) {
val (src0, dst0) = first.moveOperands() ?: continue
val (src1, dst1) = second.moveOperands() ?: continue
if (src0 == "d0" && dst1 == "d0" && dst0 == src1 && dst0.isSafeMemoryOperand()) {
mods.add(Modification(lines[1].index, true, null))
}
}
}
return mods
}
private fun optimizeBounceToGlobal(linesBy: Sequence<List<TrimmedLine>>, allPretrimmed: List<String>): List<Modification> {
// REGFILE-DEPENDENT: explicitly matches p8_regfile spill followed by copy
// to a global. Depends on the current strategy of spilling all intermediate
// values to p8_regfile. If regfile is refactored, this bounce disappears.
// move.S d0, p8_regfile+OFF ; spill result to regfile temp
// move.S p8_regfile+OFF, GLOBAL ; copy temp -> global
// ->
// move.S d0, GLOBAL ; store directly, skip regfile bounce
//
// Only safe when p8_regfile+OFF is NOT read again in the remainder of the current
// subroutine. We scan forward from the global store to the next subroutine boundary
// to confirm this.
val mods = mutableListOf<Modification>()
for (lines in linesBy) {
val first = lines[0].instruction
val second = lines[1].instruction
if (hasLabel(lines[0].trimmed) || hasLabel(lines[1].trimmed))
continue
if (first.isMove() && second.isMove() && first.moveSize() == second.moveSize()) {
val (src0, dst0) = first.moveOperands() ?: continue
val (src1, dst1) = second.moveOperands() ?: continue
if (src0 == "d0" && dst1 != "d0" && dst0 == src1
&& dst0.isRegfileSlot() && dst1.isSafeMemoryOperand()
&& !isSlotReadAfter(dst0, lines[1].index + 1, allPretrimmed)
) {
val size = first.moveSize()
val indent = lines[1].value.takeWhile { it.isWhitespace() }
mods.add(Modification(lines[0].index, true, null)) // remove regfile spill
mods.add(Modification(lines[1].index, false, "${indent}move.$size d0, $dst1")) // direct store
}
}
}
return mods
}
private fun optimizeJmpToNextLabel(linesBy: Sequence<List<TrimmedLine>>): List<Modification> {
// bra LABEL
// LABEL: -> remove the branch (keep any label on the branch line itself)
//
// The branch is dead code because the target label immediately follows.
// (also handles the older jmp form)
val mods = mutableListOf<Modification>()
for (lines in linesBy) {
val firstInstr = lines[0].instruction
val secondLine = lines[1].trimmed
val branchMnemonic = when {
firstInstr.startsWith("bra ") -> "bra"
firstInstr.startsWith("jmp ") -> "jmp"
else -> null
} ?: continue
val target = firstInstr.substringAfter("$branchMnemonic ").trim()
// Skip indirect jumps like jmp ([p8_regfile+12]) or jmp (a0)
if (target.startsWith("("))
continue
if (hasLabel(secondLine)) {
val label = keepLabel(secondLine).removeSuffix(":")
if (target == label) {
if (hasLabel(lines[0].trimmed)) {
// The branch line also carries a label - keep the label, drop the branch
mods.add(Modification(lines[0].index, false, keepLabel(lines[0].trimmed)))
} else {
mods.add(Modification(lines[0].index, true, null))
}
}
}
}
return mods
}
private fun optimizeTailCall(linesBy: Sequence<List<TrimmedLine>>): List<Modification> {
// bsr LABEL
// rts -> bra LABEL
//
// When a subroutine call is immediately followed by a return, replace the call with a branch.
// The called routine will return directly to the caller's caller (tail call optimization).
// Only handles direct label targets, not indirect (bsr (a0) etc.).
// (also handles the older jsr form, normalizing it to bra as well)
val mods = mutableListOf<Modification>()
for (lines in linesBy) {
val firstInstr = lines[0].instruction
val secondInstr = lines[1].instruction
// Don't optimize if the rts line has a label (it's a jump target)
if (hasLabel(lines[1].trimmed))
continue
val callMnemonic = when {
firstInstr.startsWith("bsr ") -> "bsr"
firstInstr.startsWith("jsr ") -> "jsr"
else -> null
} ?: continue
if (secondInstr == "rts") {
val target = firstInstr.substringAfter("$callMnemonic ").trim()
// Skip indirect/library calls like bsr (a0), jsr (a0) or jsr dos.VPrintf(a6)
if ('(' in target)
continue
val indent = lines[0].value.takeWhile { it.isWhitespace() }
if (hasLabel(lines[0].trimmed)) {
// Keep the label, replace call with branch and remove rts
val label = keepLabel(lines[0].trimmed)
mods.add(Modification(lines[0].index, false, "$label\n${indent}bra $target"))
mods.add(Modification(lines[1].index, true, null))
} else {
// Replace call with branch and remove rts
mods.add(Modification(lines[0].index, false, "${indent}bra $target"))
mods.add(Modification(lines[1].index, true, null))
}
}
}
return mods
}
private fun optimizeRedundantTst(linesBy: Sequence<List<TrimmedLine>>): List<Modification> {
// move.S SRC, DST
// tst.S DST -> remove the tst
//
// andi.S #imm, DST
// tst.S DST -> remove the tst
//
// ori.S #imm, DST
// tst.S DST -> remove the tst
//
// On M68k, move/andi/ori/eori set N, Z based on the result. So a tst
// immediately after to the same location is redundant.
val mods = mutableListOf<Modification>()
for (lines in linesBy) {
val first = lines[0].instruction
val second = lines[1].instruction
// Don't remove tst if it's a jump target
if (hasLabel(lines[1].trimmed))
continue
if (!second.startsWith("tst."))
continue
val dst0 = first.flagSettingDest() ?: continue
if (first.moveSize() != second.moveSize())
continue
val tstRaw = second.substringAfter('.').substringAfter(' ').trim()
val tstOperand = if (';' in tstRaw) tstRaw.substringBefore(';').trim() else tstRaw
if (dst0 == tstOperand && dst0.isSafeMemoryOperand()) {
mods.add(Modification(lines[1].index, true, null))
}
}
return mods
}
/**
* If this instruction sets N/Z flags and writes to a single destination, return that destination.
* Covers move, andi, ori, eori (the ones the codegen emits before tst).
* Note: add/sub also set condition codes, but the codegen always inserts a move before branching,
* so add/sub are never followed by a redundant tst.
*/
private fun String.flagSettingDest(): String? {
val stripped = if (';' in this) substring(0, indexOf(';')).trim() else this
return when {
isMove() -> moveOperands()?.second
startsWith("andi.") || startsWith("ori.") || startsWith("eori.") -> {
// andi.S #imm, dst
val sp = stripped.indexOf(' ')
if (sp < 0) return null
val ops = stripped.substring(sp + 1).split(',', limit = 2)
if (ops.size != 2) return null
ops[1].trim()
}
else -> null
}
}
private fun optimizeMsigbSpill(linesBy: Sequence<List<TrimmedLine>>, allPretrimmed: List<String>): List<Modification> {
// REGFILE-DEPENDENT: optimizes the spill of a word/long to p8_regfile
// followed by a byte read of its MSB. This spill is an artifact of the
// current regfile-based codegen and big-endian layout. If values stay in
// registers, the spill is never emitted.
// move.w/l SYMBOL, p8_regfile+N ; spill word/long to slot
// move.b p8_regfile+N, d0 ; read MSB
// -> move.b SYMBOL, d0 ; big-endian: MSB is at SYMBOL+0, no spill needed
// Keep the spill only if the slot is read again later in the subroutine.
val mods = mutableListOf<Modification>()
for (lines in linesBy) {
if (hasLabel(lines[0].trimmed) || hasLabel(lines[1].trimmed)) continue
val first = lines[0].instruction
val second = lines[1].instruction
if (!first.isMove() || !second.isMove()) continue
if (second.moveSize() != 'b') continue
val firstSize = first.moveSize()
if (firstSize != 'w' && firstSize != 'l') continue
val (src0, dst0) = first.moveOperands() ?: continue
val (src1, dst1) = second.moveOperands() ?: continue
if (dst1 != "d0") continue
if (src1 != dst0) continue
if (!dst0.isRegfileSlot()) continue
if (src0.isRegfileSlot() || src0.isRegister()) continue
if ('(' in src0 || ')' in src0) continue
// src0 must be a plain memory operand (symbol)
if (!src0.isSafeMemoryOperand()) continue
// If the slot is read again later, keep the spill; otherwise we can remove it
val indent2 = lines[1].value.takeWhile { it.isWhitespace() }
if (isSlotReadAfter(dst0, lines[1].index + 1, allPretrimmed)) {
// keep spill, just rewrite the byte load to read directly from the symbol
mods.add(Modification(lines[1].index, false, "${indent2}move.b $src0,d0"))
} else {
mods.add(Modification(lines[0].index, true, null))
mods.add(Modification(lines[1].index, false, "${indent2}move.b $src0,d0"))
}
}
return mods
}
private fun optimizeClampImmediate(allPretrimmed: List<String>, lines: MutableList<String>): List<Modification> {
// REGFILE-DEPENDENT: folds clamp bounds that were materialized as
// move #imm,p8_regfile+SLOT. Depends on the current codegen that spills
// clamp immediates to regfile slots before the compare block. If immediates
// are kept in registers or encoded as cmpi, this pattern disappears.
// M3: fold clamp bounds that were materialized as move #imm,SLOT.
// Only fold when we can identify the full clamp block, to avoid touching unrelated compares.
// Pattern (filtered lines, b/w/l size must match):
// cmp.S SLOT_MIN,d0
// bge/bhs .clamp_max
// move.S SLOT_MIN,d0
// bra .clamp_done
// .clamp_max:
// cmp.S SLOT_MAX,d0
// ble/bls .clamp_done
// move.S SLOT_MAX,d0
// .clamp_done:
// where SLOT_MIN/MAX were defined as move.S #imm,SLOT within a few lines before the block.
val filtered = allPretrimmed.withIndex()
.filter { it.value.isNotBlank() && !it.value.trimStart().startsWith(';') }
.map { TrimmedLine(lines[it.index], it.value.trimStart(), it.index) }
fun findDefine(slot: String, size: Char, useIdx: Int, limit: Int = 6): Pair<Int,String>? {
var count = 0
for (k in useIdx-1 downTo 0) {
if (count >= limit) break
count++
val cand = filtered[k]
val instr = cand.instruction
if (!instr.isMove()) continue
if (instr.moveSize() != size) continue
val (src, dst) = instr.moveOperands() ?: continue
if (dst != slot) continue
if (!src.startsWith("#")) continue
var overwritten = false
for (t in k+1 until useIdx) {
val mid = filtered[t]
if (mid.instruction.isMove()) {
val ops = mid.instruction.moveOperands()
if (ops != null && ops.second == slot) { overwritten = true; break }
}
}
if (!overwritten) return cand.index to src
}
return null
}
val mods = mutableListOf<Modification>()
// Scan for clamp blocks
for (idx in filtered.indices) {
val instr = filtered[idx].instruction
if (!instr.startsWith("cmp.")) continue
// need at least 8 more filtered lines for a full block
if (idx + 8 >= filtered.size) continue
val sizeMin = instr[4]
if (sizeMin != 'b' && sizeMin != 'w' && sizeMin != 'l') continue
val cmpMinParts = instr.substringAfter(' ').trim().split(',', limit = 2).map { it.trim().substringBefore(';').trim() }
if (cmpMinParts.size != 2 || cmpMinParts[1] != "d0" || !cmpMinParts[0].isRegfileSlot()) continue
val slotMin = cmpMinParts[0]
val bgeInstr = filtered[idx+1].instruction
if (!bgeInstr.startsWith("bge ") && !bgeInstr.startsWith("bhs ")) continue
// bge target should be the next label
val labelMax = bgeInstr.substringAfter(' ').trim().substringBefore(';').trim()
val moveMinInstr = filtered[idx+2].instruction
if (!moveMinInstr.startsWith("move.")) continue
if (moveMinInstr.moveSize() != sizeMin) continue
val moveMinOps = moveMinInstr.moveOperands() ?: continue
if (moveMinOps.first != slotMin || moveMinOps.second != "d0") continue
val braInstr = filtered[idx+3].instruction
if (!braInstr.startsWith("bra ")) continue
val labelDone = braInstr.substringAfter(' ').trim().substringBefore(';').trim()
val labelMaxLine = filtered[idx+4].trimmed
if (!labelMaxLine.startsWith("$labelMax:")) continue
val cmpMaxInstr = filtered[idx+5].instruction
if (!cmpMaxInstr.startsWith("cmp.")) continue
if (cmpMaxInstr[4] != sizeMin) continue
val cmpMaxParts = cmpMaxInstr.substringAfter(' ').trim().split(',', limit = 2).map { it.trim().substringBefore(';').trim() }
if (cmpMaxParts.size != 2 || cmpMaxParts[1] != "d0" || !cmpMaxParts[0].isRegfileSlot()) continue
val slotMax = cmpMaxParts[0]
val bleInstr = filtered[idx+6].instruction
if (!bleInstr.startsWith("ble ") && !bleInstr.startsWith("bls ")) continue
val bleTarget = bleInstr.substringAfter(' ').trim().substringBefore(';').trim()
if (bleTarget != labelDone) continue
val moveMaxInstr = filtered[idx+7].instruction
if (!moveMaxInstr.startsWith("move.")) continue
if (moveMaxInstr.moveSize() != sizeMin) continue
val moveMaxOps = moveMaxInstr.moveOperands() ?: continue
if (moveMaxOps.first != slotMax || moveMaxOps.second != "d0") continue
val labelDoneLine = filtered[idx+8].trimmed
if (!labelDoneLine.startsWith("$labelDone:")) continue
// Found a clamp block; try to fold each bound if its slot has an immediate define
val defineMin = findDefine(slotMin, sizeMin, idx)
if (defineMin != null) {
val immMin = defineMin.second
val indentCmp = filtered[idx].value.takeWhile { it.isWhitespace() }
val indentMove = filtered[idx+2].value.takeWhile { it.isWhitespace() }
mods.add(Modification(filtered[idx].index, false, "${indentCmp}cmpi.$sizeMin $immMin,d0"))
mods.add(Modification(filtered[idx+2].index, false, "${indentMove}move.$sizeMin $immMin,d0"))
// If the slot is not read after the clamp block, the defining move is dead
val defineIdx = defineMin.first
if (!isSlotReadAfter(slotMin, filtered[idx+8].index + 1, allPretrimmed)) {
mods.add(Modification(defineIdx, true, null))
}
}
val defineMax = findDefine(slotMax, sizeMin, idx+5)
if (defineMax != null) {
val immMax = defineMax.second
val indentCmp2 = filtered[idx+5].value.takeWhile { it.isWhitespace() }
val indentMove2 = filtered[idx+7].value.takeWhile { it.isWhitespace() }
mods.add(Modification(filtered[idx+5].index, false, "${indentCmp2}cmpi.$sizeMin $immMax,d0"))
mods.add(Modification(filtered[idx+7].index, false, "${indentMove2}move.$sizeMin $immMax,d0"))
val defineIdxMax = defineMax.first
if (!isSlotReadAfter(slotMax, filtered[idx+8].index + 1, allPretrimmed)) {
mods.add(Modification(defineIdxMax, true, null))
}
}
}
return mods
}
// REGFILE-DEPENDENT helper: identifies operands that refer to the current
// p8_regfile spill area. If the regfile is refactored, this predicate and
// all peepholes that use it must be updated.
/** True if this operand is a prog8 register-file slot (e.g. "p8_regfile+268", "p8_fregfile+12"). */
private fun String.isRegfileSlot(): Boolean =
startsWith("p8_regfile+") || startsWith("p8_fregfile+")
// REGFILE-DEPENDENT helper: scans for future reads of a regfile slot.
// Used by bounce/msigb/clamp/fuse peepholes to decide if a spill can be removed.
/**
* Scans forward from [startIndex] to the next subroutine boundary (or end of file) and
* returns true if [slot] is referenced in any instruction line in that range.
* A "reference" means the slot string appears as a standalone operand (not as a prefix
* of a longer offset like p8_regfile+640 when checking p8_regfile+64).
*/
private fun isSlotReadAfter(slot: String, startIndex: Int, allPretrimmed: List<String>): Boolean {
for (i in startIndex until allPretrimmed.size) {
val line = allPretrimmed[i]
if (line.startsWith("; ---- Subroutine:") || line.startsWith("; End of subroutine:")
|| line.startsWith("prog8_program_end"))
break
if (line.isBlank() || line.startsWith(';')) continue
if (isSlotReferencedInLine(slot, line)) return true
}
return false
}
// REGFILE-DEPENDENT helper
private fun isSlotReferencedInLine(slot: String, line: String): Boolean {
var from = 0
while (true) {
val idx = line.indexOf(slot, from)
if (idx < 0) return false
val after = idx + slot.length
// Ensure it's not a prefix of a longer offset (e.g. p8_regfile+6 vs p8_regfile+64)
if (after < line.length && line[after].isDigit()) {
from = after
continue
}
return true
}
}
/**
* REGFILE-DEPENDENT: fuses the LOADX+JUMPI / LOADX+CALLI sequence for
* `on .. goto` / `on .. call` / `when` dispatch. The codegen emits an index
* load, table lookup into d0, spill to p8_regfile, reload to a0 and indirect
* jump/call. If the regfile is refactored so the table lookup can target a0
* (or an address register) directly, this spill/reload disappears and the
* peephole becomes dead code.
*
* Current codegen (byte index, 68000-style JUMPI/CALLI):
* moveq #0, d0
* move.b p8_regfile+N, d0 ; or move.w p8_regfile+N,d0 for word index
* lea table_label, a0
* move.l (a0,d0.w), d0 ; table lookup (unscaled, see scaled-indexing-IR.md)
* move.l d0, p8_regfile+M ; spill (LOADX tail)
* movea.l p8_regfile+M, a0 ; reload (JUMPI/CALLI head, 68000)
* jmp/jsr (a0)
* or for 68020+ JUMPI/CALLI:
* jmp/jsr ([p8_regfile+M]) ; memory-indirect
*
* 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 ; or (a0,d0.w*4), a0 on 68020+
* jmp/jsr (a0)
*/
private fun optimizeFuseLoadxJumpi(allPretrimmed: List<String>, lines: MutableList<String>): List<Modification> {
val filtered = allPretrimmed.withIndex()
.filter { it.value.isNotBlank() && !it.value.trimStart().startsWith(';') }
.map { TrimmedLine(lines[it.index], it.value.trimStart(), it.index) }
val mods = mutableListOf<Modification>()
val alreadyModified = mutableSetOf<Int>()
fun 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? {
// "move.l d0, p8_regfile+M"
if (!instr.startsWith("move.l")) return null
val ops = instr.moveOperands() ?: return null
if (ops.first != "d0") return null
if (!ops.second.isRegfileSlot()) return null
return ops.second.removePrefix("p8_regfile+").substringBefore(',').substringBefore(' ').trim()
}
for (leaIdx in filtered.indices) {
if (filtered[leaIdx].index in alreadyModified) continue
val leaInstr = filtered[leaIdx].instruction
if (!leaInstr.startsWith("lea ")) continue
if (!leaInstr.endsWith(",a0")) continue
// The index is usually already in d0 at this point (scaled via lsl.w #2
// or zero-extended). Earlier peepholes may have removed the explicit
// reload of the scaled index from p8_regfile, so we do not require a
// preceding moveq/move.b or move.w pattern. We only verify the lea
// itself and the following LOADX+JUMPI spill/jump sequence.
// Need at least: lea, loadIndexed, spill, jump/reload
if (leaIdx + 2 >= filtered.size) continue
val loadIdx = leaIdx + 1
val spillIdx = leaIdx + 2
if (filtered[loadIdx].index in alreadyModified || filtered[spillIdx].index in alreadyModified) continue
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),
// 68020 uses memory-indirect jmp/jsr ([slot])
val nextIdx = leaIdx + 3
if (nextIdx >= filtered.size) continue
val nextInstr = filtered[nextIdx].instruction
val is68000Reload = nextInstr.startsWith("movea.l") && slotM in nextInstr && nextInstr.trim().endsWith(",a0")
val is68020MemIndirectJmp = nextInstr.startsWith("jmp") && "([p8_regfile+$slotM" in nextInstr.replace(" ", "")
val is68020MemIndirectJsr = nextInstr.startsWith("jsr") && "([p8_regfile+$slotM" in nextInstr.replace(" ", "")
if (is68000Reload) {
// Need jmp/jsr (a0) after the reload
if (nextIdx + 1 >= filtered.size) continue
val jmpIdx = nextIdx + 1
if (filtered[jmpIdx].index in alreadyModified) continue
val jmpInstr = filtered[jmpIdx].instruction
val isJmp = jmpInstr == "jmp (a0)" || jmpInstr.replace(" ", "") == "jmp(a0)"
val isJsr = jmpInstr.startsWith("jsr") && "(a0)" in jmpInstr.replace(" ", "")
if (!isJmp && !isJsr) continue
// No labels on lea, load, spill, reload, jmp/jsr
if ((0..1).any { hasLabel(filtered[loadIdx + it].trimmed) }) continue
if (hasLabel(filtered[leaIdx].trimmed)) continue
if (hasLabel(filtered[spillIdx].trimmed) || hasLabel(filtered[nextIdx].trimmed) || hasLabel(filtered[jmpIdx].trimmed)) continue
val loadIndent = lines[filtered[loadIdx].index].takeWhile { it.isWhitespace() }
mods.add(Modification(filtered[loadIdx].index, false, "${loadIndent}move.l $loadOperand, a0"))
mods.add(Modification(filtered[spillIdx].index, true, null))
mods.add(Modification(filtered[nextIdx].index, true, null))
alreadyModified.add(filtered[loadIdx].index)
alreadyModified.add(filtered[spillIdx].index)
alreadyModified.add(filtered[nextIdx].index)
// jmp/jsr stays as is
} else if (is68020MemIndirectJmp || is68020MemIndirectJsr) {
if (hasLabel(filtered[spillIdx].trimmed) || hasLabel(filtered[nextIdx].trimmed)) continue
if (hasLabel(filtered[leaIdx].trimmed) || hasLabel(filtered[loadIdx].trimmed)) continue
val loadIndent = lines[filtered[loadIdx].index].takeWhile { it.isWhitespace() }
val jmpIndent = lines[filtered[nextIdx].index].takeWhile { it.isWhitespace() }
val mnemonic = if (is68020MemIndirectJsr) "jsr" else "jmp"
mods.add(Modification(filtered[loadIdx].index, false, "${loadIndent}move.l $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)
alreadyModified.add(filtered[spillIdx].index)
alreadyModified.add(filtered[nextIdx].index)
}
}
return mods
}
@@ -0,0 +1,182 @@
package prog8.codegen.m68k
import prog8.code.assembly.IAssemblyProgram
import prog8.code.core.*
import prog8.code.target.Amiga1200Target
import prog8.code.target.Amiga500Target
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) : IAssemblyProgram {
override val irInstructionCount: Int = 0
override val irChunkCount: Int = 0
override val irRegisterCount: Int = 0
private fun elfFile(outs: OutputFiles): Path = outs.programFile
/**
* 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)
if (quiet)
proc.redirectOutput(ProcessBuilder.Redirect.DISCARD)
val process = try {
proc.start()
} catch (e: Exception) {
when (tool) {
"vasm" -> {
System.err.println("Cannot find '${command[0]}' (vasm assembler for m68k). Install it via your package manager if it's on there, or build it from source: http://sun.hasenbraten.de/vasm/")
}
"vlink" -> {
System.err.println("Cannot find 'vlink' (linker). Install it via your package manager if it's on there, or build it from source: http://sun.hasenbraten.de/vlink/")
}
else -> {
System.err.println("process failed to start: ${e.message}")
}
}
return false
}
if (!quiet) {
process.inputStream.bufferedReader().use { reader ->
reader.forEachLine { println(it) }
}
}
return process.waitFor() == 0
}
override fun assemble(options: CompilationOptions, errors: IErrorReporter): Boolean {
val cpu = when(options.compTarget.cpu) {
CpuType.M68000 -> "68000"
CpuType.M68020 -> "68020"
else -> error("invalid cpu type for m68k codegen ${options.compTarget.cpu}")
}
val assemblerCpu = if(options.compTarget.name == "amiga500" && options.floats) "68020" else cpu
val outs = OutputFiles.of(options, name)
when(options.output) {
OutputType.ELF -> {
// Step 1: assemble to ELF object file
val objFile = outs.obj()
val listFile = outs.list()
val assembleCmd = mutableListOf(
"vasmm68k_mot",
"-m$assemblerCpu",
"-m68881", // enable FPU
"-Felf",
"-opt-speed",
"-warnunaligned",
"-ldots",
"-spaces",
"-o", objFile.toString(),
outs.asm().toString()
)
if (options.asmListfile)
assembleCmd.addAll(listOf("-L", listFile.toString()))
if (options.asmQuiet)
assembleCmd.add("-quiet")
if (!runProcess(assembleCmd, options.quiet, "vasm"))
return false
// clean up any leftover ELF/obj files from previous builds
outs.binFile().deleteIfExists()
// Step 2: write linker script and link to ELF executable
val linkScript = outs.linkScript()
val elfFile = elfFile(outs)
linkScript.writeText(resolveLinkerScript(options.compTarget))
val linkCmd = listOf(
"vlink",
"-b", "elf32m68k",
"-n",
"-T", linkScript.toString(),
"-o", elfFile.toString(),
objFile.toString()
)
val linkOk = runProcess(linkCmd, options.quiet, "vlink")
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 = outs.programFile
val listfile = outs.list()
val assembleCmd = when(options.compTarget) {
is Amiga500Target -> {
// amiga 500 with kickstart 1.3
mutableListOf(
"vasmm68k_mot",
"-m$assemblerCpu",
*if(options.floats) arrayOf("-m68881") else emptyArray(),
"-Fhunkexe",
"-kick1hunks", // old hunk format compatible with AmigaDOS 1.3
"-opt-speed",
"-warnunaligned",
"-ldots",
"-spaces",
"-nosym", // no debug symbols
"-o", exefile.toString(),
outs.asm().toString()
).also { cmd ->
if (options.asmListfile) {
cmd.add("-L")
cmd.add(listfile.toString())
cmd.add("-Lns")
}
if (options.asmQuiet)
cmd.add("-quiet")
}
}
is Amiga1200Target -> {
// amiga 1200 with 68020 and optional FPU
mutableListOf(
"vasmm68k_mot",
"-m$assemblerCpu",
"-m68881", // enable FPU
"-Fhunkexe",
"-opt-speed",
"-warnunaligned",
"-ldots",
"-spaces",
"-nosym", // no debug symbols
"-o", exefile.toString(),
outs.asm().toString()
).also { cmd ->
if (options.asmListfile) {
cmd.add("-L")
cmd.add(listfile.toString())
}
if (options.asmQuiet)
cmd.add("-quiet")
}
}
else -> error("unsupported target for AMIGAHUNK output: ${options.compTarget.name}")
}
if (!runProcess(assembleCmd, options.quiet, "vasm"))
return false
if(!options.quiet)
println("Executable written to '$exefile'")
return true
}
else -> error("Unsupported output type: ${options.output}")
}
}
}
@@ -0,0 +1,820 @@
package prog8.codegen.m68k
import prog8.code.core.CpuType
import prog8.intermediate.*
internal fun AsmGen.translateArithmetic(insn: IRInstruction) {
val type = insn.type ?: IRDataType.BYTE
if (type == IRDataType.FLOAT) {
translateFloatArithmetic(insn)
return
}
when (insn.opcode) {
Opcode.INC -> {
val reg = insn.requireIntDest().intNumber
emitLine("addq${dtSuffix(type)} #1, ${regAddr(reg)}")
invalidateD0CacheForSlot(reg)
}
Opcode.INCM -> {
val target = resolveMemory(insn.requireMemory())
emitLine("addq${dtSuffix(type)} #1, $target")
invalidateD0CacheForAddress(target)
}
Opcode.DEC -> {
val reg = insn.requireIntDest().intNumber
emitLine("subq${dtSuffix(type)} #1, ${regAddr(reg)}")
invalidateD0CacheForSlot(reg)
}
Opcode.DECM -> {
val target = resolveMemory(insn.requireMemory())
emitLine("subq${dtSuffix(type)} #1, $target")
invalidateD0CacheForAddress(target)
}
Opcode.NEG -> {
val reg = insn.requireIntDest().intNumber
emitLine("neg${dtSuffix(type)} ${regAddr(reg)}")
invalidateD0CacheForSlot(reg)
}
Opcode.NEGM -> {
val target = resolveMemory(insn.requireMemory())
emitLine("neg${dtSuffix(type)} $target")
invalidateD0CacheForAddress(target)
}
Opcode.ADDR -> {
val dstReg = insn.requireIntDest().intNumber
val srcReg = insn.requireIntSourceA().intNumber
loadRegOrZeroExtendToD0(srcReg, type)
emitLine("add${dtSuffix(type)} d0, ${regAddr(dstReg)}")
invalidateD0CacheForSlot(dstReg)
}
Opcode.ADD -> {
val reg = insn.requireIntDest().intNumber
val value = insn.requireImmediateInt()
if(value in 1..8) {
emitLine("addq${dtSuffix(type)} #$value, ${regAddr(reg)}")
} else {
emitLine("add${dtSuffix(type)} #${immVal(value, type)}, ${regAddr(reg)}")
}
invalidateD0CacheForSlot(reg)
}
Opcode.ADDM -> {
val reg = insn.requireIntSourceA().intNumber
val target = resolveMemory(insn.requireMemory())
val sv = dtSuffix(type)
emitLoadD0(reg, type)
emitLine("add$sv d0, $target")
invalidateD0CacheForAddress(target)
}
Opcode.ADDIM -> {
val value = insn.requireImmediateInt()
val target = resolveMemory(insn.requireMemory())
val sv = dtSuffix(type)
if(value in 1..8) {
emitLine("addq$sv #$value,$target")
} else {
when (type) {
IRDataType.BYTE -> {
require(value in 2..255)
emitLine("add$sv #$value,$target")
}
IRDataType.WORD -> {
require(value in 2..65535)
emitLine("add$sv #$value,$target")
}
IRDataType.LONG, IRDataType.POINTER -> {
require(value in 2..0x7fffffff)
emitLine("add$sv #$value,$target")
}
}
}
invalidateD0CacheForAddress(target)
}
Opcode.SUBR -> {
val dstReg = insn.requireIntDest().intNumber
val srcReg = insn.requireIntSourceA().intNumber
loadRegOrZeroExtendToD0(srcReg, type)
emitLine("sub${dtSuffix(type)} d0, ${regAddr(dstReg)}")
invalidateD0CacheForSlot(dstReg)
}
Opcode.SUB -> {
val reg = insn.requireIntDest().intNumber
val value = insn.requireImmediateInt()
if(value in 1..8) {
emitLine("subq${dtSuffix(type)} #$value, ${regAddr(reg)}")
} else {
emitLine("sub${dtSuffix(type)} #${immVal(value, type)}, ${regAddr(reg)}")
}
invalidateD0CacheForSlot(reg)
}
Opcode.SUBM -> {
val reg = insn.requireIntSourceA().intNumber
val target = resolveMemory(insn.requireMemory())
val sv = dtSuffix(type)
emitLoadD0(reg, type)
emitLine("sub$sv d0, $target")
invalidateD0CacheForAddress(target)
}
Opcode.SUBIM -> {
val value = insn.requireImmediateInt()
val target = resolveMemory(insn.requireMemory())
val sv = dtSuffix(type)
if(value in 1..8) {
emitLine("subq$sv #$value,$target")
} else {
when (type) {
IRDataType.BYTE -> {
require(value in 2..255)
emitLine("sub$sv #$value,$target")
}
IRDataType.WORD -> {
require(value in 2..65535)
emitLine("sub$sv #$value,$target")
}
IRDataType.LONG, IRDataType.POINTER -> {
require(value in 2..0x7fffffff)
emitLine("sub$sv #$value,$target")
}
}
}
invalidateD0CacheForAddress(target)
}
// --- Multiply (unsigned) ---
// M68k has no mulu.b or muls.b; use .w with zero-extension for byte.
Opcode.MULR -> emitMulOp(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type, unsigned=true, imm=null, target=null)
Opcode.MUL -> emitMulOp(insn.requireIntDest().intNumber, null, type, unsigned=true, imm=insn.requireImmediateInt(), target=null)
Opcode.MULM -> emitMulOp(insn.requireIntSourceA().intNumber, null, type, unsigned=true, imm=null, target=resolveMemory(insn.requireMemory()))
// --- Multiply (signed) ---
Opcode.MULSR -> emitMulOp(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type, unsigned=false, imm=null, target=null)
Opcode.MULS -> emitMulOp(insn.requireIntDest().intNumber, null, type, unsigned=false, imm=insn.requireImmediateInt(), target=null)
Opcode.MULSM -> emitMulOp(insn.requireIntSourceA().intNumber, null, type, unsigned=false, imm=null, target=resolveMemory(insn.requireMemory()))
// --- Divide (unsigned) ---
Opcode.DIVR -> emitDivOp(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type, unsigned=true, imm=null, target=null)
Opcode.DIV -> emitDivOp(insn.requireIntDest().intNumber, null, type, unsigned=true, imm=insn.requireImmediateInt(), target=null)
Opcode.DIVM -> emitDivOp(insn.requireIntSourceA().intNumber, null, type, unsigned=true, imm=null, target=resolveMemory(insn.requireMemory()))
// --- Divide (signed) ---
Opcode.DIVSR -> emitDivOp(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type, unsigned=false, imm=null, target=null)
Opcode.DIVS -> emitDivOp(insn.requireIntDest().intNumber, null, type, unsigned=false, imm=insn.requireImmediateInt(), target=null)
Opcode.DIVSM -> emitDivOp(insn.requireIntSourceA().intNumber, null, type, unsigned=false, imm=null, target=resolveMemory(insn.requireMemory()))
// --- Modulus ---
// Use divu.w/divs.w remainder (upper 16 bits) for byte/word; divul/divsl for long (68020+)
Opcode.MODR -> emitModOp(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type, unsigned=true, imm=null)
Opcode.MOD -> emitModOp(insn.requireIntDest().intNumber, null, type, unsigned=true, imm=insn.requireImmediateInt())
Opcode.MODSR -> emitModOp(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type, unsigned=false, imm=null)
Opcode.MODS -> emitModOp(insn.requireIntDest().intNumber, null, type, unsigned=false, imm=insn.requireImmediateInt())
// --- DIVMOD (unsigned) ---
// 68020+ divul.l gives quotient + remainder in one instruction
Opcode.DIVMODR -> emitDivModOp(insn.requireIntDest().intNumber, insn.requireDestB().intNumber, type, unsigned=true, imm=null)
Opcode.DIVMOD -> emitDivModOp(insn.requireIntDest().intNumber, insn.requireDestB().intNumber, type, unsigned=true, imm=insn.requireImmediateInt())
Opcode.SDIVMODR -> emitDivModOp(insn.requireIntDest().intNumber, insn.requireDestB().intNumber, type, unsigned=false, imm=null)
Opcode.SDIVMOD -> emitDivModOp(insn.requireIntDest().intNumber, insn.requireDestB().intNumber, type, unsigned=false, imm=insn.requireImmediateInt())
// --- Integer sqrt/square ---
Opcode.SQRT -> {
val dstReg = insn.requireIntDest().intNumber
val srcReg = insn.requireIntSourceA().intNumber
when (type) {
IRDataType.BYTE -> {
emitLine("move.b ${regAddr(srcReg)}, math._sqrt_ub.value", "sqrt byte")
invalidateD0Cache()
emitLine("bsr math._sqrt_ub")
emitStoreD0(dstReg, IRDataType.BYTE)
}
IRDataType.WORD -> {
emitLine("move.w ${regAddr(srcReg)}, math._sqrt_uw.value", "sqrt word")
invalidateD0Cache()
emitLine("bsr math._sqrt_uw")
emitStoreD0(dstReg, IRDataType.BYTE)
}
IRDataType.LONG, IRDataType.POINTER -> {
emitLine("move.l ${regAddr(srcReg)}, math._sqrt_l.value", "sqrt long")
invalidateD0Cache()
emitLine("bsr math._sqrt_l")
emitStoreD0(dstReg, IRDataType.WORD)
}
}
}
Opcode.SQUARE -> {
val dstReg = insn.requireIntDest().intNumber
val srcReg = insn.requireIntSourceA().intNumber
when (type) {
IRDataType.BYTE -> {
emitLoadD0(srcReg, IRDataType.BYTE)
emitLine($$"and.l #$ff, d0", "zero extend")
invalidateD0Cache()
emitLine("mulu.w d0, d0")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.BYTE)
}
IRDataType.WORD -> {
emitLoadD0(srcReg, IRDataType.WORD)
emitLine("mulu.w d0, d0")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.WORD)
}
IRDataType.LONG, IRDataType.POINTER -> {
emitLoadD0(srcReg, IRDataType.LONG)
emitLine("mulu.l d0, d0")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.LONG)
}
}
}
Opcode.CMP -> {
val leftReg = insn.requireIntSourceA().intNumber
val rightReg = insn.requireSrcB().intNumber
val s = dtSuffix(type)
emitLoadD0(leftReg, type)
emitLine("cmp$s ${regAddr(rightReg)}, d0")
}
Opcode.CMPI -> {
val reg = insn.requireIntSourceA().intNumber
val masked = immVal(insn.requireImmediateInt(), type)
// cmpi.x #0, operand can be replaced by the faster tst.x operand
if(masked==0)
emitLine("tst${dtSuffix(type)} ${regAddr(reg)}")
else
emitLine("cmpi${dtSuffix(type)} #$masked, ${regAddr(reg)}")
}
else -> error("Unknown arithmetic opcode: ${insn.opcode}")
}
}
// === Multiply / Divide / Modulus / DIVMOD for all int sizes (68030-aware) ===
// emit a call to one of the 68000 long math helper routines (utility.library fast path + software fallback)
private fun AsmGen.emitLongMathCall(dstReg: Int, srcReg: Int?, imm: Int?, target: String?, routine: String, resultReg: String) {
when {
srcReg != null -> {
emitLoadD0(dstReg, IRDataType.LONG)
emitLine("move.l ${regAddr(srcReg)}, d1")
}
imm != null -> {
emitLoadD0(dstReg, IRDataType.LONG)
emitLine("move.l #$imm, d1")
}
target != null -> {
emitLoadD0FromAddress(target, IRDataType.LONG)
emitLine("move.l ${regAddr(dstReg)}, d1")
}
}
invalidateD0Cache()
emitLine("bsr $routine")
val storeTarget = target ?: regAddr(dstReg)
if (resultReg == "d0") {
emitStoreD0ToAddress(storeTarget, IRDataType.LONG)
} else {
invalidateD0CacheForAddress(storeTarget)
emitLine("move.l $resultReg, $storeTarget")
}
}
private fun AsmGen.emitMulOp(dstReg: Int, srcReg: Int?, type: IRDataType, unsigned: Boolean, imm: Int?, target: String?) {
val op = if (unsigned) "mulu" else "muls"
when (type) {
IRDataType.BYTE -> {
// No .b multiply on M68k: zero-extend to word, mulu.w/muls.w, store low byte
when {
srcReg != null -> {
emitLoadD0(dstReg, IRDataType.BYTE)
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
invalidateD0Cache()
emitLine("move.w d0, d2")
emitLoadD0(srcReg, IRDataType.BYTE)
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
invalidateD0Cache()
emitLine("$op.w d2, d0")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.BYTE)
}
imm != null -> {
emitLoadD0(dstReg, IRDataType.BYTE)
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
invalidateD0Cache()
emitLine("$op.w #${imm.and(0xffff)}, d0")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.BYTE)
}
target != null -> {
emitLine("move.b $target, d1")
if (unsigned) emitLine($$"and.l #$ff, d1") else emitSignExtendByteToLong("d1")
emitLine("move.w d1, d2")
emitLoadD0(dstReg, IRDataType.BYTE)
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
invalidateD0Cache()
emitLine("$op.w d2, d0")
invalidateD0Cache()
emitStoreD0ToAddress(target, IRDataType.BYTE)
}
}
}
IRDataType.WORD -> {
// mulu.w/muls.w is native (16x16→32, lower 16 are result)
// destination must be a data register, not memory
when {
srcReg != null -> {
emitLoadD0(dstReg, IRDataType.WORD)
emitLine("$op.w ${regAddr(srcReg)}, d0")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.WORD)
}
imm != null -> {
emitLoadD0(dstReg, IRDataType.WORD)
emitLine("$op.w #${imm.and(0xffff)}, d0")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.WORD)
}
target != null -> {
emitLoadD0(dstReg, IRDataType.WORD)
emitLine("$op.w $target, d0")
invalidateD0Cache()
emitStoreD0ToAddress(target, IRDataType.WORD)
}
}
}
IRDataType.LONG -> {
if(program.options.compTarget.cpu < CpuType.M68020) {
// 68000: no mulu.l/muls.l; use a helper routine (utility.library or software fallback)
val routine = if (unsigned) "p8_umult32" else "p8_smult32"
emitLongMathCall(dstReg, srcReg, imm, target, routine, "d0")
} else {
// 68020+ mulu.l/muls.l (32x32→64, lower 32 are result)
// destination must be a data register, not memory
when {
srcReg != null -> {
emitLoadD0(dstReg, IRDataType.LONG)
emitLine("$op.l ${regAddr(srcReg)}, d0")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.LONG)
}
imm != null -> {
emitLoadD0(dstReg, IRDataType.LONG)
emitLine("$op.l #${imm}, d0")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.LONG)
}
target != null -> {
emitLoadD0(dstReg, IRDataType.LONG)
emitLine("$op.l $target, d0")
invalidateD0Cache()
emitStoreD0ToAddress(target, IRDataType.LONG)
}
}
}
}
else -> TODO("MUL for ${type.name}")
}
}
private fun AsmGen.emitDivOp(dstReg: Int, srcReg: Int?, type: IRDataType, unsigned: Boolean, imm: Int?, target: String?) {
val op = if (unsigned) "divu" else "divs"
when (type) {
IRDataType.BYTE -> {
// No .b divide on M68k: extend to word, divu.w/divs.w, take quotient from low byte
when {
srcReg != null -> {
emitLoadD0(dstReg, IRDataType.BYTE)
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
invalidateD0Cache()
emitLine("move.b ${regAddr(srcReg)}, d1")
if (unsigned) emitLine($$"and.l #$ff, d1") else emitSignExtendByteToLong("d1")
emitLine("$op.w d1, d0")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.BYTE)
}
imm != null -> {
emitLoadD0(dstReg, IRDataType.BYTE)
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
invalidateD0Cache()
emitLine("$op.w #${imm.and(0xffff)}, d0")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.BYTE)
}
target != null -> {
emitLoadD0FromAddress(target, IRDataType.BYTE)
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
invalidateD0Cache()
emitLine("move.b ${regAddr(dstReg)}, d1")
if (unsigned) emitLine($$"and.l #$ff, d1") else emitSignExtendByteToLong("d1")
emitLine("$op.w d1, d0")
invalidateD0Cache()
emitStoreD0ToAddress(target, IRDataType.BYTE)
}
}
}
IRDataType.WORD -> {
// divu.w/divs.w divide a 32-bit dividend (Dd) by a 16-bit divisor.
// divs.w requires the 32-bit dividend to be SIGN-extended; for unsigned it
// must be ZERO-extended. A plain `move.w` does not extend the upper word, so
// extend explicitly here (this is what was wrong: signed dividends ended up
// zero-extended, e.g. -1000 became +64536).
fun loadDividend() {
emitLoadD0(dstReg, IRDataType.WORD)
if (unsigned) {
emitLine($$"and.l #$ffff, d0", "zero-extend upper word for divu.w")
invalidateD0Cache()
} else {
emitLine("ext.l d0", "sign-extend upper word for divs.w (signed 32-bit dividend)")
invalidateD0Cache()
}
}
when {
srcReg != null -> {
loadDividend()
emitLine("$op.w ${regAddr(srcReg)}, d0")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.WORD)
}
imm != null -> {
loadDividend()
emitLine("$op.w #${imm.and(0xffff)}, d0")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.WORD)
}
target != null -> {
emitLoadD0FromAddress(target, IRDataType.WORD)
if (unsigned) {
emitLine($$"and.l #$ffff, d0", "zero-extend upper word for divu.w")
invalidateD0Cache()
} else {
emitLine("ext.l d0", "sign-extend upper word for divs.w (signed 32-bit dividend)")
invalidateD0Cache()
}
emitLine("$op.w ${regAddr(dstReg)}, d0")
invalidateD0Cache()
emitStoreD0ToAddress(target, IRDataType.WORD)
}
}
}
IRDataType.LONG -> {
if(program.options.compTarget.cpu < CpuType.M68020) {
// 68000: no divu.l/divs.l; use a helper routine (utility.library or software fallback)
val routine = if (unsigned) "p8_udivmod32" else "p8_sdivmod32"
emitLongMathCall(dstReg, srcReg, imm, target, routine, "d0")
} else {
// 68020+ divu.l/divs.l (32/32→32)
when {
srcReg != null -> {
emitLoadD0(dstReg, IRDataType.LONG)
emitLine("$op.l ${regAddr(srcReg)}, d0")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.LONG)
}
imm != null -> {
emitLoadD0(dstReg, IRDataType.LONG)
emitLine("$op.l #${imm}, d0")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.LONG)
}
target != null -> {
emitLoadD0FromAddress(target, IRDataType.LONG)
emitLine("$op.l ${regAddr(dstReg)}, d0")
invalidateD0Cache()
emitStoreD0ToAddress(target, IRDataType.LONG)
}
}
}
}
else -> TODO("DIV for ${type.name}")
}
}
private fun AsmGen.emitModOp(dstReg: Int, srcReg: Int?, type: IRDataType, unsigned: Boolean, imm: Int?) {
val opDiv = if (unsigned) "divu" else "divs"
when (type) {
IRDataType.BYTE -> {
// divu.w gives quotient low word, remainder high word. Swap to get remainder.
when {
srcReg != null -> {
emitLoadD0(dstReg, IRDataType.BYTE)
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
invalidateD0Cache()
emitLine("move.b ${regAddr(srcReg)}, d1")
if (unsigned) emitLine($$"and.l #$ff, d1") else emitSignExtendByteToLong("d1")
emitLine("$opDiv.w d1, d0")
invalidateD0Cache()
}
imm != null -> {
emitLoadD0(dstReg, IRDataType.BYTE)
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
invalidateD0Cache()
emitLine("$opDiv.w #${imm.and(0xffff)}, d0")
invalidateD0Cache()
}
}
// After divu.w/divs.w: quotient in lower word, remainder in upper word of d0.
emitLine("swap d0", "remainder to low word")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.BYTE)
}
IRDataType.WORD -> {
// divu.w/divs.w: remainder in upper 16 bits, swap to get it.
// divs.w requires the 32-bit dividend to be SIGN-extended; for unsigned it
// must be ZERO-extended. A plain `move.w` does not extend the upper word, so
// extend explicitly (signed dividends were wrongly zero-extended before).
fun loadDividend() {
emitLoadD0(dstReg, IRDataType.WORD)
if (unsigned) {
emitLine($$"and.l #$ffff, d0", "zero-extend upper word for divu.w")
invalidateD0Cache()
} else {
emitLine("ext.l d0", "sign-extend upper word for divs.w (signed 32-bit dividend)")
invalidateD0Cache()
}
}
when {
srcReg != null -> {
loadDividend()
emitLine("$opDiv.w ${regAddr(srcReg)}, d0")
invalidateD0Cache()
}
imm != null -> {
loadDividend()
emitLine("$opDiv.w #${imm.and(0xffff)}, d0")
invalidateD0Cache()
}
}
emitLine("swap d0", "remainder to low word")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.WORD)
}
IRDataType.LONG -> {
if(program.options.compTarget.cpu < CpuType.M68020) {
// 68000: no divul.l/divsl.l; use a helper routine (utility.library or software fallback)
val routine = if (unsigned) "p8_udivmod32" else "p8_sdivmod32"
emitLongMathCall(dstReg, srcReg, imm, null, routine, "d1")
} else {
// 68020+ divul.l <ea>, Dr, Dq: Dr=remainder, Dq=quotient
val opLong = if (unsigned) "divul.l" else "divsl.l"
when {
srcReg != null -> {
emitLoadD0(dstReg, IRDataType.LONG)
emitLine("$opLong ${regAddr(srcReg)}, d1:d0")
invalidateD0Cache()
invalidateD0CacheForSlot(dstReg)
emitLine("move.l d1, ${regAddr(dstReg)}", "remainder")
}
imm != null -> {
emitLoadD0(dstReg, IRDataType.LONG)
emitLine("move.l #${imm}, d2", "divisor")
emitLine("$opLong d2, d1:d0")
invalidateD0Cache()
invalidateD0CacheForSlot(dstReg)
emitLine("move.l d1, ${regAddr(dstReg)}", "remainder")
}
}
}
}
else -> TODO("MOD for ${type.name}")
}
}
private fun AsmGen.emitDivModOp(dstReg: Int, remainderReg: Int, type: IRDataType, unsigned: Boolean, imm: Int?) {
val opLong = if (unsigned) "divul.l" else "divsl.l"
when (type) {
IRDataType.BYTE -> {
val opDiv = if (unsigned) "divu.w" else "divs.w"
emitLoadD0(dstReg, IRDataType.BYTE)
if (unsigned) emitLine($$"and.l #$ff, d0") else emitSignExtendByteToLong("d0")
invalidateD0Cache()
if(imm!=null) {
if (unsigned)
emitLine("move.l #${imm.and(0xff)}, d1")
else
emitLine("moveq #${imm.and(0xff)}, d1")
if (!unsigned) emitSignExtendByteToLong("d1")
} else {
emitLine("move.b ${regAddr(remainderReg)}, d1")
if (unsigned) emitLine($$"and.l #$ff, d1") else emitSignExtendByteToLong("d1")
}
emitLine("$opDiv d1, d0")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.BYTE)
emitLine("swap d0", "remainder to low word")
invalidateD0Cache()
emitStoreD0(remainderReg, IRDataType.BYTE)
}
IRDataType.WORD -> {
val opDiv = if (unsigned) "divu.w" else "divs.w"
if (unsigned) {
emitLine("moveq #0, d0", "clear upper word for divu.w")
invalidateD0Cache()
}
emitLoadD0(dstReg, IRDataType.WORD)
if (!unsigned) {
emitLine("ext.l d0", "sign-extend for divs.w")
invalidateD0Cache()
}
if(imm!=null) {
emitLine("$opDiv #${imm.and(0xffff)}, d0")
} else {
emitLine("$opDiv ${regAddr(remainderReg)}, d0")
}
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.WORD)
emitLine("swap d0", "remainder to low word")
invalidateD0Cache()
emitStoreD0(remainderReg, IRDataType.WORD)
}
IRDataType.LONG -> {
if(program.options.compTarget.cpu < CpuType.M68020) {
// 68000: no divul.l/divsl.l; use a helper routine (utility.library or software fallback)
val routine = if (unsigned) "p8_udivmod32" else "p8_sdivmod32"
emitLoadD0(dstReg, IRDataType.LONG)
if(imm!=null) {
emitLine("move.l #${imm}, d1", "divisor")
} else {
emitLine("move.l ${regAddr(remainderReg)}, d1", "divisor")
}
invalidateD0Cache()
emitLine("bsr $routine")
emitStoreD0(dstReg, IRDataType.LONG)
invalidateD0CacheForSlot(remainderReg)
emitLine("move.l d1, ${regAddr(remainderReg)}", "remainder")
} else {
emitLoadD0(dstReg, IRDataType.LONG)
if(imm!=null) {
emitLine("move.l #${imm}, d1", "divisor")
} else {
emitLine("move.l ${regAddr(remainderReg)}, d1", "divisor")
}
emitLine("$opLong d1, d0")
invalidateD0Cache()
emitStoreD0(dstReg, IRDataType.LONG)
invalidateD0CacheForSlot(remainderReg)
emitLine("move.l d1, ${regAddr(remainderReg)}", "remainder")
}
}
else -> TODO("DIVMOD for ${type.name}")
}
}
// === Float arithmetic via 68881 FPU ===
// Float virtual registers live in the memory regfile. Physical fp0 (FP_ACC) and
// fp1 (FP_SRC) are used as scratch during FPU operations.
private fun AsmGen.emitFloatBinaryOp(op: String, dst: RegisterNum, src: RegisterNum) {
emitLine("fmove.s ${floatRegFileAddr(dst)}, $FP_ACC")
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_SRC")
emitLine("$op $FP_SRC, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
private fun AsmGen.emitFloatOpWithConstant(fpReg: RegisterNum, op: String, value: Double) {
emitLine("fmove.s ${floatRegFileAddr(fpReg)}, $FP_ACC")
val native = nativeFloatConst(value)
if (native != null) {
emitLine("fmovecr #$native, $FP_SRC")
} else {
val label = makeFloatConstLabel(value)
emitLine("lea $label, a0")
emitLine("fmove.s (a0), $FP_SRC")
}
emitLine("$op $FP_SRC, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(fpReg)}")
}
private fun AsmGen.translateFloatArithmetic(insn: IRInstruction) {
when (insn.opcode) {
Opcode.INCM -> emitFloatMemUnary("fadd.s", resolveMemory(insn.requireMemory(), forFloat = true), immediateStr = "#1.0")
Opcode.DECM -> emitFloatMemUnary("fsub.s", resolveMemory(insn.requireMemory(), forFloat = true), immediateStr = "#1.0")
Opcode.NEGM -> emitFloatMemUnary("fneg", resolveMemory(insn.requireMemory(), forFloat = true))
Opcode.INC -> {
val dst = insn.requireFloatDest().floatNumber
emitLine("fmove.s ${floatRegFileAddr(dst)}, $FP_ACC")
emitLine("fadd.s #1.0, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.DEC -> {
val dst = insn.requireFloatDest().floatNumber
emitLine("fmove.s ${floatRegFileAddr(dst)}, $FP_ACC")
emitLine("fsub.s #1.0, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.NEG -> {
val dst = insn.requireFloatDest().floatNumber
emitLine("fmove.s ${floatRegFileAddr(dst)}, $FP_ACC")
emitLine("fneg $FP_ACC, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.ADDR -> emitFloatBinaryOp("fadd", insn.requireFloatDest().floatNumber, insn.requireFloatSourceA().floatNumber)
Opcode.ADD -> emitFloatOpWithConstant(insn.requireFloatDest().floatNumber, "fadd", insn.requireImmediateFloat())
Opcode.SUBR -> emitFloatBinaryOp("fsub", insn.requireFloatDest().floatNumber, insn.requireFloatSourceA().floatNumber)
Opcode.SUB -> emitFloatOpWithConstant(insn.requireFloatDest().floatNumber, "fsub", insn.requireImmediateFloat())
Opcode.MULR, Opcode.MULSR -> emitFloatBinaryOp("fmul", insn.requireFloatDest().floatNumber, insn.requireFloatSourceA().floatNumber)
Opcode.MUL, Opcode.MULS -> emitFloatOpWithConstant(insn.requireFloatDest().floatNumber, "fmul", insn.requireImmediateFloat())
Opcode.DIVR, Opcode.DIVSR -> emitFloatBinaryOp("fdiv", insn.requireFloatDest().floatNumber, insn.requireFloatSourceA().floatNumber)
Opcode.DIV, Opcode.DIVS -> emitFloatOpWithConstant(insn.requireFloatDest().floatNumber, "fdiv", insn.requireImmediateFloat())
Opcode.SQRT -> {
val dst = insn.requireFloatDest().floatNumber
val src = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
emitLine("fsqrt $FP_ACC, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.SQUARE -> emitFloatBinaryOp("fmul", insn.requireFloatDest().floatNumber, insn.requireFloatSourceA().floatNumber)
Opcode.ADDM -> emitFloatMemBinary("fadd", insn.requireFloatSourceA().floatNumber, resolveMemory(insn.requireMemory(), forFloat = true))
Opcode.SUBM -> emitFloatMemBinary("fsub", insn.requireFloatSourceA().floatNumber, resolveMemory(insn.requireMemory(), forFloat = true))
Opcode.MULM, Opcode.MULSM -> emitFloatMemBinary("fmul", insn.requireFloatSourceA().floatNumber, resolveMemory(insn.requireMemory(), forFloat = true))
Opcode.DIVM, Opcode.DIVSM -> emitFloatMemBinary("fdiv", insn.requireFloatSourceA().floatNumber, resolveMemory(insn.requireMemory(), forFloat = true))
Opcode.ADDIM -> emitFloatMemBinaryImmediate("fadd", insn.requireImmediateFloat(), resolveMemory(insn.requireMemory(), forFloat = true))
Opcode.SUBIM -> emitFloatMemBinaryImmediate("fsub", insn.requireImmediateFloat(), resolveMemory(insn.requireMemory(), forFloat = true))
else -> TODO("FLOAT arithmetic: ${insn.opcode}")
}
}
private fun AsmGen.emitFloatMemBinaryImmediate(op: String, value: Double, target: String) {
emitLine("fmove.s $target,$FP_ACC")
when (value) {
0.0 -> { emitLine("fmove.s #0.0,$FP_SRC"); emitLine("$op $FP_SRC,$FP_ACC") }
1.0 -> {
if (op == "fadd" || op == "fsub") {
emitLine("f${op.substring(1)}.s #1.0,$FP_ACC")
} else {
return // mul/div by 1.0 is no-op, just keep loaded value
}
}
else -> {
val lbl = makeFloatConstLabel(value)
emitLine("lea $lbl,a0")
emitLine("fmove.s (a0),$FP_SRC")
emitLine("$op $FP_SRC,$FP_ACC")
}
}
emitLine("fmove.s $FP_ACC,$target")
}
private fun AsmGen.emitFloatMemUnary(op: String, target: String, immediateStr: String? = null) {
emitLine("fmove.s $target, fp0")
if (immediateStr != null)
emitLine("$op $immediateStr, fp0")
else
emitLine("$op fp0, fp0")
emitLine("fmove.s fp0, $target")
}
private fun AsmGen.emitFloatMemBinary(op: String, fpReg1: RegisterNum, target: String) {
emitLine("fmove.s $target, $FP_ACC")
emitLine("fmove.s ${floatRegFileAddr(fpReg1)}, $FP_SRC")
emitLine("$op $FP_SRC, $FP_ACC")
emitLine("fmove.s $FP_ACC, $target")
}
private fun immVal(value: Int, type: IRDataType): Int = when(type) {
IRDataType.BYTE -> value and 0xff
IRDataType.WORD -> value and 0xffff
IRDataType.LONG, IRDataType.POINTER -> value
else -> value and 0xffff
}
@@ -0,0 +1,424 @@
/*
* Bitwise and logical IR instruction translations for the M68k code generator.
*
* Handles: AND, OR, XOR (register/imm/memory), INV (bitwise not),
* shifts (LSL, LSR, ASR) by constant or variable count,
* rotates (ROL, ROR, ROXL, ROXR) and bit manipulation (BITTST, BITSET, BITCLR, BITTOG).
*
* M68k has native .B/.W/.L support for all logical operations,
* including immediate and register-count shifts.
* Rotates ROL/ROR are true rotates (bit wraps around, no carry involvement for entry).
* ROXL/ROXR rotate through the extend (X/C) bit.
*/
package prog8.codegen.m68k
import prog8.code.target.Qemu68kTarget
import prog8.intermediate.IRDataType
import prog8.intermediate.IRInstruction
import prog8.intermediate.Opcode
import prog8.intermediate.intNumber
import prog8.intermediate.requireImmediateInt
import prog8.intermediate.requireIntDest
import prog8.intermediate.requireIntSourceA
import prog8.intermediate.requireMemory
internal fun AsmGen.translateBitwise(insn: IRInstruction) {
val type = insn.type ?: IRDataType.BYTE
when (insn.opcode) {
Opcode.ANDR -> andRegisters(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type)
Opcode.AND -> andImmediate(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type)
Opcode.ANDM -> andMemory(insn.requireIntSourceA().intNumber, resolveMemory(insn.requireMemory()), type)
Opcode.ORR -> orRegisters(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type)
Opcode.OR -> orImmediate(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type)
Opcode.ORM -> orMemory(insn.requireIntSourceA().intNumber, resolveMemory(insn.requireMemory()), type)
Opcode.XORR -> xorRegisters(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type)
Opcode.XOR -> xorImmediate(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type)
Opcode.XORM -> xorMemory(insn.requireIntSourceA().intNumber, resolveMemory(insn.requireMemory()), type)
Opcode.INV -> invertRegister(insn.requireIntDest().intNumber, type)
Opcode.INVM -> invertMemory(resolveMemory(insn.requireMemory()), type)
Opcode.ASRN -> arithmeticShiftRightVar(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type)
Opcode.LSRN -> logicalShiftRightVar(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type)
Opcode.LSLN -> logicalShiftLeftVar(insn.requireIntDest().intNumber, insn.requireIntSourceA().intNumber, type)
Opcode.ASRNM -> shiftMemoryVar(resolveMemory(insn.requireMemory()), insn.requireIntSourceA().intNumber, type, isArithmetic = true)
Opcode.LSRNM -> shiftMemoryVar(resolveMemory(insn.requireMemory()), insn.requireIntSourceA().intNumber, type, isArithmetic = false)
Opcode.LSLNM -> shiftMemoryLeftVar(resolveMemory(insn.requireMemory()), insn.requireIntSourceA().intNumber, type)
// Immediate-count shifts (LSLI/LSRI/ASRI): the count is a literal in the immediate operand.
Opcode.ASRI -> shiftRegister(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type, isArithmetic = true, isLeft = false)
Opcode.LSRI -> shiftRegister(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type, isArithmetic = false, isLeft = false)
Opcode.LSLI -> shiftRegister(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type, isArithmetic = false, isLeft = true)
Opcode.ASR -> shiftRegister(insn.requireIntDest().intNumber, 1, type, isArithmetic = true, isLeft = false)
Opcode.ASRM -> memoryShiftRotate(resolveMemory(insn.requireMemory()), 1, type, isArithmetic = true, isLeft = false, isRotate = false, throughCarry = false)
Opcode.LSR -> shiftRegister(insn.requireIntDest().intNumber, 1, type, isArithmetic = false, isLeft = false)
Opcode.LSRM -> memoryShiftRotate(resolveMemory(insn.requireMemory()), 1, type, isArithmetic = false, isLeft = false, isRotate = false, throughCarry = false)
Opcode.LSL -> shiftRegister(insn.requireIntDest().intNumber, 1, type, isArithmetic = false, isLeft = true)
Opcode.LSLM -> memoryShiftRotate(resolveMemory(insn.requireMemory()), 1, type, isArithmetic = false, isLeft = true, isRotate = false, throughCarry = false)
Opcode.ROR -> rotateRight(insn.requireIntDest().intNumber, type)
Opcode.RORM -> memoryShiftRotate(resolveMemory(insn.requireMemory()), 1, type, isArithmetic = false, isLeft = false, isRotate = true, throughCarry = false)
Opcode.ROL -> rotateLeft(insn.requireIntDest().intNumber, type)
Opcode.ROLM -> memoryShiftRotate(resolveMemory(insn.requireMemory()), 1, type, isArithmetic = false, isLeft = true, isRotate = true, throughCarry = false)
Opcode.ROXR -> rotateRightThroughCarry(insn.requireIntDest().intNumber, type)
Opcode.ROXRM -> memoryShiftRotate(resolveMemory(insn.requireMemory()), 1, type, isArithmetic = false, isLeft = false, isRotate = true, throughCarry = true)
Opcode.ROXL -> rotateLeftThroughCarry(insn.requireIntDest().intNumber, type)
Opcode.ROXLM -> memoryShiftRotate(resolveMemory(insn.requireMemory()), 1, type, isArithmetic = false, isLeft = true, isRotate = true, throughCarry = true)
Opcode.BITTST -> bitTest(insn.requireIntSourceA().intNumber, insn.requireImmediateInt(), type)
Opcode.BITSET -> bitSet(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type)
Opcode.BITCLR -> bitClear(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type)
Opcode.BITTOG -> bitToggle(insn.requireIntDest().intNumber, insn.requireImmediateInt(), type)
else -> error("Unknown bitwise opcode: ${insn.opcode}")
}
}
// === AND ===
private fun AsmGen.andRegisters(dstReg: Int, srcReg: Int, type: IRDataType) {
val s = dtSuffix(type)
emitLoadD0(srcReg, type)
emitLine("and$s d0, ${regAddr(dstReg)}")
invalidateD0CacheForSlot(dstReg)
}
private fun AsmGen.andImmediate(dstReg: Int, value: Int, type: IRDataType) {
val s = dtSuffix(type)
val mask = when (type) {
IRDataType.BYTE -> value and 0xff
IRDataType.WORD -> value and 0xffff
IRDataType.LONG -> value
else -> error("unsupported type for AND immediate")
}
emitLine("andi$s #$mask, ${regAddr(dstReg)}")
invalidateD0CacheForSlot(dstReg)
}
private fun AsmGen.andMemory(dstReg: Int, sourceAddress: String, type: IRDataType) {
val s = dtSuffix(type)
emitLoadD0(dstReg, type)
emitLine("and$s d0, $sourceAddress")
invalidateD0CacheForAddress(sourceAddress)
}
// === OR ===
private fun AsmGen.orRegisters(dstReg: Int, srcReg: Int, type: IRDataType) {
val s = dtSuffix(type)
emitLoadD0(srcReg, type)
emitLine("or$s d0, ${regAddr(dstReg)}")
invalidateD0CacheForSlot(dstReg)
}
private fun AsmGen.orImmediate(dstReg: Int, value: Int, type: IRDataType) {
val s = dtSuffix(type)
val mask = when (type) {
IRDataType.BYTE -> value and 0xff
IRDataType.WORD -> value and 0xffff
IRDataType.LONG -> value
else -> error("unsupported type for OR immediate")
}
emitLine("ori$s #$mask, ${regAddr(dstReg)}")
invalidateD0CacheForSlot(dstReg)
}
private fun AsmGen.orMemory(dstReg: Int, sourceAddress: String, type: IRDataType) {
val s = dtSuffix(type)
emitLoadD0(dstReg, type)
emitLine("or$s d0, $sourceAddress")
invalidateD0CacheForAddress(sourceAddress)
}
// === XOR ===
private fun AsmGen.xorRegisters(dstReg: Int, srcReg: Int, type: IRDataType) {
val s = dtSuffix(type)
emitLoadD0(srcReg, type)
emitLine("eor$s d0, ${regAddr(dstReg)}")
invalidateD0CacheForSlot(dstReg)
}
private fun AsmGen.xorImmediate(dstReg: Int, value: Int, type: IRDataType) {
val s = dtSuffix(type)
val mask = when (type) {
IRDataType.BYTE -> value and 0xff
IRDataType.WORD -> value and 0xffff
IRDataType.LONG -> value
else -> error("unsupported type for XOR immediate")
}
emitLine("eori$s #$mask, ${regAddr(dstReg)}")
invalidateD0CacheForSlot(dstReg)
}
private fun AsmGen.xorMemory(dstReg: Int, sourceAddress: String, type: IRDataType) {
val s = dtSuffix(type)
emitLoadD0(dstReg, type)
emitLine("eor$s d0, $sourceAddress")
invalidateD0CacheForAddress(sourceAddress)
}
// === Invert ===
private fun AsmGen.invertRegister(reg: Int, type: IRDataType) {
val s = dtSuffix(type)
emitLine("not$s ${regAddr(reg)}")
invalidateD0CacheForSlot(reg)
}
private fun AsmGen.invertMemory(target: String, type: IRDataType) {
val s = dtSuffix(type)
emitLine("not$s $target")
invalidateD0CacheForAddress(target)
}
// === Shift/rotate size helpers ===
private fun AsmGen.shiftOpcode(isLeft: Boolean, isArithmetic: Boolean, isRotate: Boolean = false): String {
if (isRotate) {
// Always use roxl/roxr (never rol/ror). X is managed as rotate-carry.
return if (isLeft) "roxl" else "roxr"
}
return if (isLeft) {
"lsl"
} else {
if (isArithmetic) "asr" else "lsr"
}
}
// === Shifts by 1 (constant count 1) ===
// Emit a .w count=1 memory-form shift/rotate. The absolute form (`op.w addr`)
// is one instruction and optimal, but 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) {
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")
}
}
}
private fun AsmGen.shiftRegister(reg: Int, count: Int, type: IRDataType, isArithmetic: Boolean, isLeft: Boolean) {
val s = dtSuffix(type)
val op = shiftOpcode(isLeft, isArithmetic)
if (isLeft && type == IRDataType.LONG && count == 16) {
// x << 16 for long: `swap; clr.w`. Left shift by 16 has the same
// result for signed and unsigned long: the sign bit lives in the
// top half, which is replaced by the bottom half; the bottom
// half is always cleared.
emitLoadD0(reg, IRDataType.LONG)
emitLine("swap d0")
emitLine("clr.w d0")
emitStoreD0(reg, IRDataType.LONG)
return
}
if (type == IRDataType.WORD && count == 1) {
// m68k memory shift/rotate is supported for .w count=1; collapse the
// d0 round-trip into a single memory form
emitMemoryWordShiftOrRotate(op, regAddr(reg))
invalidateD0CacheForSlot(reg)
return
}
if (count in 1..8) {
emitLoadD0(reg, type)
emitLine("$op$s #$count, d0")
emitStoreD0(reg, type)
} else {
// m68k immediate-shift range is 1..8; for larger counts the count
// has to go into a data register first. `move.w` preserves the
// count up to 32767 (Prog8 shift counts are 0..255, so this is
// always safe).
emitLine("move.w #$count, d1")
emitLoadD0(reg, type)
emitLine("$op$s d1, d0")
emitStoreD0(reg, type)
}
}
private fun AsmGen.memoryShiftRotate(target: String, count: Int, type: IRDataType, isArithmetic: Boolean, isLeft: Boolean, isRotate: Boolean, throughCarry: Boolean) {
// For logical rotates (ROL/ROR), clear X before roxl/roxr
// so the injected bit is 0 (logical rotate, not through-carry).
if (isRotate && !throughCarry) {
emitLine($$"andi #$ef, ccr")
}
// On 68k, shift/rotate instructions can operate directly on memory operands
// (only for .w size, and shift/rotate count must be 1)
if (type == IRDataType.WORD && count == 1) {
val op = shiftOpcode(isLeft, isArithmetic, isRotate)
emitMemoryWordShiftOrRotate(op, target)
invalidateD0CacheForAddress(target)
return
}
val s = dtSuffix(type)
val op = shiftOpcode(isLeft, isArithmetic, isRotate)
emitLoadD0FromAddress(target, type)
emitLine("$op$s #$count, d0")
emitStoreD0ToAddress(target, type)
}
// === Variable-count shifts ===
private fun AsmGen.logicalShiftLeftVar(reg: Int, countReg: Int, type: IRDataType) {
val s = dtSuffix(type)
emitLine("move.b ${regAddr(countReg)}, d1")
emitLoadD0(reg, type)
emitLine("lsl$s d1, d0")
emitStoreD0(reg, type)
}
private fun AsmGen.logicalShiftRightVar(reg: Int, countReg: Int, type: IRDataType) {
val s = dtSuffix(type)
emitLine("move.b ${regAddr(countReg)}, d1")
emitLoadD0(reg, type)
emitLine("lsr$s d1, d0")
emitStoreD0(reg, type)
}
private fun AsmGen.arithmeticShiftRightVar(reg: Int, countReg: Int, type: IRDataType) {
val s = dtSuffix(type)
emitLine("move.b ${regAddr(countReg)}, d1")
emitLoadD0(reg, type)
emitLine("asr$s d1, d0")
emitStoreD0(reg, type)
}
// === Memory variable-count shifts ===
private fun AsmGen.shiftMemoryVar(target: String, countReg: Int, type: IRDataType, isArithmetic: Boolean) {
val s = dtSuffix(type)
val op = if (isArithmetic) "asr" else "lsr"
emitLine("move.b ${regAddr(countReg)}, d1")
emitLoadD0FromAddress(target, type)
emitLine("$op$s d1, d0")
emitStoreD0ToAddress(target, type)
}
private fun AsmGen.shiftMemoryLeftVar(target: String, countReg: Int, type: IRDataType) {
val s = dtSuffix(type)
emitLine("move.b ${regAddr(countReg)}, d1")
emitLoadD0FromAddress(target, type)
emitLine("lsl$s d1, d0")
emitStoreD0ToAddress(target, type)
}
// === Rotates ===
// On M68k, the X (extend/CCR bit 4) bit serves as the "carry for rotates",
// while C (carry/CCR bit 0) is used for comparisons.
// This maps naturally: ROL/ROR IR (logical rotate) = clear X + roxl/roxr,
// ROXL/ROXR IR (rotate through carry) = roxl/roxr directly.
// CLC/SEC manage both C+X to keep them in sync.
private fun AsmGen.rotateLeft(reg: Int, type: IRDataType) {
// IR ROL: logical rotate left (inject 0 into LSB)
if (type == IRDataType.WORD) {
emitLine($$"andi #$ef, ccr") // clear X (and leave C alone)
emitMemoryWordShiftOrRotate("roxl", regAddr(reg))
invalidateD0CacheForSlot(reg)
return
}
val s = dtSuffix(type)
emitLoadD0(reg, type)
emitLine($$"andi #$ef, ccr") // clear X (and leave C alone)
emitLine("roxl$s #1, d0") // rotate through X: X=0 -> bit 0 gets 0
emitStoreD0(reg, type)
}
private fun AsmGen.rotateRight(reg: Int, type: IRDataType) {
// IR ROR: logical rotate right (inject 0 into MSB)
if (type == IRDataType.WORD) {
emitLine($$"andi #$ef, ccr") // clear X (and leave C alone)
emitMemoryWordShiftOrRotate("roxr", regAddr(reg))
invalidateD0CacheForSlot(reg)
return
}
val s = dtSuffix(type)
emitLoadD0(reg, type)
emitLine($$"andi #$ef, ccr") // clear X (and leave C alone)
emitLine("roxr$s #1, d0") // rotate through X: X=0 -> MSB gets 0
emitStoreD0(reg, type)
}
// === Rotates through carry (extend) ===
private fun AsmGen.rotateLeftThroughCarry(reg: Int, type: IRDataType) {
// IR ROXL: rotate left through carry (on M68k, X serves as rotate-carry)
if (type == IRDataType.WORD) {
emitMemoryWordShiftOrRotate("roxl", regAddr(reg))
invalidateD0CacheForSlot(reg)
return
}
val s = dtSuffix(type)
emitLoadD0(reg, type)
emitLine("roxl$s #1, d0")
emitStoreD0(reg, type)
}
private fun AsmGen.rotateRightThroughCarry(reg: Int, type: IRDataType) {
// IR ROXR: rotate right through carry (on M68k, X serves as rotate-carry)
if (type == IRDataType.WORD) {
emitMemoryWordShiftOrRotate("roxr", regAddr(reg))
invalidateD0CacheForSlot(reg)
return
}
val s = dtSuffix(type)
emitLoadD0(reg, type)
emitLine("roxr$s #1, d0")
emitStoreD0(reg, type)
}
// === Bit manipulation ===
private fun AsmGen.bitOpMem(op: String, reg: Int, bit: Int, type: IRDataType) {
// The register slot is big-endian in memory, so the target bit lives in
// byte (size-1-bit/8) at bit position (bit % 8). A byte-sized bit op with
// an immediate bit number operates on that byte directly, no d0 round-trip.
val size = when(type) {
IRDataType.BYTE -> 1
IRDataType.WORD -> 2
IRDataType.LONG -> 4
else -> error("bit op on unsupported type $type")
}
val byteOffset = size - 1 - bit / 8
val bitInByte = bit % 8
emitLine("$op #$bitInByte, ${regAddrByte(reg, byteOffset)}")
}
private fun AsmGen.bitTest(reg: Int, bit: Int, type: IRDataType) {
bitOpMem("btst", reg, bit, type)
}
private fun AsmGen.bitSet(reg: Int, bit: Int, type: IRDataType) {
bitOpMem("bset", reg, bit, type)
}
private fun AsmGen.bitClear(reg: Int, bit: Int, type: IRDataType) {
bitOpMem("bclr", reg, bit, type)
}
private fun AsmGen.bitToggle(reg: Int, bit: Int, type: IRDataType) {
bitOpMem("bchg", reg, bit, type)
}
@@ -0,0 +1,155 @@
/*
* Branch/compare IR instruction translations for the M68k code generator.
*
* Status bit branches (BSTCC/BSTCS/etc.) map directly to M68k conditional
* branch instructions (BCC/BCS/BEQ/BNE/BMI/BPL/BVC/BVS).
*
* Comparison branches (BGT/BGE/BLT/BLE and signed variants) first emit
* a CMP or CMPI between the operands, then branch using the appropriate
* M68k condition code:
*
* Unsigned: BHI (C=0 & Z=0), BHS/BCC (C=0), BLO/BCS (C=1), BLS (C=1 | Z=1)
* Signed: BGT (N=V & Z=0), BGE (N=V), BLT (N!=V), BLE (Z=1 | N!=V)
*
* M68k CMP/CMPI sets all flags correctly for .B, .W, and .L — no cascading
* or overflow correction needed (unlike 6502).
*/
package prog8.codegen.m68k
import prog8.intermediate.CodeReference
import prog8.intermediate.IRDataType
import prog8.intermediate.IRInstruction
import prog8.intermediate.Opcode
import prog8.intermediate.intNumber
import prog8.intermediate.requireImmediateInt
import prog8.intermediate.requireSrcA
import prog8.intermediate.requireSrcB
import prog8.intermediate.requireTarget
internal fun AsmGen.translateBranch(insn: IRInstruction) {
val 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, 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", 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", 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", 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", 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}")
}
}
// === Unsigned comparisons: register vs immediate ===
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()
val s = dtSuffix(type)
// cmpi.x #imm, <ea> and tst.x <ea> accept memory operands, so the
// register-file load into d0 is redundant
if(imm==0)
emitLine("tst$s ${regAddr(reg)}")
else
emitLine("cmpi$s #$imm, ${regAddr(reg)}")
emitBranch(branchOp, label, leavesSubroutine)
}
// === Unsigned comparisons: register vs register ===
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, leavesSubroutine)
}
// === Signed comparisons: register vs immediate ===
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()
val s = dtSuffix(type)
// cmpi.x #imm, <ea> and tst.x <ea> accept memory operands, so the
// register-file load into d0 is redundant
if(imm==0)
emitLine("tst$s ${regAddr(reg)}")
else
emitLine("cmpi$s #$imm, ${regAddr(reg)}")
emitBranch(branchOp, label, leavesSubroutine)
}
// === Signed comparisons: register vs register ===
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, leavesSubroutine)
}
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()
if (!leavesSubroutine || !frameActive) {
emitLine("$branchOp $label")
return
}
val skipLabel = makeLabel("branch_keep_frame")
emitLine("${inverseBranchOps.getValue(branchOp)} $skipLabel")
emitFrameUnlk()
emitLine("jmp $label")
emitLabel(skipLabel)
}
@@ -0,0 +1,937 @@
/*
* Control flow IR instruction translations for the M68k code generator.
*
* Status flag returns (Pc/Pz/Pv/Pn) are handled by the IR's branch-based pattern
* (bsteq/bstneg/bstvs). The backend skips status flag returns in translateCall/translateReturnValue
* and trusts the IR to emit the appropriate branch instructions.
*
* PUSHST/POPST: save/restore CCR via the stack (Scc moves CCR to D0).
*
* Handles: JUMP, JUMPI, CALL, CALLI, CALLFAR, CALLFARVB, SYSCALL,
* RETURN, RETURNR, RETURNI, PUSH, POP, PUSHST, POPST,
* CLC, SEC, CLI, SEI, ALIGN,
* byte/word extraction (LSIGB, LSIGW, MSIGB, MSIGW, BSIGB, MIDB),
* sign extension (EXT, EXTS), and sign (SGN).
*/
package prog8.codegen.m68k
import prog8.code.core.CpuType
import prog8.code.core.toHex
import prog8.code.target.Amiga500Target
import prog8.intermediate.*
internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall: ImmediateCallOptimization? = null) {
when (insn.opcode) {
Opcode.JUMP -> {
invalidateD0Cache()
when (val t = insn.requireTarget()) {
is CodeReference.Label -> {
// 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")
}
}
Opcode.JUMPI -> {
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)}])")
} else {
emitLine("movea.l ${regAddr(reg).removeSuffix("+0")}, a0")
emitLine("jmp (a0)")
}
}
Opcode.CALL -> {
val callSite = insn.requireCallSite()
val fnLabel = insn.labelTarget?.let { fixNameSymbols(it) }
?: (insn.codeTarget as? CodeReference.Absolute)?.address?.value?.toHex()
?: error("CALL needs label or address")
invalidateD0Cache()
translateCall(fnLabel, callSite, forwardedImmediateCall)
}
Opcode.CALLI -> {
val ref = insn.codeTarget as? CodeReference.Indirect ?: error("CALLI needs an indirect target register")
val reg = ref.pointer.intNumber
invalidateD0Cache()
if(program.options.compTarget.cpu >= CpuType.M68020) {
// 68020+ supports memory-indirect addressing: fetch the target address from memory directly.
emitLine("jsr ([${regAddr(reg)}])")
} else {
emitLine("movea.l ${regAddr(reg).removeSuffix("+0")}, a0")
emitLine("jsr (a0)")
}
}
Opcode.CALLFAR -> {
// On the amiga target, CALLFAR is repurposed to encode automatic library LVO calls:
// library = library number (1=exec, 2=dos, 3=graphics, 4=intuition, ...)
// lvo = negative LVO offset (the offset into the library's jump table)
// name = symbolic function name, emitted as the displacement in jsr Symbol(a6)
if(program.options.compTarget.name.contains("amiga")) {
val callSite = insn.requireCallSite()
val amigaTarget = callSite.target as? CallTarget.AmigaLibrary ?: error("CALLFAR needs an amiga library target")
val offset = amigaTarget.lvo
require(offset<0) { "amiga libcall offsets should all be <0" }
val bank = amigaTarget.library
if(bank==1) {
// exec.library is always at 4.w
emitLine("move.l 4.w,a6")
} else {
val library = Amiga500Target.LibraryNumbers.getValue(bank)
val baseVar = "sys.${library}Base"
emitLine("move.l $baseVar,a6")
}
invalidateD0Cache()
for(arg in callSite.arguments)
translateArgument(arg)
if(amigaTarget.name!=null) {
emitLine("jsr ${amigaTarget.name}(a6)") // do the actual call (symbolic ref)
} else {
emitLine("jsr $offset(a6)") // do the actual call
}
// Skip status flag returns: handled by IR branch pattern.
// In multi-assign context (returns.size > 1), also skip slot-based returns:
// the IR generates LOADHR for them.
// In single-return context, process slot returns normally.
// 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)
continue
if (isMultiReturn)
continue
translateReturnValue(ret)
}
}
else error("CALLFAR not applicable on this M68k")
}
Opcode.CALLFARVB -> {
error("CALLFARVB not applicable on M68k")
}
Opcode.SYSCALL -> {
val callSite = insn.requireCallSite()
val num = (callSite.target as CallTarget.SystemCall).number
invalidateD0Cache()
if (callSite.arguments.isNotEmpty())
translateSyscall(num, callSite)
else
emitLine("; syscall #$num (no args)")
}
Opcode.RETURN -> {
invalidateD0Cache()
emitFrameUnlk()
emitLine("rts")
}
Opcode.RETURNR -> {
invalidateD0Cache()
val type = insn.type ?: IRDataType.BYTE
if (type == IRDataType.FLOAT) {
val fpReg = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(fpReg)}, $FP_ACC")
} else {
val reg = insn.requireIntSourceA().intNumber
emitLoadD0(reg, type)
}
emitFrameUnlk()
emitLine("rts")
}
Opcode.RETURNI -> {
invalidateD0Cache()
val value = insn.immediate?.integerValue ?: 0
val type = insn.type ?: IRDataType.BYTE
val s = dtSuffix(type)
if (value in -128..127)
emitLine("moveq #$value, d0")
else
emitLine("move$s #$value, d0")
emitFrameUnlk()
emitLine("rts")
}
// === Stack operations ===
Opcode.PUSH -> {
invalidateD0Cache()
val reg = insn.requireIntSourceA().intNumber
val type = insn.type ?: IRDataType.BYTE
val s = dtSuffix(type)
emitLine("move$s ${regAddr(reg)}, -(sp)")
}
Opcode.POP -> {
invalidateD0Cache()
val reg = insn.requireIntDest().intNumber
val type = insn.type ?: IRDataType.BYTE
val s = dtSuffix(type)
emitLine("move$s (sp)+, ${regAddr(reg)}")
}
// === Status flag stack ops via Scc ===
Opcode.PUSHST -> {
// 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)")
emitLine("move ccr, -(sp)")
}
Opcode.POPST -> {
// 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)")
emitLine("move (sp)+, ccr")
}
// === Flag manipulation ===
// On M68k, X (extend/CCR bit 4) serves as the rotate-carry,
// while C (carry/CCR bit 0) is used for comparisons.
// CLC/SEC must manage both to keep them in sync.
Opcode.CLC -> emitLine($$"andi #$ee, ccr") // clear C and X
Opcode.SEC -> emitLine($$"ori #$11, ccr") // set C and X
Opcode.CLI -> emitLine($$"andi #$fb, ccr")
Opcode.SEI -> emitLine($$"ori #$04, ccr")
Opcode.ALIGN -> {
val alignment = insn.immediate?.integerValue ?: 2
emitLine("ALIGN $alignment")
}
// === Byte/word extraction (no Scc needed) ===
Opcode.LSIGB -> {
val dstReg = insn.requireIntDest().intNumber
val srcReg = insn.requireIntSourceA().intNumber
val type = insn.type ?: IRDataType.WORD
emitLoadD0(srcReg, type)
emitStoreD0(dstReg, IRDataType.BYTE)
}
Opcode.LSIGW -> {
val dstReg = insn.requireIntDest().intNumber
val srcReg = insn.requireIntSourceA().intNumber
emitLoadD0(srcReg, IRDataType.LONG)
emitStoreD0(dstReg, IRDataType.WORD)
}
Opcode.MSIGB -> {
val dstReg = insn.requireIntDest().intNumber
val srcReg = insn.requireIntSourceA().intNumber
// Big-endian: MSB is at byte offset 0 for both word and long.
emitLoadD0FromAddress(regAddrByte(srcReg, 0), IRDataType.BYTE)
emitStoreD0(dstReg, IRDataType.BYTE)
}
Opcode.MSIGW -> {
val dstReg = insn.requireIntDest().intNumber
val srcReg = insn.requireIntSourceA().intNumber
// Big-endian: word 0 (offset+0) is most significant. Extract directly.
emitLoadD0FromAddress(regAddrByte(srcReg, 0), IRDataType.WORD)
emitStoreD0(dstReg, IRDataType.WORD)
}
Opcode.BSIGB -> {
val dstReg = insn.requireIntDest().intNumber
val srcReg = insn.requireIntSourceA().intNumber
// Big-endian: bits 16-23 are at byte offset 1 of a long.
emitLoadD0FromAddress(regAddrByte(srcReg, 1), IRDataType.BYTE)
emitStoreD0(dstReg, IRDataType.BYTE)
}
Opcode.MIDB -> {
val dstReg = insn.requireIntDest().intNumber
val srcReg = insn.requireIntSourceA().intNumber
// Big-endian: bits 8-15 are at byte offset 2 of a long.
emitLoadD0FromAddress(regAddrByte(srcReg, 2), IRDataType.BYTE)
emitStoreD0(dstReg, IRDataType.BYTE)
}
// === Sign/zero extension ===
Opcode.EXT -> {
val dstReg = insn.requireIntDest().intNumber
val srcReg = insn.requireIntSourceA().intNumber
val type = insn.type ?: IRDataType.WORD
when (type) {
IRDataType.BYTE -> {
// zero-extend byte to word
invalidateD0Cache()
emitLine("moveq #0, d0")
emitLoadD0(srcReg, IRDataType.BYTE)
emitStoreD0(dstReg, IRDataType.WORD)
}
IRDataType.WORD -> {
// zero-extend word to long
invalidateD0Cache()
emitLine("moveq #0, d0")
emitLoadD0(srcReg, IRDataType.WORD)
emitStoreD0(dstReg, IRDataType.LONG)
}
IRDataType.LONG -> {
// no extension needed
emitLoadD0(srcReg, IRDataType.LONG)
emitStoreD0(dstReg, IRDataType.LONG)
}
else -> TODO("EXT for ${type.name}")
}
}
Opcode.EXTS -> {
val dstReg = insn.requireIntDest().intNumber
val srcReg = insn.requireIntSourceA().intNumber
val type = insn.type ?: IRDataType.WORD
when (type) {
IRDataType.BYTE -> {
// sign-extend byte to word
emitLoadD0(srcReg, IRDataType.BYTE)
emitLine("ext.w d0")
emitStoreD0(dstReg, IRDataType.WORD)
}
IRDataType.WORD -> {
// sign-extend word to long
emitLoadD0(srcReg, IRDataType.WORD)
emitLine("ext.l d0")
emitStoreD0(dstReg, IRDataType.LONG)
}
IRDataType.LONG -> {
// no extension needed
emitLoadD0(srcReg, IRDataType.LONG)
emitStoreD0(dstReg, IRDataType.LONG)
}
else -> TODO("EXTS for ${type.name}")
}
}
Opcode.EXTL -> {
val dstReg = insn.requireIntDest().intNumber
val srcReg = insn.requireIntSourceA().intNumber
// zero-extend byte to long: move.b only writes the low byte, so clear d0 first
invalidateD0Cache()
emitLine("moveq #0, d0")
emitLoadD0(srcReg, IRDataType.BYTE)
emitStoreD0(dstReg, IRDataType.LONG)
}
Opcode.EXTLS -> {
val dstReg = insn.requireIntDest().intNumber
val srcReg = insn.requireIntSourceA().intNumber
// sign-extend byte to long
emitLoadD0(srcReg, IRDataType.BYTE)
emitSignExtendByteToLong("d0")
emitStoreD0(dstReg, IRDataType.LONG)
}
// === Concatenation ===
Opcode.CONCAT -> {
val dstReg = insn.requireIntDest().intNumber
val srcReg2 = insn.requireIntSourceA().intNumber
val srcReg3 = insn.requireSrcB().intNumber
val type = insn.type ?: IRDataType.BYTE
when (type) {
IRDataType.BYTE -> {
// r1 = WORD(r2 as MSB, r3 as LSB)
emitLoadD0(srcReg2, IRDataType.BYTE)
emitLine("lsl.w #8, d0")
invalidateD0Cache()
emitLine("or.b ${regAddr(srcReg3)}, d0")
emitStoreD0(dstReg, IRDataType.WORD)
}
IRDataType.WORD -> {
// r1 = LONG(r2 as MSW, r3 as LSW)
emitLoadD0(srcReg2, IRDataType.WORD)
emitLine("swap d0")
invalidateD0Cache()
emitLine("clr.w d0")
emitLine("or.w ${regAddr(srcReg3)}, d0")
emitStoreD0(dstReg, IRDataType.LONG)
}
else -> TODO("CONCAT for ${type.name}")
}
}
// === Math ===
Opcode.SGN -> {
val dstReg = insn.requireIntDest().intNumber
if (insn.type == IRDataType.FLOAT) {
val srcFp = insn.requireFloatSourceA().floatNumber
invalidateD0Cache()
emitLine("ftst.s ${floatRegFileAddr(srcFp)}")
emitLine("fslt d0")
emitLine("fsgt d1")
emitLine("neg.b d1")
emitLine("or.b d1, d0")
emitStoreD0(dstReg, IRDataType.BYTE)
} else {
val srcReg = insn.requireIntSourceA().intNumber
val type = insn.type ?: IRDataType.BYTE
val s = dtSuffix(type)
val zeroLabel = makeLabel("sgn_zero")
val doneLabel = makeLabel("sgn_done")
emitLoadD0(srcReg, type)
emitLine("tst$s d0")
emitLine("beq $zeroLabel")
emitLine("smi d1")
emitLine("bmi $doneLabel")
emitLine("moveq #1, d1")
emitLine("bra $doneLabel")
emitLabel(zeroLabel)
emitLine("moveq #0, d1")
emitLabel(doneLabel)
emitLine("move.b d1, ${regAddr(dstReg)}")
invalidateD0CacheForSlot(dstReg) // raw store bypasses emitStoreD0
}
}
// === Floating point operations via 68881/68882 FPU ===
Opcode.FFROMUB -> {
val fpDst = insn.requireFloatDest().floatNumber
val srcReg = insn.requireIntSourceA().intNumber
emitLoadD0(srcReg, IRDataType.BYTE)
emitLine($$"and.l #$ff, d0")
invalidateD0Cache()
emitLine("fmove.l d0, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(fpDst)}")
}
Opcode.FFROMSB -> {
val fpDst = insn.requireFloatDest().floatNumber
val srcReg = insn.requireIntSourceA().intNumber
emitLoadD0(srcReg, IRDataType.BYTE)
emitSignExtendByteToLong("d0")
emitLine("fmove.l d0, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(fpDst)}")
}
Opcode.FFROMUW -> {
val fpDst = insn.requireFloatDest().floatNumber
val srcReg = insn.requireIntSourceA().intNumber
emitLoadD0(srcReg, IRDataType.WORD)
emitLine($$"and.l #$ffff, d0")
invalidateD0Cache()
emitLine("fmove.l d0, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(fpDst)}")
}
Opcode.FFROMSW -> {
val fpDst = insn.requireFloatDest().floatNumber
val srcReg = insn.requireIntSourceA().intNumber
emitLoadD0(srcReg, IRDataType.WORD)
emitLine("ext.l d0")
emitLine("fmove.l d0, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(fpDst)}")
}
Opcode.FFROMSL -> {
val fpDst = insn.requireFloatDest().floatNumber
val srcReg = insn.requireIntSourceA().intNumber
emitLoadD0(srcReg, IRDataType.LONG)
emitLine("fmove.l d0, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(fpDst)}")
}
Opcode.FTOUB -> {
val dstReg = insn.requireIntDest().intNumber
val fpSrc = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(fpSrc)}, $FP_ACC")
invalidateD0Cache()
emitLine("fmove.b $FP_ACC, d0")
emitLine($$"and.l #$ff, d0")
emitStoreD0(dstReg, IRDataType.BYTE)
}
Opcode.FTOSB -> {
val dstReg = insn.requireIntDest().intNumber
val fpSrc = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(fpSrc)}, $FP_ACC")
invalidateD0Cache()
emitLine("fmove.b $FP_ACC, d0")
emitStoreD0(dstReg, IRDataType.BYTE)
}
Opcode.FTOUW -> {
val dstReg = insn.requireIntDest().intNumber
val fpSrc = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(fpSrc)}, $FP_ACC")
invalidateD0Cache()
emitLine("fmove.w $FP_ACC, d0")
emitStoreD0(dstReg, IRDataType.WORD)
}
Opcode.FTOSW -> {
val dstReg = insn.requireIntDest().intNumber
val fpSrc = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(fpSrc)}, $FP_ACC")
invalidateD0Cache()
emitLine("fmove.w $FP_ACC, d0")
emitStoreD0(dstReg, IRDataType.WORD)
}
Opcode.FTOSL -> {
val dstReg = insn.requireIntDest().intNumber
val fpSrc = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(fpSrc)}, $FP_ACC")
invalidateD0Cache()
emitLine("fmove.l $FP_ACC, d0")
emitStoreD0(dstReg, IRDataType.LONG)
}
Opcode.FABS -> {
val dst = insn.requireFloatDest().floatNumber
val src = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
emitLine("fabs $FP_ACC, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.FSIN -> {
val dst = insn.requireFloatDest().floatNumber
val src = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
emitLine("fsin $FP_ACC, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.FCOS -> {
val dst = insn.requireFloatDest().floatNumber
val src = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
emitLine("fcos $FP_ACC, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.FTAN -> {
val dst = insn.requireFloatDest().floatNumber
val src = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
emitLine("ftan $FP_ACC, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.FATAN -> {
val dst = insn.requireFloatDest().floatNumber
val src = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
emitLine("fatan $FP_ACC, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.FPOW -> {
val dst = insn.requireFloatDest().floatNumber
val src = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
emitLine("fpow $FP_ACC, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.FLN -> {
val dst = insn.requireFloatDest().floatNumber
val src = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
emitLine("flogn $FP_ACC, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.FLOG -> {
val dst = insn.requireFloatDest().floatNumber
val src = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
emitLine("flog2 $FP_ACC, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.FROUND -> {
val dst = insn.requireFloatDest().floatNumber
val src = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
emitLine("fround $FP_ACC, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.FFLOOR -> {
val dst = insn.requireFloatDest().floatNumber
val src = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
emitLine("ffloor $FP_ACC, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.FCEIL -> {
// 68881 has no fceil; implement as: if x == int(x) then x else x > 0 ? int(x)+1 : int(x)
val dst = insn.requireFloatDest().floatNumber
val src = insn.requireFloatSourceA().floatNumber
val isIntLabel = makeLabel("fceil_is_int")
val doneLabel = makeLabel("fceil_done")
val posLabel = makeLabel(".fceil_pos")
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
emitLine("fmove.s $FP_ACC, $FP_SRC")
emitLine("fintrz $FP_SRC, $FP_SRC") // truncate toward zero
emitLine("fcmp $FP_ACC, $FP_SRC")
emitLine("fbeq $isIntLabel") // if equal, already integer
emitLine("ftst $FP_ACC")
emitLine("fbgt $posLabel") // if >0, need to add 1
emitLine("bra $doneLabel")
emitLabel(posLabel)
emitLine("fadd.s #1.0, $FP_SRC")
emitLine("bra $doneLabel")
emitLabel(isIntLabel)
// dst already holds the integer (from fintrz)
emitLabel(doneLabel)
emitLine("fmove.s $FP_SRC, ${floatRegFileAddr(dst)}")
}
Opcode.FCOMP -> {
val dstReg = insn.requireIntDest().intNumber
val fr1 = insn.requireFloatSourceA().floatNumber
val fr2 = insn.requireSrcB().floatNumber
val eqLabel = makeLabel("fcomp_eq")
val doneLabel = makeLabel("fcomp_done")
val gtLabel = makeLabel("fcomp_gt")
invalidateD0Cache()
emitLine("fmove.s ${floatRegFileAddr(fr1)}, $FP_ACC")
emitLine("fcmp.s ${floatRegFileAddr(fr2)}, $FP_ACC")
emitLine("fbeq $eqLabel")
emitLine("fbgt $gtLabel")
emitLine("moveq #-1, d0")
emitLine("bra $doneLabel")
emitLabel(gtLabel)
emitLine("moveq #1, d0")
emitLine("bra $doneLabel")
emitLabel(eqLabel)
emitLine("moveq #0, d0")
emitLabel(doneLabel)
emitStoreD0(dstReg, IRDataType.BYTE)
}
Opcode.LOADHFACZERO, Opcode.LOADHFACONE, Opcode.STOREHFACZERO, Opcode.STOREHFACONE ->
error("${insn.opcode} should have been handled by translateLoadStore")
else -> error("Unknown control opcode: ${insn.opcode}")
}
}
// === CALL translation with argument handling ===
private fun AsmGen.translateCall(fnLabel: String, callSite: CallSite, forwardedImmediateCall: ImmediateCallOptimization? = null) {
if (fnLabel == "sys.memcopy" && emitInlineMemcopyCall(callSite, forwardedImmediateCall))
return
// Emit non-constant arguments first (from regfile), then constant immediates
// just before the BSR/JSR to avoid clobbering the constant registers while
// evaluating non-const arguments (e.g. address calculations that may use D0/A0 as scratch).
// 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 ((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
val inlineTarget = program.allAsmSubs().find { it.label == fnLabel && it.isInline }
if (inlineTarget != null) {
emitRaw(inlineTarget.asmChunk.assembly)
} else {
// PC-relative call; vasm picks the optimal size and falls back to jsr if out of range
emitLine("bsr $fnLabel")
}
// 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:
// the IR generates LOADHR for them, and emitting here would clobber CPU flags
// before the branch pattern can read them.
// In single-return expression context, process all slot returns normally
// (the IR doesn't generate LOADHR for single-return calls).
// 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)
continue
if (isMultiReturn)
continue
translateReturnValue(ret)
}
}
private fun AsmGen.emitInlineMemcopyCall(callSite: CallSite, forwardedImmediateCall: ImmediateCallOptimization?): Boolean {
if (callSite.arguments.size != 3) return false
val fwd = forwardedImmediateCall ?: return false
val srcArg = callSite.arguments[0]
val tgtArg = callSite.arguments[1]
val countArg = callSite.arguments[2]
// size must be a constant immediate forwarded to the call; non-constant -> always call
val countLoad = fwd.loads[countArg.source.register] ?: return false
val count = countLoad.immediate?.integerValue ?: return false
if (count <= 0) return true
// Only use .w/.l if we can 100% prove both pointers are aligned.
// At codegen we only know immediates; computed addresses (e.g. &arr[i]) are not immediate
// so we conservatively fall back to byte copies. A future IR alignment analysis could prove more.
val srcImm = fwd.loads[srcArg.source.register]?.immediate?.integerValue
val tgtImm = fwd.loads[tgtArg.source.register]?.immediate?.integerValue
val bothEven = srcImm != null && tgtImm != null && srcImm % 2 == 0 && tgtImm % 2 == 0
val bothLongAligned = srcImm != null && tgtImm != null && srcImm % 4 == 0 && tgtImm % 4 == 0
val useLong = bothLongAligned && count % 4 == 0
val useWord = bothEven && count % 2 == 0
// Heuristics: inline small copies; use a dbra loop for the medium "small" range
// to keep code size reasonable. The call to memcopy/CopyMem wins for large copies.
val threshold = when {
useLong -> 64
useWord -> 32
else -> 16
}
if (count > threshold) return false
// Load pointers into a0/a1 (benefit from forwarded immediates)
translateArgument(srcArg, "sys.memcopy", fwd)
translateArgument(tgtArg, "sys.memcopy", fwd)
// count is constant, no need to load d0
// For larger small counts, a dbra loop is smaller than unrolled moves.
// Threshold: a dbra loop needs ~10 bytes setup (move.w #count,dN + label + dbra),
// so it wins over more than ~5 unrolled moves of the same size.
val unrolledLimit = 5
when {
useLong -> {
val longs = count / 4
emitRaw(" ; inline memcopy $count bytes as $longs longwords")
if (longs <= unrolledLimit) {
repeat(longs) { emitLine("move.l (a0)+,(a1)+") }
} else {
emitInlineCopyLoop(longs, 4)
}
}
useWord -> {
val words = count / 2
emitRaw(" ; inline memcopy $count bytes as $words words")
if (words <= unrolledLimit) {
repeat(words) { emitLine("move.w (a0)+,(a1)+") }
} else {
emitInlineCopyLoop(words, 2)
}
}
else -> {
emitRaw(" ; inline memcopy $count bytes")
if (count <= unrolledLimit) {
repeat(count) { emitLine("move.b (a0)+,(a1)+") }
} else {
emitInlineCopyLoop(count, 1)
}
}
}
return true
}
private fun AsmGen.emitInlineCopyLoop(iterations: Int, size: Int) {
val loopLabel = makeLabel("memcpy_inline")
val moveInsn = when (size) {
4 -> "move.l"
2 -> "move.w"
else -> "move.b"
}
emitLine("moveq #$iterations, d0")
emitLabel(loopLabel)
emitLine("$moveInsn (a0)+,(a1)+")
emitLine("dbra d0,$loopLabel")
}
private fun AsmGen.translateArgument(
arg: CallArgument,
fnLabel: String? = null,
forwardedImmediateCall: ImmediateCallOptimization? = null,
paramName: String? = null
) {
val argReg = arg.source
val forwarded = forwardedImmediateCall?.loads?.get(argReg.register)
// If the argument has a calling convention slot, load it into that hardware register
val slot = (arg.location as? CallLocation.HardwareRegister)?.slot
if (slot != null) {
val hwReg = m68kSlotRegister(slot)
if (argReg.isFloat) {
if (forwarded != null) {
emitFloadConstantTo(hwReg, forwarded.requireImmediateFloat())
} else {
emitLine("fmove.s ${floatRegFileAddr(argReg.floatNumber)}, $hwReg")
}
} else {
if (forwarded != null) {
val value = forwarded.requireImmediateInt()
val s = dtSuffix(argReg.type)
if (hwReg.startsWith("a")) {
// address registers only accept movea/suba, not clr or moveq
when (value) {
0 -> emitLine("suba.l $hwReg, $hwReg")
else -> emitLine("movea.l #$value, $hwReg")
}
} else {
if (value == 0)
emitLine("clr$s $hwReg")
else {
// use moveq (2 bytes, 4 cycles) when the value fits in its signed 8-bit range;
// for byte args the value is unsigned 0-255, so map 128-255 to -128--1 (low byte is the same)
val moveqValue = if (argReg.type == IRDataType.BYTE && value in 128..255) value - 256 else value
if (moveqValue in -128..127)
emitLine("moveq #$moveqValue, $hwReg")
else
emitLine("move$s #$value, $hwReg")
}
}
} else {
val source = regAddr(argReg.intNumber)
if (hwReg.startsWith("a") && argReg.type in setOf(IRDataType.LONG, IRDataType.POINTER)) {
// loading a pointer into an address register; movea.l is the proper form and skips the +0 offset
emitLine("movea.l ${source.removeSuffix("+0")}, $hwReg")
} else {
val s = dtSuffix(argReg.type)
emitLine("move$s $source, $hwReg")
}
}
}
} else {
// 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) {
val target = fixNameSymbols("$fnLabel.$paramName")
if (argReg.isFloat) {
emitLine("fmove.s ${floatRegFileAddr(argReg.floatNumber)}, $FP_ACC")
emitLine("fmove.s $FP_ACC, $target")
} else {
val s = dtSuffix(argReg.type)
emitLine("move$s ${regAddr(argReg.intNumber)}, $target")
}
}
}
}
private fun AsmGen.translateReturnValue(ret: CallResult) {
// Status flag returns are handled by the IR's branch-based pattern (bsteq/bstneg/bstvs).
// Do NOT emit Scc here - it would clobber the flags before the branch can read them.
if (ret.location is CallLocation.StatusFlag)
return
val destination = ret.destination ?: return
// Otherwise, return value is in a hardware register
val slot = (ret.location as? CallLocation.HardwareRegister)?.slot
if (slot != null) {
val hwReg = m68kSlotRegister(slot)
if (destination.isFloat) {
emitLine("fmove.s $hwReg, ${floatRegFileAddr(destination.floatNumber)}")
} else {
val s = dtSuffix(destination.type)
emitLine("move$s $hwReg, ${regAddr(destination.intNumber)}")
}
} else {
// Default: return value in d0 (standard m68k calling convention)
// All non-float returns go through d0-d7 (not a0-a6) because the calling convention
// expects values in data registers. Using A0 for pointers would be inconsistent:
// callers read return values from data regs unless the slot annotation says otherwise.
// Slightly inefficient: m68k pointers ideally live in address registers, but returning
// them in d0 is simpler and avoids ambiguity. Explicit @A0 can be used for hot paths.
if (destination.isFloat) {
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(destination.floatNumber)}")
} else {
emitStoreD0(destination.intNumber, destination.type)
}
}
}
// === Slot to M68k hardware register mapping ===
// Slots 0..7 are the 6502/cx16-style scalar registers (A, X, Y, AX, AY, XY,
// FAC1, FAC2) used by target-independent builtins (e.g. divmod returns its
// quotient in AY). Map them onto distinct M68k hardware registers so the
// backend can emit code for them.
fun m68kSlotRegister(slot: CallingConventionSlot): String = when (slot.value) {
in 0..7 -> error("slots 0-7 should never be used on the M68K they are 6502 cpu registers")
in 10..17 -> "d${slot.value - 10}" // M68k slots: D0-D7
in 18..24 -> "a${slot.value - 18}" // M68k slots: A0-A6
in 25..32 -> "fp${slot.value - 25}" // M68k slots: FP0-FP7
else -> error("unknown calling convention slot: $slot")
}
@@ -0,0 +1,255 @@
package prog8.codegen.m68k
import prog8.intermediate.IRDataType
import prog8.intermediate.IRInstruction
import prog8.intermediate.ImmediateOperand
import prog8.intermediate.Opcode
import prog8.intermediate.floatNumber
import prog8.intermediate.intNumber
import prog8.intermediate.requireFloatDest
import prog8.intermediate.requireFloatSourceA
import prog8.intermediate.requireHardwareSlot
import prog8.intermediate.requireImmediate
import prog8.intermediate.requireImmediateFloat
import prog8.intermediate.requireImmediateInt
import prog8.intermediate.requireIntDest
import prog8.intermediate.requireIntSourceA
import prog8.intermediate.requireMemory
internal fun AsmGen.translateLoadStore(insn: IRInstruction, suppressRegfileStore: Boolean = false) {
val type = insn.type ?: IRDataType.BYTE
if (type == IRDataType.FLOAT) {
translateFloatLoadStore(insn, suppressRegfileStore)
return
}
val s = dtSuffix(type)
when (insn.opcode) {
Opcode.LOAD -> {
val dst = insn.requireIntDest().intNumber
when (val value = insn.requireImmediate()) {
is ImmediateOperand.Integer -> {
if (suppressRegfileStore)
return
if(value.value == 0)
emitLine("clr$s ${regAddr(dst)}")
else
emitLine("move$s #${value.value}, ${regAddr(dst)}")
invalidateD0CacheForSlot(dst)
}
is ImmediateOperand.SymbolAddress -> {
val resolved = resolveSymbolRef(value.symbol)
val symOff = if (value.offset != 0) "$resolved+${value.offset}" else resolved
emitLine("move.l #$symOff, ${regAddr(dst)}")
invalidateD0CacheForSlot(dst)
}
is ImmediateOperand.FloatValue -> error("LOAD: float immediate for an integer register")
}
}
Opcode.LOADM -> {
val dst = insn.requireIntDest().intNumber
val target = resolveMemory(insn.requireMemory())
emitLine("move$s $target, ${regAddr(dst)}")
invalidateD0CacheForSlot(dst)
}
Opcode.LOADR -> {
val dst = insn.requireIntDest().intNumber
val src = insn.requireIntSourceA().intNumber
emitLine("move$s ${regAddr(src)}, ${regAddr(dst)}")
invalidateD0CacheForSlot(dst)
}
Opcode.LOADX, Opcode.LOADI -> {
val dst = insn.requireIntDest().intNumber
val source = resolveMemory(insn.requireMemory())
emitLoadD0FromAddress(source, type)
emitStoreD0(dst, type)
}
Opcode.LOADHR -> {
val dst = insn.requireIntDest().intNumber
val hwReg = m68kSlotRegister(insn.requireHardwareSlot().slot)
emitLine("move$s $hwReg, ${regAddr(dst)}")
invalidateD0CacheForSlot(dst)
}
Opcode.STOREM -> {
val src = insn.requireIntSourceA().intNumber
val target = resolveMemory(insn.requireMemory())
emitLine("move$s ${regAddr(src)}, $target")
invalidateD0CacheForAddress(target)
}
Opcode.STOREIM -> {
val value = insn.requireImmediateInt()
val target = resolveMemory(insn.requireMemory())
if(value == 0)
emitLine("clr$s $target")
else
emitLine("move$s #$value, $target")
invalidateD0CacheForAddress(target)
}
Opcode.STOREX -> {
val value = insn.requireIntSourceA().intNumber
val target = resolveMemory(insn.requireMemory())
// the indexed effective address uses a0/d0 as scratch, so stage the value in d1
emitLine("move$s ${regAddr(value)}, d1")
emitLine("move$s d1, $target")
}
Opcode.STOREZM, Opcode.STOREZI, Opcode.STOREZX -> {
val target = resolveMemory(insn.requireMemory())
emitLine("clr$s $target")
invalidateD0CacheForAddress(target)
}
Opcode.STOREHR -> {
val src = insn.requireIntSourceA().intNumber
val hwReg = m68kSlotRegister(insn.requireHardwareSlot().slot)
emitLine("move$s ${regAddr(src)}, $hwReg")
}
Opcode.STOREI -> {
val value = insn.requireIntSourceA().intNumber
val target = resolveMemory(insn.requireMemory())
emitLine("move$s ${regAddr(value)}, $target")
}
Opcode.LOADP_INC -> {
val dst = insn.requireIntDest().intNumber
// r1 = *a; a += sizeof(type) where a is a pointer variable (LONG) in memory
val target = resolveMemory(insn.requireMemory())
emitLine("move.l $target, a0")
emitLoadD0FromAddress("(a0)+", type)
emitLine("move.l a0, $target")
invalidateD0CacheForAddress(target)
emitStoreD0(dst, type)
}
Opcode.STOREP_INC -> {
val value = insn.requireIntSourceA().intNumber
// *a = r1; a += sizeof(type)
val target = resolveMemory(insn.requireMemory())
emitLine("move.l $target, a0")
emitLine("move$s ${regAddr(value)}, (a0)+")
emitLine("move.l a0, $target")
invalidateD0CacheForAddress(target)
}
else -> error("Unknown load/store opcode: ${insn.opcode}")
}
}
// === Float load/store via FPU (68881/68882) ===
private fun AsmGen.translateFloatLoadStore(insn: IRInstruction, suppressRegfileStore: Boolean = false) {
when (insn.opcode) {
Opcode.STOREZM, Opcode.STOREZI, Opcode.STOREZX -> {
val target = resolveMemory(insn.requireMemory(), forFloat = true)
emitLine($$"fmovecr #$0f, fp0")
emitLine("fmove.s fp0, $target")
invalidateD0CacheForAddress(target)
}
Opcode.STOREIM -> {
val target = resolveMemory(insn.requireMemory(), forFloat = true)
emitFloadConstantTo("fp0", insn.requireImmediateFloat())
emitLine("fmove.s fp0, $target")
invalidateD0CacheForAddress(target)
}
Opcode.LOAD -> {
val dst = insn.requireFloatDest().floatNumber
when (val value = insn.requireImmediate()) {
is ImmediateOperand.FloatValue -> {
if (suppressRegfileStore)
return
emitFloadConstantToAcc(value.value)
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
is ImmediateOperand.SymbolAddress -> {
val resolved = resolveSymbolRef(value.symbol)
val symOff = if (value.offset != 0) "$resolved+${value.offset}" else resolved
emitLine("lea $symOff, a0")
emitLine("fmove.s (a0), $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
is ImmediateOperand.Integer -> error("LOAD.f: integer immediate for a float register")
}
}
Opcode.LOADM, Opcode.LOADX, Opcode.LOADI -> {
val dst = insn.requireFloatDest().floatNumber
val source = resolveMemory(insn.requireMemory(), forFloat = true)
emitLine("fmove.s $source, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.LOADR -> {
val dst = insn.requireFloatDest().floatNumber
val src = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.LOADHR -> {
val dst = insn.requireFloatDest().floatNumber
val hwReg = m68kSlotRegister(insn.requireHardwareSlot().slot)
emitLine("fmove $hwReg, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.STOREM, Opcode.STOREX, Opcode.STOREI -> {
val src = insn.requireFloatSourceA().floatNumber
val target = resolveMemory(insn.requireMemory(), forFloat = true)
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
emitLine("fmove.s $FP_ACC, $target")
invalidateD0CacheForAddress(target)
}
Opcode.STOREHR -> {
val src = insn.requireFloatSourceA().floatNumber
val hwReg = m68kSlotRegister(insn.requireHardwareSlot().slot)
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
emitLine("fmove $FP_ACC, $hwReg")
}
// The 'FAC' hardware float registers only exist on the 6502 targets. On m68k they are
// modelled with the two FPU scratch registers; loading them yields the 0.0 / 1.0 constants
// that the 6502 targets keep in FAC1 / FAC2.
Opcode.LOADHFACZERO -> {
val dst = insn.requireFloatDest().floatNumber
emitLine($$"fmovecr #$0f, $$FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.LOADHFACONE -> {
val dst = insn.requireFloatDest().floatNumber
emitLine("fmovecr #$32, $FP_ACC")
emitLine("fmove.s $FP_ACC, ${floatRegFileAddr(dst)}")
}
Opcode.STOREHFACZERO -> {
val src = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_ACC")
}
Opcode.STOREHFACONE -> {
val src = insn.requireFloatSourceA().floatNumber
emitLine("fmove.s ${floatRegFileAddr(src)}, $FP_SRC")
}
else -> error("Unknown float load/store opcode: ${insn.opcode}")
}
}
private var floatConstCounter = 0
internal fun AsmGen.makeFloatConstLabel(value: Double): String {
floatConstCounter++
val label = "p8c_fconst_$floatConstCounter"
this.dataFloatConstants.add(Pair(label, value))
return label
}
@@ -0,0 +1,256 @@
/*
* SYSCALL instruction translations for the M68k code generator.
*
* Handles inline expansion of intermediate representation syscall
* instructions into M68k assembly code.
*/
package prog8.codegen.m68k
import prog8.intermediate.CallSite
import prog8.intermediate.IMSyscall
import prog8.intermediate.IRDataType
import prog8.intermediate.RegisterOperand
import prog8.intermediate.floatNumber
import prog8.intermediate.intNumber
// === SYSCALL dispatch ===
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)
IMSyscall.CLAMP_WORD.number -> translateSyscallClamp(site, IRDataType.WORD, true)
IMSyscall.CLAMP_UWORD.number -> translateSyscallClamp(site, IRDataType.WORD, false)
IMSyscall.CLAMP_LONG.number -> translateSyscallClamp(site, IRDataType.LONG, true)
else -> TODO("syscall $num on m68k")
}
}
private fun CallSite.argument(index: Int): RegisterOperand =
arguments.getOrNull(index)?.source ?: error("syscall is missing argument $index")
private fun CallSite.resultRegister(index: Int): RegisterOperand =
results.getOrNull(index)?.destination ?: error("syscall is missing result $index")
// Compare two strings by delegating to the library routine prog8_lib.strcmp,
// which already implements an efficient case-sensitive comparison that
// correctly handles 32 bits pointers (the pointer type on M68k targets).
private fun AsmGen.translateSyscallStringCompare(site: CallSite) {
val arg1 = site.argument(0)
val arg2 = site.argument(1)
val resultReg = site.resultRegister(0).intNumber
emitLoadD0(arg1.intNumber, arg1.type)
loadStringArg(arg2.intNumber, arg2.type, "d1")
invalidateD0Cache() // d0 is caller-saved across the subroutine call
emitLine("bsr prog8_lib.strcmp")
emitStoreD0(resultReg, IRDataType.BYTE)
}
// Load a string pointer argument (16 or 32 bits) into the given data register,
// matching the calling convention of strings.compare (full 32 bits address).
private fun AsmGen.loadStringArg(reg: Int, dt: IRDataType, dreg: String) {
if (dt == IRDataType.LONG) {
emitLine("move.l ${regAddr(reg)}, $dreg")
} else {
emitLine("move.w ${regAddr(reg)}, $dreg")
}
}
// 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(".ac_loop")
val labelFound = makeLabel(".ac_found")
val labelDone = makeLabel(".ac_done")
emitLoadD0(regElem, dt)
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:")
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)}")
emitLine("bra $labelDone")
emitRaw("$labelFound:")
emitLine("move.b #1, ${regAddrByte(resultReg, 0)}")
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
val maxReg = site.argument(2).intNumber
val resultReg = site.resultRegister(0).intNumber
val labelCheckMax = makeLabel(".clamp_max")
val labelDone = makeLabel(".clamp_done")
val bge = if (signed) "bge" else "bhs"
val ble = if (signed) "ble" else "bls"
when (dt) {
IRDataType.BYTE -> {
emitLoadD0(valueReg, IRDataType.BYTE)
emitLine("cmp.b ${regAddr(minReg)}, d0")
emitLine("$bge $labelCheckMax")
emitLoadD0(minReg, IRDataType.BYTE)
emitLine("bra $labelDone")
emitRaw("$labelCheckMax:")
invalidateD0Cache() // branch target: d0 still holds valueReg, not minReg
emitLine("cmp.b ${regAddr(maxReg)}, d0")
emitLine("$ble $labelDone")
emitLoadD0(maxReg, IRDataType.BYTE)
emitRaw("$labelDone:")
invalidateD0Cache() // branch target: d0 differs per incoming path
emitStoreD0(resultReg, IRDataType.BYTE)
}
IRDataType.WORD -> {
emitLoadD0(valueReg, IRDataType.WORD)
emitLine("cmp.w ${regAddr(minReg)}, d0")
emitLine("$bge $labelCheckMax")
emitLoadD0(minReg, IRDataType.WORD)
emitLine("bra $labelDone")
emitRaw("$labelCheckMax:")
invalidateD0Cache() // branch target: d0 still holds valueReg, not minReg
emitLine("cmp.w ${regAddr(maxReg)}, d0")
emitLine("$ble $labelDone")
emitLoadD0(maxReg, IRDataType.WORD)
emitRaw("$labelDone:")
invalidateD0Cache() // branch target: d0 differs per incoming path
emitStoreD0(resultReg, IRDataType.WORD)
}
IRDataType.LONG -> {
emitLoadD0(valueReg, IRDataType.LONG)
emitLine("cmp.l ${regAddr(minReg)}, d0")
emitLine("$bge $labelCheckMax")
emitLoadD0(minReg, IRDataType.LONG)
emitLine("bra $labelDone")
emitRaw("$labelCheckMax:")
invalidateD0Cache() // branch target: d0 still holds valueReg, not minReg
emitLine("cmp.l ${regAddr(maxReg)}, d0")
emitLine("$ble $labelDone")
emitLoadD0(maxReg, IRDataType.LONG)
emitRaw("$labelDone:")
invalidateD0Cache() // branch target: d0 differs per incoming path
emitStoreD0(resultReg, IRDataType.LONG)
}
else -> emitLine("; clamp: unsupported dt $dt")
}
}
@@ -0,0 +1,46 @@
package prog8.codegen.m68k
import prog8.code.ICodeGeneratorBackend
import prog8.code.SymbolTable
import prog8.code.assembly.IAssemblyProgram
import prog8.code.ast.PtProgram
import prog8.code.core.CompilationOptions
import prog8.code.core.IErrorReporter
import prog8.codegen.intermediate.IRCodeGen
import prog8.codegen.intermediate.StackFrameLayout
import prog8.intermediate.IRDataType
import prog8.intermediate.IRFileWriter
import prog8.intermediate.VirtualRegister
import prog8.intermediate.dumpVariables
class M68kCodeGenerator(val retainSSA: Boolean): ICodeGeneratorBackend {
override fun generate(
program: PtProgram,
symbolTable: SymbolTable,
options: CompilationOptions,
errors: IErrorReporter
): IAssemblyProgram {
val irCodeGen = IRCodeGen(program, symbolTable, options, errors, retainSSA)
val irProgram = irCodeGen.generate()
val virtualRegisterTypes = irCodeGen.registerTypes().entries.associate { (num, type) ->
val register: VirtualRegister = if (type == IRDataType.FLOAT) VirtualRegister.float(num.value) else VirtualRegister.int(num.value)
register to type
}
irProgram.verifyRegisterTypes(virtualRegisterTypes)
if (options.compTarget.supportsStackFrames)
StackFrameLayout(irProgram, errors).apply()
if (options.dumpVariables)
dumpVariables(irProgram)
IRFileWriter(irProgram, null).write()
val gen = AsmGen(irProgram, irProgram.options.compTarget, errors)
if (!gen.generate())
throw RuntimeException("M68k assembly generation failed")
return AssemblyProgramM68k(irProgram.name)
}
}
@@ -0,0 +1,45 @@
package prog8.codegen.m68k
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.main
import com.github.ajalt.clikt.parameters.arguments.argument
import prog8.code.core.CpuType
import prog8.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()
cli.main(args)
val inputFile = cli.inputFile
val reader = IRFileReader()
val source = Path(inputFile).readText()
val program = reader.read(source)
val target = program.options.compTarget
println("Loaded IR program: ${program.name}")
println("Target system and CPU: ${target.name} / ${target.cpu}")
when(target.cpu) {
CpuType.M68000, CpuType.M68020 -> {
val gen = AsmGen(program, target)
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.")
}
}
}
private class M68kCli : CliktCommand(name = "prog8-m68kgen") {
val inputFile by argument(help = "path to .p8ir")
override fun run() = Unit
}
File diff suppressed because it is too large. Load diff
Loaded 100 of 640 files, more files were not shown because too many files have changed in this diff. Show more