Compare commits

...
247 Commits
Author SHA1 Message Date
Irmen de Jong 30435b6778 version 12.3.3 2026-09-07 19:18:15 +02:00
Irmen de Jong 1358a535e3 Fix inliner crash when inlining a sub that assigns to its own parameter
Also error on a variable declaration directly followed by another statement on the same line.

fixes #235
2026-09-04 22:13:11 +02:00
Irmen de Jong c8f309307f fix inlining bug: subroutines with register-mapped parameters cannot be inlined 2026-09-02 19:10:21 +02:00
Irmen de Jong b702ea50da no longer mangle inline asm that uses pipe operator (in other spots that is used as an instruction separator...) 2026-09-02 02:02:53 +02:00
Irmen de Jong 05ba1237dc fix %asm line grouping that tripped the asm optimizer to think stuff was one line where it wasn't 2026-09-01 22:04:33 +02:00
Irmen de Jong 739fb6dc42 release 12.3.2 2026-08-30 22:39:01 +02:00
Irmen de Jong aa3f8e3038 fix daemon error 2026-08-28 05:14:12 +02:00
Irmen de Jong 2bbbacc46e normal error message instead of crash when typed pointer arrays are used as struct field (currently not supported by the compiler) 2026-08-24 19:21:30 +02:00
Irmen de Jong 296295011e Fix 6502 indexed pointer array store correctness bugs / missing code gen implementations 2026-08-23 20:54:32 +02:00
Irmen de Jong de20c09790 fix missing array bounds check for struct field arrays 2026-08-23 18:42:57 +02:00
Irmen de Jong 4912c96f78 various codegen bug fixes 2026-08-23 00:12:12 +02:00
Irmen de Jong 45cb50ebb1 fix crash on typecast of array element to long as operand of shift/add 2026-08-22 19:05:14 +02:00
Irmen de Jong 2cd2bb0121 optimized the 3d rotation examples 2026-08-22 19:03:14 +02:00
Irmen de Jong 65a1300147 cpu6502: implement pointer inplace invert/negate (byte/word/long/float) 2026-08-21 01:33:43 +02:00
Irmen de Jong ba6179a4d3 fix crash and errors on parenthesized/casted struct pointer field access (e.g. (t.next).flag, (t.port as ^^Thing).flag) 2026-08-21 00:48:19 +02:00
Irmen de Jong 6b91a464c0 fix spelling errors 2026-08-20 23:55:26 +02:00
Irmen de Jong 3aab457a47 fix const evaluation type truncation error for bitwise operations between constants of different types 2026-08-18 23:48:06 +02:00
Irmen de Jong 08b4168311 IR: preserve floating point value precision in struct fields 2026-08-16 21:30:25 +02:00
Irmen de Jong 36b12e9897 fix uword double increment/decrement bug 2026-08-16 07:26:21 +02:00
Irmen de Jong 9c2d8541c3 cx16: fix invalid code in sys.restore_irq(). Fixes #233 2026-08-16 03:33:40 +02:00
Irmen de Jong 48da22866c simplify and fix daemon output messages once more 2026-08-12 23:48:28 +02:00
Irmen de Jong 4dedc27e5b release 12.3.1 2026-08-10 20:54:26 +02:00
Irmen de Jong 5ba494c5e5 fix IR codegen needing explicit CMP instructions because of changed status bits contract (see commit #19238cc36) 2026-08-09 22:36:39 +02:00
Recoordinate 5c16ebe377 Fix typo in comment (#232) 2026-08-09 12:35:32 +02:00
Irmen de Jong b5dfc0cd97 fix debug message and tests 2026-08-09 00:32:58 +02:00
Irmen de Jong d276fb9ed5 fix the overdraw in filled disc drawing routines 2026-08-09 00:20:22 +02:00
Irmen de Jong ec9567a669 fix error output text in daemon mode 2026-08-08 16:44:43 +02:00
Irmen de Jong 70d81298c1 fix multi-value initializer error when in nested scope 2026-08-06 23:32:08 +02:00
Irmen de Jong 1de1c10b41 IR: fix register type error when 0-assigning into an array 2026-08-06 19:44:52 +02:00
Irmen de Jong 25f883bb85 fix dangling bullet in history list 2026-08-05 11:03:02 +02:00
Irmen de Jong adc6d6626a attempt to fix Windows library file import bug (InvalidPathException) 2026-08-05 10:34:52 +02:00
Irmen de Jong 90ff904b6e add doc about newcodegen 2026-08-05 03:45:14 +02:00
Irmen de Jong ba54601eab release 12.3 2026-08-05 03:25:32 +02:00
Irmen de Jong 1c71f702f4 fix invalid const-folding of certain strings char classification routines such as isupper() 2026-08-05 02:55:36 +02:00
Irmen de Jong 3d3c567ea6 fix memory-byte read sign extension error when the byte was signed 2026-08-05 02:54:59 +02:00
Irmen de Jong 539c75bbf6 fix expression operand swap bug when there are side effects 2026-08-05 02:51:37 +02:00
Irmen de Jong 2683b6388a update history 2026-08-05 02:40:30 +02:00
Irmen de Jong a919ea63e4 remove wrong ast node copy() overrides 2026-08-04 04:54:53 +02:00
Irmen de Jong 07910fc543 IR: fix register type error for power of 2 multiplies as shifts 2026-08-04 00:06:09 +02:00
Irmen de Jong 13e032e1a3 add new IR instructions ADDIM and SUBIM 2026-08-03 19:31:57 +02:00
Irmen de Jong c3b5ee3dfd optimize imports 2026-08-03 14:31:42 +02:00
Irmen de Jong b0e0058310 split huge test file 2026-08-03 14:23:21 +02:00
Irmen de Jong 9cbbf85a85 fix chained expression optimization bug 2026-08-03 14:22:46 +02:00
Irmen de Jong 9936a855ab fix ast remainder optimization check 2026-08-03 13:05:26 +02:00
Irmen de Jong bb1d1e2007 gradle --non-interactive didn't work 2026-08-02 17:27:39 +02:00
Irmen de Jong 0aa6c130e8 update to kotlin 2.4.10 2026-08-02 17:23:22 +02:00
Irmen de Jong 51257e47da structs in symboldumps 2026-07-14 08:05:03 +02:00
Irmen de Jong 67ddd97f3d fix skill to allow fixed-size arrays as struct fields 2026-07-14 07:31:10 +02:00
Irmen de Jong abea7bf329 fix const evaluation type problem in bitwise operations 2026-07-14 07:28:31 +02:00
Irmen de Jong bc3f49dc13 unit test for bitwise long const errors 2026-07-14 07:27:44 +02:00
Irmen de Jong cf2a5ae4f9 fix test sizes 2026-07-11 23:06:26 +02:00
Irmen de Jong e5865a1446 fix delete root node bug in binarytree.p8 2026-07-11 22:16:51 +02:00
Irmen de Jong 5b5f553d37 improved error message when attempting to alias types,
fix size test
2026-07-11 17:04:52 +02:00
Irmen de Jong b9fe1f2057 fix fibonacci sequence to start from 0 2026-07-11 14:48:52 +02:00
Irmen de Jong cfd1f951f0 should call through 2026-07-08 23:36:48 +02:00
Irmen de Jong b0e55eb25b fix another float to long cast error 2026-07-08 23:27:10 +02:00
Irmen de Jong b405ffb8bd fix missing float to long cast, fix missing inlined floats routines prefix 2026-07-08 20:42:54 +02:00
Irmen de Jong 0fa08310e7 add missing floats.pow() to the docs, optimize the signature of many float functions 2026-07-08 19:28:34 +02:00
Irmen de Jong 968d9819db removed -nostdlib option 2026-07-07 23:31:20 +02:00
Irmen de Jong f85c59b12c add sys.MAX_LONG and sys.MIN_LONG 2026-07-07 04:37:59 +02:00
Irmen de Jong 34eedd1fe7 fix missing return values in symboldumps 2026-07-07 00:41:14 +02:00
Andy De George 2c10e2b102 Fix the swap documentation now that it's a statement and no longer a function (#229)
* [docs] Remove swap from built in functions

* Move swap info to programming doc

* Reintroduce swap statement documentation

* Apply suggestion from @Thraka
2026-07-07 00:08:29 +02:00
Irmen de Jong 05bea61a69 builtin functions now have a signature in the symbol dump files 2026-07-05 01:11:40 +02:00
Irmen de Jong 2a8b24074b remove error about memory mapped pointers 2026-07-05 00:53:13 +02:00
Irmen de Jong 1f184a14ae diskio: document path length limit 2026-07-04 18:24:19 +02:00
Irmen de Jong 8be1cd1090 pet32: added some alternative hopefully unused ZP locations (as long as you're not using tape I/O which is a common assumption for every other cbm target as well) 2026-07-04 18:12:31 +02:00
Irmen de Jong c3ec400ee7 pet32: don't use problematic zeropage locations 2026-07-04 00:03:22 +02:00
Irmen de Jong 5130a74754 removed expericodegen 2026-07-02 01:07:21 +02:00
Irmen de Jong e61db5ed3f cleanup 2026-07-01 22:16:00 +02:00
Irmen de Jong cc5ef21fd6 don't publish unfinished codegens yet 2026-07-01 21:40:00 +02:00
Irmen de Jong f1f925ecee clarify sqrt() behavior on values <0 2026-07-01 20:51:06 +02:00
Irmen de Jong ab422c0ebc fix wrong codegen and 'empty' library subroutines 2026-07-01 20:25:00 +02:00
Irmen de Jong 05a13463f4 tweak agent skills and helper routine visibilities 2026-07-01 17:55:13 +02:00
Irmen de Jong 19238cc366 IR: introduce strict and broad status flags contract for cpu targets
fix cmpi for words and longs
2026-07-01 01:38:35 +02:00
Irmen de Jong e3d4401d99 fix callfar 2026-07-01 00:02:26 +02:00
Irmen de Jong 5da4efc2a3 shorten long operations by using a Y-loop 2026-06-30 22:47:50 +02:00
Irmen de Jong c0c44156f1 new codegen: implement roxl/roxr longs, float casting, callfar 2026-06-30 22:18:41 +02:00
Irmen de Jong 8645833319 new codegen: float support and CALLI 2026-06-30 20:40:34 +02:00
Irmen de Jong 6a867d98eb new codegen: syscall support started 2026-06-30 05:57:19 +02:00
Irmen de Jong 58363ff4e4 Add required support for inlined asmsub routines in IR and new codegen 2026-06-30 03:17:19 +02:00
Irmen de Jong 2555aa72ca improve codegen argument slot processing (func call arg order) 2026-06-30 02:45:19 +02:00
Irmen de Jong 3b94fd4c4b IR: more consistent BSS handling 2026-06-30 01:14:31 +02:00
Irmen de Jong 95f500c32f fixing codegen 2026-06-29 23:10:55 +02:00
Irmen de Jong 870c2e8827 add name to StExtSubParameter to pass this to the codegen (required for @R0 etc) 2026-06-29 04:43:24 +02:00
Irmen de Jong af2360648f implement codegen mul and div routines, and fix symboloffsets 2026-06-29 04:43:24 +02:00
Irmen de Jong bb95d40d6d asm syntax 2026-06-29 04:43:24 +02:00
Irmen de Jong 55ea599fca Remove hardcoded external KERNAL entry points in codegen 2026-06-29 04:43:24 +02:00
Irmen de Jong fd5fdf3d13 IR codegen: Emit extsub as inline assembly equate for codegen references 2026-06-29 04:43:24 +02:00
Irmen de Jong ca129edce4 adding new 6502 codegen 2026-06-29 04:43:23 +02:00
Irmen de Jong c1694a4b08 add m68k codegen skeleton 2026-06-29 04:43:23 +02:00
Irmen de Jong a348bbbebe adding new 6502 codegen based on IR files 2026-06-29 04:43:23 +02:00
Irmen de Jong e6b0a89d64 asm coder skill 2026-06-29 04:43:09 +02:00
Irmen de Jong 9a4821d1e3 extra asm syntax rule in skill 2026-06-28 22:50:43 +02:00
Irmen de Jong 71583a5b93 Move symbol prefixing logic from 6502 codegen to IR generation. 2026-06-28 21:33:10 +02:00
Irmen de Jong 407ba34c72 IR fix certain setmsb/setlsb and rol/ror crashes on indexed pointers 2026-06-27 14:50:45 +02:00
Irmen de Jong fc4782a35b Support &blockname in IR generation 2026-06-27 12:54:36 +02:00
Irmen de Jong 7a83b7db1b Fix dark mode styling for ..data:: headers in documentation 2026-06-27 10:03:15 +02:00
Irmen de Jong a827f4c4ce Support block-level %align in IR 2026-06-27 09:51:08 +02:00
Irmen de Jong 01c5794799 IR: fixing zeropage array initialization 2026-06-27 02:42:55 +02:00
Irmen de Jong 1c093cca2d fix zeropage array initialization for split word arrays 2026-06-27 01:39:38 +02:00
Irmen de Jong a85b267872 streamline asm generation 2026-06-26 20:33:40 +02:00
Irmen de Jong 5e0c1c9794 Optimize parser by introducing SLL prediction mode with LL fallback for improved performance and error handling. 2026-06-25 03:53:18 +02:00
Irmen de Jong 887df44359 fix IR loadhr/storehr parsing from p8ir 2026-06-25 03:30:10 +02:00
Irmen de Jong a5fc162c71 Optimize block sorting and subroutine reordering logic in AST processing steps. 2026-06-25 02:22:44 +02:00
Irmen de Jong 9d3fc99d71 ignores 2026-06-25 01:58:14 +02:00
Irmen de Jong a871713907 some more explanations 2026-06-23 20:57:06 +02:00
Irmen de Jong 2a19e5682a add cx16 dynamicbanking example 2026-06-23 20:45:34 +02:00
Irmen de Jong 22d4fa73d6 rename bankmanager to bankselector 2026-06-23 19:07:31 +02:00
Irmen de Jong 2ed95c52ef tune JSRFAR bank byte setup 2026-06-23 01:01:35 +02:00
Irmen de Jong 73fb04aaf5 Add support for unique call-site IDs in banked subroutines 2026-06-23 00:28:22 +02:00
Irmen de Jong f97a7f426c improved cmp() documentation 2026-06-22 19:53:45 +02:00
Irmen de Jong c1189617f3 fix 64tass repo link 2026-06-22 00:12:55 +02:00
Irmen de Jong 78d43c3cb6 Merge branch 'refs/heads/bugfix12.2.1'
# Conflicts:
#	examples/test.p8
2026-06-20 19:50:42 +02:00
Irmen de Jong 86f9b6aa69 fixed cmp() sign/flags errors 2026-06-20 19:49:45 +02:00
Irmen de Jong 8df2bb8d2e Merge branch 'bugfix12.2.1' 2026-06-20 15:47:41 +02:00
Irmen de Jong 33f30d925e Update documentation with proper example links 2026-06-20 15:45:01 +02:00
Irmen de Jong c8c51245af Merge branch 'bugfix12.2.1' 2026-06-20 15:18:14 +02:00
Irmen de Jong db227679a8 fix empty/full boundary checks on various datastructions in the buffers module 2026-06-20 15:05:37 +02:00
Irmen de Jong 02110de844 optimized floats minf, maxf, clampf routines 2026-06-20 13:56:17 +02:00
Irmen de Jong 7d77a0eca6 Merge branch 'bugfix12.2.1'
# Conflicts:
#	compiler/test/TestCompilerOnExamples.kt
2026-06-19 23:37:49 +02:00
Irmen de Jong 0f3475128a fix divmod_uw_asm to not clobber R15 anymore 2026-06-19 23:23:53 +02:00
Irmen de Jong e6e4714b02 adjust pet music tempo a little
todo about memory mapping pointers
2026-06-19 19:59:54 +02:00
Irmen de Jong d83ece397d add configurable default launcher types for custom compilation targets 2026-06-19 18:58:05 +02:00
Irmen de Jong a3c768bc6b add floats.mod() 2026-06-19 18:35:09 +02:00
Irmen de Jong a3d14003cb Enhanced inlining: support subroutines with one simple parameter. NOTE: risky optimization, maybe still broken in obscure cases
also improve parent pointer relinking, fix namespace scoping, and refine assembly checks.
2026-06-19 04:57:54 +02:00
Irmen de Jong d451953e40 improve prog8 skill 2026-06-19 02:18:41 +02:00
Irmen de Jong d505c42d15 improve undefined symbol error handling
(cherry picked from commit 8c92663824)
2026-06-19 02:16:11 +02:00
Irmen de Jong 8c92663824 improve undefined symbol error handling 2026-06-19 02:13:03 +02:00
Irmen de Jong 10d86eb4d8 Merge remote-tracking branch 'origin/master' 2026-06-19 00:45:32 +02:00
markjreed ee5e734f23 feat: add symbols for enharmonic flats (#226) 2026-06-19 00:44:50 +02:00
Irmen de Jong 6b233b7da3 pet: add sys.set_irq() and sys.restore_irq() support routines 2026-06-19 00:34:53 +02:00
Irmen de Jong a140519374 fix pet music example to use higher octave to be able to play correct B section end note 2026-06-18 23:57:38 +02:00
Irmen de Jong 2b118f2e42 pet32: add petsnd REST constant for silence, and set_gap() for intra note silence gap 2026-06-18 23:13:13 +02:00
Irmen de Jong be9d9fe377 pet32: added new playback functions to petsnd module to play songs (blocking or non-blocking). New 'music' example added for pet. 2026-06-18 21:02:33 +02:00
Irmen de Jong d1345aafec don't limit callfar/banked call support on fixed set of built in targets
but you have to set supports_banked_calls = true in the custom target config file.
2026-06-18 19:09:10 +02:00
Irmen de Jong d5932a7a57 improved bankin support error message 2026-06-17 23:45:30 +02:00
Irmen de Jong 5751beb72f fix some more LSP tests 2026-06-17 20:52:59 +02:00
Irmen de Jong 02194a735a Add supports_banked_calls property to target configuration files 2026-06-17 20:30:21 +02:00
Irmen de Jong b5fc25b2f4 Merge branch 'v12.2.1'
# Conflicts:
#	compiler/test/TestCompilerOnExamples.kt
#	docs/source/_static/symboldumps/skeletons-cx16.txt
2026-06-17 19:41:34 +02:00
Irmen de Jong 5a593b9ef3 Preserve cx16.r15 register in % for words. Update related code and documentation accordingly. 2026-06-17 19:23:19 +02:00
Irmen de Jong b411914624 Document virtual register clobbering details and update related comments. 2026-06-17 03:00:33 +02:00
Irmen de Jong 0f80b695e7 Fix invalid optimizer edge cases 2026-06-17 01:18:39 +02:00
Irmen de Jong 63c425f247 Merge branch 'refs/heads/v12.2.1' 2026-06-16 23:25:18 +02:00
Irmen de Jong 276627abcb remove broken expression optimization that combined a=x, a*=y into a=x*y 2026-06-16 23:19:42 +02:00
Irmen de Jong 34225d10aa add missing division and modulus code generation support for LONG variables 2026-06-16 23:18:07 +02:00
Irmen de Jong 69261c51b3 IR: Add single-bit manipulation opcodes (BITTST, BITSET, BITCLR, BITTOG) 2026-06-16 21:52:31 +02:00
Irmen de Jong 63d7ef7105 IR: Replace 6502-specific hardware register LOAD/STORE opcodes with generic, parameterized LOADHR/STOREHR opcodes 2026-06-16 20:59:51 +02:00
Irmen de Jong 88a3e5b23f Merge branch 'v12.2.1'
# Conflicts:
#	compiler/test/TestCompilerOnExamples.kt
#	compiler/test/codegeneration/TestArrayThings.kt
#	examples/test.p8
2026-06-16 00:48:35 +02:00
Irmen de Jong 282c3aa00b fix a return split word array codegen issue where it didn't use lsb suffix 2026-06-16 00:39:51 +02:00
Irmen de Jong 19f3b03ac3 remove problematic inlining for subroutines having 1 or more parameters, to avoid an endless loop 2026-06-16 00:02:05 +02:00
Irmen de Jong 1b5efe1e5d fix datatype size comparison for strings vs pointers 2026-06-15 09:03:13 +02:00
Irmen de Jong 2b5a15a178 limit iteration cycles in AST processing steps to prevent endless loops 2026-06-15 03:35:14 +02:00
Irmen de Jong a9f22d9375 handle library path files earlier during import to mark them as library files 2026-06-14 19:29:44 +02:00
Irmen de Jong f9b3514f2a fixes and tests for embedded arrays in structs 2026-06-14 16:18:23 +02:00
Irmen de Jong be12fb507e arrays in structs 2026-06-14 15:35:30 +02:00
Irmen de Jong b2dcd02ee4 prioritize frequently accessed variables for zeropage allocation based on usage scores 2026-06-14 13:41:31 +02:00
Irmen de Jong 5c23471254 Merge branch 'v12.2.1' 2026-06-13 12:16:14 +02:00
Irmen de Jong 6cb6f37871 exclude private symbols from completion suggestions in the LSP 2026-06-13 12:15:11 +02:00
Irmen de Jong 67e284b680 complexity based argument evaluation order in sub calls to optimize register spill handling 2026-06-13 03:22:15 +02:00
Irmen de Jong 1e5ba48fe0 tweak banked call support checks on compilation targets 2026-06-13 02:01:34 +02:00
Irmen de Jong 8e6a27d7c1 @bank now also allows the name of a subroutine for dynamic / overlay banking 2026-06-12 23:47:16 +02:00
Irmen de Jong d0edc0b484 prevent TCO with defer blocks 2026-06-12 03:05:26 +02:00
Irmen de Jong ccd3d94ec0 prevent TCO with defer blocks 2026-06-12 03:02:58 +02:00
Irmen de Jong 98355d3850 avoid shouldContain asserts on large strings in tests to avoid massive log dumps 2026-06-12 02:48:41 +02:00
Irmen de Jong 0e53113b85 Merge branch 'v12.2.1'
# Conflicts:
#	gradle.properties
2026-06-11 23:12:28 +02:00
Irmen de Jong e9a3ed457e fix optimizer problem that removed var inits for pointer vars in certain cases. Fixes #225 2026-06-11 22:37:51 +02:00
Irmen de Jong 6001da30d8 add comments to float library routines about the Z flag wrapper routines
(cherry picked from commit c55f5e4b50)
2026-06-11 21:50:25 +02:00
Irmen de Jong c55f5e4b50 add comments to float library routines about the Z flag wrapper routines 2026-06-11 21:49:51 +02:00
Irmen de Jong e983c0c2b9 fixed the c64 float heisenbug
(cherry picked from commit 532bf13057)
2026-06-11 21:40:42 +02:00
Irmen de Jong 532bf13057 fixed the c64 float heisenbug 2026-06-11 21:29:06 +02:00
Irmen de Jong de706e79de add friendly error for invalid 2D array syntax 2026-06-10 04:14:32 +02:00
Irmen de Jong a19f337ebe update prog8-coder skill 2026-06-10 02:49:48 +02:00
Irmen de Jong 041a904e44 fix serialdownload size test 2026-06-09 19:24:02 +02:00
Irmen de Jong 131134d24b fix daemon output path 2026-06-09 02:06:07 +02:00
Irmen de Jong 97fb5298a8 fix daemon output path 2026-06-09 01:59:06 +02:00
Irmen de Jong b580a177d4 serial module now has configurable baud rate, and some uart tweaks 2026-06-09 01:46:46 +02:00
Irmen de Jong a0b1ca5d9f Merge branch 'next-version-12.3' 2026-06-08 21:35:53 +02:00
Irmen de Jong 3a291c56b4 ci: split build step into compile, test, and package for better granularity 2026-06-08 21:07:25 +02:00
Irmen de Jong ed41d5dbc5 update libs and Kotlin
remove docs and examples imls
2026-06-08 20:58:27 +02:00
Irmen de Jong 628bd41740 update kotest 2026-06-08 20:43:54 +02:00
Irmen de Jong de988f72c6 switch from IDEA iml to Gradle build config, this finally fixes Kotest integration as well 2026-06-08 20:43:54 +02:00
Irmen de Jong 869c5ef544 move -srcdirs directories in front of everything else.
add -traceimports option to trace module imports.
2026-06-08 20:35:39 +02:00
Irmen de Jong 17ee233e8b tweak import path search order and document it 2026-06-08 20:34:17 +02:00
Irmen de Jong f2d1dffbe0 preparing for 12.2 release 2026-06-08 20:15:19 +02:00
Irmen de Jong 4008280a62 fix 64tass output order in daemon 2026-06-07 02:45:52 +02:00
Irmen de Jong 2e7235ec02 fix daemon mode error reporting paths 2026-06-07 02:31:06 +02:00
Irmen de Jong a4330f5048 fix -emu program name problem with -daemon 2026-06-07 00:34:35 +02:00
Irmen de Jong f65aeff231 -emu works again together with -daemon 2026-06-07 00:08:07 +02:00
Irmen de Jong 505243aff7 fix a optimizer crash 2026-06-06 23:29:16 +02:00
Irmen de Jong d0f2fc13ba add dummy rambank() and rombank() support for virtual target 2026-06-06 23:26:47 +02:00
Irmen de Jong 53d51dcd38 improve -libsearch by adding fuzzy search fallback 2026-06-06 19:28:53 +02:00
Irmen de Jong c1aa0e49fe fix a possible vera registers clobbering issue when using psg2 from irq handler 2026-06-06 17:30:08 +02:00
Irmen de Jong e1e72d4317 fix crash when using const ubyte pointer as string argument 2026-06-06 17:12:15 +02:00
Irmen de Jong feec343cbc made -daemon more robust 2026-06-06 12:41:15 +02:00
Irmen de Jong 5cdc4233f3 ideas & update prog8-coder skill 2026-06-04 01:16:41 +02:00
Irmen de Jong 1ae89d18cc prevent string deduplication for struct initializers 2026-06-03 01:43:50 +02:00
Irmen de Jong eb19d00a1e prevent faulty inlining of subs with register-aliased parameters 2026-06-02 23:14:55 +02:00
Irmen de Jong e01ee10ef0 fix output binary path displayed to the user 2026-06-02 21:05:03 +02:00
Irmen de Jong f4b3cb12e1 history 2026-06-01 23:19:31 +02:00
Irmen de Jong 77ced60ad6 fix psg2 release timing to make it linear just as attack.
clarify envelope timing details in docs.
2026-06-01 22:58:42 +02:00
Irmen de Jong 6fad90af6e long for loop warning 2026-05-31 23:42:58 +02:00
Irmen de Jong 3076318d7e implement missing for loops on long rages for 6502 2026-05-31 23:38:35 +02:00
Irmen de Jong 9c99110738 doc 2026-05-31 23:07:16 +02:00
Irmen de Jong 1457d36997 beta3 2026-05-31 17:47:37 +02:00
Irmen de Jong 43fb9296dd clarify serial module docs 2026-05-31 17:46:33 +02:00
Irmen de Jong 8bf744cc4a fix some bitwise expression optimization bugs 2026-05-31 07:35:32 +02:00
Irmen de Jong f1f5f070cf fix invalid asm for some in-place long operations on zp vars 2026-05-31 05:14:33 +02:00
Irmen de Jong 191a240d0d adjust extsub jump table offsets to more closely match reality 2026-05-30 23:42:52 +02:00
Irmen de Jong cafb2e355b implement long loop counter support 2026-05-30 23:36:08 +02:00
Irmen de Jong c99c5751f8 tweak serial 2026-05-30 21:11:19 +02:00
Irmen de Jong 435b7e56fa IR VM refactor to reduce class size 2026-05-29 23:14:44 +02:00
Irmen de Jong 14f13fac53 IR tweaks 2026-05-29 22:17:56 +02:00
Irmen de Jong f8b5037924 spelling corrections 2026-05-29 20:58:06 +02:00
Irmen de Jong d2870cb02e implement hex mode file transfer in serial module
as an attempt to have a working file transfer because the binary mode is still failing right now
2026-05-29 20:04:40 +02:00
Irmen de Jong 8e43fc426c IR: handle extsub with banked addresses properly 2026-05-28 03:25:04 +02:00
Irmen de Jong ba1b6e127a IR: properly consume registers for void targets 2026-05-28 00:57:00 +02:00
Irmen de Jong 4f36a6958c fix a few if-expression typecast issues 2026-05-28 00:55:15 +02:00
Irmen de Jong df8f08b4ab more serial tweaks to attempt to fix binary downloads 2026-05-27 23:42:55 +02:00
Irmen de Jong fd5746d3fc tweak the serial module a bit for more robustness 2026-05-27 02:22:35 +02:00
Irmen de Jong 6ab74fd51d don't erroneously allow taking the address of const values 2026-05-27 00:58:01 +02:00
Irmen de Jong 245082ca1d plain console output in github action builds 2026-05-24 12:40:43 +02:00
Irmen de Jong 845e58ea10 64tass install step 2026-05-24 12:33:47 +02:00
Irmen de Jong 2a5dbc1ef7 update project urls that have been moved from github to codeberg 2026-05-24 12:29:09 +02:00
Irmen de Jong 02a1deb1e0 update github build actions 2026-05-23 00:13:14 +02:00
Irmen de Jong 3ba8e5de10 Add serial download example for CX16 and fix serial module import 2026-05-23 00:05:40 +02:00
Irmen de Jong 819d1b4af7 Add serial library for CX16 with UART and ZiModem support
version 12.2-BETA2
2026-05-22 23:52:58 +02:00
Irmen de Jong cdd3d37376 update asmsub inlining behavior and tests: improve support for parameterized calls 2026-05-22 04:17:43 +02:00
Irmen de Jong d0bc01d618 improve inliner: support slightly more complex return expressions 2026-05-22 03:15:57 +02:00
Irmen de Jong ffab73334a enforce errors for empty enum and struct declarations, add corresponding tests 2026-05-22 02:39:09 +02:00
Irmen de Jong 83aeb8846c add support for private keyword for structs, enums, aliases, variables, and subroutines 2026-05-22 02:27:38 +02:00
Irmen de Jong 3a2087310b background daemon compiler mode via -daemon option 2026-05-21 19:08:29 +02:00
Irmen de Jong 61b616701e fix -watch mode to not use stale cached files on subsequent runs 2026-05-20 22:54:52 +02:00
Irmen de Jong bddf872906 better romable awareness in IR 2026-05-20 19:54:12 +02:00
Irmen de Jong 48214a6fdb more IR peephole optimizations, added some implementation plans to the todo 2026-05-19 00:26:35 +02:00
Irmen de Jong b7340856df fix long expression evaluation bug clobbering R14R15 intermediary results 2026-05-18 20:07:23 +02:00
Irmen de Jong 2685854113 memory("name") with only 1 arg now allowed to refer to the actual memory reservation call elsewhere (with all three name, size, align args). 2026-05-16 23:24:41 +02:00
Irmen de Jong ad92f39469 fix error message for conflicting memory() calls 2026-05-16 23:01:49 +02:00
Irmen de Jong d3fe041e01 update kotlin and ksim65 2026-05-16 15:33:03 +02:00
Irmen de Jong f32bac5004 implement 6502 long division routine 2026-05-16 15:04:09 +02:00
Irmen de Jong 9a8e4ac105 lineclip.inside() at top level to check a single point 2026-05-15 21:53:13 +02:00
Irmen de Jong 78857e5c22 pet32: diskio.list_filenames() now checks buffer size 2026-05-15 19:38:45 +02:00
Irmen de Jong 1bb828875d improved vm waitvsync 2026-05-15 18:05:01 +02:00
Irmen de Jong 82aa67ffda lineclip module added 2026-05-15 17:45:13 +02:00
Irmen de Jong 7c44835772 const folding is always done even when -noopt is used 2026-05-15 12:22:52 +02:00
Irmen de Jong 8b9c63fe90 lineclip 2026-05-15 00:31:27 +02:00
Irmen de Jong e77fcc8c16 fix an unsigned long problem 2026-05-10 21:29:42 +02:00
Irmen de Jong 08c12131d4 tweak the rules for turning pointers into consts 2026-05-10 19:51:06 +02:00
334 changed files with 16997 additions and 7462 deletions

No files matched your search

+143 -18
View File
@@ -1,23 +1,29 @@
---
name: asm6502-coder
description: Write 6502/65C02 assembly code using 64tass syntax, for use within Prog8 programs
license: MIT
compatibility: opencode
description: Writing or understanding 6502/65C02 assembly code using 64tass syntax, for use as stand alone .asm or inline within Prog8 programs
---
# 65(C)02 Assembly Coder Skill
You are writing **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.
## Debugging Generated Assembly
- The prog8 compiler outputs `*.asm` (assembly source) and (with -asmlist option) also `*.list` (full listing with address/symbols) when compiling. Inspect these to debug generated code, verify optimizations, and trace instruction sequences.
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.
## 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`).
- **NOT ca65/cc65** or other assemblers. Key differences:
- `.proc` / `.pend` for procedures (scoping)
- `_label` for local labels (prefixed with underscore, scoped to `.proc`)
- **Anonymous labels**: defined as `+` (forward) or `-` (backward) at the start of a line. Reference them in branches using `+`, `++`, `+++` etc. (first/second/third *upcoming* anonymous forward label) and `-`, `--`, `---` etc. (first/second/third *preceding* anonymous backward label).
- Crucial: `+` refers to the NEXT upcoming `+` label, `++` refers to the ONE AFTER that, etc.
- **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:
@@ -28,27 +34,51 @@ You are writing **6502/65C02 assembly** using **64tass syntax**, in separate `*.
+ dex ; forward label '+'
bne + ; branch to this same '+' (forward)
sta $400
+ lda #0 ; second '+'
bne ++ ; branch to the '++' below
+ lda #0 ; second '+' label definition (just '+')
bne ++ ; branch to the '++' below (third forward '+')
rts
++ inc $d020 ; third forward label
+ inc $d020 ; third forward label definition (just '+', referred to as '++')
```
- Data directives: `.byte`, `.word`, `.dword`
- 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`)
## Instructions
- **Instructions like `rol`, `ror`, `asl`, `lsr`, `php`, `pla` require an explicit operand for accumulator**: write `rol a`, `ror a`, not just `rol`/`ror`
- **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
### 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.
## 6502 vs 65C02
- **CX16 target only**: can use 65C02 instructions — `stz`, `phx`, `plx`, `phy`, `ply`, `bra`, `trb`, `tsb`, `stp`, `wai`, `clr`, `ina`, `dea`, `cmp (zp)`, `dec`/`inc abs,x` etc.
- **C64, C128, PET32 targets**: original 6502 only — no `stz`, no `phx`/`plx`/`phy`/`ply`, no `bra`
- **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).
## Commander X16 Memory Map
```
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
@@ -70,8 +100,9 @@ You are writing **6502/65C02 assembly** using **64tass syntax**, in separate `*.
- 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
- **Parameter names are documentation only** — use the actual registers in assembly, NOT parameter names
- **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).
Example:
```prog8
@@ -126,15 +157,38 @@ All Prog8 symbols are prefixed when accessed from assembly:
- 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
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)
@@ -164,3 +218,74 @@ loop ; do work here
lda p8v_myword ; loads LSB
ldy p8v_myword+1 ; loads MSB (word variables are stored LSB-first)
```
## CPU Quirks and Pitfalls
### 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.
### BRK Instruction and the "Signature Byte"
- **Behavior**: After a `BRK` instruction, the return address on the stack is incremented by **2**. This means the CPU skips the byte immediately following the `BRK` opcode.
- **Usage**: This skipped byte is often used as a "signature" or parameter byte for the BRK handler.
### BIT Instruction Flags
- **Absolute/Zero-page**: `bit $1234` copies bit 7 of the memory value to the **N** flag and bit 6 to the **V** flag. The **Z** flag is set based on `A AND memory`.
- **Immediate (65C02 only)**: `bit #$01` only affects the **Z** flag; it does **NOT** modify N or V.
- **NMOS 6502**: Does **not** support `bit #imm`.
### The "B" (Break) Flag
- **Quirk**: The B flag (bit 4 of the status register) doesn't actually exist in the hardware status register. It only exists on the stack after a `PHP` or `BRK` instruction (set to 1) or a hardware IRQ/NMI (set to 0).
- **Detection**: To tell if an interrupt was caused by `BRK` or a hardware IRQ, your handler must `pla`, `and #$10`, and check the result.
### Decimal Mode Flag (D) Persistence
- **Pitfall**: On NMOS 6502, the `D` flag is **not** cleared on interrupt. Always use `cld` in IRQ handlers. On 65C02, it is cleared automatically, but `cld` is still good practice.
- **ADC/SBC**: Be extremely careful with arithmetic if you haven't explicitly set or cleared the `D` flag, as its state might be unknown.
## Optimization Tips
- **`stz` (65C02 only)**: Saves cycles and bytes compared to `lda #0`, `sta ...`.
- **`bra` (65C02 only)**: Shorter and usually faster than `jmp`.
- **Avoid `clc` before `bcc`**: `cmp` already sets the carry flag correctly for `bcc`/`bcs`.
- **`inx` / `dex` vs `clc`+`adc #1`**: Incrementing/decrementing is faster and doesn't affect the carry flag.
- **Zero-page usage**: Accessing variables in zeropage is 1 cycle faster and 1 byte shorter than absolute addressing.
- **Cycle Counting**: Most instructions take 2-4 cycles. `jsr` takes 6, `rts` takes 6. Branches take 2 (no branch), 3 (branch taken), or 4 (branch taken across page boundary).
## 64tass Macros
Define macros for common tasks:
```asm
pushax .macro
pha
txa
pha
.endm
popax .macro
pla
tax
pla
.endm
```
Call them with `#pushax` and `#popax`.
## IRQ Handler Best Practices
- **Clear the Decimal Flag (`cld`)**: On the original 6502, the decimal flag (`D`) is **not** automatically cleared when an interrupt occurs. If the interrupted code was in decimal mode, your handler will also run in decimal mode, causing arithmetic errors. Always call `cld` at the beginning of your handler. The 65C02 clears it automatically, but `cld` is still recommended for portability.
## Invoking the Assembler (64tass)
If you need to manually invoke `64tass` to assemble a generated `.asm` file, you should be aware of the default arguments that `prog8c` supplies to ensure compatibility with the generated code:
- `--ascii`: **CRITICAL.** Prog8 generates character and string data in ASCII. Without this flag, `64tass` defaults to PETSCII, which will garble your strings.
- `--case-sensitive`: Prog8 is case-sensitive and expects the assembler to be as well.
- `--long-branch`: Enables automatic conversion of relative branches (`beq`, `bne`, etc.) to absolute jumps if the target is out of range. Prog8 relies on this.
- `-Wno-implied-reg`: Suppresses warnings when the accumulator `a` is omitted from instructions like `rol`, `lsr`, etc. (though the skill recommends always using `rol a` for clarity).
- `-Wall`: Enables all warnings.
- `--cbm-prg` (or `--atari-xex` / `--nostart`): Sets the output format and adds the appropriate load address header.
### Optional but Recommended for Debugging:
- `--vice-labels --labels=labels.txt`: Generates a label file that can be loaded into the VICE monitor (`load_labels "labels.txt"`) to see your Prog8 symbol names while debugging.
- `--list=listing.txt`: Generates a full assembly listing file with addresses and opcodes.
### Example manual invocation:
```bash
64tass --ascii --case-sensitive --long-branch -Wall --cbm-prg -o myprogram.prg myprogram.asm
```
+76 -49
View File
@@ -1,30 +1,23 @@
---
name: prog8-coder
description: Write Prog8 programs (6502-targeted language with 8-bit retro systems as targets)
license: MIT
compatibility: opencode
description: Writing or understanding Prog8 programs
---
# Prog8 Coder Skill
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.
## 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`)
- **Virtual target for testing**: Use `prog8c -target virtual -emu input.p8` or `prog8c -vm input.p8ir` for fast testing
- **Fast syntax check**: `prog8c -check input.p8` — does NOT produce output files (.prg, .asm, etc.), just errors
- **Output directory**: `prog8c -out outdir input.p8` (default: same dir as source)
- **Compilation outputs**: `*.prg` (program binary), `*.asm` (assembly listing), `*.list` (full listing), `*.p8ir` (IR for VM), `*.vice-mon-list` (Vice debug symbols)
- `.p8ir` files contain the **Intermediate Representation** — a sequence of named chunks, each containing typed instructions and virtual registers. This is a target-independent representation of the program, executable by the built-in Virtual Machine via `prog8c -vm file.p8ir`. Useful for debugging the compiler's code generation path without involving 6502 assembly.
- **Debugging switches**: `-noopt` (disable optimizations), `-printast1` (parsed AST), `-printast2` (optimized simple AST), `-compareir` (compare IR outputs)
- **TODO list**: Check `docs/source/todo.rst` to see what features are NOT yet implemented
- **Test programs**: add `%zeropage basicsafe` and `%option no_sysinit` at top
- `.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 debugging**: Add `%encoding iso`, call `txt.iso()` in `start()`, end with `sys.poweroff_system()`. For emulator: `x16emu -echo iso -run -prg input.prg 2>&1 | grep ...`
- **CX16 programs**: add `%encoding iso`, call `txt.iso()` in `start()`, end with `sys.poweroff_system()`
## Datatypes & Variables
- Primitives: `bool`, `byte`, `ubyte`, `word`, `uword`, `long`, `float`, `str`
@@ -32,17 +25,31 @@ Follow ALL the rules below carefully.
- **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
- Struct initialization: `^^StructType ptr = ^^StructType:[val1,val2,...]` (the `^^StructType:` can be omitted if inferable)
- Struct fields: only simple types + `str` allowed. NO arrays as fields. `str` in struct = `^^ubyte`
- 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` marks variables as "used by external code" (assembly), prevents optimization
- `@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. R12-R15 may be clobbered by long ops. Save/restore in IRQs with `cx16.save_virtual_registers()`/`cx16.restore_virtual_registers()`
- 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
## Recursion & Stack Management
@@ -62,6 +69,7 @@ No call stack for variable storage — recursion overwrites locals. To handle it
- 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
@@ -74,12 +82,24 @@ No call stack for variable storage — recursion overwrites locals. To handle it
- 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)
- Use `when` with choice blocks instead of multiple `if`
- If-expressions for simple value assignments based on a choice
- `defer` defers statement execution until scope exit
- **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!
### 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.
## Loop Constructs
Prog8 supports these loop types. All support `break` and `continue` (except `unroll`).
@@ -157,43 +177,53 @@ unroll 80 {
```
## Subroutines & Return Values
- Symbols public by default; use `private` to restrict to block
- **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). Cannot use builtin names (msw, lsw, msb, lsb, mkword, mklong, peek, peekw, peekl, etc.) as variable/sub names
- 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
## Assembly Subroutines (asmsub)
For kernel (ROM) routines or low-level assembly that gets arguments via registers.
- Parameter passing via registers: `@A` (accu), `@X`, `@Y`, `@AX`/`@AY` (16-bit), `@R0`-`@R15` (virtual regs), `@FAC1`/`@FAC2` (float), `@Pc` (carry), `@Pz` (zero)
- Return value: `-> type @register` (e.g., `-> ubyte @A`, `-> bool @Pz` for immediate branch use)
- Clobbers: `clobbers (A, X, Y)` — list all modified registers
- Parameter names are documentation only — use registers in assembly, not parameter names
- Create symbolic aliases at assembly start: `x1 = cx16.r0`, `x1_lo = cx16.r0L`
- Example:
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.
## 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
asmsub line(uword x1 @R0, ubyte y1 @A, uword x2 @R1, ubyte y2 @Y) clobbers (A, X, Y) {
%asm {{
x1 = cx16.r0
x2 = cx16.r1
lda x1
}}
}
; CX16 KERNAL CHROUT
extsub $FFD2 = chrout(ubyte char @A) clobbers(A, X, Y)
; Routine in a specific RAM bank
extsub @bank 10 $C09F = audio_init() clobbers(A, X, Y) -> bool @Pc
```
- Symbol prefixes in assembly: `p8v_` (variables), `p8s_` (subroutines), `p8b_` (blocks), `p8c_` (constants), `p8l_` (labels), `p8t_` (structs), `p8_` (other)
- Fully qualified: `p8b_blockname.p8v_varname`, `p8b_blockname.p8s_subname.p8v_localvar`
- Within a `.proc`, short names often work. `%option no_symbol_prefixing` disables prefixes (used by `cbm`, `cx16`, `txt`)
- Split word arrays: append `_lsb` / `_msb` to variable name (e.g., `p8v_myarray_lsb`)
- CX16 target only: use 65C02 instructions (STZ etc). Others: 6502 only
- Assembly syntax: 64tass assembler. `.proc`/`.pend`, `_label` for locals, `.byte`/`.word`/`.dword` for data, `=` for equates
- Instructions like `rol`, `ror`, `asl`, `lsr` require explicit operand: `rol a`, not just `rol`
- Anonymous labels: `+` (forward), `-` (backward), branch with `+`, `++`, `+++` or `-`, `--`, `---`
## 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.
## 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()`)
- 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
@@ -207,15 +237,12 @@ For kernel (ROM) routines or low-level assembly that gets arguments via register
- **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 block scope**: `for`/`if` don't introduce scope. Only subroutines do. Variables in blocks are hoisted to subroutine level
- **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. Use enums for related values, `const` for standalone
- 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)
- **No block scope** — variables in `if`/`for`/`repeat` blocks are hoisted to subroutine level
- **No reassigning strings** after declaration — use `strings.copy()`/`strings.ncopy()`
- **Pre-allocate string buffers** — `""` allocates nothing
+2
View File
@@ -13,5 +13,7 @@ dist/
build/
out/
output/
gradle/
.gradle/
.kotlin/
.git/
+26 -12
View File
@@ -10,32 +10,46 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: build and install recent 64tass
- name: install 64tass
run: |
sudo apt-get install -y make build-essential
git clone --depth=1 https://github.com/irmen/64tass
cd 64tass
make -j4
sudo make install
sudo apt update
sudo apt install 64tass
64tass --version
- name: Set up JDK 17
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
java-version: 17
distribution: temurin
- name: Build and test with Gradle
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
- name: Compile
run: ./gradlew compileKotlin compileTestKotlin --console=plain
- name: Test
run: ./gradlew test --continue --console=plain
- name: Package
run: |
./gradlew build shadowJar --no-daemon
./gradlew shadowJar --console=plain
sha256sum -b compiler/build/libs/*-all.jar > compiler/build/libs/hash.txt
- name: Create compiler shadowJar artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: prog8-compiler-jar-zipped
path: |
compiler/build/libs/*-all.jar
compiler/build/libs/hash.txt
- name: Upload test results
if: always()
uses: actions/upload-artifact@v6
with:
name: test-results
path: compiler/build/test-results/test/
+2 -1
View File
@@ -5,6 +5,7 @@
.idea/shelf/
.junie/
.junie/memory/
.serena
build/
dist/
output/
@@ -16,6 +17,7 @@ out-old/
*.prg
*.bin
*.p8ir
*.bankedcalls
*.labels.txt
*.vm.txt
*.vice-mon-list
@@ -38,7 +40,6 @@ parsetab.py
.pytest_cache/
.attach_pid*
compiler/lib/
.gradle
sd*.img
*.d64
+11 -1
View File
@@ -2,6 +2,16 @@
<project version="4">
<component name="CompilerConfiguration">
<option name="BUILD_PROCESS_HEAP_SIZE" value="3000" />
<bytecodeTargetLevel target="11" />
<bytecodeTargetLevel target="11">
<module name="prog8.codeGenExperimental" target="17" />
<module name="prog8.codeGenExperimental.main" target="17" />
<module name="prog8.codeGenExperimental.test" target="17" />
<module name="prog8.codeGenM68k" target="17" />
<module name="prog8.codeGenM68k.main" target="17" />
<module name="prog8.codeGenM68k.test" target="17" />
<module name="prog8.codeGenNew6502" target="17" />
<module name="prog8.codeGenNew6502.main" target="17" />
<module name="prog8.codeGenNew6502.test" target="17" />
</bytecodeTargetLevel>
</component>
</project>
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="LOCAL" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/codeCore" />
<option value="$PROJECT_DIR$/codeGenCpu6502" />
<option value="$PROJECT_DIR$/codeGenIntermediate" />
<option value="$PROJECT_DIR$/codeOptimizers" />
<option value="$PROJECT_DIR$/compiler" />
<option value="$PROJECT_DIR$/compilerAst" />
<option value="$PROJECT_DIR$/intermediate" />
<option value="$PROJECT_DIR$/languageServer" />
<option value="$PROJECT_DIR$/parser" />
<option value="$PROJECT_DIR$/simpleAst" />
<option value="$PROJECT_DIR$/virtualmachine" />
</set>
</option>
<option name="myGradleHome" value="/usr/share/java/gradle" />
</GradleProjectSettings>
</option>
</component>
</project>
+5 -4
View File
@@ -7,13 +7,14 @@
<option name="jvmTarget" value="11" />
</component>
<component name="KotlinCommonCompilerArguments">
<option name="apiVersion" value="2.3" />
<option name="languageVersion" value="2.3" />
<option name="apiVersion" value="2.4" />
<option name="languageVersion" value="2.4" />
</component>
<component name="KotlinCompilerSettings">
<option name="additionalArguments" value="-jvm-default=no-compatibility" />
</component>
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.3.20" />
<option name="externalSystemId" value="Gradle" />
<option name="version" value="2.4.10" />
</component>
</project>
</project>
-23
View File
@@ -1,23 +0,0 @@
<component name="libraryTable">
<library name="KotlinJavaRuntime" type="repository">
<properties maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.3.20" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.20/kotlin-stdlib-jdk8-2.3.20.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.20/kotlin-stdlib-2.3.20.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.3.20/kotlin-stdlib-jdk7-2.3.20.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.20/kotlin-stdlib-jdk8-2.3.20-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.20/kotlin-stdlib-2.3.20-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.3.20/kotlin-stdlib-jdk7-2.3.20-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.20/kotlin-stdlib-jdk8-2.3.20-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.20/kotlin-stdlib-2.3.20-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.3.20/kotlin-stdlib-jdk7-2.3.20-sources.jar!/" />
</SOURCES>
</library>
</component>
-18
View File
@@ -1,18 +0,0 @@
<component name="libraryTable">
<library name="antlr.antlr4" type="repository">
<properties maven-id="org.antlr:antlr4:4.13.2">
<exclude>
<dependency maven-id="com.ibm.icu:icu4j" />
</exclude>
</properties>
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/antlr/antlr4/4.13.2/antlr4-4.13.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/antlr/antlr4-runtime/4.13.2/antlr4-runtime-4.13.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/antlr/antlr-runtime/3.5.3/antlr-runtime-3.5.3.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/antlr/ST4/4.3.4/ST4-4.3.4.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/abego/treelayout/org.abego.treelayout.core/1.0.3/org.abego.treelayout.core-1.0.3.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>
-13
View File
@@ -1,13 +0,0 @@
<component name="libraryTable">
<library name="eclipse.lsp4j" type="repository">
<properties maven-id="org.eclipse.lsp4j:org.eclipse.lsp4j:1.0.0" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/lsp4j/org.eclipse.lsp4j/1.0.0/org.eclipse.lsp4j-1.0.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/lsp4j/org.eclipse.lsp4j.jsonrpc/1.0.0/org.eclipse.lsp4j.jsonrpc-1.0.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/com/google/code/gson/gson/2.14.0/gson-2.14.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.48.0/error_prone_annotations-2.48.0.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>
-14
View File
@@ -1,14 +0,0 @@
<component name="libraryTable">
<library name="github.irmen.ksim65" type="repository">
<properties maven-id="com.github.irmen:ksim65:v2.0" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/github/irmen/ksim65/v2.0/ksim65-v2.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.20/kotlin-stdlib-2.3.20.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.20/kotlin-stdlib-jdk8-2.3.20.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.3.20/kotlin-stdlib-jdk7-2.3.20.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>
-23
View File
@@ -1,23 +0,0 @@
<component name="libraryTable">
<library name="io.kotest.framework.datatest" type="repository">
<properties maven-id="io.kotest:kotest-framework-datatest:5.9.1" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/io/kotest/kotest-framework-datatest/5.9.1/kotest-framework-datatest-5.9.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/kotest/kotest-framework-datatest-jvm/5.9.1/kotest-framework-datatest-jvm-5.9.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.9.23/kotlin-stdlib-1.9.23.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.8.0/kotlinx-coroutines-core-jvm-1.8.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/kotest/kotest-common-jvm/5.9.1/kotest-common-jvm-5.9.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-reflect/1.9.23/kotlin-reflect-1.9.23.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/kotest/kotest-framework-api-jvm/5.9.1/kotest-framework-api-jvm-5.9.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/kotest/kotest-assertions-shared-jvm/5.9.1/kotest-assertions-shared-jvm-5.9.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/kotest/kotest-assertions-api-jvm/5.9.1/kotest-assertions-api-jvm-5.9.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-coroutines-jdk8/1.8.0/kotlinx-coroutines-jdk8-1.8.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/github/java-diff-utils/java-diff-utils/4.12/java-diff-utils-4.12.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-coroutines-test-jvm/1.8.0/kotlinx-coroutines-test-jvm-1.8.0.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>
-41
View File
@@ -1,41 +0,0 @@
<component name="libraryTable">
<library name="io.kotest.runner.junit5.jvm" type="repository">
<properties maven-id="io.kotest:kotest-runner-junit5-jvm:5.9.1" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/io/kotest/kotest-runner-junit5-jvm/5.9.1/kotest-runner-junit5-jvm-5.9.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/kotest/kotest-framework-api-jvm/5.9.1/kotest-framework-api-jvm-5.9.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/kotest/kotest-assertions-shared-jvm/5.9.1/kotest-assertions-shared-jvm-5.9.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/github/java-diff-utils/java-diff-utils/4.12/java-diff-utils-4.12.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-coroutines-test-jvm/1.8.0/kotlinx-coroutines-test-jvm-1.8.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/kotest/kotest-common-jvm/5.9.1/kotest-common-jvm-5.9.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/kotest/kotest-framework-engine-jvm/5.9.1/kotest-framework-engine-jvm-5.9.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/github/classgraph/classgraph/4.8.172/classgraph-4.8.172.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/com/github/ajalt/mordant/1.2.1/mordant-1.2.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/com/github/ajalt/colormath/1.2.0/colormath-1.2.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-coroutines-debug/1.8.0/kotlinx-coroutines-debug-1.8.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/net/java/dev/jna/jna/5.9.0/jna-5.9.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/net/java/dev/jna/jna-platform/5.9.0/jna-platform-5.9.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/net/bytebuddy/byte-buddy/1.10.9/byte-buddy-1.10.9.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/net/bytebuddy/byte-buddy-agent/1.10.9/byte-buddy-agent-1.10.9.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/kotest/kotest-framework-discovery-jvm/5.9.1/kotest-framework-discovery-jvm-5.9.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/kotest/kotest-assertions-core-jvm/5.9.1/kotest-assertions-core-jvm-5.9.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-coroutines-jdk8/1.8.0/kotlinx-coroutines-jdk8-1.8.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/kotest/kotest-assertions-api-jvm/5.9.1/kotest-assertions-api-jvm-5.9.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/kotest/kotest-extensions-jvm/5.9.1/kotest-extensions-jvm-5.9.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/kotest/kotest-framework-concurrency-jvm/5.9.1/kotest-framework-concurrency-jvm-5.9.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.8.0/kotlinx-coroutines-core-jvm-1.8.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/23.0.0/annotations-23.0.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/platform/junit-platform-engine/1.8.2/junit-platform-engine-1.8.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/platform/junit-platform-commons/1.8.2/junit-platform-commons-1.8.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/platform/junit-platform-suite-api/1.8.2/junit-platform-suite-api-1.8.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/platform/junit-platform-launcher/1.8.2/junit-platform-launcher-1.8.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter-api/5.8.2/junit-jupiter-api-5.8.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.9.23/kotlin-stdlib-1.9.23.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-reflect/1.9.23/kotlin-reflect-1.9.23.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>
-10
View File
@@ -1,10 +0,0 @@
<component name="libraryTable">
<library name="jetbrains.kotlinx.cli.jvm" type="repository">
<properties include-transitive-deps="false" maven-id="org.jetbrains.kotlinx:kotlinx-cli-jvm:0.3.6" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-cli-jvm/0.3.6/kotlinx-cli-jvm-0.3.6.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>
-20
View File
@@ -1,20 +0,0 @@
<component name="libraryTable">
<library name="michael.bull.kotlin.result.jvm" type="repository">
<properties maven-id="com.michael-bull.kotlin-result:kotlin-result-jvm:2.3.1" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/michael-bull/kotlin-result/kotlin-result-jvm/2.3.1/kotlin-result-jvm-2.3.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.10/kotlin-stdlib-2.3.10.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/com/michael-bull/kotlin-result/kotlin-result-jvm/2.3.1/kotlin-result-jvm-2.3.1-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.10/kotlin-stdlib-2.3.10-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/com/michael-bull/kotlin-result/kotlin-result-jvm/2.3.1/kotlin-result-jvm-2.3.1-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.10/kotlin-stdlib-2.3.10-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-sources.jar!/" />
</SOURCES>
</library>
</component>
+1
View File
@@ -20,6 +20,7 @@
<component name="Black">
<option name="sdkName" value="Python 3.11" />
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<type id="Python" />
</component>
-14
View File
@@ -3,22 +3,8 @@
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/benchmark-program/benchmark-program.iml" filepath="$PROJECT_DIR$/benchmark-program/benchmark-program.iml" />
<module fileurl="file://$PROJECT_DIR$/codeCore/codeCore.iml" filepath="$PROJECT_DIR$/codeCore/codeCore.iml" />
<module fileurl="file://$PROJECT_DIR$/codeGenCpu6502/codeGenCpu6502.iml" filepath="$PROJECT_DIR$/codeGenCpu6502/codeGenCpu6502.iml" />
<module fileurl="file://$PROJECT_DIR$/codeGenExperimental/codeGenExperimental.iml" filepath="$PROJECT_DIR$/codeGenExperimental/codeGenExperimental.iml" />
<module fileurl="file://$PROJECT_DIR$/codeGenIntermediate/codeGenIntermediate.iml" filepath="$PROJECT_DIR$/codeGenIntermediate/codeGenIntermediate.iml" />
<module fileurl="file://$PROJECT_DIR$/codeGenVirtual/codeGenVirtual.iml" filepath="$PROJECT_DIR$/codeGenVirtual/codeGenVirtual.iml" />
<module fileurl="file://$PROJECT_DIR$/codeOptimizers/codeOptimizers.iml" filepath="$PROJECT_DIR$/codeOptimizers/codeOptimizers.iml" />
<module fileurl="file://$PROJECT_DIR$/compiler/compiler.iml" filepath="$PROJECT_DIR$/compiler/compiler.iml" />
<module fileurl="file://$PROJECT_DIR$/compilerAst/compilerAst.iml" filepath="$PROJECT_DIR$/compilerAst/compilerAst.iml" />
<module fileurl="file://$PROJECT_DIR$/docs/docs.iml" filepath="$PROJECT_DIR$/docs/docs.iml" />
<module fileurl="file://$PROJECT_DIR$/examples/examples.iml" filepath="$PROJECT_DIR$/examples/examples.iml" />
<module fileurl="file://$PROJECT_DIR$/intermediate/intermediate.iml" filepath="$PROJECT_DIR$/intermediate/intermediate.iml" />
<module fileurl="file://$PROJECT_DIR$/languageServer/languageServer.iml" filepath="$PROJECT_DIR$/languageServer/languageServer.iml" />
<module fileurl="file://$PROJECT_DIR$/parser/parser.iml" filepath="$PROJECT_DIR$/parser/parser.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/prog8.iml" filepath="$PROJECT_DIR$/.idea/modules/prog8.iml" />
<module fileurl="file://$PROJECT_DIR$/simpleAst/simpleAst.iml" filepath="$PROJECT_DIR$/simpleAst/simpleAst.iml" />
<module fileurl="file://$PROJECT_DIR$/virtualmachine/virtualmachine.iml" filepath="$PROJECT_DIR$/virtualmachine/virtualmachine.iml" />
</modules>
</component>
</project>
Generated
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>
+33 -14
View File
@@ -30,7 +30,7 @@
- The prog8 compiler is written mostly in Kotlin, those files have the .kt extension.
- The standard library is mostly written in Prog8 and assembly code, and can be found in the "compiler" module, in the 'res/prog8lib' directory.
- **Library Reference**: For a quick overview of all available modules, routines, and their signatures, **consult the skeleton files (symboldumps)** in `docs/source/_static/symboldumps/`. They are named `skeletons-<target>.txt`.
- Kotlin version 2.3 is used for the compiler implementation.
- Kotlin version 2.4 is used for the compiler implementation.
- Java 17 is used as Java runtime version.
- 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
@@ -127,6 +127,12 @@ Steps when encountering a compiler crash:
### 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.
**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
val bill = $$"The value is $3.99"
```
## Prog8 language information
When the task involves writing or understanding `.p8` (Prog8 source) or `.p8ir` (Prog8 intermediate representation) files, **load the `prog8-coder` skill** — it contains the full language reference, syntax rules, and standard library guidance.
@@ -153,18 +159,6 @@ When the task involves writing or understanding `.p8` (Prog8 source) or `.p8ir`
- **git log/history queries can be useful** for understanding when/why a feature was added or tracking down when a bug was introduced, but for locating code use grep_search or glob instead.
- Architecture decisions: separation of frontend/parser, IR intermediate representation, multiple backends
## Junie Agent Commitments
These instructions apply specifically to the Junie agent:
* **Code Discovery and Navigation**: Use the `get_file_structure` tool to understand the API and structure of files before reading or editing them.
* **Symbol-Aware Searching**: Use the `search_project` tool as the primary method for finding symbol definitions (classes, methods, variables) across the project.
* **Project-Wide Refactoring**: Exclusively use the `rename_element` tool for any symbol renames to ensure all references, imports, and documentation are updated correctly.
* **Structured Test Execution**: Use the `run_test` tool for rapid, isolated verification of Kotlin logic changes.
* **Contextual Exploration**: Use the `open` tool with specific `line_number` parameters to navigate directly to relevant code sections.
* **Gradle Console Output**: Always use the `--console=plain` flag with `gradle` commands. Fancy console output (like progress bars) will mess up the coding agent's text interface.
**Note on Terminal usage**: Junie uses `gradle` and terminal commands primarily for project lifecycle tasks (e.g., `gradle installdist` to re-embed standard library resources), manual compiler verification with `prog8c`, and full regression testing before submission. When using `gradle`, always append `--console=plain`.
# Dev environment tips
## Development Workflows
@@ -202,9 +196,10 @@ These instructions apply specifically to the Junie agent:
- This compiles AND installs, but still skips running tests (faster than `gradle build`)
### When to run full build with tests
- **Before committing changes**: Always run `gradle build` to ensure all tests pass
- **Before final verification**: Always run `gradle build` to ensure all tests pass
- **After major refactoring**: Run `gradle build` to catch regressions
- **When debugging test failures**: Use `gradle test --tests "*TestName*"` for specific tests
- **When working on a standalone codegen module** (one that reads `.p8ir` files): Run `gradle :<module>:test --console=plain` instead of full `gradle build`. Compiler unit tests are irrelevant for IR-level changes. Do run full `gradle build` before final verification.
### Build command summary
@@ -225,6 +220,7 @@ These instructions apply specifically to the Junie agent:
- **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**.
- **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
- `*.prg` - The final compiled program file for the target system (e.g., Commander X16)
@@ -238,6 +234,10 @@ These instructions apply specifically to the Junie agent:
- `prog8c -target targetname -emu input.p8` - Compile and execute a prog8 file in the emulator for the given target (cx16, c64, pet32, c128, virtual)
- `prog8c -vm input.p8ir` - Execute an existing prog8 program, compiled in IR form, in the Virtual Machine
### Library Tools
- **Library search**: `prog8c -libsearch <regex>` — search for a regex pattern in the embedded stdlib files. Extremely useful to quickly find library routines, variables, or signatures (e.g., `prog8c -libsearch "txt\."` lists all textio routines)
- **Library dump**: `prog8c -libdump <dir>` — extract all embedded library source files into a directory for direct inspection
## Testing and Verification
- Never assume your changes simply work, always test!
@@ -284,6 +284,25 @@ gradle :compiler:compileTestKotlin --info 2>&1 | grep "^e:"
- **When writing test programs**, add at the top: `%zeropage basicsafe` and `%option no_sysinit`
- When a test fails, the output shows "There were failing tests. See the report at:" - **read that HTML report**
### 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)
- **Use `cx16` target** (not `virtual`, unless specifically testing the VM)
- **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
### Testing Assembly Output
When testing generated assembly in Kotlin tests:
- **NEVER dump raw assembly in string assertions** (e.g., `asm shouldContain "jsr label"`). On failure, this dumps the ENTIRE assembly listing into test logs.
- **Instead, parse into lines and check specific properties:**
```kotlin
val lines = asm.lines().map { it.trim() }
lines.any { it == "lda #42" } shouldBe true
lines.count { it.startsWith("jsr") } shouldBe 0
```
- Prefer `lines.any { ... }` for existence checks and `lines.count { ... } shouldBe 0` for absence checks.
### Manual Verification & Emulators
**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.
+2 -2
View File
@@ -12,9 +12,9 @@ endif
all:
$(GRADLE) installdist installshadowdist
@echo "compiler launch script can be found here: compiler/build/install/compiler-shadow/bin/prog8c"
@echo "compiler launch script can be found here: compiler/build/install/prog8c-shadow/bin/prog8c"
test:
$(GRADLE) build
@echo "compiler launch script can be found here: compiler/build/install/compiler-shadow/bin/prog8c"
@echo "compiler launch script can be found here: compiler/build/install/prog8c-shadow/bin/prog8c"
+7 -4
View File
@@ -15,10 +15,12 @@ which aims to provide many conveniences over raw assembly code (even when using
**Want to buy me a coffee or a pizza perhaps?**
This project was created over the last couple of years by dedicating thousands of hours of my free time to it, to make it the best I possibly can.
This project was created over the last couple of years by dedicating a huge amount of my free time to it, to make it the best I possibly can.
If you like Prog8, and think it's worth a nice cup of hot coffee or a delicious pizza,
you can help me out a little bit over at [ko-fi.com/irmen](https://ko-fi.com/irmen) or [PayPal](https://paypal.me/irmendejong)
[![JetBrains logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSource)
I am using JetBrains products to develop the Prog8 compiler.
Documentation
-------------
@@ -63,6 +65,7 @@ What does Prog8 provide?
- programs run very fast because it's compiled to native machine code
- compiled code is very small; much smaller than equivalent C code compiled with CC65, and usually runs faster as well
- modularity, symbol scoping, subroutines. No need for forward declarations.
- Can make vars and routines private, as a simple way to restrict access and hide implementation details
- various data types other than just bytes (16-bit words, long integers, floats, strings)
- Structs and typed pointers
- 2D arrays (`matrix[row][col]` syntax)
@@ -91,7 +94,7 @@ What does Prog8 provide?
*Rapid edit-compile-run-debug cycle:*
- use a modern PC to do the work on, use nice editors and enjoy quick compilation times
- can automatically run the program in the Vice emulator after succesful compilation
- can automatically run the program in the Vice emulator after successful compilation
- breakpoints, that let the Vice emulator drop into the monitor if execution hits them
- source code labels automatically loaded in Vice emulator so it can show them in disassembly
@@ -110,8 +113,8 @@ Additional required tools
-------------------------
[64tass](https://sourceforge.net/projects/tass64/) - cross assembler. Install this on your shell path.
A recent .exe version of this tool for Windows can be obtained from my [clone](https://github.com/irmen/64tass/releases) of this project.
For other platforms it is very easy to compile it yourself (make ; make install).
If you're on Windows, download a recent .exe from the official repository's file section <https://sourceforge.net/projects/tass64/files/binaries/>.
For other systems (Mac, Linux) it is very easy to get it via your package manager or to compile it yourself.
A **Java runtime (jre or jdk), version 11 or newer** is required to run a prepackaged version of the compiler.
If you want to build it from source, you'll need a Java SDK + Kotlin 1.3.x SDK (or for instance,
+1 -2
View File
@@ -61,8 +61,7 @@ life {
math.rndseed(12345,9999)
uword offset = STRIDE+1
ubyte x
ubyte y
for y in 0 to HEIGHT-1 {
repeat HEIGHT {
for x in 0 to WIDTH-1 {
active_world[offset+x] = math.rnd() & 1
}
+3 -3
View File
@@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
kotlin("jvm") version "2.3.20"
kotlin("jvm") version "2.4.10"
}
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:5.9.1"))
// implementation("com.github.irmen:ksim65:v2.0")
testImplementation(platform("io.kotest:kotest-bom:6.2.3"))
// implementation("com.github.irmen:ksim65:v2.1")
}
// Common sourceSets configuration
-1
View File
@@ -8,5 +8,4 @@ dependencies {
implementation("com.michael-bull.kotlin-result:kotlin-result-jvm:2.3.1")
testImplementation("io.kotest:kotest-runner-junit5")
testImplementation("io.kotest:kotest-framework-datatest")
}
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
<orderEntry type="library" name="michael.bull.kotlin.result.jvm" level="project" />
<orderEntry type="library" scope="TEST" name="io.kotest.runner.junit5.jvm" level="project" />
<orderEntry type="library" scope="TEST" name="io.kotest.framework.datatest" level="project" />
</component>
</module>
@@ -111,7 +111,7 @@ val BuiltinFunctions: Map<String, FSignature> = mapOf(
"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)),
"abs" to FSignature(true, emptyArray(), FParam("value", *NumericDatatypes)),
"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)),
"abs__long" to FSignature(true, arrayOf(BaseDataType.LONG), FParam("value", BaseDataType.LONG)),
@@ -120,7 +120,7 @@ val BuiltinFunctions: Map<String, FSignature> = mapOf(
"sizeof" to FSignature(true, arrayOf(BaseDataType.UBYTE), FParam("object", *(BaseDataType.entries - BaseDataType.STRUCT_INSTANCE).toTypedArray())),
"offsetof" to FSignature(true, arrayOf(BaseDataType.UBYTE), FParam("field", BaseDataType.UBYTE)),
"sgn" to FSignature(true, arrayOf(BaseDataType.BYTE), FParam("value", *NumericDatatypes)),
"sqrt" to FSignature(true, emptyArray(), FParam("value", *NumericDatatypes)),
"sqrt" to FSignature(true, arrayOf(BaseDataType.UNDEFINED), FParam("value", *NumericDatatypes)),
"sqrt__ubyte" to FSignature(true, arrayOf(BaseDataType.UBYTE), FParam("value", BaseDataType.UBYTE)),
"sqrt__uword" to FSignature(true, arrayOf(BaseDataType.UBYTE), FParam("value", BaseDataType.UWORD)),
"sqrt__long" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("value", BaseDataType.LONG)),
@@ -140,19 +140,19 @@ val BuiltinFunctions: Map<String, FSignature> = mapOf(
"mkword" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("msb", BaseDataType.UBYTE), FParam("lsb", BaseDataType.UBYTE)),
"mklong" to FSignature(true, arrayOf(BaseDataType.LONG), FParam("msb", BaseDataType.UBYTE), FParam("b2", BaseDataType.UBYTE), FParam("b1", BaseDataType.UBYTE), FParam("lsb", BaseDataType.UBYTE)),
"mklong2" to FSignature(true, arrayOf(BaseDataType.LONG), FParam("msw", BaseDataType.UWORD), FParam("lsw", BaseDataType.UWORD)),
"clamp" to FSignature(true, emptyArray(), FParam("value", BaseDataType.BYTE), FParam("minimum", BaseDataType.BYTE), FParam("maximum", BaseDataType.BYTE)),
"clamp" to FSignature(true, arrayOf(BaseDataType.UNDEFINED), FParam("value", BaseDataType.BYTE), FParam("minimum", BaseDataType.BYTE), FParam("maximum", BaseDataType.BYTE)),
"clamp__byte" to FSignature(true, arrayOf(BaseDataType.BYTE), FParam("value", BaseDataType.BYTE), FParam("minimum", BaseDataType.BYTE), FParam("maximum", BaseDataType.BYTE)),
"clamp__ubyte" to FSignature(true, arrayOf(BaseDataType.UBYTE), FParam("value", BaseDataType.UBYTE), FParam("minimum", BaseDataType.UBYTE), FParam("maximum", BaseDataType.UBYTE)),
"clamp__word" to FSignature(true, arrayOf(BaseDataType.WORD), FParam("value", BaseDataType.WORD), FParam("minimum", BaseDataType.WORD), FParam("maximum", BaseDataType.WORD)),
"clamp__uword" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("value", BaseDataType.UWORD), FParam("minimum", BaseDataType.UWORD), FParam("maximum", BaseDataType.UWORD)),
"clamp__long" to FSignature(true, arrayOf(BaseDataType.LONG), FParam("value", BaseDataType.LONG), FParam("minimum", BaseDataType.LONG), FParam("maximum", BaseDataType.LONG)),
"min" to FSignature(true, emptyArray(), FParam("val1", BaseDataType.BYTE), FParam("val2", BaseDataType.BYTE)),
"min" to FSignature(true, arrayOf(BaseDataType.UNDEFINED), FParam("val1", BaseDataType.BYTE), FParam("val2", BaseDataType.BYTE)),
"min__byte" to FSignature(true, arrayOf(BaseDataType.BYTE), FParam("val1", BaseDataType.BYTE), FParam("val2", BaseDataType.BYTE)),
"min__ubyte" to FSignature(true, arrayOf(BaseDataType.UBYTE), FParam("val1", BaseDataType.UBYTE), FParam("val2", BaseDataType.UBYTE)),
"min__word" to FSignature(true, arrayOf(BaseDataType.WORD), FParam("val1", BaseDataType.WORD), FParam("val2", BaseDataType.WORD)),
"min__uword" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("val1", BaseDataType.UWORD), FParam("val2", BaseDataType.UWORD)),
"min__long" to FSignature(true, arrayOf(BaseDataType.LONG), FParam("val1", BaseDataType.LONG), FParam("val2", BaseDataType.LONG)),
"max" to FSignature(true, emptyArray(), FParam("val1", BaseDataType.BYTE), FParam("val2", BaseDataType.BYTE)),
"max" to FSignature(true, arrayOf(BaseDataType.UNDEFINED), FParam("val1", BaseDataType.BYTE), FParam("val2", BaseDataType.BYTE)),
"max__byte" to FSignature(true, arrayOf(BaseDataType.BYTE), FParam("val1", BaseDataType.BYTE), FParam("val2", BaseDataType.BYTE)),
"max__ubyte" to FSignature(true, arrayOf(BaseDataType.UBYTE), FParam("val1", BaseDataType.UBYTE), FParam("val2", BaseDataType.UBYTE)),
"max__word" to FSignature(true, arrayOf(BaseDataType.WORD), FParam("val1", BaseDataType.WORD), FParam("val2", BaseDataType.WORD)),
@@ -173,6 +173,7 @@ val BuiltinFunctions: Map<String, FSignature> = mapOf(
"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)),
@@ -25,7 +25,6 @@ class CompilationOptions(val output: OutputType,
var includeSourcelines: Boolean = false,
var dumpVariables: Boolean = false,
var dumpSymbols: Boolean = false,
var experimentalCodegen: Boolean = false,
var varsHighBank: Int? = null,
var varsGolden: Boolean = false,
var slabsHighBank: Int? = null,
@@ -66,7 +65,6 @@ class CompilationOptions(val output: OutputType,
private var includeSourcelines: Boolean = false
private var dumpVariables: Boolean = false
private var dumpSymbols: Boolean = false
private var experimentalCodegen: Boolean = false
private var varsHighBank: Int? = null
private var varsGolden: Boolean = false
private var slabsHighBank: Int? = null
@@ -97,7 +95,6 @@ class CompilationOptions(val output: OutputType,
fun includeSourcelines(includeSourcelines: Boolean) = apply { this.includeSourcelines = includeSourcelines }
fun dumpVariables(dumpVariables: Boolean) = apply { this.dumpVariables = dumpVariables }
fun dumpSymbols(dumpSymbols: Boolean) = apply { this.dumpSymbols = dumpSymbols }
fun experimentalCodegen(experimentalCodegen: Boolean) = apply { this.experimentalCodegen = experimentalCodegen }
fun varsHighBank(varsHighBank: Int?) = apply { this.varsHighBank = varsHighBank }
fun varsGolden(varsGolden: Boolean) = apply { this.varsGolden = varsGolden }
fun slabsHighBank(slabsHighBank: Int?) = apply { this.slabsHighBank = slabsHighBank }
@@ -113,7 +110,7 @@ class CompilationOptions(val output: OutputType,
return CompilationOptions(
output, launcher, zeropage, zpReserved, zpAllowed, floats, noSysInit, romable, compTarget, compilerVersion,
loadAddress, memtopAddress, warnSymbolShadowing, warnImplicitTypeCast, optimize, asmQuiet, asmListfile,
includeSourcelines, dumpVariables, dumpSymbols, experimentalCodegen, varsHighBank, varsGolden,
includeSourcelines, dumpVariables, dumpSymbols, varsHighBank, varsGolden,
slabsHighBank, slabsGolden, breakpointCpuInstruction, ignoreFootguns, outputDir, quiet,
profilingInstrumentation, symbolDefs
)
@@ -98,3 +98,24 @@ fun String.unescape(): String {
}
return result.joinToString("")
}
// === Symbol name prefixing ===
// These are used by code generators to add p8b_/p8s_/p8v_/etc. prefixes to symbol names.
// The prefix character depends on the symbol type (block='b', sub='s', variable='v', etc.)
// Scoped names (containing dots) have each part prefixed according to its role.
fun prefixScopedName(name: String, type: Char): String {
val generatedPrefix = "p8_label_gen_"
if('.' !in name) {
if(name.startsWith(generatedPrefix))
return name
return "p8${type}_$name"
}
val parts = name.split('.')
val firstPrefixed = "p8b_${parts[0]}"
val lastPart = parts.last()
val lastPrefixed = if(lastPart.startsWith(generatedPrefix)) lastPart else "p8${type}_$lastPart"
val inbetweenPrefixed = parts.drop(1).dropLast(1).map{ "p8s_$it" }
val prefixed = listOf(firstPrefixed) + inbetweenPrefixed + listOf(lastPrefixed)
return prefixed.joinToString(".")
}
+10 -2
View File
@@ -32,8 +32,8 @@ enum class BaseDataType {
this == LONG -> other.isByteOrBool || other.isWord
this == STR && other == UWORD || this == UWORD && other == STR -> false
this.isArray && other.isArray -> false
this.isArray -> other != FLOAT
this == STR -> other != FLOAT
this.isArray -> other.isByteOrBool
this == STR -> other.isByteOrBool
this.isPointer -> other.isByteOrBool
else -> true
}
@@ -73,6 +73,9 @@ val BaseDataType.isWordOrByteOrBool get() = this in setOf(BaseDataType.UBYTE, Ba
val BaseDataType.isNumeric get() = this == BaseDataType.FLOAT || this.isInteger
val BaseDataType.isNumericOrBool get() = this == BaseDataType.BOOL || this.isNumeric
val BaseDataType.isSigned get() = this in setOf(BaseDataType.BYTE, BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
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.isPointer get() = this == BaseDataType.POINTER
val BaseDataType.isStructInstance get() = this == BaseDataType.STRUCT_INSTANCE
@@ -216,6 +219,11 @@ class DataType private constructor(
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!!)
}
else arrayFor(base, false)
}
@@ -2,10 +2,48 @@ package prog8.code.core
import java.nio.file.Path
enum class CpuType {
CPU6502,
CPU65C02,
VIRTUAL
/**
* Identifies the target CPU type and its key characteristics that affect IR semantics.
*
* The most important property is [statusBitsOnMultiByteOps], which controls the IR
* status-bit contract (see IRInstructions.kt for the full spec):
*
* - When `true`, the IR generator is allowed to skip emitting explicit CMP/CMPI before
* branches that depend on the result of a previous "status-setting" instruction
* (LOAD, INC, DEC, AND, OR, XOR, etc.). The contract is: "this instruction sets
* Z and N based on the full multi-byte value." Backends for these targets must
* honor this contract (e.g., M68000's `move.w #imm,d0` naturally sets Z based on
* the 16-bit value, so the IR can rely on it).
*
* - When `false`, multi-byte operations on this target do NOT set status bits
* correctly for the full value (e.g., 6502's `dec lo` only sets Z based on the
* low byte of a 16-bit decrement). The IR generator must therefore always emit
* an explicit CMP/CMPI before any branch that depends on the result. Backends
* for these targets should NOT add `ora` postambles to set Z; instead they
* rely on the explicit CMP/CMPI emitted by the IR generator.
*
* For 8-bit CPUs (6502, 65C02) the natural status flags from a multi-byte
* cascade are misleading: Z is set based on the LAST byte operated on, not
* the full value. So we treat the contract as "no instruction sets status
* bits except CMP/CMPI/SEC/CLC/SGN/BITTST" and have the IR generator emit
* explicit compares. This avoids the trap of "this works for BYTE but
* silently breaks for WORD/LONG".
*/
enum class CpuType(val statusBitsOnMultiByteOps: Boolean) {
/** 8-bit NMOS 6502. Multi-byte ops don't set Z based on full value. */
CPU6502(false),
/** 8-bit CMOS 65C02. Same status-bit issue as plain 6502. */
CPU65C02(false),
/**
* Virtual machine target. We use `false` here so the IR generator always
* emits explicit CMP/CMPI; the VM's setResultReg then does NOT set Z/N
* flags (only CMP/CMPI/SEC/CLC/SGN/BITTST do). This matches the VM's
* existing documentation (VirtualMachine.kt top comment) and keeps the
* behavior consistent across all current targets.
*/
VIRTUAL(false)
}
interface ICompilationTarget: IStringEncoding, IMemSizer {
@@ -14,7 +52,6 @@ interface ICompilationTarget: IStringEncoding, IMemSizer {
val FLOAT_MAX_NEGATIVE: Double
val FLOAT_MAX_POSITIVE: Double
val FLOAT_MEM_SIZE: UInt
val STARTUP_CODE_RESERVED_SIZE: UInt // this is here, so that certain compiler targets are able to tune this
val PROGRAM_LOAD_ADDRESS : UInt
val PROGRAM_MEMTOP_ADDRESS: UInt
val BSSHIGHRAM_START: UInt
@@ -28,6 +65,8 @@ interface ICompilationTarget: IStringEncoding, IMemSizer {
val customLauncher: List<String>
val additionalAssemblerOptions: List<String>
val defaultOutputType: OutputType
val defaultLauncherType: CbmPrgLauncherType
val supportsBankedCalls: Boolean
fun initializeMemoryAreas(compilerOptions: CompilationOptions)
fun getFloatAsmBytes(num: Number): String
@@ -13,9 +13,10 @@ import kotlin.io.path.Path
object ImportFileSystem {
val userHome: Path get() = Path.of(System.getProperty("user.home"))
fun expandTilde(path: String): String = if (path.startsWith("~")) {
val userHome = System.getProperty("user.home")
userHome + path.drop(1)
userHome.toString() + path.drop(1)
} else {
path
}
@@ -35,6 +36,11 @@ object ImportFileSystem {
}
}
fun clearCaches() {
cache.clear()
lineSpanCache.clear()
}
fun retrieveSourceLine(position: Position): String {
if(SourceCode.isLibraryResource(position.file)) {
val key = SourceCode.withoutPrefix(position.file)
+2 -1
View File
@@ -11,11 +11,13 @@ class C128Target: ICompilationTarget,
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE) {
override val name = NAME
override val supportsBankedCalls = true
override val defaultEncoding = Encoding.PETSCII
override val libraryPath = null
override val customLauncher = emptyList<String>()
override val additionalAssemblerOptions = emptyList<String>()
override val defaultOutputType = OutputType.PRG
override val defaultLauncherType = CbmPrgLauncherType.BASIC
companion object {
const val NAME = "c128"
@@ -27,7 +29,6 @@ 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 STARTUP_CODE_RESERVED_SIZE = 20u
override val PROGRAM_LOAD_ADDRESS = 0x1c01u
override val PROGRAM_MEMTOP_ADDRESS = 0xc000u
+2 -1
View File
@@ -12,11 +12,13 @@ class C64Target: ICompilationTarget,
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE) {
override val name = NAME
override val supportsBankedCalls = true
override val defaultEncoding = Encoding.PETSCII
override val libraryPath = null
override val customLauncher = emptyList<String>()
override val additionalAssemblerOptions = emptyList<String>()
override val defaultOutputType = OutputType.PRG
override val defaultLauncherType = CbmPrgLauncherType.BASIC
companion object {
const val NAME = "c64"
@@ -30,7 +32,6 @@ 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 STARTUP_CODE_RESERVED_SIZE = 20u
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
@@ -15,16 +15,17 @@ import kotlin.io.path.nameWithoutExtension
class ConfigFileTarget(
override val name: String,
override val supportsBankedCalls: Boolean,
override val defaultEncoding: Encoding,
override val cpu: CpuType,
override val PROGRAM_LOAD_ADDRESS: UInt,
override val PROGRAM_MEMTOP_ADDRESS: UInt,
override val STARTUP_CODE_RESERVED_SIZE: UInt,
override val BSSHIGHRAM_START: UInt,
override val BSSHIGHRAM_END: UInt,
override val BSSGOLDENRAM_START: UInt,
override val BSSGOLDENRAM_END: UInt,
override val defaultOutputType: OutputType,
override val defaultLauncherType: CbmPrgLauncherType,
override val libraryPath: Path,
override val customLauncher: List<String>,
override val additionalAssemblerOptions: List<String>,
@@ -112,19 +113,26 @@ class ConfigFileTarget(
val assemblerOptionsStr = props.getProperty("assembler_options", "").trim()
val outputTypeString = props.getProperty("output_type", "PRG")
val defaultOutputType = OutputType.valueOf(outputTypeString.uppercase())
val launcherString = props.getProperty("launcher", "BASIC")
val defaultLauncherType = try {
CbmPrgLauncherType.valueOf(launcherString.uppercase())
} catch (_: IllegalArgumentException) {
CbmPrgLauncherType.BASIC
}
return ConfigFileTarget(
configfile.nameWithoutExtension,
props.getProperty("supports_banked_calls", "false").toBoolean(),
Encoding.entries.first { it.prefix==props.getString("encoding") },
cpuType,
props.getInteger("load_address"),
props.getInteger("memtop"),
0u, // used only in a very specific error condition check in a certain scenario...
props.getInteger("bss_highram_start"),
props.getInteger("bss_highram_end"),
props.getInteger("bss_goldenram_start"),
props.getInteger("bss_goldenram_end"),
defaultOutputType,
defaultLauncherType,
libraryPath,
customLauncher,
if(assemblerOptionsStr=="") emptyList() else assemblerOptionsStr.split(" "),
+2 -1
View File
@@ -11,11 +11,13 @@ class Cx16Target: ICompilationTarget,
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE) {
override val name = NAME
override val supportsBankedCalls = true
override val defaultEncoding = Encoding.PETSCII
override val libraryPath = null
override val customLauncher = emptyList<String>()
override val additionalAssemblerOptions = emptyList<String>()
override val defaultOutputType = OutputType.PRG
override val defaultLauncherType = CbmPrgLauncherType.BASIC
companion object {
const val NAME = "cx16"
@@ -27,7 +29,6 @@ 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 STARTUP_CODE_RESERVED_SIZE = 20u
override val PROGRAM_LOAD_ADDRESS = 0x0801u
override val PROGRAM_MEMTOP_ADDRESS = 0x9f00u
@@ -30,10 +30,10 @@ internal class NormalMemSizer(val floatsize: Int): IMemSizer {
dt.isFloat -> floatsize * (numElements ?: 1)
dt.isLong -> 4 * (numElements ?: 1)
dt.isPointer -> 2 // pointer is just a uword
dt.isStructInstance -> dt.subType!!.memsize(this)
dt.isStructInstance -> dt.subType?.memsize(this) ?: 0
dt.isUndefined -> throw IllegalArgumentException("undefined has no memory size")
else -> 2 * (numElements ?: 1)
}
}
}
}
+2 -1
View File
@@ -11,11 +11,13 @@ class PETTarget: ICompilationTarget,
IMemSizer by NormalMemSizer(Mflpt5.FLOAT_MEM_SIZE) {
override val name = NAME
override val supportsBankedCalls = false
override val defaultEncoding = Encoding.PETSCII
override val libraryPath = null
override val customLauncher = emptyList<String>()
override val additionalAssemblerOptions = emptyList<String>()
override val defaultOutputType = OutputType.PRG
override val defaultLauncherType = CbmPrgLauncherType.BASIC
companion object {
const val NAME = "pet32"
@@ -26,7 +28,6 @@ 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 STARTUP_CODE_RESERVED_SIZE = 20u
override val PROGRAM_LOAD_ADDRESS = 0x0401u
override val PROGRAM_MEMTOP_ADDRESS = 0x8000u
+2 -1
View File
@@ -13,11 +13,13 @@ class VMTarget: ICompilationTarget,
IMemSizer by NormalMemSizer(FLOAT_MEM_SIZE) {
override val name = NAME
override val supportsBankedCalls = false
override val defaultEncoding = Encoding.ISO
override val libraryPath = null
override val customLauncher = emptyList<String>()
override val additionalAssemblerOptions = emptyList<String>()
override val defaultOutputType = OutputType.PRG
override val defaultLauncherType = CbmPrgLauncherType.BASIC
companion object {
const val NAME = "virtual"
@@ -29,7 +31,6 @@ class VMTarget: ICompilationTarget,
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 STARTUP_CODE_RESERVED_SIZE = 0u // not actually used
override val PROGRAM_LOAD_ADDRESS = 0u // not actually used
override val PROGRAM_MEMTOP_ADDRESS = 0xffffu // not actually used
@@ -8,7 +8,7 @@ import java.io.CharConversionException
object PetsciiEncoding {
// decoding: from Petscii/Screencodes (0-255) to Unicode
// character tables used from https://github.com/irmen/cbmcodecs2
// character tables used from https://codeberg.org/irmen/cbmcodecs2.git
private val decodingPetsciiLowercase = charArrayOf(
'\u0000', // 0x00 -> \u0000
@@ -32,7 +32,10 @@ class PETZeropage(options: CompilationOptions) : Zeropage(options) {
}
ZeropageType.FLOATSAFE,
ZeropageType.BASICSAFE -> {
free.addAll(0xb1u..0xbau) // TODO more?
// Only scratch register area is safe for transient use.
// $b5 and $ba are clobbered by PET BASIC/Kernal calls like CHROUT ($ffd2).
// User variables requiring persistent storage must NOT be placed here.
free.addAll(listOf(0xb1u, 0xb2u, 0xb3u, 0xb4u, 0xb6u, 0xb7u, 0xb8u, 0xb9u, 0xfbu, 0xfcu, 0xfdu, 0xfeu))
}
ZeropageType.DONTUSE -> {
free.clear() // don't use zeropage at all
@@ -83,4 +86,4 @@ class PETZeropage(options: CompilationOptions) : Zeropage(options) {
allocatedVariables["cx16.r12r13sl"] = VarAllocation((base+6*4).toUInt(), DataType.LONG, 4)
allocatedVariables["cx16.r14r15sl"] = VarAllocation((base+7*4).toUInt(), DataType.LONG, 4)
}
}
}
@@ -18,6 +18,7 @@ class ErrorReporterForTests(
val errors = mutableListOf<String>()
val warnings = mutableListOf<String>()
val infos = mutableListOf<String>()
val printedErrors = mutableListOf<String>()
override fun err(msg: String, position: Position) {
val text = "${position.toClickableStr()} $msg"
@@ -47,7 +48,7 @@ class ErrorReporterForTests(
!errors.any { ":${position.line}:" in it }
override fun printSingleError(errormessage: String) {
// prints nothing in tests
printedErrors.add(errormessage)
}
override fun report() {
@@ -229,6 +229,16 @@ class TestDataType: FunSpec({
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()
}
test("BaseDataType.equalsSize") {
@@ -236,6 +246,11 @@ class TestDataType: FunSpec({
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()
}
// ============================================================================
@@ -256,16 +256,17 @@ class TestZeropageFreeList: FunSpec({
zp.availableBytes() shouldBe 0
}
test("BASICSAFE should have minimal locations") {
test("BASICSAFE should have minimal free locations") {
val options = createOptions(PETTarget(), ZeropageType.BASICSAFE)
val zp = PETZeropage(options)
// BASICSAFE has minimal locations (0xB1-0xBA per code, minus scratch locations)
zp.free.isNotEmpty().shouldBeTrue()
// After scratch removal, some locations in 0xb1-0xba range should still be free
// Just verify the free list is small but non-empty
zp.free.size shouldBeGreaterThan 0
// BASICSAFE includes scratch register addresses ($b1-$b4, $b6-$b9)
// and $fb-$fe. After scratch removal, $fb-$fe remain usable.
zp.free.size shouldBeLessThan 20
zp.free.contains(0xfbu) shouldBe true
zp.free.contains(0xfcu) shouldBe true
zp.free.contains(0xfdu) shouldBe true
zp.free.contains(0xfeu) shouldBe true
verifyScratchLocationsReserved(zp)
}
@@ -279,6 +280,9 @@ class TestZeropageFreeList: FunSpec({
// FLOATSAFE and BASICSAFE should have same free locations for PET
floatZp.free.size shouldBe basicZp.free.size
setOf(0xfbu, 0xfcu, 0xfdu, 0xfeu).forEach { addr ->
floatZp.free.contains(addr) shouldBe true
}
}
test("KERNALSAFE should exclude IRQ locations") {
@@ -628,13 +632,17 @@ class TestZeropageFreeList: FunSpec({
}
}
test("BASICSAFE - minimal locations around 0xB1-0xBA") {
test("BASICSAFE - extra fb-fe free after scratch removal") {
val options = createOptions(PETTarget(), ZeropageType.BASICSAFE)
val zp = PETZeropage(options)
// BASICSAFE has minimal locations (0xB1-0xBA minus scratch)
zp.free.size shouldBeGreaterThan 0
// BASICSAFE includes scratch register addresses ($b1-$b4, $b6-$b9)
// and $fb-$fe. After removing scratch registers from the pool,
// only $fb-$fe remain free.
zp.free.size shouldBeLessThan 15
setOf(0xfbu, 0xfcu, 0xfdu, 0xfeu).forEach { addr ->
zp.free.contains(addr) shouldBe true
}
}
test("KERNALSAFE - excludes IRQ locations") {
-1
View File
@@ -9,5 +9,4 @@ dependencies {
testImplementation(testFixtures(project(":codeCore")))
testImplementation("io.kotest:kotest-runner-junit5")
testImplementation("io.kotest:kotest-framework-datatest")
}
-19
View File
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
<orderEntry type="module" module-name="codeCore" />
<orderEntry type="module" module-name="simpleAst" />
<orderEntry type="library" name="michael.bull.kotlin.result.jvm" level="project" />
<orderEntry type="library" scope="TEST" name="io.kotest.runner.junit5.jvm" level="project" />
<orderEntry type="library" scope="TEST" name="io.kotest.framework.datatest" level="project" />
</component>
</module>
+327 -321
View File
@@ -15,7 +15,10 @@ 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): ICodeGeneratorBackend {
class AsmGen6502(val prefixSymbols: Boolean,
private val lastGeneratedLabelSequenceNr: Int,
private val preassignedCallSiteIds: Map<PtAsmSub, UByte> = emptyMap()
): ICodeGeneratorBackend {
override fun generate(
program: PtProgram,
symbolTable: SymbolTable,
@@ -23,319 +26,29 @@ class AsmGen6502(val prefixSymbols: Boolean, private val lastGeneratedLabelSeque
errors: IErrorReporter,
): IAssemblyProgram? {
val st = if(prefixSymbols) prefixSymbols(program, options, symbolTable) else symbolTable
val asmgen = AsmGen6502Internal(program, st, options, errors, lastGeneratedLabelSequenceNr)
val asmgen = AsmGen6502Internal(program, st, options, errors, lastGeneratedLabelSequenceNr, preassignedCallSiteIds)
return asmgen.compileToAssembly()
}
private fun prefixSymbols(program: PtProgram, options: CompilationOptions, st: SymbolTable): SymbolTable {
val nodesToPrefix = mutableListOf<Pair<PtNode, Int>>() // parent + index
val functionCallsToPrefix = mutableListOf<Pair<PtNode, Int>>() // parent + index
val expressionsToFixSubtype = mutableListOf<PtExpression>()
val variablesToFixSubtype = mutableListOf<IPtVariable>()
fun prefixNamedNode(node: PtNamedNode) {
when(node) {
is PtAsmSub, is PtSub -> node.name = "p8s_${node.name}"
is PtBlock -> node.name = "p8b_${node.name}"
is PtLabel -> if(!node.name.startsWith(GENERATED_LABEL_PREFIX)) node.name = "p8l_${node.name}" // only prefix user defined labels
is PtVariable, is PtMemMapped, is PtSubroutineParameter -> node.name = "p8v_${node.name}"
is PtStructDecl -> node.name = "p8t_${node.name}"
}
}
fun prefixSymbols(node: PtNode) {
when(node) {
is PtAsmSub -> {
prefixNamedNode(node)
node.parameters.forEach { (_, param) -> prefixNamedNode(param) }
if(node.address?.varbank!=null) {
node.address!!.varbank = node.address!!.varbank!!.prefix(node, st)
}
}
is PtSub -> prefixNamedNode(node)
is PtFunctionCall -> {
if(node.builtin) {
// could be a struct instance creation
if(node.name=="prog8_lib_structalloc") {
val struct = node.type.subType!!
if(struct is StStruct) {
// update link to active symboltable node
node.type.subType = st.lookup(struct.scopedNameString) as StStruct
}
}
} else {
val stNode = st.lookup(node.name)!!
if (stNode.astNode?.definingBlock()?.options?.noSymbolPrefixing != true) {
val index = node.parent.children.indexOf(node)
functionCallsToPrefix += node.parent to index
}
}
}
is PtIdentifier -> {
// check if the identifier is part of a pointer dereference (which means you cannot look it up in the symboltable because it's a struct field)
val pexpr = node.parent as? PtBinaryExpression
if(pexpr?.operator==".") {
if(pexpr.left is PtArrayIndexer) {
val arrayvar = (pexpr.left as PtArrayIndexer).variable!!
if(arrayvar.type.subType!=null) {
// don't prefix a struct field name here, we take care of that at asm generation time, which was a lot easier
//nodesToPrefix += node.parent to node.parent.children.indexOf(node)
return
} else
throw AssemblyError("can only use deref expression on struct type")
} else {
TODO("handle other left operand ${pexpr.left }in dereferencing of struct fields ${node.position}")
}
}
// normal (non pointer deref) expression
var lookupName = node.name
if(node.type.isSplitWordArray && (lookupName.endsWith("_lsb") || lookupName.endsWith("_msb"))) {
lookupName = lookupName.dropLast(4)
}
val stNode = st.lookup(lookupName) ?:
throw AssemblyError("unknown identifier $node")
if(stNode.astNode!!.definingBlock()?.options?.noSymbolPrefixing!=true) {
val index = node.parent.children.indexOf(node)
nodesToPrefix += node.parent to index
}
}
is PtBlock -> prefixNamedNode(node)
is PtConstant -> node.name = "p8c_${node.name}"
is PtLabel -> prefixNamedNode(node)
is PtMemMapped -> prefixNamedNode(node)
is PtSubroutineParameter -> prefixNamedNode(node)
is PtVariable -> {
val index = node.parent.children.indexOf(node)
nodesToPrefix += node.parent to index
}
is PtStructDecl -> prefixNamedNode(node) // note: field names are not prefixed here, we take care of that at asm generation time, which was a lot easier
else -> { }
}
if(node is IPtVariable) {
if(node.type.subType!=null)
variablesToFixSubtype.add(node)
} else if(node is PtExpression) {
if(node.type.subType!=null)
expressionsToFixSubtype.add(node)
}
node.children.forEach { prefixSymbols(it) }
}
fun maybePrefixFunctionCallsAndIdentifierReferences(node: PtNode) {
if(node is PtFunctionCall && !node.builtin) {
// function calls to subroutines defined in a block that does NOT have NoSymbolPrefixing, still have to be prefixed at the call site
val stNode = st.lookup(node.name)!!
if(stNode.astNode!!.definingBlock()?.options?.noSymbolPrefixing!=true) {
val index = node.parent.children.indexOf(node)
functionCallsToPrefix += node.parent to index
}
}
else if (node is PtIdentifier) {
// identifier references to things defined in a block that does NOT have NoSymbolPrefixing, still have to be prefixed at the referencing point
var lookupName = node.name
if(node.type.isSplitWordArray && (lookupName.endsWith("_lsb") || lookupName.endsWith("_msb"))) {
lookupName = lookupName.dropLast(4)
}
val stNode = st.lookup(lookupName) ?: throw AssemblyError("unknown identifier $node")
if(stNode.astNode!!.definingBlock()?.options?.noSymbolPrefixing!=true) {
val index = node.parent.children.indexOf(node)
nodesToPrefix += node.parent to index
}
}
node.children.forEach { maybePrefixFunctionCallsAndIdentifierReferences(it) }
}
program.allBlocks().forEach { block ->
if (!block.options.noSymbolPrefixing) {
prefixSymbols(block)
} else {
maybePrefixFunctionCallsAndIdentifierReferences(block)
}
}
nodesToPrefix.forEach { (parent, index) ->
when(val node = parent.children[index]) {
is PtIdentifier -> parent.setChild(index, node.prefix(parent, st))
is PtFunctionCall -> throw AssemblyError("PtFunctionCall should be processed in their own list, last")
is PtVariable -> parent.setChild(index, node.prefix(parent, st))
else -> throw AssemblyError("weird node to prefix $node")
}
}
// reversed so inner calls (such as arguments to a function call) get processed before the actual function call itself
functionCallsToPrefix.reversed().forEach { (parent, index) ->
val node = parent.children[index]
if(node is PtFunctionCall) {
val prefixedName = PtIdentifier(node.name, DataType.UNDEFINED, node.position).prefix(parent, st)
val prefixedNode = node.withNewName(prefixedName.name)
parent.setChild(index, prefixedNode)
} else {
throw AssemblyError("expected PtFunctionCall")
}
}
val updatedSt = SymbolTableMaker(program, options).make()
fun findSubtypeReplacement(sub: ISubType): StStruct? {
if(updatedSt.lookup(sub.scopedNameString)!=null)
return null
val old = st.lookup(sub.scopedNameString) ?: throw AssemblyError("old subtype not found: ${sub.scopedNameString}")
val prefixed = ArrayDeque<String>()
var node: StNode = old
while(node.type!= StNodeType.GLOBAL) {
val typeChar = typePrefixChar(node.type)
prefixed.addFirst("p8${typeChar}_${node.name}")
node=node.parent
}
return updatedSt.lookup(prefixed.joinToString(".")) as StStruct
}
expressionsToFixSubtype.forEach { node ->
findSubtypeReplacement(node.type.subType!!)?.let {
node.type.subType = it
}
}
variablesToFixSubtype.forEach { node ->
findSubtypeReplacement(node.type.subType!!)?.let {
node.type.subType = it
}
}
return updatedSt
}
}
private fun prefixScopedName(name: String, type: Char): String {
if('.' !in name) {
if(name.startsWith(GENERATED_LABEL_PREFIX))
return name
return "p8${type}_$name"
}
val parts = name.split('.')
val firstPrefixed = "p8b_${parts[0]}"
val lastPart = parts.last()
val lastPrefixed = if(lastPart.startsWith(GENERATED_LABEL_PREFIX)) lastPart else "p8${type}_$lastPart"
// the parts in between are assumed to be subroutine scopes.
val inbetweenPrefixed = parts.drop(1).dropLast(1).map{ "p8s_$it" }
val prefixed = listOf(firstPrefixed) + inbetweenPrefixed + listOf(lastPrefixed)
return prefixed.joinToString(".")
}
private fun PtVariable.prefix(parent: PtNode, st: SymbolTable): PtVariable {
name = prefixScopedName(name, 'v')
if(value==null)
return this
val arrayValue = value as? PtArray
return if(arrayValue!=null && arrayValue.children.any { it !is PtNumber && it !is PtBool } ) {
val newValue = PtArray(arrayValue.type, arrayValue.position)
arrayValue.children.forEach { elt ->
when(elt) {
is PtBool -> newValue.add(elt)
is PtNumber -> newValue.add(elt)
is PtConstant -> newValue.add(elt)
is PtAddressOf -> {
if(elt.definingBlock()?.options?.noSymbolPrefixing==true)
newValue.add(elt)
else {
val newAddr = PtAddressOf(elt.type, false, elt.position)
newAddr.add(elt.identifier!!.prefix(newAddr, st))
if (elt.arrayIndexExpr != null)
newAddr.add(elt.arrayIndexExpr!!)
newAddr.parent = arrayValue
newValue.add(newAddr)
}
}
is PtFunctionCall if elt.builtin -> {
if (elt.name != "prog8_lib_structalloc")
throw AssemblyError("weird array value element $elt")
else
newValue.add(elt)
}
else -> throw AssemblyError("weird array value element $elt")
}
}
val result = PtVariable(name, type, zeropage, align, dirty, newValue, arraySize, position)
result.parent = parent
result
}
else this
}
//private fun PtFunctionCall.prefix(targetType: Char): PtFunctionCall {
// val newName = prefixScopedName(name, targetType)
// return this.withNewName(newName)
//}
private fun PtFunctionCall.withNewName(name: String): PtFunctionCall {
val call = PtFunctionCall(name, builtin, hasNoSideEffects, returntypes, position)
call.addAll(children)
call.children.forEach { it.parent = call }
call.parent = parent
return call
}
private fun PtIdentifier.prefix(parent: PtNode, st: SymbolTable): PtIdentifier {
val targetNt: StNodeType
val target = st.lookup(name)
if(target?.astNode?.definingBlock()?.options?.noSymbolPrefixing==true)
return this
if(target==null) {
if(name.endsWith("_lsb") || name.endsWith("_msb")) {
val target2 = st.lookup(name.dropLast(4))
if(target2?.astNode?.definingBlock()?.options?.noSymbolPrefixing==true)
return this
targetNt = target2!!.type
} else {
// if no target found, assume that the identifier is a struct field
targetNt = StNodeType.STATICVAR
}
} else {
targetNt = target.type
}
val prefixType = typePrefixChar(targetNt)
val newName = prefixScopedName(name, prefixType)
val node = PtIdentifier(newName, type, position)
node.parent = parent
return node
}
private fun typePrefixChar(targetNt: StNodeType): Char {
return when(targetNt) {
StNodeType.BLOCK -> 'b'
StNodeType.SUBROUTINE, StNodeType.EXTSUB -> 's'
StNodeType.LABEL -> 'l'
StNodeType.STATICVAR, StNodeType.MEMVAR -> 'v'
StNodeType.CONSTANT -> 'c'
StNodeType.BUILTINFUNC -> 's'
StNodeType.MEMORYSLAB -> 'v'
StNodeType.STRUCT -> 't'
StNodeType.STRUCTINSTANCE -> 'i'
else -> '?'
return prog8.code.prefixSymbols(program, options, st)
}
}
class AsmGen6502Internal (
val program: PtProgram,
internal val symbolTable: SymbolTable,
internal val options: CompilationOptions,
internal val errors: IErrorReporter,
private var generatedLabelSequenceNumber: Int
private var generatedLabelSequenceNumber: Int,
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)
internal val loopEndLabels = ArrayDeque<String>()
private val zeropage = options.compTarget.zeropage
private val allocator = VariableAllocator(symbolTable, options, errors)
private val allocator = VariableAllocator(symbolTable, options, errors, program)
private val assembly = mutableListOf<String>()
private val breakpointLabels = mutableListOf<String>()
private val forloopsAsmGen = ForLoopsAsmGen(this, zeropage)
@@ -348,6 +61,7 @@ class AsmGen6502Internal (
private val ifElseAsmgen = IfElseAsmGen(program, symbolTable, this, pointerGen, assignmentAsmGen, errors)
private val ifExpressionAsmgen = IfExpressionAsmGen(this, pointerGen, assignmentAsmGen, errors)
private val augmentableAsmGen = AugmentableAssignmentAsmGen(program, assignmentAsmGen, this, pointerGen, allocator)
internal val extsubCallSiteIds: MutableMap<PtAsmSub, UByte> = preassignedCallSiteIds.toMutableMap()
init {
assignmentAsmGen.augmentableAsmGen = augmentableAsmGen
@@ -366,7 +80,7 @@ class AsmGen6502Internal (
if(errors.noErrors()) {
val output = options.outputDir.resolve("${program.name}.asm")
val asmLines = assembly.flatMapTo(mutableListOf()) { it.split('\n') }
val asmLines = assembly.toMutableList()
if(options.compTarget.name==Cx16Target.NAME) {
scanInvalid65816instructions(asmLines)
if(!errors.noErrors()) {
@@ -375,9 +89,7 @@ class AsmGen6502Internal (
}
}
if(options.optimize) {
while(optimizeAssembly(asmLines, options.compTarget, symbolTable)>0) {
// optimize the assembly source code
}
optimizeAssembly(asmLines, options.compTarget, symbolTable)
output.writeLines(asmLines)
} else {
// write the unmodified code
@@ -471,14 +183,21 @@ class AsmGen6502Internal (
}
internal fun out(str: String, splitlines: Boolean = true) {
val fragment = (if(splitlines && " | " in str) str.replace("|", "\n") else str).trim('\r', '\n')
if (splitlines) {
for (line in fragment.splitToSequence('\n')) {
val trimmed = if (line.startsWith(' ')) "\t" + line.trim() else line
// trimmed = trimmed.replace(Regex("^\\+\\s+"), "+\t") // sanitize local label indentation
assembly.add(trimmed)
if ('\n' in str || '|' in str) {
val fragment = if (" | " in str) str.replace("|", "\n").trim('\r', '\n') else str.trim('\r', '\n')
val lines = fragment.split('\n')
for (i in lines.indices) {
val line = lines[i]
assembly.add(if (line.startsWith(' ')) "\t${line.trim()}" else line)
}
} else {
val trimmed = str.trim()
assembly.add(if (str.startsWith(' ')) "\t$trimmed" else trimmed)
}
} else assembly.add(fragment)
} else {
assembly.add(str.trim('\r', '\n'))
}
}
fun asmSymbolName(regs: RegisterOrPair): String =
@@ -519,12 +238,35 @@ class AsmGen6502Internal (
return e
}
// Try to get a static address for a simple variable or constant-indexed array element.
// Returns the asm label if the expression is a variable/array with constant index,
// or null if it is more complex. The requested variableMemSize must match the
// variable's actual size, otherwise null is returned to force the generic path
// that correctly handles size conversions (e.g. ubyte promoted to uword).
fun tryGetStaticAddress(expr: PtExpression, variableMemSize: Int): String? {
val e = unwrapCasts(expr)
if (e is PtIdentifier) return asmVariableName(e)
if (e is PtIdentifier) {
val actualSize = when {
e.type.isByte -> 1
e.type.isWord -> 2
e.type.isLong -> 4
e.type.isFloat -> options.compTarget.FLOAT_MEM_SIZE
else -> variableMemSize
}
if (actualSize != variableMemSize) return null
return asmVariableName(e)
}
if (e is PtArrayIndexer) {
val idx = e.index.asConstInteger()
if (idx != null && e.variable != null) {
val actualSize = when {
e.type.isByte -> 1
e.type.isWord -> 2
e.type.isLong -> 4
e.type.isFloat -> options.compTarget.FLOAT_MEM_SIZE
else -> variableMemSize
}
if (actualSize != variableMemSize) return null
val offset = program.memsizer.memorySize(e.type, idx)
if (offset + (variableMemSize - 1) < 256) return "${asmVariableName(e.variable!!)}+$offset"
}
@@ -1024,7 +766,7 @@ class AsmGen6502Internal (
assignExpressionToRegister(value, target.register!!)
} else {
assignExpressionToRegister(value, RegisterOrPair.AX)
pointerGen.assignWordReg(PtrTarget(target), RegisterOrPair.AX)
assignmentAsmGen.assignRegisterpairWord(target, RegisterOrPair.AX)
}
}
target.datatype.isWord || target.datatype.isPassByRef -> {
@@ -1051,7 +793,14 @@ class AsmGen6502Internal (
lda #$${hex.take(2)}
sta ${target.asmVarname}+3""")
}
TargetStorageKind.ARRAY -> TODO("assign long to array ${target.position}")
TargetStorageKind.ARRAY -> {
val deref = target.array!!.pointerderef
if(deref!=null) {
pointerGen.assignLong(IndexedPtrTarget(target), value.asConstInteger()!!)
} else {
TODO("assign long to array ${target.position}")
}
}
TargetStorageKind.MEMORY -> throw AssemblyError("memory is bytes not long ${target.position}")
TargetStorageKind.REGISTER -> assignExpressionToRegister(value, target.register!!, true)
TargetStorageKind.POINTER -> pointerGen.assignLong(target.pointer!!, value.number.toInt())
@@ -1074,7 +823,14 @@ class AsmGen6502Internal (
)
}
TargetStorageKind.ARRAY -> TODO("assign long to array ${target.position}")
TargetStorageKind.ARRAY -> {
val deref = target.array!!.pointerderef
if(deref!=null) {
pointerGen.assignLongVar(IndexedPtrTarget(target), asmSymbolName(value))
} else {
TODO("assign long to array ${target.position}")
}
}
TargetStorageKind.MEMORY -> throw AssemblyError("memory is bytes not long ${target.position}")
TargetStorageKind.REGISTER -> assignExpressionToRegister(value, target.register!!, true)
TargetStorageKind.POINTER -> pointerGen.assignLongVar(target.pointer!!, asmSymbolName(value))
@@ -1089,10 +845,23 @@ class AsmGen6502Internal (
out(" lda $valuesym | sta ${target.asmVarname}")
signExtendLongVariable(target.asmVarname, value.value.type.base)
}
TargetStorageKind.ARRAY -> TODO("assign typecasted long to array ${target.position}")
TargetStorageKind.ARRAY -> {
val deref = target.array!!.pointerderef
if(deref!=null) {
// generic fallback: evaluate full long value into register then store via indexed pointer
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R12R13, true)
assignmentAsmGen.assignRegisterLong(target, RegisterOrPair.R12R13)
} else {
TODO("assign typecasted long to array ${target.position}")
}
}
TargetStorageKind.MEMORY -> throw AssemblyError("memory is bytes not long ${target.position}")
TargetStorageKind.REGISTER -> assignExpressionToRegister(value, target.register!!, true)
TargetStorageKind.POINTER -> TODO("assign typecasted long into pointer ${target.position}")
TargetStorageKind.POINTER -> {
// generic fallback for pointer
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R12R13, true)
assignmentAsmGen.assignRegisterLong(target, RegisterOrPair.R12R13)
}
TargetStorageKind.VOID -> { /* do nothing */ }
}
} else if(value.value.type.isWord) {
@@ -1105,7 +874,16 @@ class AsmGen6502Internal (
sta ${target.asmVarname}+1""")
signExtendLongVariable(target.asmVarname, value.value.type.base)
}
TargetStorageKind.ARRAY -> TODO("assign typecasted long to array ${target.position}")
TargetStorageKind.ARRAY -> {
val deref = target.array!!.pointerderef
if(deref!=null) {
// generic fallback: evaluate full long value into register then store via indexed pointer
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R12R13, true)
assignmentAsmGen.assignRegisterLong(target, RegisterOrPair.R12R13)
} else {
TODO("assign typecasted long to array ${target.position}")
}
}
TargetStorageKind.MEMORY -> throw AssemblyError("memory is bytes not long ${target.position}")
TargetStorageKind.REGISTER -> {
require(target.register in CombinedLongRegisters)
@@ -1117,12 +895,154 @@ class AsmGen6502Internal (
sta cx16.$startreg+1""")
signExtendLongVariable("cx16.$startreg", value.value.type.base)
}
TargetStorageKind.POINTER -> TODO("assign typecasted long into pointer ${target.position}")
TargetStorageKind.POINTER -> {
// generic fallback for pointer
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R12R13, true)
assignmentAsmGen.assignRegisterLong(target, RegisterOrPair.R12R13)
}
TargetStorageKind.VOID -> { /* do nothing */ }
}
} else throw AssemblyError("weird casted type")
} else if(value.value.type.isFloat) {
// first evaluate the float expression into FAC1
assignExpressionToRegister(value.value, RegisterOrPair.FAC1)
// then convert float in FAC1 to long and store to target
when (target.kind) {
TargetStorageKind.VARIABLE -> {
out("""
jsr floats.QINT
lda floats.FAC_ADDR+4
sta ${target.asmVarname}
lda floats.FAC_ADDR+3
sta ${target.asmVarname}+1
lda floats.FAC_ADDR+2
sta ${target.asmVarname}+2
lda floats.FAC_ADDR+1
sta ${target.asmVarname}+3""")
}
TargetStorageKind.REGISTER -> {
require(target.register in CombinedLongRegisters)
val startreg = target.register!!.startregname()
out("""
jsr floats.QINT
lda floats.FAC_ADDR+4
sta cx16.$startreg
lda floats.FAC_ADDR+3
sta cx16.$startreg+1
lda floats.FAC_ADDR+2
sta cx16.$startreg+2
lda floats.FAC_ADDR+1
sta cx16.$startreg+3""")
}
TargetStorageKind.ARRAY -> TODO("assign typecasted float to long array ${target.position}")
TargetStorageKind.MEMORY -> throw AssemblyError("memory is bytes not long ${target.position}")
TargetStorageKind.POINTER -> TODO("assign typecasted float to long pointer ${target.position}")
TargetStorageKind.VOID -> { /* do nothing */ }
}
} else throw AssemblyError("weird casted type ${value.value.type} as ${value.type} at $value")
} else {
TODO("assign typecasted expression $value to a long target ${target.kind} at ${target.position} - please report this issue. Use simple expressions and temporary variables for now")
// inner value is not a plain identifier (e.g. an array element or function result)
when {
value.value.type.isByte -> {
when (target.kind) {
TargetStorageKind.VARIABLE -> {
assignExpressionToRegister(value.value, RegisterOrPair.A, false)
out(" sta ${target.asmVarname}")
signExtendLongVariable(target.asmVarname, value.value.type.base)
}
TargetStorageKind.ARRAY -> {
val deref = target.array!!.pointerderef
if(deref!=null) {
// generic fallback: evaluate full long value into register then store via indexed pointer
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R12R13, true)
assignmentAsmGen.assignRegisterLong(target, RegisterOrPair.R12R13)
} else {
TODO("assign typecasted long to array ${target.position}")
}
}
TargetStorageKind.MEMORY -> throw AssemblyError("memory is bytes not long ${target.position}")
TargetStorageKind.REGISTER -> assignExpressionToRegister(value, target.register!!, true)
TargetStorageKind.POINTER -> {
// generic fallback for pointer
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R12R13, true)
assignmentAsmGen.assignRegisterLong(target, RegisterOrPair.R12R13)
}
TargetStorageKind.VOID -> { /* do nothing */ }
}
}
value.value.type.isWord -> {
when (target.kind) {
TargetStorageKind.VARIABLE -> {
assignExpressionToRegister(value.value, RegisterOrPair.AY, value.value.type.isSigned)
out("""
sta ${target.asmVarname}
sty ${target.asmVarname}+1""")
signExtendLongVariable(target.asmVarname, value.value.type.base)
}
TargetStorageKind.ARRAY -> {
val deref = target.array!!.pointerderef
if(deref!=null) {
// generic fallback: evaluate full long value into register then store via indexed pointer
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R12R13, true)
assignmentAsmGen.assignRegisterLong(target, RegisterOrPair.R12R13)
} else {
TODO("assign typecasted long to array ${target.position}")
}
}
TargetStorageKind.MEMORY -> throw AssemblyError("memory is bytes not long ${target.position}")
TargetStorageKind.REGISTER -> {
require(target.register in CombinedLongRegisters)
val startreg = target.register!!.startregname()
assignExpressionToRegister(value.value, RegisterOrPair.AY, value.value.type.isSigned)
out("""
sta cx16.$startreg
sty cx16.$startreg+1""")
signExtendLongVariable("cx16.$startreg", value.value.type.base)
}
TargetStorageKind.POINTER -> {
// generic fallback for pointer
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R12R13, true)
assignmentAsmGen.assignRegisterLong(target, RegisterOrPair.R12R13)
}
TargetStorageKind.VOID -> { /* do nothing */ }
}
}
value.value.type.isFloat -> {
assignExpressionToRegister(value.value, RegisterOrPair.FAC1)
when (target.kind) {
TargetStorageKind.VARIABLE -> {
out("""
jsr floats.QINT
lda floats.FAC_ADDR+4
sta ${target.asmVarname}
lda floats.FAC_ADDR+3
sta ${target.asmVarname}+1
lda floats.FAC_ADDR+2
sta ${target.asmVarname}+2
lda floats.FAC_ADDR+1
sta ${target.asmVarname}+3""")
}
TargetStorageKind.REGISTER -> {
require(target.register in CombinedLongRegisters)
val startreg = target.register!!.startregname()
out("""
jsr floats.QINT
lda floats.FAC_ADDR+4
sta cx16.$startreg
lda floats.FAC_ADDR+3
sta cx16.$startreg+1
lda floats.FAC_ADDR+2
sta cx16.$startreg+2
lda floats.FAC_ADDR+1
sta cx16.$startreg+3""")
}
TargetStorageKind.ARRAY -> TODO("assign typecasted float to long array ${target.position}")
TargetStorageKind.MEMORY -> throw AssemblyError("memory is bytes not long ${target.position}")
TargetStorageKind.POINTER -> TODO("assign typecasted float to long pointer ${target.position}")
TargetStorageKind.VOID -> { /* do nothing */ }
}
}
else -> throw AssemblyError("weird casted type ${value.value.type} as ${value.type} at $value")
}
}
} else if(target.kind == TargetStorageKind.REGISTER) {
assignExpressionToRegister(value, target.register!!, true)
@@ -1175,6 +1095,7 @@ class AsmGen6502Internal (
when {
iterations == 0 -> {}
iterations == 1 -> translate(stmt.statements)
iterations > 65536 -> repeatLongCount(iterations.toLong(), stmt)
iterations !in 0..65536 -> throw AssemblyError("invalid number of iterations")
iterations <= 256 -> repeatByteCount(iterations, stmt)
else -> repeatWordCount(iterations, stmt)
@@ -1186,26 +1107,36 @@ class AsmGen6502Internal (
val name = asmVariableName(stmt.count as PtIdentifier)
when {
vardecl.type.isByte -> {
assignVariableToRegister(name, RegisterOrPair.Y, stmt.definingISub(), stmt.count.position)
assignVariableToRegister(name, RegisterOrPair.Y, stmt.definingISub(), stmt.count.position, vardecl.type.isSigned)
repeatCountInY(stmt, endLabel)
}
vardecl.type.isWord -> {
assignVariableToRegister(name, RegisterOrPair.AY, stmt.definingISub(), stmt.count.position)
assignVariableToRegister(name, RegisterOrPair.AY, stmt.definingISub(), stmt.count.position, vardecl.type.isSigned)
repeatWordCountInAY(endLabel, stmt)
}
vardecl.type.isLong -> {
val reg = CombinedLongRegisters.last()
assignVariableToRegister(name, reg, stmt.definingISub(), (stmt.count as PtIdentifier).position, vardecl.type.isSigned)
repeatLongCountInReg(reg, endLabel, stmt)
}
else -> throw AssemblyError("invalid loop variable datatype ${vardecl.type}")
}
}
else -> {
when {
stmt.count.type.isByte -> {
assignExpressionToRegister(stmt.count, RegisterOrPair.Y)
assignExpressionToRegister(stmt.count, RegisterOrPair.Y, stmt.count.type.isSigned)
repeatCountInY(stmt, endLabel)
}
stmt.count.type.isWord -> {
assignExpressionToRegister(stmt.count, RegisterOrPair.AY)
assignExpressionToRegister(stmt.count, RegisterOrPair.AY, stmt.count.type.isSigned)
repeatWordCountInAY(endLabel, stmt)
}
stmt.count.type.isLong -> {
val reg = CombinedLongRegisters.last()
assignExpressionToRegister(stmt.count, reg, stmt.count.type.isSigned)
repeatLongCountInReg(reg, endLabel, stmt)
}
else -> throw AssemblyError("invalid loop expression datatype ${stmt.count.type}")
}
}
@@ -1213,6 +1144,75 @@ class AsmGen6502Internal (
loopEndLabels.removeLast()
}
private fun repeatLongCount(iterations: Long, stmt: PtRepeatLoop) {
require(iterations > 65536) { "invalid repeat count ${stmt.position}" }
val repeatLabel = makeLabel("repeat")
val counterVar = createTempVarReused(BaseDataType.LONG, true, stmt)
val bytes = mutableListOf<Int>()
var currentN = iterations
while (currentN > 0) {
val b = (currentN % 256).toInt()
bytes.add(b)
currentN = if (b == 0) currentN / 256 else currentN / 256 + 1
if (currentN == 1L) break
}
for (i in bytes.indices) {
val v = bytes[i]
val suffix = if (i > 0) "+$i" else ""
if (v == 0 && isTargetCpu(CpuType.CPU65C02)) {
out(" stz $counterVar$suffix")
} else {
out(" lda #$v")
out(" sta $counterVar$suffix")
}
}
out(repeatLabel)
translate(stmt.statements)
for (i in bytes.indices) {
val suffix = if (i > 0) "+$i" else ""
out(" dec $counterVar$suffix")
out(" bne $repeatLabel")
}
}
private fun repeatLongCountInReg(reg: RegisterOrPair, endLabel: String, stmt: PtRepeatLoop) {
require(reg in CombinedLongRegisters)
val repeatLabel = makeLabel("repeat")
val regName = "cx16." + reg.startregname()
val d1 = makeLabel("skip_dec1")
val d2 = makeLabel("skip_dec2")
val d3 = makeLabel("skip_dec3")
out("""
lda $regName
ora $regName+1
ora $regName+2
ora $regName+3
beq $endLabel
$repeatLabel""")
translate(stmt.statements)
out("""
lda $regName
bne $d3
lda $regName+1
bne $d2
lda $regName+2
bne $d1
dec $regName+3
$d1 dec $regName+2
$d2 dec $regName+1
$d3 dec $regName
lda $regName
ora $regName+1
ora $regName+2
ora $regName+3
bne $repeatLabel""")
out(endLabel)
}
private fun repeatWordCount(iterations: Int, stmt: PtRepeatLoop) {
require(iterations in 257..65536) { "invalid repeat count ${stmt.position}" }
@@ -2028,7 +2028,11 @@ $repeatLabel""")
if(asm.isIR)
throw AssemblyError("%asm containing IR code cannot be translated to 6502 assembly")
else
assembly.add(asm.assembly.trimEnd().trimStart('\r', '\n'))
// User-supplied assembly should be passed through verbatim. It may contain
// characters such as '|' (64tass bitwise OR) that out() would otherwise
// misinterpret as a statement separator, and the user's indentation/spacing
// should be preserved rather than re-tabbed.
assembly.add(asm.assembly.trim('\r', '\n'))
}
private fun translate(incbin: PtIncludeBinary) {
@@ -2153,6 +2157,8 @@ $repeatLabel""")
internal fun isZpVar(variable: PtIdentifier): Boolean = allocator.isZpVar(variable.name)
internal fun isZpVar(scopedName: String): Boolean = allocator.isZpVar(scopedName)
internal fun jmp(asmLabel: String, indirect: Boolean=false, indexedX: Boolean=false) {
if(indirect) {
if(indexedX)
@@ -13,39 +13,42 @@ import prog8.code.core.ICompilationTarget
// TrimmedLine has .value (original), .trimmed (trimStart), and .instruction (instructionPart).
internal fun optimizeAssembly(lines: MutableList<String>, machine: ICompilationTarget, symbolTable: SymbolTable): Int {
var numberOfOptimizations = 0
internal fun optimizeAssembly(lines: MutableList<String>, machine: ICompilationTarget, symbolTable: SymbolTable) {
val pretrimmed = lines.map { it.trimStart() }.toMutableList()
/** Runs an optimization pass, applies modifications if any, and recomputes line windows. */
fun runPass(
mods: List<Modification>,
windowSize: Int,
currentLines: Sequence<List<TrimmedLine>>
): Sequence<List<TrimmedLine>> {
if (mods.isNotEmpty()) {
apply(mods, lines, pretrimmed)
numberOfOptimizations++
return getLinesBy(pretrimmed, lines, windowSize)
while(true) {
var modified = false
/** Runs a single optimization pass, applies modifications if any, and recomputes line windows. */
fun runPass(
mods: List<Modification>,
windowSize: Int,
currentLines: Sequence<List<TrimmedLine>>
): Sequence<List<TrimmedLine>> {
if (mods.isNotEmpty()) {
apply(mods, lines, pretrimmed)
modified = true
return getLinesBy(pretrimmed, lines, windowSize)
}
return currentLines
}
return currentLines
var linesByFour = getLinesBy(pretrimmed, lines, 4)
linesByFour = runPass(optimizeIncDec(linesByFour), 4, linesByFour)
linesByFour = runPass(optimizeStoreLoadSame(linesByFour, machine, symbolTable), 4, linesByFour)
linesByFour = runPass(optimizeJsrRtsAndOtherCombinations(linesByFour), 4, linesByFour)
linesByFour = runPass(optimizeUselessPushPopStack(linesByFour), 4, linesByFour)
linesByFour = runPass(optimizeUnneededTempvarInAdd(linesByFour), 4, linesByFour)
linesByFour = runPass(optimizeTSBtoRegularOr(linesByFour), 4, linesByFour)
var linesByFourteen = getLinesBy(pretrimmed, lines, 14)
linesByFourteen = runPass(optimizeSameAssignments(linesByFourteen, machine, symbolTable), 14, linesByFourteen)
linesByFourteen = runPass(optimizeSamePointerIndexingAndUselessBeq(linesByFourteen), 14, linesByFourteen)
linesByFourteen = runPass(optimizeAddWordToSameVariableOrExtraRegisterLoadInWordStore(linesByFourteen), 14, linesByFourteen)
if(!modified)
break
}
var linesByFour = getLinesBy(pretrimmed, lines, 4)
linesByFour = runPass(optimizeIncDec(linesByFour), 4, linesByFour)
linesByFour = runPass(optimizeStoreLoadSame(linesByFour, machine, symbolTable), 4, linesByFour)
linesByFour = runPass(optimizeJsrRtsAndOtherCombinations(linesByFour), 4, linesByFour)
linesByFour = runPass(optimizeUselessPushPopStack(linesByFour), 4, linesByFour)
linesByFour = runPass(optimizeUnneededTempvarInAdd(linesByFour), 4, linesByFour)
linesByFour = runPass(optimizeTSBtoRegularOr(linesByFour), 4, linesByFour)
var linesByFourteen = getLinesBy(pretrimmed, lines, 14)
linesByFourteen = runPass(optimizeSameAssignments(linesByFourteen, machine, symbolTable), 14, linesByFourteen)
linesByFourteen = runPass(optimizeSamePointerIndexingAndUselessBeq(linesByFourteen), 14, linesByFourteen)
linesByFourteen = runPass(optimizeAddWordToSameVariableOrExtraRegisterLoadInWordStore(linesByFourteen), 14, linesByFourteen)
return numberOfOptimizations
}
internal fun String.isBranch() = this.startsWith("b")
@@ -166,6 +169,18 @@ internal fun String.hasInstr(mnemonic: String): Boolean {
return false
}
private fun replaceMnemonic(raw: String, oldMnemonic: String, newMnemonic: String): String {
val trimmed = raw.trimStart()
val instruction = trimmed.instructionPart()
if(!instruction.startsWith(oldMnemonic))
return raw.replaceFirst(oldMnemonic, newMnemonic)
val instrStartInTrimmed = trimmed.indexOf(instruction)
if(instrStartInTrimmed<0) return raw.replaceFirst(oldMnemonic, newMnemonic)
val leadingLen = raw.length - trimmed.length
val idxInRaw = leadingLen + instrStartInTrimmed
return raw.substring(0, idxInRaw) + newMnemonic + raw.substring(idxInRaw + oldMnemonic.length)
}
/** Extracts the instruction part of a trimmed assembly line, skipping any label prefix.
* For "mylabel: lda #1" returns "lda #1". For "lda #1" returns "lda #1". */
private fun String.instructionPart(): String {
@@ -670,7 +685,8 @@ private fun optimizeJsrRtsAndOtherCombinations(linesByFour: Sequence<List<Trimme
}
else if (tfirst.hasInstr("jsr") && tsecond.hasInstr("rts")) {
if(!tfirst.contains("floats.pushFAC") && !tfirst.contains("floats.popFAC")) { // these 2 routines depend on being called with JSR!!
mods += Modification(lines[0].index, false, lines[0].value.replace("jsr", "jmp"))
val replacement = replaceMnemonic(lines[0].value, "jsr", "jmp")
mods += Modification(lines[0].index, false, replacement)
mods += Modification(lines[1].index, true, null)
}
}
@@ -747,7 +763,7 @@ private fun optimizeJsrRtsAndOtherCombinations(linesByFour: Sequence<List<Trimme
val firstMnemonic = tfirst.take(3)
branchInversions[firstMnemonic]?.let { invertedBranch ->
if (sameLabel(tfirst, tsecond, tthird)) {
val branch = lines[1].value.replace("jmp", invertedBranch)
val branch = replaceMnemonic(lines[1].value, "jmp", invertedBranch)
mods.add(Modification(lines[0].index, true, null))
mods.add(Modification(lines[1].index, false, branch))
}
@@ -1,12 +1,14 @@
package prog8.codegen.cpu6502
import prog8.code.ast.PtAsmSub
import prog8.code.ast.PtSubroutineParameter
import prog8.code.core.CombinedLongRegisters
import prog8.code.core.Cx16VirtualRegisters
import prog8.code.core.RegisterOrPair
import prog8.code.core.RegisterOrStatusflag
fun asmsub6502ArgsEvalOrder(sub: PtAsmSub): List<Int> {
fun asmsub6502ArgsEvalOrder(sub: PtAsmSub, argComplexity: List<Boolean>): List<Int> {
val order = mutableListOf<Int>()
// order is:
// 1) combined cx16 virtual registers (longs),
@@ -15,6 +17,8 @@ fun asmsub6502ArgsEvalOrder(sub: PtAsmSub): List<Int> {
// 4) single CPU registers (order Y,X,A),
// 5) floating point registers (FAC1, FAC2),
// 6) CPU Carry status flag
// Within each group, complex expressions are evaluated first (they may clobber registers,
// so running them before simple expressions avoids needing to save/restore already-loaded values).
val args = sub.parameters.withIndex()
val (combinedLongRegs, args1) = args.partition { it.value.first.registerOrPair in CombinedLongRegisters }
val (cx16regs, args2) = args1.partition { it.value.first.registerOrPair in Cx16VirtualRegisters }
@@ -23,12 +27,17 @@ fun asmsub6502ArgsEvalOrder(sub: PtAsmSub): List<Int> {
val (singleRegsMixed, rest) = args3.partition { it.value.first.registerOrPair != null }
val (singleCpuRegs, floatRegs) = singleRegsMixed.partition {it.value.first.registerOrPair != RegisterOrPair.FAC1 && it.value.first.registerOrPair != RegisterOrPair.FAC2 }
combinedLongRegs.forEach { order += it.index }
cx16regs.forEach { order += it.index }
pairedRegs.forEach { order += it.index }
singleCpuRegs.sortedBy { it.value.first.registerOrPair!!.asCpuRegister() }.asReversed().forEach { order += it.index }
combinedLongRegs.sortedByDescending { argComplexity[it.index] }.forEach { order += it.index }
cx16regs.sortedByDescending { argComplexity[it.index] }.forEach { order += it.index }
pairedRegs.sortedByDescending { argComplexity[it.index] }.forEach { order += it.index }
singleCpuRegs
.sortedWith(
compareByDescending<IndexedValue<Pair<RegisterOrStatusflag, PtSubroutineParameter>>> { argComplexity[it.index] }
.thenByDescending { it.value.first.registerOrPair!!.asCpuRegister() }
)
.forEach { order += it.index }
require(rest.all { it.value.first.registerOrPair==null && it.value.first.statusflag!=null})
floatRegs.forEach { order += it.index }
floatRegs.sortedByDescending { argComplexity[it.index] }.forEach { order += it.index }
rest.forEach { order += it.index }
require(order.size==sub.parameters.size)
@@ -136,9 +136,27 @@ internal class AssemblyProgram(
}
val proc = ProcessBuilder(assemblerCommand)
if(!options.quiet)
proc.inheritIO()
val process = proc.start()
.redirectErrorStream(true)
if (options.quiet) {
proc.redirectOutput(ProcessBuilder.Redirect.DISCARD)
}
val process = try {
proc.start()
} catch (e: Exception) {
System.err.println("daemon: assembler failed to start: ${e.message}")
return false
}
if (!options.quiet) {
process.inputStream.bufferedReader().use { reader ->
reader.forEachLine {
println(it)
}
}
}
val result = process.waitFor()
if (result == 0) {
removeGeneratedLabelsFromMonlist()
@@ -275,8 +275,14 @@ import prog8.codegen.cpu6502.assignment.*
asmgen.assignWordOperandsToAYAndVar(fcall.args[1], fcall.args[0], "P8ZP_SCRATCH_W1")
// math.divmod_uw_asm: -- divide two unsigned words (16 bit each) into 16 bit results
// input: P8ZP_SCRATCH_W1 in ZP: 16-bit number, A/Y: 16 bit divisor
// output: cx16.r15: 16-bit remainder, A/Y: 16 bit division result
// output: P8ZP_SCRATCH_W2: 16-bit remainder, A/Y: 16 bit division result
asmgen.out(" jsr prog8_math.divmod_uw_asm")
// copy remainder from P8ZP_SCRATCH_W2 to R15 (needed as second return value)
asmgen.out("""
ldx P8ZP_SCRATCH_W2
stx cx16.r15L
ldx P8ZP_SCRATCH_W2+1
stx cx16.r15H""".trimIndent())
return arrayOf(RegisterOrPair.AY, RegisterOrPair.R15)
}
@@ -396,14 +402,11 @@ import prog8.codegen.cpu6502.assignment.*
private fun funcCallFar(fcall: PtFunctionCall): Array<RegisterOrPair> {
val targetName = asmgen.options.compTarget.name
if(targetName !in arrayOf("cx16", "c64", "c128"))
throw AssemblyError("callfar only works on cx16, c64 and c128 targets at this time")
val jsrfar = when(targetName) {
"cx16" -> "cx16.JSRFAR"
"c64" -> "c64.x16jsrfar"
"c128" -> "c128.x16jsrfar"
else -> TODO("jsrfar routine")
else -> "$targetName.x16jsrfar"
}
val constBank = fcall.args[0].asConstInteger()
val constAddress = fcall.args[1].asConstInteger()
@@ -494,83 +497,115 @@ import prog8.codegen.cpu6502.assignment.*
val arg2 = fcall.args[1]
if(arg1.type.isByte) {
if(arg2.type.isByte) {
val op = if (arg1.type.isSigned || arg2.type.isSigned) "sec | sbc" else "cmp"
when (arg2) {
is PtIdentifier -> {
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.A)
asmgen.out(" cmp ${asmgen.asmVariableName(arg2)}")
asmgen.out(" $op ${asmgen.asmVariableName(arg2)}")
}
is PtNumber -> {
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.A)
asmgen.out(" cmp #${arg2.number.toInt()}")
asmgen.out(" $op #${arg2.number.toInt()}")
}
is PtBool -> {
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.A)
asmgen.out(" cmp #${arg2.asInt()}")
asmgen.out(" $op #${arg2.asInt()}")
}
is PtMemoryByte -> {
if(arg2.address is PtNumber) {
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.A)
asmgen.out(" cmp ${arg2.address.asConstInteger()!!.toHex()}")
asmgen.out(" $op ${arg2.address.asConstInteger()!!.toHex()}")
} else {
asmgen.assignByteOperandsToAAndVar(arg1, arg2, "P8ZP_SCRATCH_B1")
asmgen.out(" cmp P8ZP_SCRATCH_B1")
asmgen.out(" $op P8ZP_SCRATCH_B1")
}
}
else -> {
asmgen.assignByteOperandsToAAndVar(arg1, arg2, "P8ZP_SCRATCH_B1")
asmgen.out(" cmp P8ZP_SCRATCH_B1")
asmgen.out(" $op P8ZP_SCRATCH_B1")
}
}
} else
throw AssemblyError("args for cmp() should have same dt")
} else if(arg1.type.isWord) {
if(arg2.type.isWord) {
if(arg1.type.isSigned) {
if(arg1.type.isSigned || arg2.type.isSigned) {
when (arg2) {
is PtIdentifier -> {
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.AY)
val var2 = asmgen.asmVariableName(arg2)
asmgen.out("""
sec
sbc ${asmgen.asmVariableName(arg2)}
sbc $var2
sta P8ZP_SCRATCH_B1
tya
sbc ${asmgen.asmVariableName(arg2)}+1""")
sbc $var2+1
php
bne +
lda P8ZP_SCRATCH_B1
beq +
pla
and #%11111101
pha
+ plp""")
}
is PtBool -> TODO("word compare against bool ${arg2.position}")
is PtNumber -> {
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.AY)
val num = arg2.number.toInt()
asmgen.out("""
sec
sbc #<${arg2.number.toInt()}
sbc #<($num)
sta P8ZP_SCRATCH_B1
tya
sbc #>${arg2.number.toInt()}""")
sbc #>($num)
php
bne +
lda P8ZP_SCRATCH_B1
beq +
pla
and #%11111101
pha
+ plp""")
}
else -> {
asmgen.assignWordOperandsToAYAndVar(arg1, arg2, "P8ZP_SCRATCH_W1")
asmgen.out("""
sec
sbc P8ZP_SCRATCH_W1
sta P8ZP_SCRATCH_B1
tya
sbc P8ZP_SCRATCH_W1+1""")
sbc P8ZP_SCRATCH_W1+1
php
bne +
lda P8ZP_SCRATCH_B1
beq +
pla
and #%11111101
pha
+ plp""")
}
}
} else {
when (arg2) {
is PtIdentifier -> {
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.AY)
val var2 = asmgen.asmVariableName(arg2)
asmgen.out("""
cpy ${asmgen.asmVariableName(arg2)}+1
cpy $var2+1
bne +
cmp ${asmgen.asmVariableName(arg2)}
+""")
cmp $var2
+""")
}
is PtBool -> TODO("word compare against bool ${arg2.position}")
is PtNumber -> {
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.AY)
val num = arg2.number.toInt()
asmgen.out("""
cpy #>${arg2.number.toInt()}
cpy #>($num)
bne +
cmp #<${arg2.number.toInt()}
+""")
cmp #<($num)
+""")
}
else -> {
asmgen.assignWordOperandsToAYAndVar(arg1, arg2, "P8ZP_SCRATCH_W1")
@@ -578,7 +613,7 @@ import prog8.codegen.cpu6502.assignment.*
cpy P8ZP_SCRATCH_W1+1
bne +
cmp P8ZP_SCRATCH_W1
+""")
+""")
}
}
}
@@ -593,12 +628,25 @@ import prog8.codegen.cpu6502.assignment.*
sec
lda $var1
sbc #$${hex.substring(6, 8)}
sta P8ZP_SCRATCH_B1
lda $var1+1
sbc #$${hex.substring(4, 6)}
ora P8ZP_SCRATCH_B1
sta P8ZP_SCRATCH_B1
lda $var1+2
sbc #$${hex.substring(2, 4)}
ora P8ZP_SCRATCH_B1
sta P8ZP_SCRATCH_B1
lda $var1+3
sbc #$${hex.take(2)}""")
sbc #$${hex.take(2)}
php
bne +
lda P8ZP_SCRATCH_B1
beq +
pla
and #%11111101
pha
+ plp""")
} else if(arg1 is PtIdentifier && arg2 is PtIdentifier) {
val var1 = asmgen.asmVariableName(arg1)
val var2 = asmgen.asmVariableName(arg2)
@@ -606,26 +654,55 @@ import prog8.codegen.cpu6502.assignment.*
sec
lda $var1
sbc $var2
sta P8ZP_SCRATCH_B1
lda $var1+1
sbc $var2+1
ora P8ZP_SCRATCH_B1
sta P8ZP_SCRATCH_B1
lda $var1+2
sbc $var2+2
ora P8ZP_SCRATCH_B1
sta P8ZP_SCRATCH_B1
lda $var1+3
sbc $var2+3""")
sbc $var2+3
php
bne +
lda P8ZP_SCRATCH_B1
beq +
pla
and #%11111101
pha
+ plp""")
} else {
// cmp() doesn't return a value and as such can't be used in an expression, so no need to save the temp registers' original values
assignAsmGen.assignExpressionToRegister(arg2, RegisterOrPair.R14R15, true)
if(!assignAsmGen.isRightTrivial(arg1))
asmgen.pushLongRegisters(RegisterOrPair.R14R15, 1)
assignAsmGen.assignExpressionToRegister(arg1, RegisterOrPair.R12R13, true)
if(!assignAsmGen.isRightTrivial(arg1))
asmgen.popLongRegisters(RegisterOrPair.R14R15, 1)
asmgen.out("""
sec
lda cx16.r12
sbc cx16.r14
sta P8ZP_SCRATCH_B1
lda cx16.r12+1
sbc cx16.r14+1
ora P8ZP_SCRATCH_B1
sta P8ZP_SCRATCH_B1
lda cx16.r12+2
sbc cx16.r14+2
ora P8ZP_SCRATCH_B1
sta P8ZP_SCRATCH_B1
lda cx16.r12+3
sbc cx16.r14+3""")
sbc cx16.r14+3
php
bne +
lda P8ZP_SCRATCH_B1
beq +
pla
and #%11111101
pha
+ plp""")
}
} else
throw AssemblyError("args for cmp() should have same dt")
@@ -683,7 +760,7 @@ import prog8.codegen.cpu6502.assignment.*
when (what) {
is PtArrayIndexer -> {
if(what.variable==null)
TODO("ptr indexing ${what.position}")
TODO("ror2 byte ptr indexing ${what.position}")
asmgen.loadScaledArrayIndexIntoRegister(what, CpuRegister.X)
val varname = asmgen.asmVariableName(what.variable!!)
@@ -709,7 +786,7 @@ import prog8.codegen.cpu6502.assignment.*
when (what) {
is PtArrayIndexer -> {
if(what.variable==null)
TODO("ptr indexing ${what.position}")
TODO("ror2 word ptr indexing ${what.position}")
asmgen.loadScaledArrayIndexIntoRegister(what, CpuRegister.X)
val varname = asmgen.asmVariableName(what.variable!!)
if(what.splitWords)
@@ -761,7 +838,7 @@ import prog8.codegen.cpu6502.assignment.*
if(!what.index.isSimple()) asmgen.out(" plp")
if(what.variable==null)
TODO("ptr indexing ${what.position}")
TODO("ror byte ptr indexing ${what.position}")
val varname = asmgen.asmVariableName(what.variable!!)
asmgen.out(" ror ${varname},x")
}
@@ -803,7 +880,7 @@ import prog8.codegen.cpu6502.assignment.*
asmgen.loadScaledArrayIndexIntoRegister(what, CpuRegister.X)
if(!what.index.isSimple()) asmgen.out(" plp")
if(what.variable==null)
TODO("ptr indexing ${what.position}")
TODO("ror word ptr indexing ${what.position}")
val varname = asmgen.asmVariableName(what.variable!!)
if(what.splitWords)
asmgen.out(" ror ${varname}_msb,x | ror ${varname}_lsb,x")
@@ -870,7 +947,7 @@ import prog8.codegen.cpu6502.assignment.*
when (what) {
is PtArrayIndexer -> {
if(what.variable==null)
TODO("ptr indexing ${what.position}")
TODO("rol2 byte ptr indexing ${what.position}")
asmgen.loadScaledArrayIndexIntoRegister(what, CpuRegister.X)
val varname = asmgen.asmVariableName(what.variable!!)
asmgen.out(" lda ${varname},x | cmp #$80 | rol a | sta ${varname},x")
@@ -896,7 +973,7 @@ import prog8.codegen.cpu6502.assignment.*
is PtArrayIndexer -> {
asmgen.loadScaledArrayIndexIntoRegister(what, CpuRegister.X)
if(what.variable==null)
TODO("ptr indexing ${what.position}")
TODO("rol2 word ptr indexing ${what.position}")
val varname = asmgen.asmVariableName(what.variable!!)
if(what.splitWords)
asmgen.out(" asl ${varname}_lsb,x | rol ${varname}_msb,x | bcc + | inc ${varname}_lsb,x |+")
@@ -944,7 +1021,7 @@ import prog8.codegen.cpu6502.assignment.*
asmgen.loadScaledArrayIndexIntoRegister(what, CpuRegister.X)
if(!what.index.isSimple()) asmgen.out(" plp")
if(what.variable==null)
TODO("ptr indexing ${what.position}")
TODO("rol byte ptr indexing ${what.position}")
val varname = asmgen.asmVariableName(what.variable!!)
asmgen.out(" rol ${varname},x")
}
@@ -986,7 +1063,7 @@ import prog8.codegen.cpu6502.assignment.*
asmgen.loadScaledArrayIndexIntoRegister(what, CpuRegister.X)
if(!what.index.isSimple()) asmgen.out(" plp")
if(what.variable==null)
TODO("ptr indexing ${what.position}")
TODO("rol word ptr indexing ${what.position}")
val varname = asmgen.asmVariableName(what.variable!!)
if(what.splitWords)
asmgen.out(" rol ${varname}_lsb,x | rol ${varname}_msb,x")
@@ -1117,20 +1194,17 @@ import prog8.codegen.cpu6502.assignment.*
val arg = fcall.args.single()
if(arg is PtIdentifier) {
val varname = asmgen.asmVariableName(arg)
when(val dt = arg.type.base) {
BaseDataType.UBYTE -> asmgen.out(" lda $varname | jsr prog8_lib.func_sign_ub_into_A")
BaseDataType.BYTE -> asmgen.out(" lda $varname | jsr prog8_lib.func_sign_b_into_A")
BaseDataType.UWORD -> asmgen.out(" lda $varname+1 | jsr prog8_lib.func_sign_ub_into_A")
BaseDataType.WORD -> asmgen.out(" lda $varname+1 | jsr prog8_lib.func_sign_b_into_A")
BaseDataType.LONG -> asmgen.out(" lda $varname+3 | jsr prog8_lib.func_sign_b_into_A")
BaseDataType.FLOAT -> {
translateArguments(fcall, null, scope)
asmgen.out(" jsr floats.func_sign_f_into_A")
when(arg.type.base) {
BaseDataType.UBYTE -> {
asmgen.out(" lda $varname | jsr prog8_lib.func_sign_ub_into_A")
return arrayOf(RegisterOrPair.A)
}
else -> throw AssemblyError("weird type $dt")
BaseDataType.BYTE -> {
asmgen.out(" lda $varname | jsr prog8_lib.func_sign_b_into_A")
return arrayOf(RegisterOrPair.A)
}
else -> {}
}
// result in A
return arrayOf(RegisterOrPair.A)
}
when (val dt = arg.type.base) {
@@ -1846,7 +1920,7 @@ import prog8.codegen.cpu6502.assignment.*
if(tryOptimizedLoopCopyWithY("", "$address"))
return emptyArray()
asmgen.assignVariableToRegister(address.toString(), RegisterOrPair.R14R15, null, fcall.position)
asmgen.assignVariableToRegister(address.toString(), RegisterOrPair.R14R15, null, fcall.position, true)
return arrayOf(RegisterOrPair.R14R15)
}
@@ -1875,7 +1949,7 @@ import prog8.codegen.cpu6502.assignment.*
if(tryOptimizedLoopCopyWithY("lda #<$varname+$offset | sta P8ZP_SCRATCH_W1 | lda #>$varname+$offset | sta P8ZP_SCRATCH_W1+1", "(P8ZP_SCRATCH_W1)"))
return emptyArray()
asmgen.assignVariableToRegister("$varname+$offset", RegisterOrPair.R14R15, null, fcall.position)
asmgen.assignVariableToRegister("$varname+$offset", RegisterOrPair.R14R15, null, fcall.position, true)
return arrayOf(RegisterOrPair.R14R15)
} else if(result.second is PtIdentifier) {
val offsetname = asmgen.asmVariableName(result.second as PtIdentifier)
@@ -1901,25 +1975,60 @@ import prog8.codegen.cpu6502.assignment.*
private fun funcClamp(fcall: PtFunctionCall): Array<RegisterOrPair> {
val signed = fcall.type.isSigned
val zpCheck = { name: String -> asmgen.isZpVar(name) }
when {
fcall.type.isByte -> {
assignAsmGen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W1", fcall.args[1].type) // minimum
assignAsmGen.assignExpressionToVariable(fcall.args[2], "P8ZP_SCRATCH_W1+1", fcall.args[2].type) // maximum
assignAsmGen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A, signed) // value
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
assignAsmGen.assignExpressionToVariable(fcall.args[2], "P8ZP_SCRATCH_W1+1", fcall.args[2].type) // maximum
assignAsmGen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A, signed) // value
} else {
// any non-simple argument may use W1 as a temporary, so evaluate value and minimum
// first and preserve them while the maximum is evaluated
assignAsmGen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A, signed) // value first (may use W1 as temp)
asmgen.out(" pha")
assignAsmGen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W1", fcall.args[1].type) // minimum
asmgen.out(" lda P8ZP_SCRATCH_W1\n pha")
assignAsmGen.assignExpressionToVariable(fcall.args[2], "P8ZP_SCRATCH_W1+1", fcall.args[2].type) // maximum (may clobber W1)
asmgen.out(" pla\n sta P8ZP_SCRATCH_W1")
asmgen.out(" pla")
}
asmgen.out(" jsr prog8_lib.func_clamp_${fcall.type.toString().lowercase()}") // result in A
return arrayOf(RegisterOrPair.A)
}
fcall.type.isWord -> {
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
assignAsmGen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.AY, signed) // value
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
assignAsmGen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.AY, signed) // value
} else {
// any non-simple argument may use P8ZP_SCRATCH_W1/W2 as temporaries (e.g. via multiply_words/divmod_w_asm),
// so evaluate value and minimum first and preserve them while the maximum is evaluated
assignAsmGen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.AY, signed) // value
asmgen.out(" pha\n tya\n pha") // save AY on stack (A low, Y high)
assignAsmGen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W1", fcall.args[1].type) // minimum
asmgen.out(" lda P8ZP_SCRATCH_W1\n pha\n lda P8ZP_SCRATCH_W1+1\n pha") // save minimum
assignAsmGen.assignExpressionToVariable(fcall.args[2], "P8ZP_SCRATCH_W2", fcall.args[2].type) // maximum (may clobber W1)
asmgen.out(" pla\n sta P8ZP_SCRATCH_W1+1\n pla\n sta P8ZP_SCRATCH_W1") // restore minimum
asmgen.out(" pla\n tay\n pla") // restore AY
}
asmgen.out(" jsr prog8_lib.func_clamp_${fcall.type.toString().lowercase()}") // result in AY
return arrayOf(RegisterOrPair.AY)
}
fcall.type.isLong -> {
asmgen.assignExpressionToRegister(fcall.args[1], RegisterOrPair.R10R11, true) // low
asmgen.assignExpressionToRegister(fcall.args[2], RegisterOrPair.R12R13, true) // high
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.R14R15, true) // val
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck) && fcall.args[2].isSimple(zpCheck)) {
asmgen.assignExpressionToRegister(fcall.args[1], RegisterOrPair.R10R11, true) // low
asmgen.assignExpressionToRegister(fcall.args[2], RegisterOrPair.R12R13, true) // high
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.R14R15, true) // val
} else {
// same ordering issue: value may clobber R10R11/R12R13 via multiply_longs/div_longs
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.R14R15, true) // val first
asmgen.out(" lda cx16.r14\n pha\n lda cx16.r14+1\n pha\n lda cx16.r15\n pha\n lda cx16.r15+1\n pha")
asmgen.assignExpressionToRegister(fcall.args[1], RegisterOrPair.R10R11, true) // low
asmgen.assignExpressionToRegister(fcall.args[2], RegisterOrPair.R12R13, true) // high
asmgen.out(" pla\n sta cx16.r15+1\n pla\n sta cx16.r15\n pla\n sta cx16.r14+1\n pla\n sta cx16.r14")
}
asmgen.out(" jsr prog8_lib.func_clamp_long")
return arrayOf(RegisterOrPair.R14R15)
}
@@ -1929,11 +2038,20 @@ import prog8.codegen.cpu6502.assignment.*
private fun funcMin(fcall: PtFunctionCall): Array<RegisterOrPair> {
val signed = fcall.type.isSigned
val zpCheck = { name: String -> asmgen.isZpVar(name) }
when {
fcall.type.isByte -> {
if (signed) {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_B1", fcall.type) // left
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_REG", fcall.type) // right
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck)) {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_B1", fcall.type) // left
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_REG", fcall.type) // right
} else {
// left or right may use B1/REG as temp (e.g. multiply), so save left
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_B1", fcall.type) // left
asmgen.out(" lda P8ZP_SCRATCH_B1\n pha")
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_REG", fcall.type) // right (may clobber B1)
asmgen.out(" pla\n sta P8ZP_SCRATCH_B1")
}
asmgen.out("""
lda P8ZP_SCRATCH_B1
sec
@@ -1946,8 +2064,18 @@ import prog8.codegen.cpu6502.assignment.*
+ lda P8ZP_SCRATCH_B1
+""")
} else {
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_B1", fcall.type) // right
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A) // left
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck)) {
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_B1", fcall.type) // right
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A) // left
} else {
// right in B1, left in A - left may use B1 as temp, so save right
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_B1", fcall.type) // right
asmgen.out(" lda P8ZP_SCRATCH_B1\n pha")
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A) // left (may clobber B1)
asmgen.out(" pla\n sta P8ZP_SCRATCH_B1")
// now A holds left, need to compare again? Actually left already in A, right in B1 restored, so cmp is still valid
// Re-do cmp after restore is not needed, the following cmp uses P8ZP_SCRATCH_B1 correctly
}
asmgen.out(" cmp P8ZP_SCRATCH_B1")
asmgen.out(" bcc +")
asmgen.out("""
@@ -1957,8 +2085,15 @@ import prog8.codegen.cpu6502.assignment.*
return arrayOf(RegisterOrPair.A)
}
fcall.type.isWord -> {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_W1", fcall.type) // left
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W2", fcall.type) // right
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck)) {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_W1", fcall.type) // left
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W2", fcall.type) // right
} else {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_W1", fcall.type) // left
asmgen.out(" lda P8ZP_SCRATCH_W1\n pha\n lda P8ZP_SCRATCH_W1+1\n pha")
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W2", fcall.type) // right (may clobber W1)
asmgen.out(" pla\n sta P8ZP_SCRATCH_W1+1\n pla\n sta P8ZP_SCRATCH_W1")
}
if(signed) {
asmgen.out("""
lda P8ZP_SCRATCH_W1
@@ -1996,7 +2131,11 @@ import prog8.codegen.cpu6502.assignment.*
}
fcall.type.isLong -> {
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.R12R13, true) // left
if(!assignAsmGen.isRightTrivial(fcall.args[1]))
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 1)
asmgen.assignExpressionToRegister(fcall.args[1], RegisterOrPair.R14R15, true) // right
if(!assignAsmGen.isRightTrivial(fcall.args[1]))
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
asmgen.out("""
lda cx16.r12
cmp cx16.r14
@@ -2028,11 +2167,19 @@ import prog8.codegen.cpu6502.assignment.*
private fun funcMax(fcall: PtFunctionCall): Array<RegisterOrPair> {
val signed = fcall.type.isSigned
val zpCheck = { name: String -> asmgen.isZpVar(name) }
when {
fcall.type.isByte -> {
if (signed) {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_B1", fcall.type) // left
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_REG", fcall.type) // right
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck)) {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_B1", fcall.type) // left
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_REG", fcall.type) // right
} else {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_B1", fcall.type) // left
asmgen.out(" lda P8ZP_SCRATCH_B1\n pha")
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_REG", fcall.type) // right (may clobber B1)
asmgen.out(" pla\n sta P8ZP_SCRATCH_B1")
}
asmgen.out("""
lda P8ZP_SCRATCH_B1
sec
@@ -2045,8 +2192,15 @@ import prog8.codegen.cpu6502.assignment.*
+ lda P8ZP_SCRATCH_B1
+""")
} else {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_B1", fcall.type) // left
asmgen.assignExpressionToRegister(fcall.args[1], RegisterOrPair.A) // right
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck)) {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_B1", fcall.type) // left
asmgen.assignExpressionToRegister(fcall.args[1], RegisterOrPair.A) // right
} else {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_B1", fcall.type) // left
asmgen.out(" lda P8ZP_SCRATCH_B1\n pha")
asmgen.assignExpressionToRegister(fcall.args[1], RegisterOrPair.A) // right (may clobber B1)
asmgen.out(" pla\n sta P8ZP_SCRATCH_B1")
}
asmgen.out(" cmp P8ZP_SCRATCH_B1")
asmgen.out(" bcs +")
asmgen.out("""
@@ -2056,8 +2210,15 @@ import prog8.codegen.cpu6502.assignment.*
return arrayOf(RegisterOrPair.A)
}
fcall.type.isWord -> {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_W1", fcall.type) // left
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W2", fcall.type) // right
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck)) {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_W1", fcall.type) // left
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W2", fcall.type) // right
} else {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_W1", fcall.type) // left
asmgen.out(" lda P8ZP_SCRATCH_W1\n pha\n lda P8ZP_SCRATCH_W1+1\n pha")
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W2", fcall.type) // right (may clobber W1)
asmgen.out(" pla\n sta P8ZP_SCRATCH_W1+1\n pla\n sta P8ZP_SCRATCH_W1")
}
if (signed) {
asmgen.out("""
lda P8ZP_SCRATCH_W1
@@ -2095,7 +2256,11 @@ import prog8.codegen.cpu6502.assignment.*
}
fcall.type.isLong -> {
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.R12R13, true) // left
if(!assignAsmGen.isRightTrivial(fcall.args[1]))
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 1)
asmgen.assignExpressionToRegister(fcall.args[1], RegisterOrPair.R14R15, true) // right
if(!assignAsmGen.isRightTrivial(fcall.args[1]))
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
asmgen.out("""
lda cx16.r12
cmp cx16.r14
@@ -42,12 +42,129 @@ internal class ForLoopsAsmGen(
when {
iterableDt.isByteArray -> forOverNonconstByteRange(stmt, iterableDt, range)
iterableDt.isWordArray && !iterableDt.isSplitWordArray -> forOverNonconstWordRange(stmt, iterableDt, range)
else -> throw AssemblyError("range expression can only be byte or word")
iterableDt.isLongArray -> forOverNonconstLongRange(stmt, iterableDt, range)
else -> throw AssemblyError("range expression can only be byte, word or long")
}
asmgen.loopEndLabels.removeLast()
}
private fun forOverNonconstLongRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
val stepsize = range.step.asConstInteger()!!
val loopLabel = asmgen.makeLabel("for_loop")
val endLabel = asmgen.makeLabel("for_end")
asmgen.loopEndLabels.add(endLabel)
val varname = asmgen.asmVariableName(stmt.variable)
asmgen.assignExpressionToVariable(range.from, varname, iterableDt.elementType())
if (stepsize == 1 || stepsize == -1)
forOverLongsRangeStepOne(range, varname, iterableDt, loopLabel, endLabel, stmt)
else
forOverLongsRangeStepGreaterOne(range, varname, iterableDt, loopLabel, endLabel, stmt)
}
private fun forOverLongsRangeStepOne(range: PtRange, varname: String, iterableDt: DataType, loopLabel: String, endLabel: String, forloop: PtForLoop) {
val stepsize = range.step.asConstInteger()!!
// Use temporary variable to store the limit
val toValueVar = asmgen.createTempVarReused(iterableDt.elementType().base, false, range)
asmgen.assignExpressionToVariable(range.to, toValueVar, iterableDt.elementType())
if (stepsize == 1) {
// Increment toValueVar to make the loop inclusive (0 to N runs N+1 times)
asmgen.out("""
inc $toValueVar
bne +
inc $toValueVar+1
bne +
inc $toValueVar+2
bne +
inc $toValueVar+3
+
""")
} else {
// Decrement toValueVar to make the loop inclusive (N downto 0 runs N+1 times)
val label1 = asmgen.makeLabel("dec_1")
val label2 = asmgen.makeLabel("dec_2")
val label3 = asmgen.makeLabel("dec_3")
asmgen.out("""
lda $toValueVar
bne $label1
lda $toValueVar+1
bne $label2
lda $toValueVar+2
bne $label3
dec $toValueVar+3
$label3: dec $toValueVar+2
$label2: dec $toValueVar+1
$label1: dec $toValueVar
""")
}
asmgen.out(loopLabel)
asmgen.translate(forloop.statements)
if (stepsize == 1) {
asmgen.out("""
inc $varname
bne +
inc $varname+1
bne +
inc $varname+2
bne +
inc $varname+3
+ lda $varname
cmp $toValueVar
bne $loopLabel
lda $varname+1
cmp $toValueVar+1
bne $loopLabel
lda $varname+2
cmp $toValueVar+2
bne $loopLabel
lda $varname+3
cmp $toValueVar+3
bne $loopLabel
""")
} else {
asmgen.out("""
lda $varname
bne +++
lda $varname+1
bne ++
lda $varname+2
bne +
dec $varname+3
+ dec $varname+2
+ dec $varname+1
+ dec $varname
lda $varname
cmp $toValueVar
bne $loopLabel
lda $varname+1
cmp $toValueVar+1
bne $loopLabel
lda $varname+2
cmp $toValueVar+2
bne $loopLabel
lda $varname+3
cmp $toValueVar+3
bne $loopLabel
""")
}
asmgen.out(endLabel)
}
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)
}
private fun forOverNonconstByteRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
val stepsize = range.step.asConstInteger()!!
val loopLabel = asmgen.makeLabel("for_loop")
@@ -9,6 +9,16 @@ import prog8.codegen.cpu6502.assignment.AsmAssignment
import prog8.codegen.cpu6502.assignment.TargetStorageKind
/*
* CRITICAL: `inline asmsub` subroutines MUST be inlined at the call site.
* They are NOT called with jsr - their assembly body is copied directly into the caller.
* This means: no .proc/.pend, no rts, no jsr. The return value is left in the appropriate
* CPU register (A/X/Y) and the caller picks it up.
*
* This is handled at line 68: `if (sub.inline) { ... }`.
* Any new codegen backend MUST replicate this behavior.
*/
internal class FunctionCallAsmGen(private val program: PtProgram, private val asmgen: AsmGen6502Internal) {
internal fun translateFunctionCallStatement(stmt: PtFunctionCall) {
@@ -33,7 +43,7 @@ internal class FunctionCallAsmGen(private val program: PtProgram, private val as
// NOTE: does NOT output code to save/restore the X register for this call! Every caller should deal with this in their own way!!
// (you can use subroutine.shouldSaveX() and saveX()/restoreX() routines as a help for this)
val symbol = asmgen.symbolTable.lookup(call.name)!!
val symbol = asmgen.symbolTable.lookup(call.name) ?: asmgen.symbolTable.lookupUnscoped(call.name) ?: throw IllegalStateException("Symbol not found: ${call.name}")
if(symbol.type == StNodeType.LABEL) {
require(call.void)
asmgen.out(" jsr ${asmgen.asmSymbolName(symbol.scopedNameString)}")
@@ -44,6 +54,26 @@ internal class FunctionCallAsmGen(private val program: PtProgram, private val as
val subAsmName = asmgen.asmSymbolName(call.name)
if(sub is PtAsmSub) {
val varBank = sub.address?.varbank
var varbank: String? = null
var bankFixLabel: String? = null
if (varBank != null) {
// the bank variable can also be the name of a subroutine that should be called to return the bank byte dynamically
val bankSymbol = asmgen.symbolTable.lookup(varBank.name) ?: asmgen.symbolTable.lookupUnscoped(varBank.name)
if (bankSymbol?.type == StNodeType.SUBROUTINE || bankSymbol?.type == StNodeType.EXTSUB) {
val extsubId = asmgen.extsubCallSiteIds[sub]
if (extsubId != null) {
asmgen.out(" lda #${extsubId.toInt()}")
}
asmgen.out(" jsr ${asmgen.asmSymbolName(varBank.name)}")
// store bank byte directly into the JSRFAR instruction data (forward label reference)
bankFixLabel = asmgen.makeLabel("bank")
asmgen.out(" sta $bankFixLabel")
} else {
varbank = asmgen.asmVariableName(varBank)
}
}
argumentsViaRegisters(sub, call)
if (sub.inline) {
// inline the subroutine. (regardless of optimization settings!)
@@ -56,54 +86,57 @@ internal class FunctionCallAsmGen(private val program: PtProgram, private val as
} else {
val bank = sub.address?.constbank?.toString()
if(bank==null) {
val varbank = if(sub.address?.varbank==null) null else asmgen.asmVariableName(sub.address!!.varbank!!)
if(varbank!=null) {
if(varbank!=null || bankFixLabel!=null) {
if(asmgen.options.romable)
TODO("no codegen yet for non-const bank in subroutine call that's usable in ROM ${call.position}")
// variable bank case: save/restore registers around the self-modifying sta
if(varbank!=null) {
asmgen.out("""
php
pha
lda $varbank
sta +
pla
plp""")
}
val label = bankFixLabel ?: "+"
// self-modifying code: set jsrfar bank argument
when(asmgen.options.compTarget.name) {
"cx16" -> {
// JSRFAR can jump to a banked RAM address as well!
asmgen.out("""
php
pha
lda $varbank
sta +
pla
plp
jsr cx16.JSRFAR
.word $subAsmName ; ${sub.address!!.address.toHex()}
+ .byte 0 ; modified"""
$label .byte 0 ; modified"""
)
}
"c64" -> {
asmgen.out("""
php
pha
lda $varbank
sta +
pla
plp
jsr c64.x16jsrfar
.word $subAsmName ; ${sub.address!!.address.toHex()}
+ .byte 0 ; modified"""
$label .byte 0 ; modified"""
)
}
"c128" -> {
asmgen.out("""
php
pha
lda $varbank
sta +
pla
plp
jsr c128.x16jsrfar
.word $subAsmName ; ${sub.address!!.address.toHex()}
+ .byte 0 ; modified"""
$label .byte 0 ; modified"""
)
}
else -> throw AssemblyError("callfar is not supported on the selected compilation target")
else -> {
if(asmgen.options.compTarget.supportsBankedCalls) {
val targetName = asmgen.options.compTarget.name
asmgen.out("""
jsr $targetName.x16jsrfar
.word $subAsmName ; ${sub.address!!.address.toHex()}
$label .byte 0 ; modified"""
)
} else
throw AssemblyError("callfar is not supported on the selected compilation target")
}
}
} else {
asmgen.out(" jsr $subAsmName")
@@ -133,7 +166,17 @@ internal class FunctionCallAsmGen(private val program: PtProgram, private val as
.byte $bank"""
)
}
else -> throw AssemblyError("callfar is not supported on the selected compilation target")
else -> {
if(asmgen.options.compTarget.supportsBankedCalls) {
val targetName = asmgen.options.compTarget.name
asmgen.out("""
jsr $targetName.x16jsrfar
.word $subAsmName ; ${sub.address!!.address.toHex()}
.byte $bank"""
)
} else
throw AssemblyError("callfar is not supported on the selected compilation target")
}
}
}
}
@@ -234,7 +277,8 @@ internal class FunctionCallAsmGen(private val program: PtProgram, private val as
if(sub.parameters.size==1) {
argumentViaRegister(sub, IndexedValue(0, sub.parameters.single().second), call.args[0])
} else {
val optimalEvalOrder = asmsub6502ArgsEvalOrder(sub)
val argComplexity = call.args.map { usesOtherRegistersWhileEvaluating(it) }
val optimalEvalOrder = asmsub6502ArgsEvalOrder(sub, argComplexity)
optimalEvalOrder.forEach {
val param = sub.parameters[it]
val arg = call.args[it]
@@ -343,7 +387,7 @@ internal class FunctionCallAsmGen(private val program: PtProgram, private val as
AsmAssignTarget.fromRegisters(register, parameter.value.type.isSigned, value.position, scope, asmgen)
}
val src = if(value.type.isPassByRef) {
if(value is PtIdentifier) {
if(value is PtIdentifier || value is PtPointerDeref) {
val addr = PtAddressOf(value.type.typeForAddressOf(false), false, value.position)
addr.add(value)
addr.parent = scope as PtNode
@@ -382,6 +426,9 @@ internal class FunctionCallAsmGen(private val program: PtProgram, private val as
if(argType.isUnsignedWord && paramType.isString)
return true
if (argType.isArray && (paramType.isString || paramType.isPointerToByte))
return true
return false
}
}
@@ -1580,7 +1580,13 @@ internal class IfElseAsmGen(private val program: PtProgram,
lda $opL+3
sbc $varR+3""")
} else {
if (varL == null && !assignmentAsmGen.isRightTrivial(r)) {
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 1)
}
asmgen.assignExpressionToRegister(r, RegisterOrPair.R14R15, true)
if (varL == null && !assignmentAsmGen.isRightTrivial(r)) {
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
}
asmgen.out("""
sec
lda $opL
@@ -83,7 +83,7 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
asmgen.out(endLabel)
assignmentAsmGen.assignRegisterByte(target, CpuRegister.A, false, false)
}
expr.type.isWord || expr.type.isString -> {
expr.type.isWord || expr.type.isString || expr.type.isPointer -> {
asmgen.assignExpressionToRegister(expr.falsevalue, RegisterOrPair.AY)
asmgen.jmp(endLabel)
asmgen.out(trueLabel)
@@ -656,7 +656,13 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
lda $opL+3
sbc $varR+3""")
} else {
if (varL == null && !assignmentAsmGen.isRightTrivial(r)) {
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 1)
}
asmgen.assignExpressionToRegister(r, RegisterOrPair.R14R15, true)
if (varL == null && !assignmentAsmGen.isRightTrivial(r)) {
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
}
asmgen.out("""
sec
lda $opL
@@ -372,13 +372,14 @@ internal class ProgramAndVarsGen(
}
private fun asmTypeString(dt: DataType): String {
val effectiveDt = if(dt.isArray) dt.elementType() else dt
return when {
dt.isBool || dt.isUnsignedByte -> ".byte"
dt.isSignedByte -> ".char"
dt.isUnsignedWord || dt.isPointer -> ".word"
dt.isSignedWord -> ".sint"
dt.isLong -> ".dint"
dt.isFloat -> ".byte"
effectiveDt.isBool || effectiveDt.isUnsignedByte -> ".byte"
effectiveDt.isSignedByte -> ".char"
effectiveDt.isUnsignedWord || effectiveDt.isPointer -> ".word"
effectiveDt.isSignedWord -> ".sint"
effectiveDt.isLong -> ".dint"
effectiveDt.isFloat -> ".byte"
else -> {
throw AssemblyError("weird dt")
}
@@ -387,42 +388,56 @@ internal class ProgramAndVarsGen(
private fun structInstances2asm() {
fun initValues(instance: StStructInstance): List<String> {
val structtype: StStruct = symboltable.lookup(instance.structName) as StStruct
return structtype.fields.zip(instance.initialValues).map { (field, value) ->
if(field.first.isFloat) {
val numValue = (value as? StArrayElement.Number)?.value
?: throw AssemblyError("expected number for float field")
"["+compTarget.getFloatAsmBytes(numValue)+"]"
} else {
when (value) {
is StArrayElement.Number -> {
if(field.first.isPointer)
"$"+value.value.toInt().toString(16)
else if(field.first.isInteger)
value.value.toInt().toString()
else
value.value.toString()
}
is StArrayElement.AddressOf -> value.symbol
is StArrayElement.MemorySlab -> "$StMemorySlabBlockName.${value.name}"
is StArrayElement.BoolValue -> if(value.value) "1" else "0"
is StArrayElement.StructInstance -> throw AssemblyError("struct instance in struct initializer")
}
fun formatValue(value: StArrayElement, dt: DataType): String {
if(dt.isFloat) {
val numValue = (value as? StArrayElement.Number)?.value
?: throw AssemblyError("expected number for float field")
return "["+compTarget.getFloatAsmBytes(numValue)+"]"
}
return when (value) {
is StArrayElement.Number -> {
if(dt.isPointer)
"$"+value.value.toInt().toString(16)
else if(dt.isInteger)
value.value.toInt().toString()
else
value.value.toString()
}
is StArrayElement.AddressOf -> value.symbol
is StArrayElement.MemorySlab -> "$StMemorySlabBlockName.${value.name}"
is StArrayElement.BoolValue -> if(value.value) "1" else "0"
is StArrayElement.StructInstance -> throw AssemblyError("struct instance in struct initializer")
}
}
fun flatInitValues(instance: StStructInstance, structtype: StStruct): List<String> {
var valueIndex = 0
return structtype.fields.flatMap { field ->
val arraySz = field.arraySize
val count = arraySz ?: 1
val elemDt = if(arraySz!=null) field.type.elementType() else field.type
(0 until count).map {
val value = instance.initialValues[valueIndex++]
formatValue(value, elemDt)
}
}
}
asmgen.out("; struct types")
symboltable.allStructTypes().distinctBy { it.name }.forEach { structtype ->
val structargs = structtype.fields.withIndex().joinToString(",") { field -> "f${field.index}" }
val paramFields = structtype.fields.filter { !it.isArray }
val structargs = paramFields.indices.joinToString(",") { "f$it" }
asmgen.out("${structtype.scopedNameString} .struct $structargs\n")
structtype.fields.withIndex().forEach { (index, field) ->
val dt = field.first
val varname = "f${index}"
val type = asmTypeString(dt)
asmgen.out("p8v_${field.second} $type \\$varname") // note: struct field symbol prefixing done here because that is a lot simpler than fixing up all expressions in the AST
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++
}
}
asmgen.out(" .endstruct\n")
}
@@ -433,14 +448,7 @@ internal class ProgramAndVarsGen(
asmgen.out("${StStructInstanceBlockName}_bss .block\n")
instancesNoInit.forEach {
val structtype: StStruct = symboltable.lookup(it.structName) as StStruct
val zerovalues = structtype.fields.map { field ->
if(field.first.isFloat) {
val floatbytes = List(compTarget.memorySize(BaseDataType.FLOAT)) { "?" }
"[${floatbytes.joinToString(",")}]"
}
else "?"
}
asmgen.out("${it.name} .dstruct ${it.structName}, ${zerovalues.joinToString(",")}\n")
asmgen.out("${it.name} .fill ${structtype.size}\n")
}
asmgen.out(" .endblock\n")
asmgen.out(" .send BSS\n")
@@ -448,9 +456,28 @@ internal class ProgramAndVarsGen(
asmgen.out("; struct instances with initialization values\n")
asmgen.out(" .section STRUCTINSTANCES\n")
asmgen.out("$StStructInstanceBlockName .block\n")
instances.forEach {
val instancename = it.name.substringAfter('.')
asmgen.out("$instancename .dstruct ${it.structName}, ${initValues(it).joinToString(",")}\n")
instances.forEach { instance ->
val instancename = instance.name.substringAfter('.')
val structtype: StStruct = symboltable.lookup(instance.structName) as StStruct
val values = flatInitValues(instance, structtype)
val hasArrays = structtype.fields.any { fld -> fld.isArray }
if(hasArrays) {
asmgen.out(instancename)
var valueIndex = 0
for(field in structtype.fields) {
val arraySz = field.arraySize
val count = arraySz ?: 1
val elemDt = if(arraySz!=null) field.type.elementType() else field.type
val type = asmTypeString(elemDt)
val fieldValues = (0 until count).joinToString(",") {
formatValue(instance.initialValues[valueIndex++], elemDt)
}
asmgen.out(" $type $fieldValues")
}
asmgen.out("\n")
} else {
asmgen.out("$instancename .dstruct ${instance.structName}, ${values.joinToString(",")}\n")
}
}
asmgen.out(" .endblock\n")
asmgen.out(" .send STRUCTINSTANCES\n")
@@ -617,18 +644,26 @@ internal class ProgramAndVarsGen(
arrayVarsWithInitInZp.forEach {
val size = it.alloc.size
val name = asmgen.asmVariableName(it.name)
asmgen.out("""
lda #<${name}_init_value
ldy #>${name}_init_value
sta cx16.r0
sty cx16.r0+1
lda #<${name}
ldy #>${name}
sta cx16.r1
sty cx16.r1+1
lda #<$size
ldy #>$size
jsr sys.memcopy""")
if(it.alloc.dt.isSplitWordArray) {
val halfSize = size / 2
if(halfSize>0) {
asmgen.out("""
ldx #${halfSize-1}
- lda ${name}_init_value_lsb,x
sta ${name}_lsb,x
lda ${name}_init_value_msb,x
sta ${name}_msb,x
dex
bpl -""")
}
} else {
asmgen.out("""
ldx #${size-1}
- lda ${name}_init_value,x
sta ${name},x
dex
bpl -""")
}
}
asmgen.out(" jmp +")
}
@@ -874,14 +909,14 @@ internal class ProgramAndVarsGen(
dt.isSplitWordArray -> {
if(dt.elementType().isUnsignedWord || dt.elementType().isPointer) {
val data = makeArrayFillDataUnsigned(dt, value, orNumberOfZeros)
asmgen.out("_array_$varname := ${data.joinToString()}")
asmgen.out("${varname}_lsb\t.byte <_array_$varname")
asmgen.out("${varname}_msb\t.byte >_array_$varname")
asmgen.out("${varname}_words := ${data.joinToString()}")
asmgen.out("${varname}_lsb\t.byte <${varname}_words")
asmgen.out("${varname}_msb\t.byte >${varname}_words")
} else {
val data = makeArrayFillDataSigned(dt, value, orNumberOfZeros)
asmgen.out("_array_$varname := ${data.joinToString()}")
asmgen.out("${varname}_lsb\t.byte <_array_$varname")
asmgen.out("${varname}_msb\t.byte >_array_$varname")
asmgen.out("${varname}_words := ${data.joinToString()}")
asmgen.out("${varname}_lsb\t.byte <${varname}_words")
asmgen.out("${varname}_msb\t.byte >${varname}_words")
}
}
dt.isUnsignedWordArray -> {
@@ -3,17 +3,21 @@ package prog8.codegen.cpu6502
import com.github.michaelbull.result.fold
import com.github.michaelbull.result.onOk
import prog8.code.*
import prog8.code.ast.*
import prog8.code.core.*
internal class VariableAllocator(private val symboltable: SymbolTable,
private val options: CompilationOptions,
private val errors: IErrorReporter
private val errors: IErrorReporter,
private val program: PtProgram
) {
private val zeropage = options.compTarget.zeropage
internal val globalFloatConsts = mutableMapOf<Double, String>() // all float values in the entire program (value -> varname)
internal val zeropageVars: Map<String, MemoryAllocator.VarAllocation>
private val allStaticVariables by lazy { collectAllVariables(symboltable) }
private val usageScores by lazy { computeUsageScores(allStaticVariables) }
init {
allocateZeropageVariables()
@@ -46,7 +50,7 @@ internal class VariableAllocator(private val symboltable: SymbolTable,
if(options.zeropage== ZeropageType.DONTUSE)
return
val allVariables = collectAllVariables(symboltable)
val allVariables = allStaticVariables
val numberOfAllocatableVariables = allVariables.size
val varsRequiringZp = allVariables.filter { it.zpwish == ZeropageWish.REQUIRE_ZEROPAGE }
@@ -90,9 +94,9 @@ internal class VariableAllocator(private val symboltable: SymbolTable,
}
// try to allocate the "don't care" interger variables into the zeropage until it is full.
// TODO some form of intelligent priorization? most often used variables first? loopcounter vars first? ...? Simply sorting by size (bytes prioritized) has BAD results
// Prioritize by usage frequency: sort by computed hotness score (access frequency × type weight × loop depth)
if(errors.noErrors()) {
val sortedList = varsDontCareWithoutAlignment.sortedByDescending { it.scopedNameString }
val sortedList = varsDontCareWithoutAlignment.sortedByDescending { usageScores[it.scopedNameString] ?: 0 }
for (variable in sortedList) {
if(variable.dt.isIntegerOrBool || variable.dt.isPointer) {
if(zeropage.free.isEmpty()) {
@@ -129,4 +133,33 @@ internal class VariableAllocator(private val symboltable: SymbolTable,
collect(st)
return vars.sortedBy { it.dt.base }
}
private fun computeUsageScores(vars: Collection<StStaticVariable>): Map<String, Int> {
val scores = mutableMapOf<String, Int>()
val varInfo = vars.associateBy { it.scopedNameString }
var loopDepth = 0
fun walk(node: PtNode) {
val isLoopEnter = node is PtForLoop || node is PtRepeatLoop
if(isLoopEnter) loopDepth++
if(node is PtIdentifier) {
val v = varInfo[node.name]
if(v != null) {
val weight = when {
v.dt.isPointer || v.dt.isWord -> 2
else -> 1
}
scores[node.name] = (scores[node.name] ?: 0) + weight * (loopDepth + 1)
}
}
node.children.forEach { walk(it) }
if(isLoopEnter) loopDepth--
}
walk(program)
return scores
}
}
@@ -125,10 +125,13 @@ internal class AnyExprAsmGen(
}
private fun assignFloatBinExpr(expr: PtBinaryExpression, assign: AsmAssignment): Boolean {
// C64/PET32 ROM T-variant entries skip CONUPK and need arisgn+Z flag set up beforehand.
// CX16 ROM entries are already fixed and handle this internally.
val tSuffix = if(asmgen.options.compTarget.name in listOf(C64Target.NAME, PETTarget.NAME)) "_NZ" else ""
when(expr.operator) {
"+" -> {
assignFloatOperandsToFACandARG(expr.left, expr.right)
asmgen.out(" jsr floats.FADDT")
asmgen.out(" jsr floats.FADDT$tSuffix")
asmgen.assignRegister(RegisterOrPair.FAC1, assign.target)
return true
}
@@ -140,13 +143,13 @@ internal class AnyExprAsmGen(
}
"*" -> {
assignFloatOperandsToFACandARG(expr.left, expr.right)
asmgen.out(" jsr floats.FMULTT")
asmgen.out(" jsr floats.FMULTT$tSuffix")
asmgen.assignRegister(RegisterOrPair.FAC1, assign.target)
return true
}
"/" -> {
assignFloatOperandsToFACandARG(expr.right, expr.left)
asmgen.out(" jsr floats.FDIVT")
asmgen.out(" jsr floats.FDIVT$tSuffix")
asmgen.assignRegister(RegisterOrPair.FAC1, assign.target)
return true
}
@@ -18,6 +18,9 @@ internal class AssignmentAsmGen(
) {
lateinit var augmentableAsmGen: AugmentableAssignmentAsmGen
internal fun isRightTrivial(expr: PtExpression): Boolean =
expr is PtNumber || expr is PtBool || expr is PtIdentifier
fun translate(assignment: PtAssignment) {
val target = AsmAssignTarget.fromAstAssignment(assignment.target, assignment.definingISub(), asmgen)
val source = AsmAssignSource.fromAstSource(assignment.value, program, asmgen).adjustSignedUnsigned(target)
@@ -224,7 +227,7 @@ internal class AssignmentAsmGen(
}
targetDt.isByte -> assignVariableByte(assign.target, variable)
targetDt.isSignedWord -> assignVariableWord(assign.target, variable, assign.source.datatype)
targetDt.isUnsignedWord -> {
targetDt.isUnsignedWord || targetDt.isPointer -> {
if(assign.source.datatype.isPassByRef)
assignAddressOf(assign.target, variable, false, assign.source.datatype, assign.source.array ?: PtNumber(BaseDataType.UBYTE, 0.0, assign.position))
else
@@ -233,7 +236,6 @@ internal class AssignmentAsmGen(
targetDt.isLong -> assignVariableLong(assign.target, variable, assign.source.datatype)
targetDt.isFloat -> assignVariableFloat(assign.target, variable)
targetDt.isString -> assignVariableString(assign.target, variable)
targetDt.isPointer -> assignVariableWord(assign.target, variable, assign.source.datatype)
else -> throw AssemblyError("unsupported assignment target type ${assign.target.datatype} ${assign.position}")
}
}
@@ -242,13 +244,217 @@ internal class AssignmentAsmGen(
val elementDt = assign.source.datatype
val valueVar = value.variable
if(valueVar==null) {
TODO("translate assignment of indexed ${elementDt} pointer ${value.position}")
val pointerDeref = value.pointerderef!!
val constIndex = value.index.asConstInteger()
val eltSize = program.memsizer.memorySize(elementDt, null)
if(constIndex != null) {
val (zpPtrVar, structOffset) = pointergen.deref(pointerDeref, false)
val totalOffset = eltSize * constIndex + structOffset.toInt()
when {
elementDt.isByteOrBool -> {
if(totalOffset > 255) {
if(zpPtrVar.startsWith("P8ZP_SCRATCH_")) {
asmgen.out(" lda $zpPtrVar+1 | clc | adc #>($totalOffset) | sta $zpPtrVar+1 | ldy #<($totalOffset) | lda ($zpPtrVar),y")
} else {
asmgen.out(" lda $zpPtrVar | sta P8ZP_SCRATCH_PTR | lda $zpPtrVar+1 | clc | adc #>($totalOffset) | sta P8ZP_SCRATCH_PTR+1 | ldy #<($totalOffset) | lda (P8ZP_SCRATCH_PTR),y")
}
} else {
asmgen.loadIndirectByte(zpPtrVar, totalOffset.toUByte())
}
assignRegisterByte(assign.target, CpuRegister.A, elementDt.isSigned, false)
}
elementDt.isWord -> {
if(totalOffset > 255) {
val yOffset = totalOffset and 0xff
if(zpPtrVar.startsWith("P8ZP_SCRATCH_")) {
asmgen.out("""
lda $zpPtrVar+1
clc
adc #>($totalOffset)
sta $zpPtrVar+1
ldy #$yOffset
lda ($zpPtrVar),y
tax
iny
lda ($zpPtrVar),y
tay
txa""")
} else {
asmgen.out("""
lda $zpPtrVar
sta P8ZP_SCRATCH_PTR
lda $zpPtrVar+1
clc
adc #>($totalOffset)
sta P8ZP_SCRATCH_PTR+1
ldy #$yOffset
lda (P8ZP_SCRATCH_PTR),y
tax
iny
lda (P8ZP_SCRATCH_PTR),y
tay
txa""")
}
} else {
asmgen.loadIndirectWordAY(zpPtrVar, totalOffset.toUByte())
}
assignRegisterpairWord(assign.target, RegisterOrPair.AY)
}
elementDt.isLong -> {
if(totalOffset > 255) {
val yOffset = totalOffset and 0xff
if(zpPtrVar.startsWith("P8ZP_SCRATCH_")) {
asmgen.out("""
lda $zpPtrVar+1
clc
adc #>($totalOffset)
sta $zpPtrVar+1
ldy #$yOffset
lda ($zpPtrVar),y
sta P8ZP_SCRATCH_W1
iny
lda ($zpPtrVar),y
sta P8ZP_SCRATCH_W1+1
iny
lda ($zpPtrVar),y
sta P8ZP_SCRATCH_W1+2
iny
lda ($zpPtrVar),y
sta P8ZP_SCRATCH_W1+3""")
} else {
asmgen.out("""
lda $zpPtrVar
sta P8ZP_SCRATCH_PTR
lda $zpPtrVar+1
clc
adc #>($totalOffset)
sta P8ZP_SCRATCH_PTR+1
ldy #$yOffset
lda (P8ZP_SCRATCH_PTR),y
sta P8ZP_SCRATCH_W1
iny
lda (P8ZP_SCRATCH_PTR),y
sta P8ZP_SCRATCH_W1+1
iny
lda (P8ZP_SCRATCH_PTR),y
sta P8ZP_SCRATCH_W1+2
iny
lda (P8ZP_SCRATCH_PTR),y
sta P8ZP_SCRATCH_W1+3""")
}
} else {
asmgen.out("""
ldy #$totalOffset
lda ($zpPtrVar),y
sta P8ZP_SCRATCH_W1
iny
lda ($zpPtrVar),y
sta P8ZP_SCRATCH_W1+1
iny
lda ($zpPtrVar),y
sta P8ZP_SCRATCH_W1+2
iny
lda ($zpPtrVar),y
sta P8ZP_SCRATCH_W1+3""")
}
assignVariableLong(assign.target, "P8ZP_SCRATCH_W1", DataType.LONG)
}
elementDt.isFloat -> {
asmgen.out("""
lda $zpPtrVar
clc
adc #<($totalOffset)
pha
lda $zpPtrVar+1
adc #>($totalOffset)
tay
pla""")
assignFloatFromAY(assign.target)
}
else ->
throw AssemblyError("weird array element type for pointer indexed read $elementDt")
}
} else {
val (zpPtrVar, _) = pointergen.deref(pointerDeref, addOffsetToPointer = true)
when {
elementDt.isByteOrBool -> {
val idx = value.index
if(idx is PtIdentifier) {
if(eltSize > 1) {
TODO("non-byte element size $eltSize for pointer array index read with variable index ${value.position}")
}
val indexVarName = asmgen.asmVariableName(idx)
asmgen.out(" ldy $indexVarName | lda ($zpPtrVar),y")
} else if(idx.type.isByte) {
asmgen.pushCpuStack(BaseDataType.UBYTE, idx)
asmgen.restoreRegisterStack(CpuRegister.Y, false)
asmgen.out(" lda ($zpPtrVar),y")
} else {
asmgen.pushCpuStack(BaseDataType.UWORD, idx)
asmgen.out(" pla | clc | adc $zpPtrVar+1 | sta $zpPtrVar+1")
if(asmgen.isTargetCpu(CpuType.CPU65C02)) asmgen.out(" ply") else asmgen.out(" pla | tay")
asmgen.out(" lda ($zpPtrVar),y")
}
assignRegisterByte(assign.target, CpuRegister.A, elementDt.isSigned, true)
}
elementDt.isWord -> {
val idx = value.index
if(idx is PtIdentifier) {
if(eltSize > 1) {
TODO("non-byte element size $eltSize for pointer array index read with variable index ${value.position}")
}
val indexVarName = asmgen.asmVariableName(idx)
asmgen.out("""
ldy $indexVarName
lda ($zpPtrVar),y
tax
iny
lda ($zpPtrVar),y
tay
txa""")
} else if(idx.type.isByte) {
asmgen.pushCpuStack(BaseDataType.UBYTE, idx)
asmgen.restoreRegisterStack(CpuRegister.Y, false)
asmgen.out("""
lda ($zpPtrVar),y
tax
iny
lda ($zpPtrVar),y
tay
txa""")
} else {
asmgen.pushCpuStack(BaseDataType.UWORD, idx)
asmgen.out(" pla | clc | adc $zpPtrVar+1 | sta $zpPtrVar+1")
if(asmgen.isTargetCpu(CpuType.CPU65C02)) asmgen.out(" ply") else asmgen.out(" pla | tay")
asmgen.out("""
lda ($zpPtrVar),y
tax
iny
lda ($zpPtrVar),y
tay
txa""")
}
assignRegisterpairWord(assign.target, RegisterOrPair.AY)
}
elementDt.isLong -> {
TODO("variable index pointer array read of long ${value.position}")
}
elementDt.isFloat -> {
TODO("variable index pointer array read of float ${value.position}")
}
else ->
throw AssemblyError("weird array element type for pointer indexed read $elementDt")
}
}
return
}
val arrayVarName = asmgen.asmVariableName(valueVar)
if(valueVar.type.isPointer) {
pointergen.assignIndexedPointer(assign.target, arrayVarName, value, valueVar.type)
pointergen.assignIndexedPointer(assign.target, arrayVarName, value)
return
}
@@ -490,13 +696,7 @@ internal class AssignmentAsmGen(
val identifier = value.identifier!!
val source = asmgen.symbolTable.lookup(identifier.name)
require(source !is StConstant) { "addressOf of a constant should have been rewritten to a simple addition expression" }
val sourceName =
if (value.isMsbForSplitArray)
asmgen.asmSymbolName(identifier) + "_msb"
else if (identifier.type.isSplitWordArray)
asmgen.asmSymbolName(identifier) + "_lsb" // the _lsb split array comes first in memory
else
asmgen.asmSymbolName(identifier)
val sourceName = asmgen.asmSymbolName(identifier)
assignAddressOf(assign.target, sourceName, value.isMsbForSplitArray, identifier.type, value.arrayIndexExpr)
} else {
val ptrderef = value.dereference!!
@@ -867,12 +1067,8 @@ internal class AssignmentAsmGen(
return if(translatedOk)
true
else {
if(expr.type.isLong && expr.operator in "/%")
TODO("long division - use floats for now ${expr.position} ")
else
anyExprGen.assignAnyExpressionUsingStack(expr, assign)
}
}
private fun optimizedComparison(expr: PtBinaryExpression, assign: AsmAssignment): Boolean {
@@ -1161,6 +1357,11 @@ internal class AssignmentAsmGen(
// assignVariableWord(target, "cx16.r15", DataType.WORD)
// return true
}
expr.type.isLong -> {
asmgen.errors.err("remainder can only be used on unsigned integer operands on 6502 target for now", expr.right.position)
return true
// TODO implement the signed long remainder asm routine
}
else -> return false
}
}
@@ -1198,6 +1399,17 @@ internal class AssignmentAsmGen(
assignRegisterpairWord(target, RegisterOrPair.AY)
return true
}
expr.type.isLong -> {
asmgen.assignExpressionToRegister(expr.left, RegisterOrPair.R12R13, true)
if(!isRightTrivial(expr.right))
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 1)
asmgen.assignExpressionToRegister(expr.right, RegisterOrPair.R14R15, true)
if(!isRightTrivial(expr.right))
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
asmgen.out(" jsr prog8_math.div_longs")
assignRegisterLong(target, RegisterOrPair.R14R15)
return true
}
else -> return false
}
}
@@ -1242,7 +1454,11 @@ internal class AssignmentAsmGen(
}
expr.type.isLong -> {
asmgen.assignExpressionToRegister(expr.left, RegisterOrPair.R12R13, true)
if(!isRightTrivial(expr.right))
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 1)
asmgen.assignExpressionToRegister(expr.right, RegisterOrPair.R14R15, true)
if(!isRightTrivial(expr.right))
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
asmgen.out(" jsr prog8_math.multiply_longs")
assignRegisterLong(target, RegisterOrPair.R14R15)
return true
@@ -2015,6 +2231,12 @@ internal class AssignmentAsmGen(
}
private fun optimizedPlusMinLongExpr(expr: PtBinaryExpression, target: AsmAssignTarget): Boolean {
// Generic fallback for indexed pointer arrays: route to generic codegen (less efficient but avoids missing specialized cases)
if(target.kind==TargetStorageKind.ARRAY && target.array?.pointerderef!=null) {
asmgen.assignExpressionToRegister(expr, RegisterOrPair.R12R13, true)
assignRegisterLong(target, RegisterOrPair.R12R13)
return true
}
val left = expr.left
val right = expr.right
when(right) {
@@ -2179,13 +2401,17 @@ internal class AssignmentAsmGen(
else
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 2)
assignExpressionToRegister(expr.left, RegisterOrPair.R14R15, expr.left.type.isSigned)
// save left operand because right eval may clobber R14R15
asmgen.pushLongRegisters(RegisterOrPair.R14R15, 1)
assignExpressionToRegister(expr.right, RegisterOrPair.R12R13, expr.right.type.isSigned)
// restore left operand
asmgen.popLongRegisters(RegisterOrPair.R14R15, 1)
augmentableAsmGen.inplacemodificationLongWithVariable("cx16.r14", expr.operator, "cx16.r12")
assignRegisterLong(target, RegisterOrPair.R14R15)
if(targetreg==RegisterOrPair.R14R15)
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
else
asmgen.popLongRegisters(RegisterOrPair.R14R15, 2)
asmgen.popLongRegisters(RegisterOrPair.R12R13, 2)
return true
}
}
@@ -2363,8 +2589,12 @@ internal class AssignmentAsmGen(
else if(varname!=null)
augmentableAsmGen.inplacemodificationLongWithVariable("cx16.r14", expr.operator, varname)
else {
// TODO: preserve R14:R15 on stack here? (the function does't do it for us)
augmentableAsmGen.inplacemodificationLongWithExpression("cx16.r14", expr.operator, expr.right)
if(!isRightTrivial(expr.right))
asmgen.pushLongRegisters(RegisterOrPair.R14R15, 1)
assignExpressionToRegister(expr.right, RegisterOrPair.R12R13, expr.right.type.isSigned)
if(!isRightTrivial(expr.right))
asmgen.popLongRegisters(RegisterOrPair.R14R15, 1)
augmentableAsmGen.inplacemodificationLongWithVariable("cx16.r14", expr.operator, "cx16.r12")
}
assignRegisterLong(target, RegisterOrPair.R14R15)
if(targetreg!=RegisterOrPair.R14R15) {
@@ -2553,8 +2783,8 @@ internal class AssignmentAsmGen(
"&" -> {
when {
value == 0 -> asmgen.out(" lda #0 | tay")
value == 0x00ff -> asmgen.out(" lda #0")
value == 0xff00 -> asmgen.out(" ldy #0")
value == 0x00ff -> asmgen.out(" ldy #0")
value == 0xff00 -> asmgen.out(" lda #0")
value and 255 == 0 -> asmgen.out(" tya | and #>$value | tay | lda #0")
value < 0x0100 -> asmgen.out(" and #<$value | ldy #0")
else -> asmgen.out(" and #<$value | tax | tya | and #>$value | tay | txa")
@@ -2774,6 +3004,21 @@ $endLabel""")
asmgen.out(" jsr prog8_lib.containment_linearwordarray")
}
}
dt.isLongArray -> {
asmgen.assignExpressionToRegister(containment.needle, RegisterOrPair.R14R15, true)
asmgen.out("""
lda cx16.r14
sta P8ZP_SCRATCH_W1
lda cx16.r14+1
sta P8ZP_SCRATCH_W1+1
lda cx16.r15
sta P8ZP_SCRATCH_W2
lda cx16.r15+1
sta P8ZP_SCRATCH_W2+1""")
assignAddressOf(AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, DataType.UWORD, containment.definingISub(), containment.position, "P8ZP_SCRATCH_PTR"), symbolName, false, null, null)
asmgen.out(" ldy #$numElements")
asmgen.out(" jsr prog8_lib.containment_longarray")
}
else -> throw AssemblyError("invalid dt")
}
}
@@ -3209,7 +3454,19 @@ $endLabel""")
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 -> TODO("cast float to long")
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")
}
}
@@ -3634,17 +3891,26 @@ $endLabel""")
}
private fun assignAddressOf(target: AsmAssignTarget, sourceName: String, msb: Boolean, arrayDt: DataType?, arrayIndexExpr: PtExpression?) {
var actualSourceName = sourceName
var actualMsb = msb
if (arrayDt?.isSplitWordArray == true) {
if (!sourceName.endsWith("_lsb") && !sourceName.endsWith("_msb")) {
actualSourceName = if (msb) sourceName + "_msb" else sourceName + "_lsb"
}
actualMsb = false
}
if(arrayIndexExpr!=null) {
if(arrayDt?.isPointer==true) {
require(!msb)
return pointergen.assignAddressOfIndexedPointer(target, sourceName, arrayDt, arrayIndexExpr)
require(!actualMsb)
return pointergen.assignAddressOfIndexedPointer(target, actualSourceName, arrayDt, arrayIndexExpr)
}
val constIndex = arrayIndexExpr.asConstInteger()
if(constIndex!=null) {
if (arrayDt!!.isUnsignedWord) {
// using a UWORD pointer with array indexing, always bytes
require(!msb)
assignVariableToRegister(sourceName, RegisterOrPair.AY, false, arrayIndexExpr.definingISub(), arrayIndexExpr.position)
require(!actualMsb)
assignVariableToRegister(actualSourceName, RegisterOrPair.AY, false, arrayIndexExpr.definingISub(), arrayIndexExpr.position)
if(constIndex in 1..255)
asmgen.out("""
clc
@@ -3666,9 +3932,9 @@ $endLabel""")
else {
if(constIndex>0) {
val offset = if(arrayDt.isSplitWordArray) constIndex else program.memsizer.memorySize(arrayDt, constIndex) // add arrayIndexExpr * elementsize to the address of the array variable.
asmgen.out(" lda #<($sourceName + $offset) | ldy #>($sourceName + $offset)")
asmgen.out(" lda #<($actualSourceName + $offset) | ldy #>($actualSourceName + $offset)")
} else {
asmgen.out(" lda #<$sourceName | ldy #>$sourceName")
asmgen.out(" lda #<$actualSourceName | ldy #>$actualSourceName")
}
}
assignRegisterpairWord(target, RegisterOrPair.AY)
@@ -3676,8 +3942,8 @@ $endLabel""")
} else {
if (arrayDt!!.isUnsignedWord) {
// using a UWORD pointer with array indexing, always bytes
require(!msb)
assignVariableToRegister(sourceName, RegisterOrPair.AY, false, arrayIndexExpr.definingISub(), arrayIndexExpr.position)
require(!actualMsb)
assignVariableToRegister(actualSourceName, RegisterOrPair.AY, false, arrayIndexExpr.definingISub(), arrayIndexExpr.position)
asmgen.saveRegisterStack(CpuRegister.A, false)
asmgen.saveRegisterStack(CpuRegister.Y, false)
if(arrayIndexExpr.type.isWord) {
@@ -3730,9 +3996,9 @@ $endLabel""")
)
} else throw AssemblyError("weird type $subtype")
asmgen.out("""
ldy #>$sourceName
ldy #>$actualSourceName
clc
adc #<$sourceName
adc #<$actualSourceName
bcc +
iny
+""")
@@ -3743,12 +4009,12 @@ $endLabel""")
}
// address of a normal variable
require(!msb)
require(!actualMsb)
when(target.kind) {
TargetStorageKind.VARIABLE -> {
asmgen.out("""
lda #<$sourceName
ldy #>$sourceName
lda #<$actualSourceName
ldy #>$actualSourceName
sta ${target.asmVarname}
sty ${target.asmVarname}+1""")
}
@@ -3756,25 +4022,25 @@ $endLabel""")
throw AssemblyError("can't store word into memory byte")
}
TargetStorageKind.ARRAY -> {
asmgen.out(" lda #<$sourceName | ldy #>$sourceName")
asmgen.out(" lda #<$actualSourceName | ldy #>$actualSourceName")
assignRegisterpairWord(target, RegisterOrPair.AY)
}
TargetStorageKind.REGISTER -> {
when(target.register!!) {
RegisterOrPair.AX -> asmgen.out(" ldx #>$sourceName | lda #<$sourceName")
RegisterOrPair.AY -> asmgen.out(" ldy #>$sourceName | lda #<$sourceName")
RegisterOrPair.XY -> asmgen.out(" ldy #>$sourceName | ldx #<$sourceName")
RegisterOrPair.AX -> asmgen.out(" ldx #>$actualSourceName | lda #<$actualSourceName")
RegisterOrPair.AY -> asmgen.out(" ldy #>$actualSourceName | lda #<$actualSourceName")
RegisterOrPair.XY -> asmgen.out(" ldy #>$actualSourceName | ldx #<$actualSourceName")
in Cx16VirtualRegisters -> {
asmgen.out("""
lda #<$sourceName
ldy #>$sourceName
lda #<$actualSourceName
ldy #>$actualSourceName
sta cx16.${target.register.toString().lowercase()}
sty cx16.${target.register.toString().lowercase()}+1""")
}
else -> throw AssemblyError("can only load address into 16 bit register")
}
}
TargetStorageKind.POINTER -> pointergen.assignAddressOf(PtrTarget(target), sourceName)
TargetStorageKind.POINTER -> pointergen.assignAddressOf(PtrTarget(target), actualSourceName)
TargetStorageKind.VOID -> { /* do nothing */ }
}
}
@@ -3942,7 +4208,7 @@ $endLabel""")
lda $arrayVarName+3,y
sta ${target.asmVarname}+3,x""")
}
TargetStorageKind.POINTER -> pointergen.assignIndexedPointer(target, arrayVarName, index, DataType.arrayFor(BaseDataType.LONG))
TargetStorageKind.POINTER -> pointergen.assignIndexedPointer(target, arrayVarName, index)
TargetStorageKind.MEMORY -> throw AssemblyError("memory is bytes not long ${target.position}")
TargetStorageKind.VOID -> { /* do nothing */ }
}
@@ -4451,6 +4717,12 @@ $endLabel""")
else throw AssemblyError("only combined vreg allowed as long target ${target.position}")
}
TargetStorageKind.ARRAY -> {
val deref = target.array!!.pointerderef
if(deref!=null) {
// indexed pointer array via struct (e.g. foo.longs[i])
pointergen.assignLongReg(IndexedPtrTarget(target), pairedRegisters)
return
}
asmgen.loadScaledArrayIndexIntoRegister(target.array!!, CpuRegister.Y)
val arrayVarName = asmgen.asmSymbolName(target.array.variable!!)
val startreg = pairedRegisters.startregname()
@@ -5680,6 +5952,7 @@ $endLabel""")
eor #255
sta ${target.asmVarname}+3""")
}
TargetStorageKind.POINTER -> pointergen.inplaceLongInvert(PtrTarget(target))
else -> TODO("LONG INVERT ${target.kind} ${target.position}")
}
}
@@ -318,14 +318,13 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
SourceStorageKind.REGISTER -> inplacemodificationLongWithVariable(targetVarName, operator, regName(value))
SourceStorageKind.MEMORY -> TODO("inplace long modification with memread value ${target.position}")
SourceStorageKind.ARRAY -> TODO("inplace long modification with array value ${target.position}")
SourceStorageKind.EXPRESSION -> {
SourceStorageKind.EXPRESSION ->
if(value.expression is PtTypeCast) {
if (tryInplaceModifyWithRemovedRedundantCast(value.expression, target, operator)) return
TODO("inplace long modification ${target.position}")
} else {
TODO("inplace long modification ${target.position}")
}
}
}
}
@@ -577,24 +576,40 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
internal fun inplacemodificationLongWithVariable(targetVar: String, operator: String, sourceVar: String) {
when(operator) {
"+" -> {
// need to unroll to correctly keep Carry flag for multi-byte arithmetic
asmgen.out("""
clc
ldx #252
- lda $targetVar+4,x
adc $sourceVar+4,x
sta $targetVar+4,x
inx
bne -""")
lda $targetVar+0
adc $sourceVar+0
sta $targetVar+0
lda $targetVar+1
adc $sourceVar+1
sta $targetVar+1
lda $targetVar+2
adc $sourceVar+2
sta $targetVar+2
lda $targetVar+3
adc $sourceVar+3
sta $targetVar+3
""")
}
"-" -> {
// need to unroll to correctly keep Carry flag for multi-byte arithmetic
asmgen.out("""
sec
ldx #252
- lda $targetVar+4,x
sbc $sourceVar+4,x
sta $targetVar+4,x
inx
bne -""")
lda $targetVar+0
sbc $sourceVar+0
sta $targetVar+0
lda $targetVar+1
sbc $sourceVar+1
sta $targetVar+1
lda $targetVar+2
sbc $sourceVar+2
sta $targetVar+2
lda $targetVar+3
sbc $sourceVar+3
sta $targetVar+3
""")
}
"<<" -> {
asmgen.out("""
@@ -667,6 +682,53 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
dey
bpl -""")
}
"/" -> {
asmgen.out("""
; Copy target to R12R13 (dividend)
ldy #3
- lda $targetVar,y
sta cx16.r12,y
dey
bpl -
; Copy source to R14R15 (divisor)
ldy #3
- lda $sourceVar,y
sta cx16.r14,y
dey
bpl -
jsr prog8_math.div_longs
; Copy result from R14R15 back to target
ldy #3
- lda cx16.r14,y
sta $targetVar,y
dey
bpl -""")
}
"%" -> {
asmgen.out("""
; Copy target to R12R13 (dividend)
ldy #3
- lda $targetVar,y
sta cx16.r12,y
dey
bpl -
; Copy source to R14R15 (divisor)
ldy #3
- lda $sourceVar,y
sta cx16.r14,y
dey
bpl -
jsr prog8_math.div_longs
; Copy remainder from P8ZP_SCRATCH_W1/W2 back to target
lda P8ZP_SCRATCH_W1
sta $targetVar
lda P8ZP_SCRATCH_W1+1
sta $targetVar+1
lda P8ZP_SCRATCH_W2
sta $targetVar+2
lda P8ZP_SCRATCH_W2+1
sta $targetVar+3""")
}
else -> {
TODO("in-place modify LONG with variable, operator=$operator")
}
@@ -1021,6 +1083,65 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
dey
bpl -""")
}
"/" -> {
// long /= constant - use division routine
val lo = value and 0xFFFF
val hi = (value ushr 16) and 0xFFFF
asmgen.out("""
; Copy variable to R12R13 (dividend)
ldy #3
- lda $variable,y
sta cx16.r12,y
dey
bpl -
; Load constant into R14R15 (divisor)
lda #<${lo}
sta cx16.r14
lda #>${lo}
sta cx16.r14+1
lda #<${hi}
sta cx16.r15
lda #>${hi}
sta cx16.r15+1
jsr prog8_math.div_longs
; Copy result from R14R15 back to variable
ldy #3
- lda cx16.r14,y
sta $variable,y
dey
bpl -""")
}
"%" -> {
// long %= constant - use division routine, remainder is in P8ZP_SCRATCH_W1/W2
val lo = value and 0xFFFF
val hi = (value ushr 16) and 0xFFFF
asmgen.out("""
; Copy variable to R12R13 (dividend)
ldy #3
- lda $variable,y
sta cx16.r12,y
dey
bpl -
; Load constant into R14R15 (divisor)
lda #<${lo}
sta cx16.r14
lda #>${lo}
sta cx16.r14+1
lda #<${hi}
sta cx16.r15
lda #>${hi}
sta cx16.r15+1
jsr prog8_math.div_longs
; Copy remainder from P8ZP_SCRATCH_W1/W2 back to variable
lda P8ZP_SCRATCH_W1
sta $variable
lda P8ZP_SCRATCH_W1+1
sta $variable+1
lda P8ZP_SCRATCH_W2
sta $variable+2
lda P8ZP_SCRATCH_W2+1
sta $variable+3""")
}
"<<" -> if (value > 0) inplaceLongShiftLeft()
">>" -> if (value > 0) inplaceLongShiftRight()
"|" -> {
@@ -3149,6 +3270,7 @@ $shortcutLabel:""")
lda $name+1
sta P8ZP_SCRATCH_W1+1
txa
oweroff
jsr prog8_math.divmod_uw_asm
lda P8ZP_SCRATCH_W2
ldy P8ZP_SCRATCH_W2+1
@@ -113,27 +113,118 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
}
internal fun inplaceByteInvert(target: PtrTarget) {
TODO("inplace byte invert pointer deref ${target.position}")
val (zpPtrVar, offset) = deref(target.pointer)
val eorVal = if(target.dt.isBool) 1 else 255
if(offset==0.toUByte() && asmgen.isTargetCpu(CpuType.CPU65C02)) {
asmgen.out(" lda ($zpPtrVar) | eor #$eorVal | sta ($zpPtrVar)")
} else {
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
eor #$eorVal
sta ($zpPtrVar),y""")
}
}
internal fun inplaceWordInvert(target: PtrTarget) {
TODO("inplace word invert pointer deref ${target.position}")
val (zpPtrVar, offset) = deref(target.pointer)
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
eor #255
sta ($zpPtrVar),y
iny
lda ($zpPtrVar),y
eor #255
sta ($zpPtrVar),y""")
}
internal fun inplaceLongInvert(target: PtrTarget) {
val (zpPtrVar, offset) = deref(target.pointer)
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
eor #255
sta ($zpPtrVar),y
iny
lda ($zpPtrVar),y
eor #255
sta ($zpPtrVar),y
iny
lda ($zpPtrVar),y
eor #255
sta ($zpPtrVar),y
iny
lda ($zpPtrVar),y
eor #255
sta ($zpPtrVar),y""")
}
internal fun inplaceByteNegate(target: PtrTarget, scope: IPtSubroutine?) {
TODO("inplace byte negate to pointer deref ${target.position}")
val (zpPtrVar, offset) = deref(target.pointer)
if(asmgen.isTargetCpu(CpuType.CPU65C02)) {
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
eor #255
ina
sta ($zpPtrVar),y""")
} else {
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
eor #255
clc
adc #1
sta ($zpPtrVar),y""")
}
}
internal fun inplaceWordNegate(target: PtrTarget, scope: IPtSubroutine?) {
TODO("inplace word negate pointer deref ${target.position}")
val (zpPtrVar, offset) = deref(target.pointer)
asmgen.out("""
ldy #$offset
sec
lda #0
sbc ($zpPtrVar),y
sta ($zpPtrVar),y
iny
lda #0
sbc ($zpPtrVar),y
sta ($zpPtrVar),y""")
}
internal fun inplaceLongNegate(target: PtrTarget, scope: IPtSubroutine?) {
TODO("inplace long negate pointer deref ${target.position}")
val (zpPtrVar, offset) = deref(target.pointer)
asmgen.out("""
ldy #$offset
sec
lda #0
sbc ($zpPtrVar),y
sta ($zpPtrVar),y
iny
lda #0
sbc ($zpPtrVar),y
sta ($zpPtrVar),y
iny
lda #0
sbc ($zpPtrVar),y
sta ($zpPtrVar),y
iny
lda #0
sbc ($zpPtrVar),y
sta ($zpPtrVar),y""")
}
internal fun inplaceFloatNegate(target: PtrTarget, scope: IPtSubroutine?) {
TODO("inplace float negate pointer deref ${target.position}")
val (zpPtrVar, offset) = deref(target.pointer)
// flip sign bit in second byte of 5-byte float
asmgen.out("""
ldy #$offset
iny
lda ($zpPtrVar),y
eor #${'$'}80
sta ($zpPtrVar),y""")
}
@@ -241,6 +332,51 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
}
}
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
asmgen.assignExpressionToVariable(target.index, "P8ZP_SCRATCH_W1", DataType.UWORD)
when (eltSize) {
1 -> {}
2 -> asmgen.out(" asl P8ZP_SCRATCH_W1 | rol P8ZP_SCRATCH_W1+1")
4 -> asmgen.out(" asl P8ZP_SCRATCH_W1 | rol P8ZP_SCRATCH_W1+1 | asl P8ZP_SCRATCH_W1 | rol P8ZP_SCRATCH_W1+1")
8 -> asmgen.out(" asl P8ZP_SCRATCH_W1 | rol P8ZP_SCRATCH_W1+1 | asl P8ZP_SCRATCH_W1 | rol P8ZP_SCRATCH_W1+1 | asl P8ZP_SCRATCH_W1 | rol P8ZP_SCRATCH_W1+1")
else -> {
// generic multiply by eltSize
asmgen.out("""
lda P8ZP_SCRATCH_W1
ldy P8ZP_SCRATCH_W1+1
sta prog8_math.multiply_words.multiplier
sty prog8_math.multiply_words.multiplier+1
lda #<$eltSize
ldy #>$eltSize
jsr prog8_math.multiply_words
sta P8ZP_SCRATCH_W1
sty P8ZP_SCRATCH_W1+1""")
}
}
// save scaled index to W2, compute base (field-corrected) into P8ZP_SCRATCH_PTR, then add
asmgen.out(" lda P8ZP_SCRATCH_W1 | sta P8ZP_SCRATCH_W2 | lda P8ZP_SCRATCH_W1+1 | sta P8ZP_SCRATCH_W2+1")
val (basePtr, _) = deref(target.pointer, addOffsetToPointer = true)
if(basePtr != "P8ZP_SCRATCH_PTR") {
// deref returned the original zp pointer variable; copy it so we don't mutate it
asmgen.out("""
lda $basePtr
sta P8ZP_SCRATCH_PTR
lda $basePtr+1
sta P8ZP_SCRATCH_PTR+1""")
}
asmgen.out("""
clc
lda P8ZP_SCRATCH_PTR
adc P8ZP_SCRATCH_W2
sta P8ZP_SCRATCH_PTR
lda P8ZP_SCRATCH_PTR+1
adc P8ZP_SCRATCH_W2+1
sta P8ZP_SCRATCH_PTR+1""")
return "P8ZP_SCRATCH_PTR"
}
internal fun assignPointerDerefExpression(target: AsmAssignTarget, value: PtPointerDeref) {
val (zpPtrVar, offset1) = deref(value)
val offset: UByte
@@ -309,7 +445,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
internal fun inplaceModification(target: PtrTarget, operator: String, value: AsmAssignSource) {
when (operator) {
"+" -> {
if(target.dt.isByte && value.number?.number==1.0 || value.number?.number==2.0) {
if(target.dt.isByte && (value.number?.number==1.0 || value.number?.number==2.0)) {
val amount = value.number.number.toInt()
inplaceByteInc(target, amount)
} else {
@@ -321,7 +457,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
}
}
"-" -> {
if(target.dt.isByte && value.number?.number==1.0 || value.number?.number==2.0) {
if(target.dt.isByte && (value.number?.number==1.0 || value.number?.number==2.0)) {
val amount = value.number.number.toInt()
inplaceByteDec(target, amount)
} else {
@@ -409,132 +545,163 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
}
internal fun assignByte(target: IndexedPtrTarget, byte: Int) {
val eltSize = asmgen.program.memsizer.memorySize(target.elementDt, null)
val constIndex = target.index.asConstInteger()
if(constIndex!=null) {
val (zpPtrVar, offset2) = deref(target.pointer)
val offset = eltSize*constIndex + offset2.toInt()
if(offset>255) {
// need to add index to the pointer (but not clobber it) to allow index>255
if(zpPtrVar.startsWith("P8ZP_SCRATCH_")) { // TODO not very robust....
asmgen.out("""
lda $zpPtrVar+1
clc
adc #>$offset
sta $zpPtrVar+1
ldy #<$offset
lda #$byte
sta ($zpPtrVar),y""")
} else {
asmgen.out("""
lda $zpPtrVar
sta P8ZP_SCRATCH_PTR
lda $zpPtrVar+1
clc
adc #>$offset
sta P8ZP_SCRATCH_PTR+1
ldy #<$offset
lda #$byte
sta (P8ZP_SCRATCH_PTR),y""")
}
} else {
asmgen.out("""
ldy #<$offset
lda #$byte
sta ($zpPtrVar),y""")
}
} else if(target.index is PtIdentifier) {
val (zpPtrVar, _) = deref(target.pointer, addOffsetToPointer=true)
val indexVarName = asmgen.asmVariableName(target.index)
if(eltSize!=1) {
TODO("multiply index by element size $eltSize ${target.position}")
// asmgen.loadScaledArrayIndexIntoRegister() ...
}
// element size is 1, can immediately add the index value
if(target.index.type.isWord) {
asmgen.out("""
clc
lda $zpPtrVar+1
adc $indexVarName+1
sta $zpPtrVar+1""")
}
asmgen.out("""
ldy $indexVarName
lda #$byte
sta ($zpPtrVar),y""")
} else {
if(eltSize!=1) {
TODO("multiply index by element size $eltSize ${target.position}")
// asmgen.loadScaledArrayIndexIntoRegister() ...
}
if(target.index.type.isByte) {
asmgen.pushCpuStack(BaseDataType.UBYTE, target.index)
val (zpPtrVar, offset) = deref(target.pointer, addOffsetToPointer = true)
require(offset==0.toUByte())
asmgen.restoreRegisterStack(CpuRegister.Y, false)
asmgen.out(" lda #$byte | sta ($zpPtrVar),y")
}
else {
asmgen.pushCpuStack(BaseDataType.UWORD, target.index)
val (zpPtrVar, offset) = deref(target.pointer, addOffsetToPointer = true)
require(offset==0.toUByte())
asmgen.out("""
pla
clc
adc $zpPtrVar+1
sta $zpPtrVar+1""")
if(asmgen.isTargetCpu(CpuType.CPU65C02)) asmgen.out(" ply") else asmgen.out(" pla | tay")
asmgen.out(" lda #$byte | sta ($zpPtrVar),y")
}
}
val ptr = indexedEffectiveAddress(target)
asmgen.storeIndirectByte(byte, ptr, 0u)
}
internal fun assignWord(target: IndexedPtrTarget, word: Int) {
TODO("array ptr assign const word ${target.position}")
val ptr = indexedEffectiveAddress(target)
asmgen.storeIndirectWord(word, ptr, 0u)
}
internal fun assignLong(target: IndexedPtrTarget, long: Int) {
TODO("array ptr assign const long ${target.position}")
val ptr = indexedEffectiveAddress(target)
val hex = long.toLongHex()
asmgen.out("""
ldy #0
lda #$${hex.substring(6,8)}
sta ($ptr),y
iny
lda #$${hex.substring(4,6)}
sta ($ptr),y
iny
lda #$${hex.substring(2,4)}
sta ($ptr),y
iny
lda #$${hex.take(2)}
sta ($ptr),y""")
}
internal fun assignFloat(target: IndexedPtrTarget, float: Double) {
TODO("array ptr assign const float ${target.position}")
val ptr = indexedEffectiveAddress(target)
asmgen.storeIndirectFloat(float, ptr, 0u)
}
internal fun assignFAC1(target: IndexedPtrTarget) {
TODO("array ptr assign FAC1 ${target.position}")
val ptr = indexedEffectiveAddress(target)
asmgen.storeIndirectFloatFP1(ptr, 0u)
}
internal fun assignFloatAY(target: IndexedPtrTarget) {
TODO("array ptr assign float AY ${target.position}")
// A/Y contains a pointer to the source float. Save it, compute destination address,
// restore source pointer into W1, load destination pointer into A/Y, and copy.
if(asmgen.isTargetCpu(CpuType.CPU65C02))
asmgen.out(" pha | phy")
else
asmgen.out(" pha | tya | pha")
val ptr = indexedEffectiveAddress(target)
if(asmgen.isTargetCpu(CpuType.CPU65C02)) {
asmgen.out("""
ply
pla
sta P8ZP_SCRATCH_W1
sty P8ZP_SCRATCH_W1+1
lda $ptr
ldy $ptr+1
jsr floats.copy_float""")
} else {
asmgen.out("""
pla
tay
pla
sta P8ZP_SCRATCH_W1
sty P8ZP_SCRATCH_W1+1
lda $ptr
ldy $ptr+1
jsr floats.copy_float""")
}
}
internal fun assignFloatVar(target: IndexedPtrTarget, varName: String) {
TODO("array ptr assign float var ${target.position}")
val ptr = indexedEffectiveAddress(target)
asmgen.storeIndirectFloatVar(varName, ptr, 0u)
}
internal fun assignByteReg(target: IndexedPtrTarget, register: CpuRegister) {
// save register on the stack so indexedEffectiveAddress can freely use scratch regs
asmgen.saveRegisterStack(register, false)
TODO("array ptr assign byte reg ${target.position}")
asmgen.saveRegisterStack(register, false)
val ptr = indexedEffectiveAddress(target)
asmgen.restoreRegisterStack(register, false)
asmgen.storeIndirectByteReg(register, ptr, 0u, false, false)
}
internal fun assignWordReg(target: IndexedPtrTarget, regs: RegisterOrPair) {
// save word reg on the stack so indexedEffectiveAddress can freely use scratch regs
saveOnStack(regs)
TODO("array ptr assign word reg ${target.position}")
val ptr = indexedEffectiveAddress(target)
restoreFromStack(regs)
asmgen.storeIndirectWordReg(regs, ptr, 0u)
}
internal fun assignByteVar(target: IndexedPtrTarget, varName: String, extendToWord: Boolean, signed: Boolean) {
TODO("array ptr assign byte var ${target.position}")
val ptr = indexedEffectiveAddress(target)
if(extendToWord) {
// need to store byte var extended to word
asmgen.out(" lda $varName")
if(signed) asmgen.signExtendAXlsb(BaseDataType.BYTE) else asmgen.signExtendAXlsb(BaseDataType.UBYTE)
asmgen.storeIndirectWordReg(RegisterOrPair.AX, ptr, 0u)
} else {
asmgen.storeIndirectByteVar(varName, ptr, 0u)
}
}
internal fun assignWordVar(target: IndexedPtrTarget, varName: String) {
TODO("array ptr assign word var ${target.position}")
val ptr = indexedEffectiveAddress(target)
asmgen.storeIndirectWordVar(varName, DataType.UWORD, ptr, 0u)
}
internal fun assignLongVar(target: IndexedPtrTarget, varName: String) {
TODO("array ptr assign long var ${target.position}")
val ptr = indexedEffectiveAddress(target)
asmgen.out("""
ldy #0
lda $varName
sta ($ptr),y
iny
lda $varName+1
sta ($ptr),y
iny
lda $varName+2
sta ($ptr),y
iny
lda $varName+3
sta ($ptr),y""")
}
internal fun assignLongReg(target: IndexedPtrTarget, regs: RegisterOrPair) {
// generic fallback for long array via pointer
val startreg = regs.startregname()
// save source long on the stack in case index expression evaluation uses the same registers
asmgen.out("""
lda cx16.$startreg
pha
lda cx16.$startreg+1
pha
lda cx16.$startreg+2
pha
lda cx16.$startreg+3
pha""")
val ptr = indexedEffectiveAddress(target)
asmgen.out("""
pla
sta cx16.$startreg+3
pla
sta cx16.$startreg+2
pla
sta cx16.$startreg+1
pla
sta cx16.$startreg
ldy #0
lda cx16.$startreg
sta ($ptr),y
iny
lda cx16.$startreg+1
sta ($ptr),y
iny
lda cx16.$startreg+2
sta ($ptr),y
iny
lda cx16.$startreg+3
sta ($ptr),y""")
}
internal fun assignLongVar(pointer: PtPointerDeref, varName: String) {
@@ -2560,7 +2727,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
}
}
internal fun assignIndexedPointer(target: AsmAssignTarget, arrayVarName: String, arrayIndexer: PtArrayIndexer, arrayDt: DataType) {
internal fun assignIndexedPointer(target: AsmAssignTarget, arrayVarName: String, arrayIndexer: PtArrayIndexer) {
// Load value from array[index] and store to pointer target
if(target.datatype.isLong) {
// Get address of array[index] into AY, then 4-byte copy loop to target pointer
+66 -47
View File
@@ -30,45 +30,45 @@ class TestAsmOptimizer: FunSpec({
symbolTable.add(StMemVar("A1", DataType.UWORD, 0x0310u, null, null))
symbolTable.add(StMemVar("A2", DataType.UWORD, 0x0312u, null, null))
fun optimize(lines: MutableList<String>): Int {
return optimizeAssembly(lines, machine, symbolTable)
fun optimize(lines: MutableList<String>) {
optimizeAssembly(lines, machine, symbolTable)
}
// --- optimizeIncDec ---
test("optimizeIncDec: removes iny+dey sequence") {
val lines = mutableListOf(" iny", " dey", " rts", " nop")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" rts", " nop")
}
test("optimizeIncDec: removes iny+dey sequence with comments") {
val lines = mutableListOf(" iny ; increment Y", " dey ; decrement Y", " rts", " nop")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" rts", " nop")
}
test("optimizeIncDec: removes inx+dex sequence") {
val lines = mutableListOf(" inx", " dex", " rts", " nop")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" rts", " nop")
}
test("optimizeIncDec: removes dey+iny sequence") {
val lines = mutableListOf(" dey", " iny", " rts", " nop")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" rts", " nop")
}
test("optimizeIncDec: removes dex+inx sequence") {
val lines = mutableListOf(" dex", " inx", " rts", " nop")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" rts", " nop")
}
test("optimizeIncDec: preserves non-canceling sequences") {
val lines = mutableListOf(" iny", " inx", " rts", " nop")
optimize(lines) shouldBe 0
optimize(lines)
lines shouldBe listOf(" iny", " inx", " rts", " nop")
}
@@ -76,42 +76,37 @@ class TestAsmOptimizer: FunSpec({
test("optimizeStoreLoadSame: removes sta X + lda X") {
val lines = mutableListOf(" lda #42", " sta myvar", " lda myvar", " rts")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" lda #42", " sta myvar", " rts")
}
test("optimizeStoreLoadSame: removes sta X + lda X with comments") {
// Note: comments don't affect the optimization, but the optimizer checks lines[1] and lines[2]
// and the comment is part of the line, so "sta myvar ; store it" doesn't match "sta myvar"
// The optimizer uses substring(4) which includes the comment
val lines = mutableListOf(" nop", " sta myvar", " lda myvar", " rts ; return")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" nop", " sta myvar", " rts ; return")
}
test("optimizeStoreLoadSame: removes pha + pla") {
// Note: optimizer checks lines[1] and lines[2] of the 4-line window
val lines = mutableListOf(" nop", " pha", " pla", " rts")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" nop", " rts")
}
test("optimizeStoreLoadSame: removes phx + plx") {
val lines = mutableListOf(" nop", " phx", " plx", " rts")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" nop", " rts")
}
test("optimizeStoreLoadSame: removes phy + ply") {
val lines = mutableListOf(" nop", " phy", " ply", " rts")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" nop", " rts")
}
test("optimizeStoreLoadSame: removes lda X + sta X") {
// Note: optimizer checks lines[1] and lines[2] of the 4-line window
val lines = mutableListOf(" nop", " lda myvar", " sta myvar", " rts")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" nop", " lda myvar", " rts")
}
@@ -119,77 +114,90 @@ class TestAsmOptimizer: FunSpec({
test("optimizeJsrRts: converts jsr+rts to jmp") {
val lines = mutableListOf(" jsr mysub", " rts", " nop", " nop")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" jmp mysub", " nop", " nop")
}
test("optimizeJsrRts: converts jsr+rts to jmp with comments") {
// Note: comments don't affect the optimization
val lines = mutableListOf(" jsr mysub ; call subroutine", " rts ; return", " nop", " nop")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" jmp mysub ; call subroutine", " nop", " nop")
}
test("optimizeJsrRts: removes rts+jmp") {
val lines = mutableListOf(" rts", " jmp somewhere", " nop", " nop")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" rts", " nop", " nop")
}
test("optimizeJsrRts: removes rts+bra") {
val lines = mutableListOf(" rts", " bra somewhere", " nop", " nop")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" rts", " nop", " nop")
}
test("optimizeJsrRts: removes lda+cmp #0") {
val lines = mutableListOf(" lda myvar", " cmp #0", " beq skip", " nop")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" lda myvar", " beq skip", " nop")
}
test("optimizeJsrRts: removes lda+cmp #0 with comments") {
val lines = mutableListOf(" lda myvar ; load variable", " cmp #0 ; compare to zero", " beq skip ; branch if equal", " nop")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" lda myvar ; load variable", " beq skip ; branch if equal", " nop")
}
test("optimizeJsrRts: removes bra+jmp") {
val lines = mutableListOf(" bra somewhere", " jmp elsewhere", " nop", " nop")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" bra somewhere", " nop", " nop")
}
test("optimizeJsrRts: preserves jsr+rts for floats.pushFAC") {
val lines = mutableListOf(" jsr floats.pushFAC", " rts", " nop", " nop")
optimize(lines) shouldBe 0
optimize(lines)
lines shouldBe listOf(" jsr floats.pushFAC", " rts", " nop", " nop")
}
test("optimizeJsrRts: three consecutive jsr only tail jsr+rts becomes jmp") {
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")
}
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")
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
jmpCount shouldBe 1
}
// --- optimizeUselessPushPopStack ---
test("optimizeUselessPushPop: removes phy+ldy+pla") {
val lines = mutableListOf(" phy", " ldy #5", " pla", " rts")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" tya", " ldy #5", " rts")
}
test("optimizeUselessPushPop: removes phx+ldx+pla") {
val lines = mutableListOf(" phx", " ldx #5", " pla", " rts")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" txa", " ldx #5", " rts")
}
test("optimizeUselessPushPop: removes pha+lda+pla") {
// Note: optimizer removes all three: pha, lda, and pla
val lines = mutableListOf(" pha", " lda #5", " pla", " rts")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" rts")
}
test("optimizeUselessPushPop: removes pha+tya+tay+pla") {
val lines = mutableListOf(" pha", " tya", " tay", " pla", " rts")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" rts")
}
@@ -197,14 +205,14 @@ class TestAsmOptimizer: FunSpec({
test("optimizeUnneededTempvarInAdd: removes scratch variable in add") {
val lines = mutableListOf(" sta P8ZP_SCRATCH_W1", " lda #5", " clc", " adc P8ZP_SCRATCH_W1", " rts")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" clc", " adc #5", " rts")
}
test("optimizeUnneededTempvarInAdd: removes scratch variable in add with comments") {
// Note: comments don't affect the optimization
val lines = mutableListOf(" sta P8ZP_SCRATCH_W1", " lda #5", " clc", " adc P8ZP_SCRATCH_W1", " rts ; return")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" clc", " adc #5", " rts ; return")
}
@@ -212,14 +220,14 @@ class TestAsmOptimizer: FunSpec({
test("optimizeTSBtoRegularOr: converts lda/tsb/lda to lda/ora/sta") {
val lines = mutableListOf(" lda var2", " tsb var1", " lda var1", " rts")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" lda var1", " ora var2", " sta var1", " rts")
}
test("optimizeTSBtoRegularOr: converts lda/tsb/lda to lda/ora/sta with comments") {
// Note: comments don't affect the optimization
val lines = mutableListOf(" lda var2", " tsb var1", " lda var1", " rts ; return")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" lda var1", " ora var2", " sta var1", " rts ; return")
}
@@ -231,7 +239,7 @@ class TestAsmOptimizer: FunSpec({
" lda #1", " ldy #0", " sta var2", " sty var2+1",
" rts", " nop", " nop", " nop", " nop", " nop"
)
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(
" lda #1", " ldy #0", " sta var1", " sty var1+1",
" sta var2", " sty var2+1",
@@ -246,7 +254,7 @@ class TestAsmOptimizer: FunSpec({
" ldy #0", " lda (ptr),y", " ora #3", " ldy #0", " sta (ptr),y", " rts",
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop"
)
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" ldy #0", " lda (ptr),y", " ora #3", " sta (ptr),y", " rts",
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop")
}
@@ -260,7 +268,7 @@ class TestAsmOptimizer: FunSpec({
" sta P8ZP_SCRATCH_PTR", " sty P8ZP_SCRATCH_PTR+1", " rts",
" nop", " nop", " nop", " nop"
)
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(
" clc", " adc P8ZP_SCRATCH_PTR", " sta P8ZP_SCRATCH_PTR",
" tya", " adc P8ZP_SCRATCH_PTR+1", " sta P8ZP_SCRATCH_PTR+1", " rts",
@@ -271,7 +279,7 @@ class TestAsmOptimizer: FunSpec({
test("optimizeExtraRegisterLoadInWordStore: removes ldx+sta+txa+ldy+sta") {
val lines = mutableListOf(" ldx #42", " sta myvar", " txa", " ldy #1", " sta myvar+1", " rts",
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop")
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" sta myvar", " lda #42", " ldy #1", " sta myvar+1", " rts",
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop")
}
@@ -280,19 +288,19 @@ class TestAsmOptimizer: FunSpec({
test("handles empty input") {
val lines = mutableListOf<String>()
optimize(lines) shouldBe 0
optimize(lines)
lines shouldBe emptyList()
}
test("handles single line input") {
val lines = mutableListOf(" rts")
optimize(lines) shouldBe 0
optimize(lines)
lines shouldBe listOf(" rts")
}
test("multiple optimizations in one pass") {
val lines = mutableListOf(" iny", " dey", " lda #1", " sta var", " lda var", " rts")
optimize(lines) shouldBe 2
optimize(lines)
lines shouldBe listOf(" lda #1", " sta var", " rts")
}
@@ -547,7 +555,9 @@ class TestAsmOptimizer: FunSpec({
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop"
)
// The second ldy should be removed since "ora" doesn't modify Y
optimize(lines) shouldBe 1
optimize(lines)
lines shouldBe listOf(" ldy #0", " lda (ptr),y", " ora myvar", " sta (ptr),y", " rts",
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop")
}
test("actual Y-modifying instructions should still be detected") {
@@ -557,7 +567,11 @@ class TestAsmOptimizer: FunSpec({
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop"
)
// The second ldy should NOT be removed since "tay" modifies Y
optimize(lines) shouldBe 0
optimize(lines)
lines shouldBe listOf(
" ldy #0", " lda (ptr),y", " tay",
" ldy #0", " sta (ptr),y", " rts",
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop")
}
test("modifiesYRegister: correctly identifies Y-modifying instructions") {
@@ -584,8 +598,13 @@ class TestAsmOptimizer: FunSpec({
" lda p8b_main.p8v_io_data", " sta p8b_main.p8v_result",
" rts", " nop"
)
val count = optimize(lines)
count shouldBe 0
optimize(lines)
lines.size shouldBe 8 // all instructions preserved
lines shouldBe listOf(
" lda p8b_main.p8v_io_data", " sta p8b_main.p8v_result",
" lda p8b_main.p8v_io_data", " sta p8b_main.p8v_result",
" lda p8b_main.p8v_io_data", " sta p8b_main.p8v_result",
" rts", " nop"
)
}
})
+1 -1
View File
@@ -170,7 +170,7 @@ class TestCodegen: FunSpec({
val st = SymbolTable(program)
st.add(StMemVar("zpmemvar", DataType.WORD, 0x20u, null, null))
st.add(StMemVar("normalmemvar", DataType.WORD, 0x9000u, null, null))
val allocator = VariableAllocator(st, getTestOptions(), ErrorReporterForTests())
val allocator = VariableAllocator(st, getTestOptions(), ErrorReporterForTests(), program)
allocator.isZpVar("zpmemvar") shouldBe true
allocator.isZpVar("normalmemvar") shouldBe false
}
-11
View File
@@ -1,11 +0,0 @@
plugins {
kotlin("jvm")
}
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")
}
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
<orderEntry type="library" name="michael.bull.kotlin.result.jvm" level="project" />
<orderEntry type="module" module-name="codeGenIntermediate" />
<orderEntry type="module" module-name="intermediate" />
<orderEntry type="module" module-name="codeCore" />
<orderEntry type="module" module-name="simpleAst" />
</component>
</module>
@@ -1,46 +0,0 @@
package prog8.codegen.experimental
import prog8.code.IAssemblyProgram
import prog8.code.ICodeGeneratorBackend
import prog8.code.SymbolTable
import prog8.code.ast.PtProgram
import prog8.code.core.CompilationOptions
import prog8.code.core.IErrorReporter
import prog8.codegen.intermediate.IRCodeGen
import prog8.intermediate.IRFileWriter
class ExperiCodeGen(val retainSSA: Boolean): ICodeGeneratorBackend {
override fun generate(
program: PtProgram,
symbolTable: SymbolTable,
options: CompilationOptions,
errors: IErrorReporter
): IAssemblyProgram {
// you could write a code generator directly on the PtProgram AST,
// but you can also use the Intermediate Representation to build a codegen on:
val irCodeGen = IRCodeGen(program, symbolTable, options, errors, retainSSA)
val irProgram = irCodeGen.generate()
irProgram.verifyRegisterTypes(irCodeGen.registerTypes())
// this stub only writes the IR program to disk but doesn't generate anything else.
IRFileWriter(irProgram, null).write()
if(!options.quiet)
println("** experimental codegen stub: no assembly generated **")
return EmptyProgram
}
}
private object EmptyProgram : IAssemblyProgram {
override val name = "<Empty Program>"
override val irInstructionCount: Int = 0
override val irChunkCount: Int = 0
override val irRegisterCount: Int = 0
override fun assemble(options: CompilationOptions, errors: IErrorReporter): Boolean {
if(!options.quiet)
println("** nothing assembled **")
return true
}
}
-1
View File
@@ -11,5 +11,4 @@ dependencies {
testImplementation(testFixtures(project(":codeCore")))
testImplementation("io.kotest:kotest-runner-junit5")
testImplementation("io.kotest:kotest-framework-datatest")
}
@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
<orderEntry type="module" module-name="codeCore" />
<orderEntry type="module" module-name="simpleAst" />
<orderEntry type="module" module-name="intermediate" />
<orderEntry type="library" name="michael.bull.kotlin.result.jvm" level="project" />
<orderEntry type="library" scope="TEST" name="io.kotest.runner.junit5.jvm" level="project" />
<orderEntry type="library" scope="TEST" name="io.kotest.framework.datatest" level="project" />
</component>
</module>
@@ -33,10 +33,10 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
extsub.returns.zip(assignmentTargets).forEach { pair ->
val returns = pair.first
val target = pair.second as PtAssignTarget
// consume the register even for void targets to keep indices aligned
val isFpRegister = returns.register.registerOrPair in listOf(RegisterOrPair.FAC1, RegisterOrPair.FAC2)
val regNumber = if(isFpRegister) fpRegs.removeAt(0) else cpuRegs.removeAt(0)
if (!target.void) {
// Determine if this return uses an FP register or CPU register
val isFpRegister = returns.register.registerOrPair in listOf(RegisterOrPair.FAC1, RegisterOrPair.FAC2)
val regNumber = if(isFpRegister) fpRegs.removeAt(0) else cpuRegs.removeAt(0)
result += assignCpuRegister(returns, regNumber, target)
}
}
@@ -55,7 +55,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
if(!target.void) {
val reg = it.second.first
val regnum = codeGen.registers.next(irType(it.second.second))
val p = StExtSubParameter(reg, it.second.second)
val p = StExtSubParameter("", it.second.second, reg)
result += assignCpuRegister(p, regnum, target)
}
}
@@ -71,7 +71,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
if(!target.void) {
val reg = it.second.first
val regnum = codeGen.registers.next(irType(it.second.second))
val p = StExtSubParameter(reg, it.second.second)
val p = StExtSubParameter("", it.second.second, reg)
result += assignCpuRegister(p, regnum, target)
}
}
@@ -91,12 +91,12 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
private fun assignCpuRegister(returns: StExtSubParameter, regNum: Int, target: PtAssignTarget): IRCodeChunks {
val result = mutableListOf<IRCodeChunkBase>()
when(returns.register.registerOrPair) {
RegisterOrPair.A -> addInstr(result, IRInstruction(Opcode.LOADHA, IRDataType.BYTE, reg1=regNum), null)
RegisterOrPair.X -> addInstr(result, IRInstruction(Opcode.LOADHX, IRDataType.BYTE, reg1=regNum), null)
RegisterOrPair.Y -> addInstr(result, IRInstruction(Opcode.LOADHY, IRDataType.BYTE, reg1=regNum), null)
RegisterOrPair.AX -> addInstr(result, IRInstruction(Opcode.LOADHAX, IRDataType.WORD, reg1=regNum), null)
RegisterOrPair.AY -> addInstr(result, IRInstruction(Opcode.LOADHAY, IRDataType.WORD, reg1=regNum), null)
RegisterOrPair.XY -> addInstr(result, IRInstruction(Opcode.LOADHXY, IRDataType.WORD, reg1=regNum), null)
RegisterOrPair.A -> addInstr(result, IRInstruction(Opcode.LOADHR, IRDataType.BYTE, reg1=regNum, immediate=0), null)
RegisterOrPair.X -> addInstr(result, IRInstruction(Opcode.LOADHR, IRDataType.BYTE, reg1=regNum, immediate=1), null)
RegisterOrPair.Y -> addInstr(result, IRInstruction(Opcode.LOADHR, IRDataType.BYTE, reg1=regNum, immediate=2), null)
RegisterOrPair.AX -> addInstr(result, IRInstruction(Opcode.LOADHR, IRDataType.WORD, reg1=regNum, immediate=3), null)
RegisterOrPair.AY -> addInstr(result, IRInstruction(Opcode.LOADHR, IRDataType.WORD, reg1=regNum, immediate=4), null)
RegisterOrPair.XY -> addInstr(result, IRInstruction(Opcode.LOADHR, IRDataType.WORD, reg1=regNum, immediate=5), null)
in Cx16VirtualRegisters -> addInstr(result, IRInstruction(Opcode.LOADM, irType(returns.type), reg1=regNum, labelSymbol = "cx16.${returns.register.registerOrPair.toString().lowercase()}"), null)
in CombinedLongRegisters -> {
require(returns.type.isLong)
@@ -224,6 +224,8 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
}
"or=" -> {
val shortcutLabel = codeGen.createLabelName()
if(!codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps)
addInstr(inplaceInstrs, IRInstruction(Opcode.CMPI, targetDt, reg1 = oldvalueReg, immediate = 0), null)
addInstr(inplaceInstrs, IRInstruction(Opcode.BSTNE, labelSymbol = shortcutLabel), null)
val valueTr = exprGen.translateExpression(value)
inplaceInstrs += valueTr.chunks
@@ -232,6 +234,8 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
}
"and=" -> {
val shortcutLabel = codeGen.createLabelName()
if(!codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps)
addInstr(inplaceInstrs, IRInstruction(Opcode.CMPI, targetDt, reg1 = oldvalueReg, immediate = 0), null)
addInstr(inplaceInstrs, IRInstruction(Opcode.BSTEQ, labelSymbol = shortcutLabel), null)
val valueTr = exprGen.translateExpression(value)
inplaceInstrs += valueTr.chunks
@@ -385,7 +389,11 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
val arrayVariableName = array.variable!!.name
val skipCarryLabel = codeGen.createLabelName()
if(constIndex!=null) {
val negLsbReg = codeGen.registers.next(IRDataType.BYTE)
addInstr(result, IRInstruction(Opcode.NEGM, IRDataType.BYTE, labelSymbol = arrayVariableName+"_lsb", symbolOffset = constIndex), null)
addInstr(result, IRInstruction(Opcode.LOADM, IRDataType.BYTE, reg1 = negLsbReg, labelSymbol = arrayVariableName+"_lsb", symbolOffset = constIndex), null)
if(!codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps)
addInstr(result, IRInstruction(Opcode.CMPI, IRDataType.BYTE, reg1 = negLsbReg, immediate = 0), null)
addInstr(result, IRInstruction(Opcode.BSTEQ, labelSymbol = skipCarryLabel), null)
addInstr(result, IRInstruction(Opcode.INCM, IRDataType.BYTE, labelSymbol = arrayVariableName+"_msb", symbolOffset = constIndex), null)
addInstr(result, IRInstruction(Opcode.NEGM, IRDataType.BYTE, labelSymbol = arrayVariableName+"_msb", symbolOffset = constIndex), skipCarryLabel)
@@ -1029,6 +1037,8 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
IRInstruction(Opcode.LOADM, vmDt, reg1=inplaceReg, address = constAddress.toAddress())
else
IRInstruction(Opcode.LOADM, vmDt, reg1=inplaceReg, labelSymbol = symbol)
if(!codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps)
it += IRInstruction(Opcode.CMPI, vmDt, reg1=inplaceReg, immediate = 0)
it += IRInstruction(Opcode.BSTEQ, labelSymbol = shortcutLabel)
}
addToResult(result, tr, tr.resultReg, -1)
@@ -1289,6 +1299,8 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
IRInstruction(Opcode.LOADM, vmDt, reg1=inplaceReg, address = constAddress.toAddress())
else
IRInstruction(Opcode.LOADM, vmDt, reg1=inplaceReg, labelSymbol = symbol)
if(!codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps)
it += IRInstruction(Opcode.CMPI, vmDt, reg1=inplaceReg, immediate = 0)
it += IRInstruction(Opcode.BSTNE, labelSymbol = shortcutLabel)
}
addToResult(result, tr, tr.resultReg, -1)
@@ -1477,11 +1489,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
if(constValue==1) {
addInstr(result, IRInstruction(Opcode.DECM, eltDt, labelSymbol = arrayVariableName, symbolOffset = constIndex*eltSize), null)
} else {
val valueReg=codeGen.registers.next(eltDt)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOAD, eltDt, reg1=valueReg, immediate = constValue)
it += IRInstruction(Opcode.SUBM, eltDt, reg1=valueReg, labelSymbol = arrayVariableName, symbolOffset = constIndex*eltSize)
}
addInstr(result, IRInstruction(Opcode.SUBIM, eltDt, immediate = constValue, labelSymbol = arrayVariableName, symbolOffset = constIndex*eltSize), null)
}
return result
}
@@ -1604,8 +1612,12 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
IRInstruction(Opcode.DECM, vmDt, address = constAddress.toAddress())
else
IRInstruction(Opcode.DECM, vmDt, labelSymbol = symbol), null)
}
else {
} else if(constValue!=null) {
addInstr(result, if(constAddress!=null)
IRInstruction(Opcode.SUBIM, vmDt, immediateFp = constValue, address = constAddress.toAddress())
else
IRInstruction(Opcode.SUBIM, vmDt, immediateFp = constValue, labelSymbol = symbol), null)
} else {
val tr = exprGen.translateExpression(operand)
addToResult(result, tr, -1, tr.resultFpReg)
addInstr(result, if(constAddress!=null)
@@ -1619,8 +1631,12 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
IRInstruction(Opcode.DECM, vmDt, address = constAddress.toAddress())
else
IRInstruction(Opcode.DECM, vmDt, labelSymbol = symbol), null)
}
else {
} else if(constValue!=null) {
addInstr(result, if(constAddress!=null)
IRInstruction(Opcode.SUBIM, vmDt, immediate = constValue.toInt(), address = constAddress.toAddress())
else
IRInstruction(Opcode.SUBIM, vmDt, immediate = constValue.toInt(), labelSymbol = symbol), null)
} else {
val tr = exprGen.translateExpression(operand)
addToResult(result, tr, tr.resultReg, -1)
addInstr(result, if(constAddress!=null)
@@ -1712,6 +1728,8 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
val lsbReg = codeGen.registers.next(IRDataType.BYTE)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOADM, IRDataType.BYTE, reg1 = lsbReg, labelSymbol = arrayVariableName+"_lsb", symbolOffset = constIndex)
if(!codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps)
it += IRInstruction(Opcode.CMPI, IRDataType.BYTE, reg1 = lsbReg, immediate = 0)
it += IRInstruction(Opcode.BSTNE, labelSymbol = skip)
it += IRInstruction(Opcode.DECM, IRDataType.BYTE, labelSymbol = arrayVariableName+"_msb", symbolOffset = constIndex)
}
@@ -1793,11 +1811,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
if(constValue==1) {
addInstr(result, IRInstruction(Opcode.INCM, elementDt, labelSymbol = arrayVariableName, symbolOffset = constIndex*eltSize), null)
} else {
val valueReg=codeGen.registers.next(elementDt)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOAD, elementDt, reg1=valueReg, immediate = constValue)
it += IRInstruction(Opcode.ADDM, elementDt, reg1=valueReg, labelSymbol = arrayVariableName, symbolOffset = constIndex*eltSize)
}
addInstr(result, IRInstruction(Opcode.ADDIM, elementDt, immediate = constValue, labelSymbol = arrayVariableName, symbolOffset = constIndex*eltSize), null)
}
return result
}
@@ -1923,8 +1937,12 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
IRInstruction(Opcode.INCM, vmDt, address = constAddress.toAddress())
else
IRInstruction(Opcode.INCM, vmDt, labelSymbol = symbol) , null)
}
else {
} else if(constValue!=null) {
addInstr(result, if (constAddress != null)
IRInstruction(Opcode.ADDIM, vmDt, immediateFp = constValue, address = constAddress.toAddress())
else
IRInstruction(Opcode.ADDIM, vmDt, immediateFp = constValue, labelSymbol = symbol) , null)
} else {
val tr = exprGen.translateExpression(operand)
addToResult(result, tr, -1, tr.resultFpReg)
addInstr(result, if (constAddress != null)
@@ -1938,8 +1956,12 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
IRInstruction(Opcode.INCM, vmDt, address = constAddress.toAddress())
else
IRInstruction(Opcode.INCM, vmDt, labelSymbol = symbol) , null)
}
else {
} else if(constValue!=null) {
addInstr(result, if (constAddress != null)
IRInstruction(Opcode.ADDIM, vmDt, immediate = constValue.toInt(), address = constAddress.toAddress())
else
IRInstruction(Opcode.ADDIM, vmDt, immediate = constValue.toInt(), labelSymbol = symbol) , null)
} else {
val tr = exprGen.translateExpression(operand)
addToResult(result, tr, tr.resultReg, -1)
if (constAddress != null)
@@ -1960,8 +1982,12 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
if(constValue==1) {
val skip = codeGen.createLabelName()
val lsbReg = codeGen.registers.next(IRDataType.BYTE)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.INCM, IRDataType.BYTE, labelSymbol = arrayVariableName+"_lsb", symbolOffset = constIndex)
it += IRInstruction(Opcode.LOADM, IRDataType.BYTE, reg1 = lsbReg, labelSymbol = arrayVariableName+"_lsb", symbolOffset = constIndex)
if(!codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps)
it += IRInstruction(Opcode.CMPI, IRDataType.BYTE, reg1 = lsbReg, immediate = 0)
it += IRInstruction(Opcode.BSTNE, labelSymbol = skip)
it += IRInstruction(Opcode.INCM, IRDataType.BYTE, labelSymbol = arrayVariableName+"_msb", symbolOffset = constIndex)
}
@@ -188,7 +188,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
else {
val resultvalueReg = codeGen.registers.next(IRDataType.WORD)
addInstr(result, IRInstruction(Opcode.LOADHAY, IRDataType.WORD, reg1=resultvalueReg), null)
addInstr(result, IRInstruction(Opcode.LOADHR, IRDataType.WORD, reg1=resultvalueReg, immediate=4), null)
ExpressionCodeResult(result, IRDataType.WORD, resultvalueReg, -1)
}
}
@@ -256,12 +256,12 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
// 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.STOREHA, IRDataType.BYTE, reg1=divisionReg)
it += IRInstruction(Opcode.STOREHR, IRDataType.BYTE, reg1=divisionReg, immediate=0)
it += IRInstruction(Opcode.STOREM, IRDataType.BYTE, reg1=remainderReg, labelSymbol = "cx16.r15")
}
} else if(type==IRDataType.WORD) {
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.STOREHAY, IRDataType.WORD, reg1=divisionReg)
it += IRInstruction(Opcode.STOREHR, IRDataType.WORD, reg1=divisionReg, immediate=4)
it += IRInstruction(Opcode.STOREM, IRDataType.WORD, reg1=remainderReg, labelSymbol = "cx16.r15")
}
} else throw AssemblyError("invalid type for DIVMOD")
@@ -300,12 +300,15 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val tr = exprGen.translateExpression(call.args[0])
addToResult(result, tr, tr.resultReg, -1)
val needsExplicitCmpi = !codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps
when (sourceDt.base) {
BaseDataType.BYTE -> {
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)
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)
}
@@ -317,6 +320,8 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val compareReg = codeGen.registers.next(IRDataType.WORD)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOADR, IRDataType.WORD, reg1=compareReg, reg2=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)
}
@@ -328,6 +333,8 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val compareReg = codeGen.registers.next(IRDataType.LONG)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOADR, IRDataType.LONG, reg1=compareReg, reg2=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)
}
@@ -468,40 +475,30 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
}
private fun funcClamp(call: PtFunctionCall): ExpressionCodeResult {
val result = mutableListOf<IRCodeChunkBase>()
val type = irType(call.type)
require(type != IRDataType.FLOAT)
val result = mutableListOf<IRCodeChunkBase>()
val valueTr = exprGen.translateExpression(call.args[0])
val minimumTr = exprGen.translateExpression(call.args[1])
val maximumTr = exprGen.translateExpression(call.args[2])
result += valueTr.chunks
result += minimumTr.chunks
result += maximumTr.chunks
if(type==IRDataType.FLOAT) {
result += codeGen.makeSyscall(
IMSyscall.CLAMP_FLOAT, listOf(
valueTr.dt to valueTr.resultFpReg,
minimumTr.dt to minimumTr.resultFpReg,
maximumTr.dt to maximumTr.resultFpReg,
), type to valueTr.resultFpReg
)
return ExpressionCodeResult(result, type, -1, valueTr.resultFpReg)
} else {
val syscall = when(call.type.base) {
BaseDataType.UBYTE -> IMSyscall.CLAMP_UBYTE
BaseDataType.BYTE -> IMSyscall.CLAMP_BYTE
BaseDataType.UWORD -> IMSyscall.CLAMP_UWORD
BaseDataType.WORD -> IMSyscall.CLAMP_WORD
BaseDataType.LONG -> IMSyscall.CLAMP_LONG
else -> throw AssemblyError("invalid dt")
}
result += codeGen.makeSyscall(syscall, listOf(
valueTr.dt to valueTr.resultReg,
minimumTr.dt to minimumTr.resultReg,
maximumTr.dt to maximumTr.resultReg,
), type to valueTr.resultReg
)
return ExpressionCodeResult(result, type, valueTr.resultReg, -1)
val syscall = when(call.type.base) {
BaseDataType.UBYTE -> IMSyscall.CLAMP_UBYTE
BaseDataType.BYTE -> IMSyscall.CLAMP_BYTE
BaseDataType.UWORD -> IMSyscall.CLAMP_UWORD
BaseDataType.WORD -> IMSyscall.CLAMP_WORD
BaseDataType.LONG -> IMSyscall.CLAMP_LONG
else -> throw AssemblyError("invalid dt")
}
result += codeGen.makeSyscall(syscall, listOf(
valueTr.dt to valueTr.resultReg,
minimumTr.dt to minimumTr.resultReg,
maximumTr.dt to maximumTr.resultReg,
), type to valueTr.resultReg
)
return ExpressionCodeResult(result, type, valueTr.resultReg, -1)
}
private fun funcMin(call: PtFunctionCall): ExpressionCodeResult {
@@ -701,7 +698,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
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.STOREHA, IRDataType.BYTE, reg1 = byteReg), null)
addInstr(result, IRInstruction(Opcode.STOREHR, IRDataType.BYTE, reg1 = byteReg, immediate = 0), null)
return ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
}
@@ -762,9 +759,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val arr = (arg as? PtArrayIndexer)
val index = arr?.index?.asConstInteger()
if(arr!=null && index!=null) {
if(arr.variable==null)
TODO("support for ptr indexing ${arr.position}")
if(arr!=null && index!=null && arr.variable!=null) {
val variable = arr.variable!!.name
if(arr.splitWords) {
result += IRCodeChunk(null, null).also {
@@ -828,11 +823,34 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
}
}
is PtArrayIndexer -> {
if(target.splitWords) {
if(target.variable==null) {
// pointer-based array indexing: compute memory address and store byte
val eltSize = codeGen.program.memsizer.memorySize(target.type, null)
val pointerTr = exprGen.translateExpression(target.pointerderef!!)
addToResult(result, pointerTr, pointerTr.resultReg, -1)
val constIndex = target.index.asConstInteger()
if(constIndex != null) {
val offset = eltSize * constIndex + if(msb) eltSize - 1 else 0
if(offset > 0)
addInstr(result, IRInstruction(Opcode.ADD, IRDataType.WORD, reg1 = pointerTr.resultReg, immediate = offset), null)
} else {
val (code, indexWordReg) = codeGen.loadIndexReg(target.index, eltSize, true, false)
result += code
addInstr(result, IRInstruction(Opcode.ADDR, IRDataType.WORD, reg1 = pointerTr.resultReg, reg2 = indexWordReg), null)
if(msb)
addInstr(result, IRInstruction(Opcode.ADD, IRDataType.WORD, reg1 = pointerTr.resultReg, immediate = eltSize - 1), null)
}
if(isConstZeroValue) {
addInstr(result, IRInstruction(Opcode.STOREZI, IRDataType.BYTE, reg1 = pointerTr.resultReg, immediate = 0), 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)
}
}
else if(target.splitWords) {
// lsb/msb in split arrays, element index 'size' is always 1
val constIndex = target.index.asConstInteger()
if(target.variable==null)
TODO("support for ptr indexing ${target.position}")
val varName = target.variable!!.name + if(msb) "_msb" else "_lsb"
if(isConstZeroValue) {
if(constIndex!=null) {
@@ -96,11 +96,10 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
is PtBinaryExpression -> translate(expr)
is PtIfExpression -> translate(expr)
is PtBranchCondExpression -> translate(expr)
is PtFunctionCall -> translate(expr)
is PtFunctionCall -> translate(expr, true)
is PtContainmentCheck -> translate(expr)
is PtPointerDeref -> translate(expr)
is PtConstant -> {
val name = expr.name
val slab = expr.memorySlab
// Check if the constant has a numeric value or is a memory slab reference
if(slab != null) {
@@ -212,6 +211,13 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
val (instructions, offset) = traverseRestOfDerefChainToCalculateFinalAddress(deref, pointerReg)
result += instructions
// For inline array fields (not pointer fields), return the address (ptr+offset) instead of loading from it
if(deref.type.isArray && !deref.derefLast) {
if(offset > 0u)
addInstr(result, IRInstruction(Opcode.ADD, IRDataType.WORD, reg1 = pointerReg, immediate = offset.toInt()), null)
return ExpressionCodeResult(result, IRDataType.WORD, pointerReg, -1)
}
return if(deref.type.isFloat) {
val resultReg = codeGen.registers.next(IRDataType.FLOAT)
addInstr(result, IRInstruction(Opcode.LOADI, IRDataType.FLOAT, fpReg1 = RegisterNum(resultReg), reg1 = pointerReg, immediate = offset.toInt()), null)
@@ -239,24 +245,16 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
if(ifExpr.condition is PtBinaryExpression) {
val useBIT = checkIfConditionCanUseBIT(ifExpr.condition as PtBinaryExpression)
if(useBIT!=null) {
// use a BIT instruction to test for bit 7 or 6 set/clear
val (testBitSet, variable, bitmask) = useBIT
val bitBranchOpcode = when(testBitSet) {
true -> when(bitmask) {
64 -> Opcode.BSTVC
128 -> Opcode.BSTPOS
else -> throw AssemblyError("need bit 6 or 7")
}
false -> when(bitmask) {
64 -> Opcode.BSTVS
128 -> Opcode.BSTNEG
else -> throw AssemblyError("need bit 6 or 7")
}
}
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.BIT, IRDataType.BYTE, labelSymbol = variable.name)
it += IRInstruction(bitBranchOpcode, labelSymbol = falseLabel)
}
// use a BITTST instruction to test any single bit
val (testBitSet, expr, bitmask) = useBIT
val bitPos = Integer.numberOfTrailingZeros(bitmask)
val tr = translateExpression(expr)
addToResult(result, tr, tr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.BITTST, tr.dt, reg1 = tr.resultReg, immediate = bitPos), null)
if(testBitSet)
addInstr(result, IRInstruction(Opcode.BSTEQ, labelSymbol = falseLabel), null)
else
addInstr(result, IRInstruction(Opcode.BSTNE, labelSymbol = falseLabel), null)
addToResult(result, trueTr, trueTr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.JUMP, labelSymbol = endLabel), null)
result += IRCodeChunk(falseLabel, null)
@@ -270,9 +268,14 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
// Use status flags from condition expression when possible, avoiding redundant CMPI #0
val condTr = translateExpression(ifExpr.condition)
addToResult(result, condTr, condTr.resultReg, -1)
// Check if the last instruction already sets status flags - if so, skip the CMPI #0
// Only skip the CMPI if the target honors the "multi-byte ops set Z based on full
// value" contract. On 8-bit targets (6502/65C02) this is false, so we always emit
// an explicit CMPI - a multi-byte op may have set Z based on only the last byte.
// See CpuType.statusBitsOnMultiByteOps for the rationale.
val lastInstr = condTr.chunks.lastOrNull()?.instructions?.lastOrNull()
val skipCmpi = lastInstr != null && lastInstr.opcode in OpcodesThatSetStatusbits
val skipCmpi = codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps
&& lastInstr != null
&& lastInstr.opcode in OpcodesThatSetStatusbits
if (!skipCmpi) {
addInstr(result, IRInstruction(Opcode.CMPI, IRDataType.BYTE, reg1=condTr.resultReg, immediate = 0), null)
}
@@ -538,7 +541,21 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
val iterableLength = codeGen.symbolTable.getLength(haystackVar.name)
addInstr(result, IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=lengthReg, immediate = iterableLength!!), null)
val resultReg = codeGen.registers.next(IRDataType.BYTE)
result += codeGen.makeSyscall(IMSyscall.WORDARRAY_CONTAINS, listOf(IRDataType.WORD to elementTr.resultReg, IRDataType.WORD to iterableTr.resultReg, IRDataType.BYTE to lengthReg), IRDataType.BYTE to resultReg)
val syscall = if(haystackVar.type.isSplitWordArray) IMSyscall.SPLIT_WORDARRAY_CONTAINS else IMSyscall.WORDARRAY_CONTAINS
result += codeGen.makeSyscall(syscall, listOf(IRDataType.WORD to elementTr.resultReg, IRDataType.WORD to iterableTr.resultReg, IRDataType.BYTE to lengthReg), IRDataType.BYTE to resultReg)
addInstr(result, IRInstruction(Opcode.CMPI, IRDataType.BYTE, reg1=resultReg, immediate = 0), null)
return ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
}
haystackVar.type.isLongArray -> {
val elementTr = translateExpression(check.needle)
addToResult(result, elementTr, elementTr.resultReg, -1)
val iterableTr = translateExpression(haystackVar)
addToResult(result, iterableTr, iterableTr.resultReg, -1)
val lengthReg = codeGen.registers.next(IRDataType.BYTE)
val resultReg = codeGen.registers.next(IRDataType.BYTE)
val iterableLength = codeGen.symbolTable.getLength(haystackVar.name)
addInstr(result, IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=lengthReg, immediate = iterableLength!!), null)
result += codeGen.makeSyscall(IMSyscall.LONGARRAY_CONTAINS, listOf(IRDataType.LONG to elementTr.resultReg, IRDataType.WORD to iterableTr.resultReg, IRDataType.BYTE to lengthReg), IRDataType.BYTE to resultReg)
addInstr(result, IRInstruction(Opcode.CMPI, IRDataType.BYTE, reg1=resultReg, immediate = 0), null)
return ExpressionCodeResult(result, IRDataType.BYTE, resultReg, -1)
}
@@ -924,7 +941,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
}
}
internal fun translate(fcall: PtFunctionCall): ExpressionCodeResult {
internal fun translate(fcall: PtFunctionCall, resultUsedAsExpression: Boolean = false): ExpressionCodeResult {
if(fcall.builtin)
return codeGen.translateBuiltinFunc(fcall)
@@ -962,11 +979,14 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
val paramDt = irType(parameter.type)
if(parameter.register==null) {
val tr = translateExpression(arg)
result += tr.chunks
// Note: explicit setCpuRegister is not needed here because the CALL instruction
// carries the same slot information in its argument specs, and the code generator
// backend uses those to load the CPU registers directly.
result += tr.chunks
if(paramDt==IRDataType.FLOAT)
argRegisters.add(FunctionCallArgs.ArgumentSpec(parameter.name, null, FunctionCallArgs.RegSpec(IRDataType.FLOAT, RegisterNum(tr.resultFpReg), null)))
argRegisters.add(FunctionCallArgs.ArgumentSpec(parameter.name, null, FunctionCallArgs.RegSpec(IRDataType.FLOAT, RegisterNum(tr.resultFpReg), null, null)))
else
argRegisters.add(FunctionCallArgs.ArgumentSpec(parameter.name, null, FunctionCallArgs.RegSpec(paramDt, RegisterNum(tr.resultReg), null)))
argRegisters.add(FunctionCallArgs.ArgumentSpec(parameter.name, null, FunctionCallArgs.RegSpec(paramDt, RegisterNum(tr.resultReg), null, null)))
} else {
require(parameter.register in Cx16VirtualRegisters || parameter.register in CombinedLongRegisters) { "can only use R0-R15 'registers' here" }
val regname = parameter.register!!.asScopedNameVirtualReg(parameter.type).joinToString(".")
@@ -985,7 +1005,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
val returnRegSpecs = if(fcall.void || callTarget.returns.size>1) emptyList() else {
callTarget.returns.map {
val returnIrType = irType(it)
FunctionCallArgs.RegSpec(returnIrType, RegisterNum(codeGen.registers.next(returnIrType)), null)
FunctionCallArgs.RegSpec(returnIrType, RegisterNum(codeGen.registers.next(returnIrType)), null, null)
}
}
// create the call
@@ -1012,12 +1032,22 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
for ((arg, parameter) in fcall.args.zip(callTarget.parameters)) {
val paramDt = irType(parameter.type)
val tr = translateExpression(arg)
val (slot, flag) = registerOrStatusflagToSlotAndFlag(parameter.register)
val argName = if(slot==null && flag==null) {
val reg = parameter.register.registerOrPair
if(reg != null && reg in Cx16VirtualRegisters) {
// CX16 virtual register parameter - use the cx16 symbol name
// so the codegen can resolve it to the correct address
"cx16.${reg.toString().lowercase()}"
} else {
parameter.name
}
} else ""
if(paramDt==IRDataType.FLOAT)
argRegisters.add(FunctionCallArgs.ArgumentSpec("", null, FunctionCallArgs.RegSpec(IRDataType.FLOAT, RegisterNum(tr.resultFpReg), parameter.register)))
argRegisters.add(FunctionCallArgs.ArgumentSpec(argName, null, FunctionCallArgs.RegSpec(IRDataType.FLOAT, RegisterNum(tr.resultFpReg), slot, flag)))
else
argRegisters.add(FunctionCallArgs.ArgumentSpec("", null, FunctionCallArgs.RegSpec(paramDt, RegisterNum(tr.resultReg), parameter.register)))
argRegisters.add(FunctionCallArgs.ArgumentSpec(argName, null, FunctionCallArgs.RegSpec(paramDt, RegisterNum(tr.resultReg), slot, flag)))
result += tr.chunks
result += codeGen.setCpuRegister(parameter.register, paramDt, tr.resultReg, tr.resultFpReg)
}
if(callTarget.returns.size>1)
@@ -1030,44 +1060,19 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
else {
val returns = callTarget.returns[0]
val returnIrType = irType(returns.type)
FunctionCallArgs.RegSpec(returnIrType, RegisterNum(codeGen.registers.next(returnIrType)), returns.register)
val (retSlot, retFlag) = registerOrStatusflagToSlotAndFlag(returns.register)
FunctionCallArgs.RegSpec(returnIrType, RegisterNum(codeGen.registers.next(returnIrType)), retSlot, retFlag)
}
}
// create the call
val returnRegs = if(returnRegSpec==null) emptyList() else listOf(returnRegSpec)
val call =
if(callTarget.address==null)
IRInstruction(Opcode.CALL, labelSymbol = fcall.name, fcallArgs = FunctionCallArgs(argRegisters, returnRegs))
else {
val address = callTarget.address!!
if(address.constbank==null && address.varbank==null) {
IRInstruction(
Opcode.CALL,
address = address.address.toAddress(),
fcallArgs = FunctionCallArgs(argRegisters, returnRegs))
} else if(address.constbank!=null) {
IRInstruction(
Opcode.CALLFAR,
address = address.address.toAddress(),
immediate = address.constbank!!.toInt()
)
} else {
val tr = translateExpression(address.varbank!!)
require(tr.dt==IRDataType.BYTE)
result += tr.chunks
IRInstruction(
Opcode.CALLFARVB,
address = address.address.toAddress(),
reg1 = tr.resultReg
)
}
}
val call = emitExtSubCall(callTarget, fcall, FunctionCallArgs(argRegisters, returnRegs), result)
addInstr(result, call, null)
var finalReturnRegister = returnRegSpec?.registerNum?.value ?: -1
if(fcall.parent is PtAssignment || fcall.parent is PtTypeCast) {
if(resultUsedAsExpression) {
// look if the status flag bit should actually be returned as a 0/1 byte value in a result register (so it can be assigned)
val statusFlagResult = returnRegSpec?.cpuRegister?.statusflag
val statusFlagResult = returnRegSpec?.statusflag
if(statusFlagResult!=null) {
// assign status flag bit to the return value register
finalReturnRegister = returnRegSpec.registerNum.value
@@ -1128,24 +1133,19 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
}
}
internal fun checkIfConditionCanUseBIT(condition: PtBinaryExpression): Triple<Boolean, PtIdentifier, Int>? {
// test for occurrence of: x & 64 != 0 (or 128) , this can be performed with a BIT instruction
data class BitTestResult(val testBitSet: Boolean, val expression: PtExpression, val bitmask: Int)
internal fun checkIfConditionCanUseBIT(condition: PtBinaryExpression): BitTestResult? {
// test for occurrence of: x & power_of_2 != 0 (or == 0) , this can be performed with a BITTST instruction
if(condition.operator == "==" || condition.operator == "!=") {
if (condition.right.asConstInteger() == 0) {
val and = condition.left as? PtBinaryExpression
if (and != null && and.operator == "&" && and.type.isUnsignedByte) {
val bitmask = and.right.asConstInteger()
if(bitmask==128 || bitmask==64) {
val variable = and.left as? PtIdentifier
if (variable != null && variable.type.isByte) {
return Triple(condition.operator=="!=", variable, bitmask)
}
val typecast = and.left as? PtTypeCast
if (typecast != null && typecast.type.isUnsignedByte) {
val castedVariable = typecast.value as? PtIdentifier
if(castedVariable!=null && castedVariable.type.isByte)
return Triple(condition.operator=="!=", castedVariable, bitmask)
}
if (and != null && and.operator == "&") {
val bitmask = and.right.asConstInteger() ?: return null
if(bitmask > 0 && bitmask and (bitmask - 1) == 0) { // power of two
val expr = and.left
if(expr is PtIdentifier || expr is PtTypeCast || expr is PtArrayIndexer)
return BitTestResult(condition.operator=="!=", expr, bitmask)
}
}
}
@@ -1162,29 +1162,67 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
// return multiple values
val returnRegisters = callTarget.returns.map {
val regnum = codeGen.registers.next(irType(it.type))
FunctionCallArgs.RegSpec(irType(it.type), RegisterNum(regnum), it.register)
val (slot, flag) = registerOrStatusflagToSlotAndFlag(it.register)
FunctionCallArgs.RegSpec(irType(it.type), RegisterNum(regnum), slot, flag)
}
// create the call
val call =
if(callTarget.address==null)
IRInstruction(Opcode.CALL, labelSymbol = fcall.name, fcallArgs = FunctionCallArgs(argRegisters, returnRegisters))
else {
val address = callTarget.address!!
if(address.constbank==null && address.varbank==null) {
IRInstruction(
Opcode.CALL,
address = address.address.toAddress(),
fcallArgs = FunctionCallArgs(argRegisters, returnRegisters)
)
}
else TODO("extsub with banked address got called ${callTarget.name} ${fcall.position}")
}
val call = emitExtSubCall(callTarget, fcall, FunctionCallArgs(argRegisters, returnRegisters), result)
addInstr(result, call, null)
val resultRegs = returnRegisters.filter{it.dt!=IRDataType.FLOAT}.map{it.registerNum.value}
val resultFpRegs = returnRegisters.filter{it.dt==IRDataType.FLOAT}.map{it.registerNum.value}
return ExpressionCodeResult(result, IRDataType.BYTE, -1, -1, resultRegs, resultFpRegs)
}
private fun emitExtSubCall(
callTarget: StExtSub,
fcall: PtFunctionCall,
fcallArgs: FunctionCallArgs,
result: MutableList<IRCodeChunkBase>
): IRInstruction {
val address = callTarget.address
return if (address == null) {
IRInstruction(Opcode.CALL, labelSymbol = fcall.name, fcallArgs = fcallArgs)
} else if (address.constbank == null && address.varbank == null) {
IRInstruction(
Opcode.CALL,
address = address.address.toAddress(),
fcallArgs = fcallArgs
)
} else if (address.constbank != null) {
IRInstruction(
Opcode.CALLFAR,
address = address.address.toAddress(),
immediate = address.constbank!!.toInt(),
fcallArgs = fcallArgs
)
} else {
val varBank = address.varbank!!
val target = codeGen.symbolTable.lookup(varBank.name) ?: codeGen.symbolTable.lookupUnscoped(varBank.name)
val bankReg = if (target?.type == StNodeType.SUBROUTINE || target?.type == StNodeType.EXTSUB) {
// generate a call to the bank subroutine with the call ID as argument
val bankFcall = PtFunctionCall(varBank.name, false, false, arrayOf(DataType.UBYTE), varBank.position)
val callId = codeGen.extsubCallSiteIds[fcall.name]
if (callId != null) {
bankFcall.add(PtNumber(BaseDataType.UBYTE, callId.toDouble(), varBank.position))
}
val tr = translate(bankFcall, true)
result += tr.chunks
tr.resultReg
} else {
val tr = translateExpression(varBank)
require(tr.dt == IRDataType.BYTE)
result += tr.chunks
tr.resultReg
}
IRInstruction(
Opcode.CALLFARVB,
address = address.address.toAddress(),
reg1 = bankReg,
fcallArgs = fcallArgs
)
}
}
private fun operatorGreaterThan(
binExpr: PtBinaryExpression,
vmDt: IRDataType,
@@ -1278,7 +1316,14 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
val leftTr = translateExpression(binExpr.left)
addToResult(result, leftTr, leftTr.resultReg, -1)
val lastInstr = leftTr.chunks.lastOrNull()?.instructions?.lastOrNull()
val skipCmpi = rightConst.toInt() == 0 && lastInstr != null && lastInstr.opcode in OpcodesThatSetStatusbits
// Only skip the CMPI if the target honors the "multi-byte ops set Z
// based on full value" contract. On 8-bit targets a previous op like
// DEC only set Z from the last byte, so the explicit CMPI is required.
// See CpuType.statusBitsOnMultiByteOps for the rationale.
val skipCmpi = rightConst.toInt() == 0
&& codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps
&& lastInstr != null
&& lastInstr.opcode in OpcodesThatSetStatusbits
if (!skipCmpi) {
addInstr(result, IRInstruction(Opcode.CMPI, leftTr.dt, reg1 = leftTr.resultReg, immediate = rightConst.toInt()), null)
}
@@ -1820,6 +1865,23 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
it += IRInstruction(Opcode.LOADI, IRDataType.WORD, reg1 = pointerReg, reg2 = pointerReg, immediate = fieldinfo.second.toInt())
}
}
private fun registerOrStatusflagToSlotAndFlag(reg: RegisterOrStatusflag): Pair<CallingConventionSlot?, Statusflag?> {
if (reg.statusflag != null) return null to reg.statusflag
if (reg.registerOrPair == null) return null to null
if (reg.registerOrPair in Cx16VirtualRegisters || reg.registerOrPair in CombinedLongRegisters) return null to null
return when (reg.registerOrPair) {
RegisterOrPair.A -> CallingConventionSlot(0) to null
RegisterOrPair.X -> CallingConventionSlot(1) to null
RegisterOrPair.Y -> CallingConventionSlot(2) to null
RegisterOrPair.AX -> CallingConventionSlot(3) to null
RegisterOrPair.AY -> CallingConventionSlot(4) to null
RegisterOrPair.XY -> CallingConventionSlot(5) to null
RegisterOrPair.FAC1 -> CallingConventionSlot(6) to null
RegisterOrPair.FAC2 -> CallingConventionSlot(7) to null
else -> throw AssemblyError("unsupported register $reg")
}
}
}
@@ -3,30 +3,44 @@ package prog8.codegen.intermediate
import prog8.code.*
import prog8.code.ast.*
import prog8.code.core.*
import prog8.code.target.VMTarget
import prog8.intermediate.*
import kotlin.io.path.readBytes
// NOTE: All symbol names in the generated IR are fully scoped (entire dotted path)
// and carry type prefixes: p8b_ (block), p8s_ (sub), p8v_ (variable), p8c_ (const),
// p8l_ (label), p8t_ (struct), p8i_ (instance). Any backend consuming the IR
// can rely on these already being present and does not need to add them itself.
class IRCodeGen(
internal val program: PtProgram,
internal val symbolTable: SymbolTable,
internal var symbolTable: SymbolTable,
internal val options: CompilationOptions,
internal val errors: IErrorReporter,
internal val retainSSA: Boolean
internal val retainSSA: Boolean,
preassignedCallSiteIds: Map<String, UByte> = emptyMap()
) {
private val expressionEval = ExpressionGen(this)
private val builtinFuncGen = BuiltinFuncGen(this, expressionEval)
private val assignmentGen = AssignmentGen(this, expressionEval)
internal val registers = RegisterPool()
internal val extsubCallSiteIds: MutableMap<String, UByte> = preassignedCallSiteIds.toMutableMap()
fun generate(): IRProgram {
// The pure "virtual" (VM) target doesn't need symbol prefixing because the VM
// doesn't emit assembly and has no risk of symbol clashes. Any other target
// (that goes through assembly code generation) MUST prefix symbols to avoid
// clashes with 64tass reserved names, BASIC tokens, and other toolchain symbols.
if(options.compTarget.name!=VMTarget.NAME)
symbolTable = prefixSymbols(program, options, symbolTable)
makeAllNodenamesScoped(program)
moveAllNestedSubroutinesToBlockScope(program)
verifyNameScoping(program, symbolTable)
changeGlobalVarInits(symbolTable)
val irSymbolTable = convertStToIRSt(symbolTable)
val irSymbolTable = convertStToIRSt(symbolTable, options.romable)
val irProg = IRProgram(program.name, irSymbolTable, options, program.encoding)
// collect global variables initializers
@@ -39,6 +53,17 @@ class IRCodeGen(
}
}
// in romable mode, generate global init code for declaration-initialized variables
// and clear their init value so they go into the BSS section
if(options.romable) {
generateRomableInits(irProg)
}
// generate global init code for string/array-initialized variables
// (the existing mechanisms only handle numeric inits; string/array data stays
// in the INIT section declarations but no runtime copy instructions are emitted)
generateStringArrayInits(irProg)
irProg.addAsmSymbols(options.symbolDefs)
for (block in program.allBlocks())
@@ -63,6 +88,11 @@ class IRCodeGen(
fun registerTypes(): Map<RegisterNum, IRDataType> = registers.getTypes()
private fun changeGlobalVarInits(symbolTable: SymbolTable) {
// In romable mode, don't pull initializers into the symbol table;
// they stay as assignments in the AST and become global init code naturally.
if(options.romable)
return
// Normally, block level (global) variables that have a numeric initialization value
// are initialized via an assignment statement.
val initsToRemove = mutableListOf<Pair<PtBlock, PtAssignment>>()
@@ -95,6 +125,153 @@ class IRCodeGen(
}
}
private fun generateRomableInits(irProg: IRProgram) {
// For romable mode, declaration-initialized variables marked inBss need
// runtime init code in the globalInits chunk, and their initializationValue
// must be cleared so they appear in the NOINIT (BSS) section of the IR file.
val replacements = mutableListOf<IRStStaticVariable>()
for(variable in irProg.st.allVariables()) {
if(variable.inBss && variable.initializationValue != null) {
when(val initValue = variable.initializationValue) {
is IRVariableInitializer.Numeric -> {
val dt = irType(variable.dt)
val tempReg = registers.next(dt)
val chunk = IRCodeChunk(null, null)
chunk += IRInstruction(Opcode.LOAD, dt, reg1 = tempReg, immediate = initValue.value.toInt())
chunk += IRInstruction(Opcode.STOREM, dt, reg1 = tempReg, labelSymbol = variable.name)
irProg.addGlobalInits(chunk)
// replace with uninitialized version so it goes to NOINIT section
replacements += IRStStaticVariable(
variable.name, variable.dt, null, variable.length,
variable.zpwish, variable.align, variable.dirty,
variable.inBss, variable.readonly
)
}
is IRVariableInitializer.Str, is IRVariableInitializer.Array -> {
// Strings and arrays: keep inline data but now marked readonly.
// They can serve as ROM-based read-only data.
}
null -> {} // uninitialized, skip
}
}
}
replacements.forEach { irProg.st.add(it) }
}
private fun generateStringArrayInits(irProg: IRProgram) {
// ZP variables with string/array init values need runtime copy code.
// The VM's varsToMemory already initializes them from the INIT section,
// but a 6502 backend needs explicit init instructions.
//
// Create a non-ZP shadow variable holding the init bytes, clear the
// original variable's init value so it becomes uninitialized/BSS, then
// emit a single MEMCOPY from the shadow to the ZP variable at startup.
val chunk = IRCodeChunk(null, null)
for(variable in irProg.st.allVariables().toList()) {
if(variable.initializationValue == null)
continue
if(variable.inBss)
continue
if(variable.zpwish == ZeropageWish.DONTCARE || variable.zpwish == ZeropageWish.NOT_IN_ZEROPAGE)
continue
if(variable.dt.isSplitWordArray)
continue
if(variable.initializationValue is IRVariableInitializer.Numeric)
continue
val initBytes = when(val initVal = variable.initializationValue) {
is IRVariableInitializer.Str -> {
irProg.encoding.encodeString(initVal.text, initVal.encoding) + 0u
}
is IRVariableInitializer.Array -> {
val elemDt = variable.dt.elementType()
val elemByteSize = when {
elemDt.isByte || elemDt.isBool -> 1
elemDt.isWord || elemDt.isPointer -> 2
else -> -1
}
if(elemByteSize < 0)
continue
val bytes = mutableListOf<UByte>()
for(element in initVal.elements) {
when(element) {
is IRStSymbolicReference.Numeric -> {
val value = element.value.toInt()
when(elemByteSize) {
1 -> bytes += value.toUByte()
2 -> {
bytes += (value and 0xFF).toUByte()
bytes += ((value shr 8) and 0xFF).toUByte()
}
}
}
is IRStSymbolicReference.BoolValue -> {
bytes += (if(element.value) 1u else 0u).toUByte()
}
is IRStSymbolicReference.Symbol -> {
bytes.clear()
break
}
}
}
if(bytes.isEmpty())
continue
bytes
}
else -> continue
}
if(initBytes.isEmpty())
continue
val shadowName = variable.name + "_init_value"
val shadowInit = IRVariableInitializer.Array(initBytes.map { IRStSymbolicReference.Numeric(it.toDouble()) })
val shadowVar = IRStStaticVariable(
shadowName,
DataType.arrayFor(BaseDataType.UBYTE, false),
shadowInit,
initBytes.size.toUInt(),
ZeropageWish.NOT_IN_ZEROPAGE,
0u,
false,
inBss = false,
readonly = false
)
irProg.st.add(shadowVar)
// Clear the original variable's init value so it becomes uninitialized (BSS/NOINIT).
// The runtime copy from the shadow variable will provide the actual initial bytes.
val clearedVar = IRStStaticVariable(
variable.name,
variable.dt,
null,
variable.length,
variable.zpwish,
variable.align,
variable.dirty,
variable.inBss,
variable.readonly
)
irProg.st.add(clearedVar)
val sourceReg = registers.next(IRDataType.WORD)
val destReg = registers.next(IRDataType.WORD)
val countReg = registers.next(IRDataType.WORD)
chunk += IRInstruction(Opcode.LOAD, IRDataType.WORD, reg1 = sourceReg, labelSymbol = shadowName)
chunk += IRInstruction(Opcode.LOAD, IRDataType.WORD, reg1 = destReg, labelSymbol = variable.name)
chunk += IRInstruction(Opcode.LOAD, IRDataType.WORD, reg1 = countReg, immediate = initBytes.size)
val args = listOf(
FunctionCallArgs.ArgumentSpec("", null, FunctionCallArgs.RegSpec(IRDataType.WORD, RegisterNum(sourceReg), null, null)),
FunctionCallArgs.ArgumentSpec("", null, FunctionCallArgs.RegSpec(IRDataType.WORD, RegisterNum(destReg), null, null)),
FunctionCallArgs.ArgumentSpec("", null, FunctionCallArgs.RegSpec(IRDataType.WORD, RegisterNum(countReg), null, null))
)
chunk += IRInstruction(Opcode.SYSCALL, immediate = IMSyscall.MEMCOPY.number, fcallArgs = FunctionCallArgs(args, emptyList()))
}
if(chunk.isNotEmpty())
irProg.addGlobalInits(chunk)
}
private fun verifyNameScoping(program: PtProgram, symbolTable: SymbolTable) {
fun verifyPtNode(node: PtNode) {
when (node) {
@@ -116,9 +293,16 @@ class IRCodeGen(
is PtPointerDeref -> require('.' in node.startpointer.name) { "node $node name is not scoped: ${node.startpointer.name}" }
is PtIdentifier -> {
if('.' !in node.name) {
// there is 1 case where the identifier is not scoped: if it's the value field name after a pointer array indexing.
// there are 2 cases where the identifier is not scoped:
// 1) it is the value field name after a pointer array indexing.
// 2) it is the operand of a PtAddressOf referring to a block name.
val expr = node.parent as? PtBinaryExpression
if (expr?.operator != "." || expr.right !== node || expr.left !is PtArrayIndexer || (!expr.left.type.isPointer && !expr.left.type.isStructInstance))
val isFieldOfPointerArrayElement = expr?.operator == "." && expr.right === node
&& expr.left is PtArrayIndexer
&& (expr.left.type.isPointer || expr.left.type.isStructInstance)
val isAddressOfBlock = node.parent is PtAddressOf
&& symbolTable.lookup(node.name)?.type == StNodeType.BLOCK
if(!isFieldOfPointerArrayElement && !isAddressOfBlock)
require('.' in node.name) { "node $node name is not scoped: ${node.name}" }
}
}
@@ -204,8 +388,9 @@ class IRCodeGen(
val old = it.first.instructions[it.second]
val formats = instructionFormats.getValue(old.opcode)
val format = formats.getOrElse(old.type) { throw IllegalArgumentException("type ${old.type} invalid for ${old.opcode}") }
val immediateValue = if(format.immediate) it.third.toInt() else null
val addressValue: UInt? = if(format.immediate) null else it.third
val addressUsed = format.address != OperandDirection.UNUSED
val immediateValue = if(addressUsed) old.immediate else if(format.immediate) it.third.toInt() else null
val addressValue: UInt? = if(addressUsed) it.third else null
it.first.instructions[it.second] = IRInstruction(
old.opcode,
@@ -216,6 +401,7 @@ class IRCodeGen(
old.fpReg1,
old.fpReg2,
immediate = immediateValue,
immediateFp = old.immediateFp,
address = addressValue?.toAddress()
)
}
@@ -230,7 +416,7 @@ class IRCodeGen(
is PtAssignment -> assignmentGen.translate(node)
is PtAugmentedAssign -> assignmentGen.translate(node)
is PtNodeGroup -> translateGroup(node.children)
is PtFunctionCall -> expressionEval.translate(node).chunks // it's not an expression so no result value
is PtFunctionCall -> expressionEval.translate(node, false).chunks // it's not an expression so no result value
is PtNop -> emptyList()
is PtReturn -> translate(node)
is PtJump -> translate(node)
@@ -508,6 +694,12 @@ class IRCodeGen(
addInstr(result, IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1 = indexReg, immediate = 0), null)
result += IRCodeChunk(loopLabel, null).also {
it += IRInstruction(Opcode.LOADX, elementDt, reg1 = tmpReg, reg2 = indexReg, labelSymbol = iterable.name)
// Emit explicit CMPI #0 before the BSTEQ branch on 8-bit targets
// (where LOADX doesn't reliably set Z for multi-byte results).
// See CpuType.statusBitsOnMultiByteOps.
if(!options.compTarget.cpu.statusBitsOnMultiByteOps) {
it += IRInstruction(Opcode.CMPI, elementDt, reg1 = tmpReg, immediate = 0)
}
it += IRInstruction(Opcode.BSTEQ, labelSymbol = endLabel)
it += IRInstruction(Opcode.STOREM, elementDt, reg1 = tmpReg, labelSymbol = loopvarSymbol)
}
@@ -607,6 +799,12 @@ class IRCodeGen(
result += addConstMem(loopvarDtIr, null, loopvarSymbol, -1)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOADM, loopvarDtIr, reg1 = fromTr.resultReg, labelSymbol = loopvarSymbol)
// Emit explicit CMPI #0 before the BSTNE branch on 8-bit targets where
// LOADM doesn't reliably set Z for multi-byte results. Skip on targets
// that honor the contract (e.g. M68000). See CpuType.statusBitsOnMultiByteOps.
if(!options.compTarget.cpu.statusBitsOnMultiByteOps) {
it += IRInstruction(Opcode.CMPI, loopvarDtIr, reg1 = fromTr.resultReg, immediate = 0)
}
it += IRInstruction(Opcode.BSTNE, labelSymbol = loopLabel)
}
}
@@ -700,6 +898,14 @@ class IRCodeGen(
}
else if(iterable.step==-1 && iterable.last==1) {
// downto 1 optimization (byte and word)
// On 8-bit targets the preceding addConstMem emitted a DECM which doesn't
// reliably set Z; we must explicitly test the loop var. Skip the explicit
// CMPI on targets that honor the contract (e.g. M68000).
// See CpuType.statusBitsOnMultiByteOps.
chunk2 += IRInstruction(Opcode.LOADM, loopvarDtIr, reg1 = indexReg, labelSymbol = loopvarSymbol)
if(!options.compTarget.cpu.statusBitsOnMultiByteOps) {
chunk2 += IRInstruction(Opcode.CMPI, loopvarDtIr, reg1 = indexReg, immediate = 0)
}
chunk2 += IRInstruction(Opcode.BSTNE, labelSymbol = loopLabel)
} else {
// downto some other value
@@ -775,20 +981,35 @@ class IRCodeGen(
}
}
else -> {
val valueReg = registers.next(dt)
if(value>0) {
code += IRInstruction(Opcode.LOAD, dt, reg1=valueReg, immediate = value)
code += if(knownAddress!=null)
IRInstruction(Opcode.ADDM, dt, reg1=valueReg, address = knownAddress.toAddress())
else
IRInstruction(Opcode.ADDM, dt, reg1=valueReg, labelSymbol = symbol)
}
else {
code += IRInstruction(Opcode.LOAD, dt, reg1=valueReg, immediate = -value)
code += if(knownAddress!=null)
IRInstruction(Opcode.SUBM, dt, reg1=valueReg, address = knownAddress.toAddress())
else
IRInstruction(Opcode.SUBM, dt, reg1=valueReg, labelSymbol = symbol)
if(dt==IRDataType.FLOAT) {
// float loop variables are not currently supported; keep the load+addm/subm form
val valueReg = registers.next(dt)
if(value>0) {
code += IRInstruction(Opcode.LOAD, dt, reg1=valueReg, immediate = value)
code += if(knownAddress!=null)
IRInstruction(Opcode.ADDM, dt, reg1=valueReg, address = knownAddress.toAddress())
else
IRInstruction(Opcode.ADDM, dt, reg1=valueReg, labelSymbol = symbol)
}
else {
code += IRInstruction(Opcode.LOAD, dt, reg1=valueReg, immediate = -value)
code += if(knownAddress!=null)
IRInstruction(Opcode.SUBM, dt, reg1=valueReg, address = knownAddress.toAddress())
else
IRInstruction(Opcode.SUBM, dt, reg1=valueReg, labelSymbol = symbol)
}
} else {
if(value>0) {
code += if(knownAddress!=null)
IRInstruction(Opcode.ADDIM, dt, immediate = value, address = knownAddress.toAddress())
else
IRInstruction(Opcode.ADDIM, dt, immediate = value, labelSymbol = symbol)
} else {
code += if(knownAddress!=null)
IRInstruction(Opcode.SUBIM, dt, immediate = -value, address = knownAddress.toAddress())
else
IRInstruction(Opcode.SUBIM, dt, immediate = -value, labelSymbol = symbol)
}
}
}
}
@@ -867,8 +1088,8 @@ class IRCodeGen(
}
else if(pow2>=1) {
// just shift multiple bits
val pow2reg = registers.next(dt)
code += IRInstruction(Opcode.LOAD, dt, reg1=pow2reg, immediate = pow2)
val pow2reg = registers.next(IRDataType.BYTE)
code += IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=pow2reg, immediate = pow2)
code += if(knownAddress!=null)
IRInstruction(Opcode.LSLNM, dt, reg1=pow2reg, address = knownAddress.toAddress())
else
@@ -985,8 +1206,8 @@ class IRCodeGen(
IRInstruction(Opcode.ASRM, dt, labelSymbol = symbol)
} else {
// just shift multiple bits (signed)
val pow2reg = registers.next(dt)
code += IRInstruction(Opcode.LOAD, dt, reg1 = pow2reg, immediate = pow2)
val pow2reg = registers.next(IRDataType.BYTE)
code += IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1 = pow2reg, immediate = pow2)
code += if (knownAddress != null)
IRInstruction(Opcode.ASRNM, dt, reg1 = pow2reg, address = knownAddress.toAddress())
else
@@ -1002,8 +1223,8 @@ class IRCodeGen(
}
else {
// just shift multiple bits (unsigned)
val pow2reg = registers.next(dt)
code += IRInstruction(Opcode.LOAD, dt, reg1=pow2reg, immediate = pow2)
val pow2reg = registers.next(IRDataType.BYTE)
code += IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=pow2reg, immediate = pow2)
code += if(knownAddress!=null)
IRInstruction(Opcode.LSRNM, dt, reg1 = pow2reg, address = knownAddress.toAddress())
else
@@ -1115,9 +1336,17 @@ class IRCodeGen(
// Fallback: materialize expression and branch on result
val tr = expressionEval.translateExpression(condition)
result += tr.chunks
// Check if the last instruction already sets status flags - if so, skip the CMPI #0
// Only skip the CMPI #0 if BOTH:
// 1) The previous instruction is documented to set status bits (Z/N), AND
// 2) The target CPU honors the "multi-byte ops set Z based on full value" contract
// (see CpuType.statusBitsOnMultiByteOps). For 8-bit targets this contract
// is false: e.g. a 16-bit DEC only sets Z from the low byte's dec, not the
// full 16-bit value, so we MUST emit an explicit CMPI here.
val lastInstr = tr.chunks.lastOrNull()?.instructions?.lastOrNull()
val skipCmpi = lastInstr != null && lastInstr.opcode in OpcodesThatSetStatusbits
val targetHonorsContract = options.compTarget.cpu.statusBitsOnMultiByteOps
val skipCmpi = targetHonorsContract
&& lastInstr != null
&& lastInstr.opcode in OpcodesThatSetStatusbits
if (!skipCmpi) {
addInstr(result, IRInstruction(Opcode.CMPI, tr.dt, reg1 = tr.resultReg, immediate = 0), null)
}
@@ -1135,14 +1364,17 @@ class IRCodeGen(
private fun translateIntegerComparison(condition: PtBinaryExpression, onTrueLabel: String?, onTrueAddress: MemoryAddress?, onFalseLabel: String?, onFalseAddress: MemoryAddress?, result: MutableList<IRCodeChunkBase>) {
val useBIT = expressionEval.checkIfConditionCanUseBIT(condition)
if (useBIT != null) {
val (testBitSet, variable, bitmask) = useBIT
addInstr(result, IRInstruction(Opcode.BIT, IRDataType.BYTE, labelSymbol = variable.name), null)
val (testBitSet, expr, bitmask) = useBIT
val bitPos = Integer.numberOfTrailingZeros(bitmask)
val leftTr = expressionEval.translateExpression(expr)
addToResult(result, leftTr, leftTr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.BITTST, leftTr.dt, reg1 = leftTr.resultReg, immediate = bitPos), null)
if ((onTrueLabel != null || onTrueAddress != null) && (onFalseLabel == null && onFalseAddress == null)) {
addInstr(result, IRInstruction(getBitBranchOpcode(bitmask, testBitSet), labelSymbol = onTrueLabel, address = onTrueAddress), null)
addInstr(result, IRInstruction(getBitBranchOpcode(testBitSet), labelSymbol = onTrueLabel, address = onTrueAddress), null)
} else if ((onFalseLabel != null || onFalseAddress != null) && (onTrueLabel == null && onTrueAddress == null)) {
addInstr(result, IRInstruction(getBitBranchOpcode(bitmask, !testBitSet), labelSymbol = onFalseLabel, address = onFalseAddress), null)
addInstr(result, IRInstruction(getBitBranchOpcode(!testBitSet), labelSymbol = onFalseLabel, address = onFalseAddress), null)
} else if ((onTrueLabel != null || onTrueAddress != null) && (onFalseLabel != null || onFalseAddress != null)) {
addInstr(result, IRInstruction(getBitBranchOpcode(bitmask, testBitSet), labelSymbol = onTrueLabel, address = onTrueAddress), null)
addInstr(result, IRInstruction(getBitBranchOpcode(testBitSet), labelSymbol = onTrueLabel, address = onTrueAddress), null)
addInstr(result, IRInstruction(Opcode.JUMP, labelSymbol = onFalseLabel, address = onFalseAddress), null)
}
return
@@ -1157,7 +1389,15 @@ class IRCodeGen(
if (number != null) {
val isComparingWithZero = number == 0
val lastInstr = leftTr.chunks.lastOrNull()?.instructions?.lastOrNull()
val canSkipCmpi = isComparingWithZero && lastInstr != null && lastInstr.opcode in OpcodesThatSetStatusbits
// Skip the CMPI only if the target honors the "multi-byte ops set Z correctly"
// contract. On 8-bit targets a 16/32-bit op like DEC only sets Z from the last
// byte, so we cannot rely on the previous instruction's status bits - the
// CMPI is required to test the full multi-byte value.
// See CpuType.statusBitsOnMultiByteOps for details.
val canSkipCmpi = isComparingWithZero
&& options.compTarget.cpu.statusBitsOnMultiByteOps
&& lastInstr != null
&& lastInstr.opcode in OpcodesThatSetStatusbits
if ((onTrueLabel != null || onTrueAddress != null) && (onFalseLabel == null && onFalseAddress == null)) {
var (opcode, useCmpi) = getIntegerComparisonBranch(condition.operator, false, signed)
@@ -1211,12 +1451,8 @@ class IRCodeGen(
}
}
private fun getBitBranchOpcode(bitmask: Int, wantSet: Boolean): Opcode {
return if (wantSet) {
if (bitmask == 64) Opcode.BSTVS else Opcode.BSTNEG
} else {
if (bitmask == 64) Opcode.BSTVC else Opcode.BSTPOS
}
private fun getBitBranchOpcode(wantSet: Boolean): Opcode {
return if (wantSet) Opcode.BSTNE else Opcode.BSTEQ
}
private fun invertOperator(operator: String) = when (operator) {
@@ -1327,13 +1563,22 @@ class IRCodeGen(
val skipRepeatLabel = createLabelName()
val constRepeats = repeat.count.asConstInteger()
val result = mutableListOf<IRCodeChunkBase>()
// The DEC instruction only sets Z correctly for BYTE (or for the full multi-byte
// value if the target honors the multi-byte status-bits contract, e.g. M68000).
// On 8-bit targets we must emit an explicit CMPI #0 before the BSTNE branch
// because DEC only sets Z based on the last byte.
// See CpuType.statusBitsOnMultiByteOps for the rationale.
val needsExplicitCmpi = !options.compTarget.cpu.statusBitsOnMultiByteOps
if(constRepeats==65536) {
// make use of the word wrap around to count to 65536
val resultRegister = registers.next(IRDataType.WORD)
addInstr(result, IRInstruction(Opcode.LOAD, IRDataType.WORD, reg1=resultRegister, immediate = 0), null)
result += labelFirstChunk(translateNode(repeat.statements), repeatLabel)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.DEC, IRDataType.WORD, reg1 = resultRegister) // sets status bits
it += IRInstruction(Opcode.DEC, IRDataType.WORD, reg1 = resultRegister)
if (needsExplicitCmpi) {
it += IRInstruction(Opcode.CMPI, IRDataType.WORD, reg1 = resultRegister, immediate = 0)
}
it += IRInstruction(Opcode.BSTNE, labelSymbol = repeatLabel)
}
} else {
@@ -1341,12 +1586,20 @@ class IRCodeGen(
val countTr = expressionEval.translateExpression(repeat.count)
addToResult(result, countTr, countTr.resultReg, -1)
if (repeat.count.asConstValue() == null) {
// check if the counter is already zero
// check if the counter is already zero. On 8-bit targets we need an
// explicit CMPI #0 because the previous load (from expressionEval) does
// not reliably set Z for multi-byte values.
if (needsExplicitCmpi) {
addInstr(result, IRInstruction(Opcode.CMPI, irDt, reg1 = countTr.resultReg, immediate = 0), null)
}
addInstr(result, IRInstruction(Opcode.BSTEQ, labelSymbol = skipRepeatLabel), null)
}
result += labelFirstChunk(translateNode(repeat.statements), repeatLabel)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.DEC, irDt, reg1 = countTr.resultReg) // sets status bits
it += IRInstruction(Opcode.DEC, irDt, reg1 = countTr.resultReg)
if (needsExplicitCmpi) {
it += IRInstruction(Opcode.CMPI, irDt, reg1 = countTr.resultReg, immediate = 0)
}
it += IRInstruction(Opcode.BSTNE, labelSymbol = repeatLabel)
}
}
@@ -1409,7 +1662,7 @@ class IRCodeGen(
?: throw AssemblyError("expected function call for multi-value return ${ret.position}")
// Translate the function call (this generates the CALL instruction)
val callResult = expressionEval.translate(fcall)
val callResult = expressionEval.translate(fcall, false)
result += callResult.chunks
// Get the return register specs for the called function
@@ -1520,7 +1773,11 @@ class IRCodeGen(
is PtNop -> { /* nothing */ }
is PtAssignment, is PtAugmentedAssign -> { /* global variable initialization is done elsewhere */ }
is PtVariable, is PtConstant, is PtMemMapped, is PtMemorySlabReservation -> { /* vars should be looked up via symbol table */ }
is PtAlign -> TODO("ir support for inline %align ${child.position}")
is PtAlign -> {
val chunk = IRCodeChunk(null, null)
chunk += IRInstruction(Opcode.ALIGN, immediate = child.align.toInt())
irBlock += chunk
}
is PtSub -> {
val sub = IRSubroutine(child.name, translateParameters(child.signature.children), child.signature.returns, child.position)
for (subchild in child.children) {
@@ -1529,11 +1786,15 @@ class IRCodeGen(
irBlock += sub
}
is PtAsmSub -> {
if(child.address!=null) {
// extmsub. No codegen needed: calls to this are jumping straight to the address.
val addr = child.address
if(addr!=null) {
// extsub: emit as inline assembly equate so that other tools (vm, 6502 codegen) can reference the symbol as an address label
require(child.children.isEmpty()) {
"extsub should be empty at ${child.position}"
}
val bank = if(addr.constbank!=null) " ; @bank ${addr.constbank}" else ""
val asm = "${child.name} = ${addr.address.toHex()}$bank"
irBlock += IRInlineAsmChunk(null, asm, false, null)
} else {
// regular asmsub
if(child.children.mapTo(mutableSetOf()) { (it as PtInlineAssembly).isIR }.size>1)
@@ -1550,7 +1811,8 @@ class IRCodeGen(
child.parameters.map { IRAsmSubroutine.IRAsmParam(it.first, it.second.type) }, // note: the name of the asmsub param is not used here anymore
child.returns.map { IRAsmSubroutine.IRAsmParam(it.first, it.second)},
asmChunk,
child.position
child.position,
child.inline
)
}
}
@@ -1635,9 +1897,10 @@ class IRCodeGen(
} else {
val reg = it.register
require(reg in Cx16VirtualRegisters || reg in CombinedLongRegisters) { "can only use R0-R15 'registers' here" }
val regname = it.register!!.asScopedNameVirtualReg(it.type).joinToString(".")
val targetVar = symbolTable.lookup(regname) as StMemVar
result += IRSubroutine.IRParam(regname, targetVar.dt)
//val regname = it.register!!.asScopedNameVirtualReg(it.type).joinToString(".")
require('.' in it.name) { "even parameter names should have been made fully scoped by now" }
val targetVar = symbolTable.lookup(it.name) as StMemVar
result += IRSubroutine.IRParam(it.name, targetVar.dt)
}
}
return result
@@ -1660,10 +1923,10 @@ class IRCodeGen(
internal fun makeSyscall(syscall: IMSyscall, params: List<Pair<IRDataType, Int>>, returns: Pair<IRDataType, Int>?, label: String?=null): IRCodeChunk {
return IRCodeChunk(label, null).also {
val args = params.map { (dt, reg)->
FunctionCallArgs.ArgumentSpec("", null, FunctionCallArgs.RegSpec(dt, RegisterNum(reg), null))
FunctionCallArgs.ArgumentSpec("", null, FunctionCallArgs.RegSpec(dt, RegisterNum(reg), null, null))
}
// for now, syscalls have 0 or 1 return value
val returnSpec = if(returns==null) emptyList() else listOf(FunctionCallArgs.RegSpec(returns.first, RegisterNum(returns.second), null))
val returnSpec = if(returns==null) emptyList() else listOf(FunctionCallArgs.RegSpec(returns.first, RegisterNum(returns.second), null, null))
it += IRInstruction(Opcode.SYSCALL, immediate = syscall.number, fcallArgs = FunctionCallArgs(args, returnSpec))
}
}
@@ -1671,12 +1934,12 @@ class IRCodeGen(
internal fun setCpuRegister(registerOrFlag: RegisterOrStatusflag, paramDt: IRDataType, resultReg: Int, resultFpReg: Int): IRCodeChunk {
val chunk = IRCodeChunk(null, null)
when(registerOrFlag.registerOrPair) {
RegisterOrPair.A -> chunk += IRInstruction(Opcode.STOREHA, IRDataType.BYTE, reg1=resultReg)
RegisterOrPair.X -> chunk += IRInstruction(Opcode.STOREHX, IRDataType.BYTE, reg1=resultReg)
RegisterOrPair.Y -> chunk += IRInstruction(Opcode.STOREHY, IRDataType.BYTE, reg1=resultReg)
RegisterOrPair.AX -> chunk += IRInstruction(Opcode.STOREHAX, IRDataType.WORD, reg1=resultReg)
RegisterOrPair.AY -> chunk += IRInstruction(Opcode.STOREHAY, IRDataType.WORD, reg1=resultReg)
RegisterOrPair.XY -> chunk += IRInstruction(Opcode.STOREHXY, IRDataType.WORD, reg1=resultReg)
RegisterOrPair.A -> chunk += IRInstruction(Opcode.STOREHR, IRDataType.BYTE, reg1=resultReg, immediate=0)
RegisterOrPair.X -> chunk += IRInstruction(Opcode.STOREHR, IRDataType.BYTE, reg1=resultReg, immediate=1)
RegisterOrPair.Y -> chunk += IRInstruction(Opcode.STOREHR, IRDataType.BYTE, reg1=resultReg, immediate=2)
RegisterOrPair.AX -> chunk += IRInstruction(Opcode.STOREHR, IRDataType.WORD, reg1=resultReg, immediate=3)
RegisterOrPair.AY -> chunk += IRInstruction(Opcode.STOREHR, IRDataType.WORD, reg1=resultReg, immediate=4)
RegisterOrPair.XY -> chunk += IRInstruction(Opcode.STOREHR, IRDataType.WORD, reg1=resultReg, immediate=5)
RegisterOrPair.FAC1 -> chunk += IRInstruction(Opcode.STOREHFACZERO, IRDataType.FLOAT, fpReg1 = RegisterNum(resultFpReg))
RegisterOrPair.FAC2 -> chunk += IRInstruction(Opcode.STOREHFACONE, IRDataType.FLOAT, fpReg1 = RegisterNum(resultFpReg))
in Cx16VirtualRegisters -> {
@@ -1701,12 +1964,12 @@ class IRCodeGen(
val chunk = IRCodeChunk(null, null)
val irType = irType(fromType)
when(registerOrFlag.registerOrPair) {
RegisterOrPair.A -> chunk += IRInstruction(Opcode.LOADHA, IRDataType.BYTE, reg1=tempReg)
RegisterOrPair.X -> chunk += IRInstruction(Opcode.LOADHX, IRDataType.BYTE, reg1=tempReg)
RegisterOrPair.Y -> chunk += IRInstruction(Opcode.LOADHY, IRDataType.BYTE, reg1=tempReg)
RegisterOrPair.AX -> chunk += IRInstruction(Opcode.LOADHAX, IRDataType.WORD, reg1=tempReg)
RegisterOrPair.AY -> chunk += IRInstruction(Opcode.LOADHAY, IRDataType.WORD, reg1=tempReg)
RegisterOrPair.XY -> chunk += IRInstruction(Opcode.LOADHXY, IRDataType.WORD, reg1=tempReg)
RegisterOrPair.A -> chunk += IRInstruction(Opcode.LOADHR, IRDataType.BYTE, reg1=tempReg, immediate=0)
RegisterOrPair.X -> chunk += IRInstruction(Opcode.LOADHR, IRDataType.BYTE, reg1=tempReg, immediate=1)
RegisterOrPair.Y -> chunk += IRInstruction(Opcode.LOADHR, IRDataType.BYTE, reg1=tempReg, immediate=2)
RegisterOrPair.AX -> chunk += IRInstruction(Opcode.LOADHR, IRDataType.WORD, reg1=tempReg, immediate=3)
RegisterOrPair.AY -> chunk += IRInstruction(Opcode.LOADHR, IRDataType.WORD, reg1=tempReg, immediate=4)
RegisterOrPair.XY -> chunk += IRInstruction(Opcode.LOADHR, IRDataType.WORD, reg1=tempReg, immediate=5)
RegisterOrPair.FAC1 -> chunk += IRInstruction(Opcode.LOADHFACZERO, IRDataType.FLOAT, fpReg1 = RegisterNum(tempReg))
RegisterOrPair.FAC2 -> chunk += IRInstruction(Opcode.LOADHFACONE, IRDataType.FLOAT, fpReg1 = RegisterNum(tempReg))
in Cx16VirtualRegisters -> {
@@ -1795,5 +2058,4 @@ class IRCodeGen(
result += multiplyByConst(DataType.UBYTE, byteIndexTr.resultReg, itemsize)
return Pair(result, byteIndexTr.resultReg)
}
}
@@ -3,14 +3,6 @@ package prog8.codegen.intermediate
import prog8.code.core.IErrorReporter
import prog8.intermediate.*
/**
* Represents what value a register currently holds for LOADR forwarding optimization.
*/
private sealed class RegisterContent {
data class Constant(val value: Any) : RegisterContent() // Int or Double
data class Register(val originalReg: Int) : RegisterContent()
}
class IRPeepholeOptimizer(private val irprog: IRProgram, private val retainSSA: Boolean) {
fun optimize(optimizationsEnabled: Boolean, errors: IErrorReporter) {
if(!optimizationsEnabled)
@@ -65,6 +57,9 @@ class IRPeepholeOptimizer(private val irprog: IRProgram, private val retainSSA:
|| removeNeedlessLoads(chunk1, indexedInstructions)
|| removeDeadStores(chunk1, indexedInstructions)
// || removeLoadrForwarding(chunk1, indexedInstructions) // DISABLED - needs debugging
|| removeSelfIdentityOps(chunk1, indexedInstructions)
|| simplifyShiftByZero(chunk1, indexedInstructions)
|| cancelAdjacentOps(chunk1, indexedInstructions)
|| removeNops(chunk1, indexedInstructions) // last time, in case one of the optimizers replaced something with a nop
} while (changed)
}
@@ -344,23 +339,28 @@ class IRPeepholeOptimizer(private val irprog: IRProgram, private val retainSSA:
}
private fun removeNeedlessCompares(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
// a CMPI with 0, after an instruction like LOAD that already sets the status bits, can be removed.
// but only if the instruction after it is not using the Carry bit because that won't be set by a LOAD instruction etc.
// A CMPI #0 after an instruction that already sets the status bits can be removed.
// The optimization is ONLY safe if the target CPU honors the "multi-byte ops set
// status bits based on the full value" contract (CpuType.statusBitsOnMultiByteOps).
// On 8-bit targets like 6502/65C02 a 16/32-bit DEC/INC/AND/OR/XOR/LOAD only sets
// Z from the last byte, so a following CMPI #0 is NOT redundant - it tests the
// full multi-byte value. In that case we keep the explicit CMPI.
// See CpuType.statusBitsOnMultiByteOps for the rationale.
val targetHonorsContract = irprog.options.compTarget.cpu.statusBitsOnMultiByteOps
if (!targetHonorsContract) return false
var changed = false
indexedInstructions.reversed().forEach { (idx, ins) ->
if(idx>0 && idx<(indexedInstructions.size-1) && ins.opcode==Opcode.CMPI && ins.immediate==0) {
val previous = indexedInstructions[idx-1].value
if(previous.reg1==ins.reg1) {
if (previous.opcode in OpcodesThatSetStatusbitsIncludingCarry) {
if (previous.opcode in OpcodesThatSetStatusbits) {
chunk.instructions.removeAt(idx)
changed = true
} else if (previous.opcode in OpcodesThatSetStatusbitsButNotCarry) {
val next = indexedInstructions[idx + 1].value
if (next.opcode !in OpcodesThatDependOnCarry) {
chunk.instructions.removeAt(idx)
changed = true
}
}
// Note: the OpcodesThatSetStatusbitsButNotCarry set is empty under
// the strict contract (see IRInstructions.kt) so we don't need a
// second branch here. Only CMP/CMPI/SGN/BITTST are recognized as
// setting flags.
}
}
}
@@ -452,7 +452,7 @@ jump p8_label_gen_2
}
}
Opcode.AND -> {
when (ins.immediate) {
when (val imm = ins.immediate) {
0 -> {
chunk.instructions[idx] = IRInstruction(Opcode.LOAD, ins.type, reg1 = ins.reg1, immediate = 0)
changed = true
@@ -470,23 +470,54 @@ jump p8_label_gen_2
changed = true
}
}
// convert AND with all-ones-except-one-bit into BITCLR
if(!changed) {
val imm = ins.immediate ?: return@forEach
val clearedBits = when(ins.type) {
IRDataType.BYTE -> imm xor 0xFF
IRDataType.WORD -> imm xor 0xFFFF
IRDataType.LONG -> imm xor -1
else -> 0
}
if(clearedBits > 0 && clearedBits and (clearedBits - 1) == 0) {
val bitPos = Integer.numberOfTrailingZeros(clearedBits)
chunk.instructions[idx] = IRInstruction(Opcode.BITCLR, ins.type, reg1 = ins.reg1, immediate = bitPos)
changed = true
}
}
}
Opcode.OR -> {
if (ins.immediate == 0) {
val imm = ins.immediate
if (imm == null) return@forEach
if (imm == 0) {
chunk.instructions.removeAt(idx)
changed = true
} else if ((ins.immediate == 255 && ins.type == IRDataType.BYTE) ||
(ins.immediate == 65535 && ins.type == IRDataType.WORD) ||
(ins.immediate == -1 && ins.type == IRDataType.LONG)) {
chunk.instructions[idx] = IRInstruction(Opcode.LOAD, ins.type, reg1 = ins.reg1, immediate = ins.immediate)
} else if ((imm == 255 && ins.type == IRDataType.BYTE) ||
(imm == 65535 && ins.type == IRDataType.WORD) ||
(imm == -1 && ins.type == IRDataType.LONG)) {
chunk.instructions[idx] = IRInstruction(Opcode.LOAD, ins.type, reg1 = ins.reg1, immediate = imm)
changed = true
}
// convert OR with power-of-2 into BITSET
if(!changed && imm > 0 && imm and (imm - 1) == 0) {
val bitPos = Integer.numberOfTrailingZeros(imm)
chunk.instructions[idx] = IRInstruction(Opcode.BITSET, ins.type, reg1 = ins.reg1, immediate = bitPos)
changed = true
}
}
Opcode.XOR -> {
if (ins.immediate == 0) {
val imm = ins.immediate
if (imm == null) return@forEach
if (imm == 0) {
chunk.instructions.removeAt(idx)
changed = true
}
// convert XOR with power-of-2 into BITTOG
if(!changed && imm > 0 && imm and (imm - 1) == 0) {
val bitPos = Integer.numberOfTrailingZeros(imm)
chunk.instructions[idx] = IRInstruction(Opcode.BITTOG, ins.type, reg1 = ins.reg1, immediate = bitPos)
changed = true
}
}
else -> {}
}
@@ -544,6 +575,98 @@ jump p8_label_gen_2
return changed
}
private fun removeSelfIdentityOps(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
var changed = false
indexedInstructions.reversed().forEach { (idx, ins) ->
when(ins.opcode) {
Opcode.LOADR -> {
if((ins.reg1 != null && ins.reg1 == ins.reg2) || (ins.fpReg1 != null && ins.fpReg1 == ins.fpReg2)) {
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
changed = true
}
}
Opcode.ANDR, Opcode.ORR -> {
if(ins.reg1 == ins.reg2) {
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
changed = true
}
}
Opcode.XORR -> {
if(ins.reg1 == ins.reg2) {
chunk.instructions[idx] = IRInstruction(Opcode.LOAD, ins.type, reg1 = ins.reg1, immediate = 0)
changed = true
}
}
else -> {}
}
}
return changed
}
private fun simplifyShiftByZero(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
var changed = false
indexedInstructions.reversed().forEach { (idx, ins) ->
if(idx>0 && ins.opcode in setOf(Opcode.LSLN, Opcode.LSRN, Opcode.ASRN) && ins.reg2 != null) {
val prev = indexedInstructions[idx-1].value
if(prev.opcode == Opcode.LOAD && prev.reg1 == ins.reg2 && prev.immediate == 0) {
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
chunk.instructions.removeAt(idx-1)
changed = true
}
}
}
return changed
}
private fun cancelAdjacentOps(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
var changed = false
indexedInstructions.reversed().forEach { (idx, ins) ->
if(idx >= chunk.instructions.size - 1)
return@forEach
val insAfter = chunk.instructions[idx+1]
when(ins.opcode) {
Opcode.INV -> {
if(insAfter.opcode == Opcode.INV && insAfter.reg1 == ins.reg1) {
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
chunk.instructions[idx+1] = IRInstruction(Opcode.NOP)
changed = true
}
}
Opcode.NEG -> {
val sameReg = (ins.reg1 != null && ins.reg1 == insAfter.reg1) || (ins.fpReg1 != null && ins.fpReg1 == insAfter.fpReg1)
if(insAfter.opcode == Opcode.NEG && sameReg) {
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
chunk.instructions[idx+1] = IRInstruction(Opcode.NOP)
changed = true
}
}
Opcode.EXT, Opcode.EXTS -> {
if(insAfter.opcode == ins.opcode && insAfter.reg1 == ins.reg1 && insAfter.type == ins.type) {
chunk.instructions[idx+1] = IRInstruction(Opcode.NOP)
changed = true
}
}
Opcode.INC -> {
if(insAfter.opcode == Opcode.DEC && insAfter.reg1 == ins.reg1) {
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
chunk.instructions[idx+1] = IRInstruction(Opcode.NOP)
changed = true
}
}
Opcode.DEC -> {
if(insAfter.opcode == Opcode.INC && insAfter.reg1 == ins.reg1) {
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
chunk.instructions[idx+1] = IRInstruction(Opcode.NOP)
changed = true
}
}
else -> {}
}
}
return changed
}
private fun removeNops(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
var changed = false
indexedInstructions.reversed().forEach { (idx, ins) ->
@@ -686,18 +809,6 @@ jump p8_label_gen_2
return changed
}
private fun isRegisterRead(ins: IRInstruction, reg: Int): Boolean {
// Check if the instruction reads from the given register
val formats = instructionFormats.getValue(ins.opcode)
val format = formats[ins.type] ?: formats[null]
val reg1Read = (format?.reg1 == OperandDirection.READ || format?.reg1 == OperandDirection.READWRITE) && (ins.reg1 == reg || ins.fpReg1?.value == reg)
val reg2Read = (format?.reg2 == OperandDirection.READ || format?.reg2 == OperandDirection.READWRITE) && (ins.reg2 == reg || ins.fpReg2?.value == reg)
val reg3Read = (format?.reg3 == OperandDirection.READ || format?.reg3 == OperandDirection.READWRITE) && (ins.reg3 == reg)
return reg1Read || reg2Read || reg3Read
}
//private fun removeLoadrForwarding(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
// Forward LOADR instructions to their original source.
// Example:
@@ -76,17 +76,31 @@ class IRUnusedCodeRemover(
}
// remove stray loads
val readRegs = mutableSetOf<Int>()
val readFpRegs = mutableSetOf<Int>()
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 })
}
instructions.toTypedArray().forEach { ins ->
if(ins.opcode in arrayOf(Opcode.LOAD, Opcode.LOADR, Opcode.LOADM)) {
if(ins.reg1!=0) {
if(instructions.count { it.reg1==ins.reg1 || it.reg2==ins.reg1 } <2) {
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(ins.fpReg1!=null) {
if (instructions.count { it.fpReg1 == ins.fpReg1 || it.fpReg2 == ins.fpReg1 } < 2) {
else if(fpReg1!=null) {
if(fpReg1.value !in readFpRegs) {
if(ins.labelSymbol!=null)
code.st.removeIfExists(ins.labelSymbol!!)
instructions.remove(ins)
@@ -193,10 +207,15 @@ class IRUnusedCodeRemover(
}
return numRemoved
}
private val entrypointNames = setOf("p8b_main.p8s_start", "main.start")
private val mainBlockNames = setOf("p8b_main", "main")
private fun removeUnreachable(allLabeledChunks: MutableMap<String, IRCodeChunkBase>): Int {
val entrypointSub = irprog.blocks.single { it.label=="main" }
.children.single { it is IRSubroutine && it.label=="main.start" }
val entrypointSub = irprog.blocks.single { it.label in mainBlockNames }
.children.single { it is IRSubroutine && it.label in entrypointNames }
val reachable = mutableSetOf((entrypointSub as IRSubroutine).chunks.first())
reachable.add(irprog.globalInits)
@@ -269,7 +288,7 @@ class IRUnusedCodeRemover(
linkedChunks += it.branchTarget!!
}
}
if (chunk.label == "main.start")
if (chunk.label in entrypointNames)
linkedChunks += chunk
}
@@ -290,6 +309,14 @@ class IRUnusedCodeRemover(
private fun removeUnlinkedChunks(linkedChunks: Set<IRCodeChunkBase>): Int {
var numRemoved = 0
irprog.foreachSub { sub ->
// 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)
val block = irprog.blocks.first { b -> b.children.any { it === sub } }
if (block.library || block.options.forceOutput)
return@foreachSub
sub.chunks.withIndex().reversed().forEach { (index, chunk) ->
if (chunk !in linkedChunks) {
if (chunk === sub.chunks[0]) {
@@ -315,4 +342,4 @@ class IRUnusedCodeRemover(
}
return numRemoved
}
}
}
@@ -1,16 +1,20 @@
package prog8.codegen.intermediate
import prog8.code.*
import prog8.code.core.DataType
import prog8.code.ast.PtStructField
import prog8.intermediate.*
import kotlin.collections.flatMap
/**
* Converter object for transforming SymbolTable nodes to IR symbol table nodes.
* Groups all conversion functions together for better organization.
*/
private object StToIrConverter {
private class StToIrConverter(val romable: Boolean) {
fun convert(struct: StStruct): IRStStructDef =
IRStStructDef(struct.scopedNameString, struct.fields, struct.size)
IRStStructDef(struct.scopedNameString, makeIrStructFieldsFrom(struct.fields), struct.size)
private fun makeIrStructFieldsFrom(fields: List<PtStructField>): List<IRStStructField> =
fields.map { IRStStructField(it.type, it.name, it.arraySize) }
private fun convertArrayElt(elt: StArrayElement): IRStSymbolicReference {
return when(elt) {
@@ -72,16 +76,21 @@ private object StToIrConverter {
}
val name = if('.' in variable.name) variable.name else variable.scopedNameString
return IRStStaticVariable(name, variable.dt, initValue, variable.length, variable.zpwish, variable.align, variable.dirty)
val hasInit = initValue != null
val inBss = !hasInit || (romable && hasInit)
val readonly = romable && hasInit
return IRStStaticVariable(name, variable.dt, initValue, variable.length, variable.zpwish, variable.align, variable.dirty, inBss, readonly)
}
fun convert(variable: StMemVar): IRStMemVar {
val readonly = romable && variable.address > 255u
if('.' in variable.name) {
return IRStMemVar(
variable.name,
variable.dt,
variable.address,
variable.length
variable.length,
readonly
)
} else {
val scopedName = try {
@@ -89,7 +98,7 @@ private object StToIrConverter {
} catch (_: UninitializedPropertyAccessException) {
variable.name
}
return IRStMemVar(scopedName, variable.dt, variable.address, variable.length)
return IRStMemVar(scopedName, variable.dt, variable.address, variable.length, readonly)
}
}
@@ -122,10 +131,17 @@ private object StToIrConverter {
IRStMemorySlab("$StMemorySlabBlockName.${mem.name}", mem.size, mem.align)
}
fun convert(instance: StStructInstance, fields: Iterable<Pair<DataType, String>>): IRStStructInstance {
val values = fields.zip(instance.initialValues).map { (field, value) ->
fun convert(instance: StStructInstance, fields: Iterable<PtStructField>): IRStStructInstance {
val expanded = fields.flatMap { field ->
val arraySz = field.arraySize
if(arraySz!=null)
List(arraySz) { field.type.sub!! }
else
listOf(field.type.base)
}
val values = expanded.zip(instance.initialValues).map { (baseType, value) ->
val elt = convertArrayElt(value)
IRStructInitValue(field.first.base, elt)
IRStructInitValue(baseType, elt)
}
return if('.' in instance.name)
IRStStructInstance(instance.name, instance.structName, values, instance.size)
@@ -134,33 +150,34 @@ private object StToIrConverter {
}
}
fun convertStToIRSt(sourceSt: SymbolTable?): IRSymbolTable {
fun convertStToIRSt(sourceSt: SymbolTable?, romable: Boolean = false): IRSymbolTable {
val st = IRSymbolTable()
val converter = StToIrConverter(romable)
if (sourceSt != null) {
sourceSt.flat.forEach {
when(it.value.type) {
StNodeType.STATICVAR -> st.add(StToIrConverter.convert(it.value as StStaticVariable))
StNodeType.MEMVAR -> st.add(StToIrConverter.convert(it.value as StMemVar))
StNodeType.STATICVAR -> st.add(converter.convert(it.value as StStaticVariable))
StNodeType.MEMVAR -> st.add(converter.convert(it.value as StMemVar))
StNodeType.CONSTANT -> {
val constant = it.value as StConstant
// If the constant has a memory() slab, add the slab to the IR symbol table first
constant.memorySlab?.let { slab ->
val converted = StToIrConverter.convert(slab)
val converted = converter.convert(slab)
st.add(converted)
}
st.add(StToIrConverter.convert(constant))
st.add(converter.convert(constant))
}
StNodeType.MEMORYSLAB -> {
val slab = it.value as StMemorySlab
val converted = StToIrConverter.convert(slab)
val converted = converter.convert(slab)
st.add(converted)
}
StNodeType.STRUCTINSTANCE -> {
val instance = it.value as StStructInstance
val struct = sourceSt.lookup(instance.structName) as StStruct
st.add(StToIrConverter.convert(instance, struct.fields))
st.add(converter.convert(instance, struct.fields))
}
StNodeType.STRUCT -> st.add(StToIrConverter.convert(it.value as StStruct))
StNodeType.STRUCT -> st.add(converter.convert(it.value as StStruct))
else -> { }
}
}
@@ -0,0 +1,303 @@
package prog8.codegen.intermediate
import prog8.code.*
import prog8.code.ast.*
import prog8.code.core.*
/**
* Prefixes all symbol names in the given program's AST with type-specific prefixes
* (p8b_ for blocks, p8s_ for subroutines, p8v_ for variables, p8c_ for constants,
* p8l_ for labels, p8t_ for structs).
*
* The AST should have simple (unscoped) names for all named nodes at this point.
* Identifiers and function calls may already have dotted scoped names.
* After prefixing, call [makeAllNodenamesScoped] to add scoping to the prefix names.
*
* Blocks with `noSymbolPrefixing=true` keep their own names intact,
* but their references to symbols in prefixed blocks still get prefixed.
*
* @return A new [SymbolTable] rebuilt from the now-prefixed AST.
*/
fun prefixSymbols(program: PtProgram, options: CompilationOptions, st: SymbolTable): SymbolTable {
val nodesToPrefix = mutableListOf<Pair<PtNode, Int>>() // parent + index
val functionCallsToPrefix = mutableListOf<Pair<PtNode, Int>>() // parent + index
val expressionsToFixSubtype = mutableListOf<PtExpression>()
val variablesToFixSubtype = mutableListOf<IPtVariable>()
fun prefixNamedNode(node: PtNamedNode) {
when(node) {
is PtAsmSub, is PtSub -> node.name = "p8s_${node.name}"
is PtBlock -> node.name = "p8b_${node.name}"
is PtLabel -> if(!node.name.startsWith(GENERATED_LABEL_PREFIX)) node.name = "p8l_${node.name}"
is PtVariable, is PtMemMapped, is PtSubroutineParameter -> node.name = "p8v_${node.name}"
is PtStructDecl -> node.name = "p8t_${node.name}"
}
}
fun prefixSymbols(node: PtNode) {
when(node) {
is PtAsmSub -> {
prefixNamedNode(node)
node.parameters.forEach { (_, param) -> prefixNamedNode(param) }
val address = node.address
if (address?.varbank != null) {
address.varbank = address.varbank!!.prefixIdentifierOrThis(node, st)
}
}
is PtSub -> prefixNamedNode(node)
is PtFunctionCall -> {
if(node.builtin) {
if(node.name=="prog8_lib_structalloc") {
val struct = node.type.subType!!
if(struct is StStruct) {
node.type.subType = st.lookup(struct.scopedNameString) as StStruct
}
}
} else {
val stNode = st.lookup(node.name)!!
if (stNode.astNode?.definingBlock()?.options?.noSymbolPrefixing != true) {
val index = node.parent.children.indexOf(node)
functionCallsToPrefix += node.parent to index
}
}
}
is PtIdentifier -> {
val pexpr = node.parent as? PtBinaryExpression
if(pexpr?.operator==".") {
if(pexpr.left is PtArrayIndexer) {
val arrayvar = (pexpr.left as PtArrayIndexer).variable!!
if(arrayvar.type.subType!=null) {
return
} else
throw AssemblyError("can only use deref expression on struct type")
} else {
TODO("handle other left operand ${pexpr.left }in dereferencing of struct fields ${node.position}")
}
}
var lookupName = node.name
if(node.type.isSplitWordArray && (lookupName.endsWith("_lsb") || lookupName.endsWith("_msb"))) {
lookupName = lookupName.dropLast(4)
}
val stNode = st.lookup(lookupName) ?:
throw AssemblyError("unknown identifier $node")
if(stNode.astNode!!.definingBlock()?.options?.noSymbolPrefixing!=true) {
val index = node.parent.children.indexOf(node)
nodesToPrefix += node.parent to index
}
}
is PtBlock -> prefixNamedNode(node)
is PtConstant -> node.name = "p8c_${node.name}"
is PtLabel -> prefixNamedNode(node)
is PtMemMapped -> prefixNamedNode(node)
is PtSubroutineParameter -> prefixNamedNode(node)
is PtVariable -> {
val index = node.parent.children.indexOf(node)
nodesToPrefix += node.parent to index
}
is PtStructDecl -> prefixNamedNode(node)
else -> { }
}
if(node is IPtVariable) {
if(node.type.subType!=null)
variablesToFixSubtype.add(node)
} else if(node is PtExpression) {
if(node.type.subType!=null)
expressionsToFixSubtype.add(node)
}
node.children.forEach { prefixSymbols(it) }
}
fun maybePrefixFunctionCallsAndIdentifierReferences(node: PtNode) {
if(node is PtFunctionCall && !node.builtin) {
val stNode = st.lookup(node.name)!!
if(stNode.astNode!!.definingBlock()?.options?.noSymbolPrefixing!=true) {
val index = node.parent.children.indexOf(node)
functionCallsToPrefix += node.parent to index
}
}
else if (node is PtIdentifier) {
var lookupName = node.name
if(node.type.isSplitWordArray && (lookupName.endsWith("_lsb") || lookupName.endsWith("_msb"))) {
lookupName = lookupName.dropLast(4)
}
val stNode = st.lookup(lookupName) ?: throw AssemblyError("unknown identifier $node")
if(stNode.astNode!!.definingBlock()?.options?.noSymbolPrefixing!=true) {
val index = node.parent.children.indexOf(node)
nodesToPrefix += node.parent to index
}
}
node.children.forEach { maybePrefixFunctionCallsAndIdentifierReferences(it) }
}
program.allBlocks().forEach { block ->
if (!block.options.noSymbolPrefixing) {
prefixSymbols(block)
} else {
maybePrefixFunctionCallsAndIdentifierReferences(block)
}
}
nodesToPrefix.forEach { (parent, index) ->
when(val node = parent.children[index]) {
is PtIdentifier -> parent.setChild(index, node.prefixIdentifierOrThis(parent, st))
is PtFunctionCall -> throw AssemblyError("PtFunctionCall should be processed in their own list, last")
is PtVariable -> parent.setChild(index, node.prefixVariableOrThis(parent, st))
else -> throw AssemblyError("weird node to prefix $node")
}
}
// reversed so inner calls (such as arguments to a function call) get processed before the actual function call itself
functionCallsToPrefix.reversed().forEach { (parent, index) ->
val node = parent.children[index]
if(node is PtFunctionCall) {
val prefixedName = PtIdentifier(node.name, DataType.UNDEFINED, node.position).prefixIdentifierOrThis(parent, st)
val prefixedNode = node.withNewName(prefixedName.name)
parent.setChild(index, prefixedNode)
} else {
throw AssemblyError("expected PtFunctionCall")
}
}
val updatedSt = SymbolTableMaker(program, options).make()
fun findSubtypeReplacement(sub: ISubType): StStruct? {
if(updatedSt.lookup(sub.scopedNameString)!=null)
return null
val old = st.lookup(sub.scopedNameString) ?: throw AssemblyError("old subtype not found: ${sub.scopedNameString}")
val prefixed = ArrayDeque<String>()
var node: StNode = old
while(node.type!= StNodeType.GLOBAL) {
val typeChar = typePrefixChar(node.type)
prefixed.addFirst("p8${typeChar}_${node.name}")
node=node.parent
}
return updatedSt.lookup(prefixed.joinToString(".")) as StStruct
}
expressionsToFixSubtype.forEach { node ->
findSubtypeReplacement(node.type.subType!!)?.let {
node.type.subType = it
}
}
variablesToFixSubtype.forEach { node ->
findSubtypeReplacement(node.type.subType!!)?.let {
node.type.subType = it
}
}
return updatedSt
}
internal fun prefixScopedName(name: String, type: Char): String {
if('.' !in name) {
if(name.startsWith(GENERATED_LABEL_PREFIX))
return name
return "p8${type}_$name"
}
val parts = name.split('.')
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 prefixed = listOf(firstPrefixed) + inbetweenPrefixed + listOf(lastPrefixed)
return prefixed.joinToString(".")
}
internal fun typePrefixChar(targetNt: StNodeType): Char {
return when(targetNt) {
StNodeType.BLOCK -> 'b'
StNodeType.SUBROUTINE, StNodeType.EXTSUB -> 's'
StNodeType.LABEL -> 'l'
StNodeType.STATICVAR, StNodeType.MEMVAR -> 'v'
StNodeType.CONSTANT -> 'c'
StNodeType.BUILTINFUNC -> 's'
StNodeType.MEMORYSLAB -> 'v'
StNodeType.STRUCT -> 't'
StNodeType.STRUCTINSTANCE -> 'i'
else -> '?'
}
}
private fun PtVariable.prefixVariableOrThis(parent: PtNode, st: SymbolTable): PtVariable {
name = prefixScopedName(name, 'v')
if(value==null)
return this
val arrayValue = value as? PtArray
return if(arrayValue!=null && arrayValue.children.any { it !is PtNumber && it !is PtBool } ) {
val newValue = PtArray(arrayValue.type, arrayValue.position)
arrayValue.children.forEach { elt ->
when(elt) {
is PtBool -> newValue.add(elt)
is PtNumber -> newValue.add(elt)
is PtConstant -> newValue.add(elt)
is PtAddressOf -> {
if(elt.definingBlock()?.options?.noSymbolPrefixing==true)
newValue.add(elt)
else {
val newAddr = PtAddressOf(elt.type, false, elt.position)
newAddr.add(elt.identifier!!.prefixIdentifierOrThis(newAddr, st))
if (elt.arrayIndexExpr != null)
newAddr.add(elt.arrayIndexExpr!!)
newAddr.parent = arrayValue
newValue.add(newAddr)
}
}
is PtFunctionCall if elt.builtin -> {
if (elt.name != "prog8_lib_structalloc")
throw AssemblyError("weird array value element $elt")
else
newValue.add(elt)
}
else -> throw AssemblyError("weird array value element $elt")
}
}
val result = PtVariable(name, type, zeropage, align, dirty, newValue, arraySize, position)
result.parent = parent
result
}
else this
}
private fun PtIdentifier.prefixIdentifierOrThis(parent: PtNode, st: SymbolTable): PtIdentifier {
val targetNt: StNodeType
val target = st.lookup(name)
if(target?.astNode?.definingBlock()?.options?.noSymbolPrefixing==true)
return this
if(target==null) {
if(name.endsWith("_lsb") || name.endsWith("_msb")) {
val target2 = st.lookup(name.dropLast(4))
if(target2?.astNode?.definingBlock()?.options?.noSymbolPrefixing==true)
return this
targetNt = target2!!.type
} else {
targetNt = StNodeType.STATICVAR
}
} else {
targetNt = target.type
}
val prefixType = typePrefixChar(targetNt)
val newName = prefixScopedName(name, prefixType)
val node = PtIdentifier(newName, type, position)
node.parent = parent
return node
}
private fun PtFunctionCall.withNewName(name: String): PtFunctionCall {
val call = PtFunctionCall(name, builtin, hasNoSideEffects, returntypes, position)
call.addAll(children)
call.children.forEach { it.parent = call }
call.parent = parent
return call
}
@@ -10,14 +10,16 @@ import prog8.codegen.intermediate.IRCodeGen
import prog8.intermediate.IRFileWriter
import prog8.intermediate.IRProgram
class VmCodeGen(val retainSSA: Boolean): ICodeGeneratorBackend {
class VmCodeGen(val retainSSA: Boolean,
private val preassignedCallSiteIds: Map<String, UByte> = emptyMap()
): ICodeGeneratorBackend {
override fun generate(
program: PtProgram,
symbolTable: SymbolTable,
options: CompilationOptions,
errors: IErrorReporter
): IAssemblyProgram {
val irCodeGen = IRCodeGen(program, symbolTable, options, errors, retainSSA)
val irCodeGen = IRCodeGen(program, symbolTable, options, errors, retainSSA, preassignedCallSiteIds)
val irProgram = irCodeGen.generate()
irProgram.verifyRegisterTypes(irCodeGen.registerTypes())
+78 -8
View File
@@ -1,6 +1,9 @@
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import prog8.code.core.*
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.IRPeepholeOptimizer
import prog8.intermediate.*
@@ -8,9 +11,9 @@ import prog8tests.helpers.ErrorReporterForTests
class TestIRPeepholeOpt: FunSpec({
fun makeIRProgram(chunks: List<IRCodeChunkBase>): IRProgram {
require(chunks.first().label=="main.start")
val block = IRBlock("main", false, IRBlock.Options(), Position.DUMMY)
val sub = IRSubroutine("main.start", emptyList(), emptyList(), Position.DUMMY)
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()
@@ -28,7 +31,7 @@ class TestIRPeepholeOpt: FunSpec({
}
fun makeIRProgram(instructions: List<IRInstruction>): IRProgram {
val chunk = IRCodeChunk("main.start", null)
val chunk = IRCodeChunk("p8b_main.p8s_start", null)
instructions.forEach { chunk += it }
return makeIRProgram(listOf(chunk))
}
@@ -48,7 +51,7 @@ class TestIRPeepholeOpt: FunSpec({
}
test("remove jmp to label below but keep labels") {
val c1 = IRCodeChunk("main.start", null)
val c1 = IRCodeChunk("p8b_main.p8s_start", null)
c1 += IRInstruction(Opcode.JUMP, labelSymbol = "label")
val c2 = IRCodeChunk("label", null)
c2 += IRInstruction(Opcode.JUMP, labelSymbol = "label2")
@@ -65,7 +68,7 @@ class TestIRPeepholeOpt: FunSpec({
opt.optimize(true, ErrorReporterForTests())
val chunks = irProg.chunks()
chunks.size shouldBe 4
chunks[0].label shouldBe "main.start"
chunks[0].label shouldBe "p8b_main.p8s_start"
chunks[1].label shouldBe "label"
chunks[2].label shouldBe "label2"
chunks[3].label shouldBe "label3"
@@ -289,4 +292,71 @@ class TestIRPeepholeOpt: FunSpec({
modInstr[0].opcode shouldBe Opcode.MOD
modInstr[0].immediate shouldBe 7
}
})
test("remove self identity operations") {
// LOADR with same fp register is allowed (constructor only blocks integer reg1==reg2)
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
))
irProg.chunks().single().instructions.size shouldBe 2
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[0].reg1 shouldBe 99
instr[0].immediate 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
))
irProg.chunks().single().instructions.size shouldBe 7
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[0].reg1 shouldBe 99
instr[0].immediate shouldBe 42
}
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
))
irProg.chunks().single().instructions.size shouldBe 13
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
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[1].opcode shouldBe Opcode.EXTS
instr[1].reg1 shouldBe 4
instr[1].reg2 shouldBe 44
instr[2].opcode shouldBe Opcode.LOAD
instr[2].immediate shouldBe 42
}
})
@@ -0,0 +1,318 @@
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.ast.*
import prog8.code.core.*
import prog8.code.source.SourceCode
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({
// --- prefixScopedName tests ---
test("prefixScopedName single name with type char") {
prefixScopedName("main", 'b') shouldBe "p8b_main"
prefixScopedName("start", 's') shouldBe "p8s_start"
prefixScopedName("x", 'v') shouldBe "p8v_x"
prefixScopedName("SIZE", 'c') shouldBe "p8c_SIZE"
prefixScopedName("label", 'l') shouldBe "p8l_label"
prefixScopedName("Point", 't') shouldBe "p8t_Point"
}
test("prefixScopedName dotted scoped name") {
prefixScopedName("main.start", 's') shouldBe "p8b_main.p8s_start"
prefixScopedName("main.start.x", 'v') shouldBe "p8b_main.p8s_start.p8v_x"
prefixScopedName("main.start.looplabel", 'l') shouldBe "p8b_main.p8s_start.p8l_looplabel"
prefixScopedName("main.SIZE", 'c') shouldBe "p8b_main.p8c_SIZE"
prefixScopedName("lib.module.func", 's') shouldBe "p8b_lib.p8s_module.p8s_func"
}
test("prefixScopedName generated label is not prefixed") {
val genLabel = "p8_label_gen_42"
prefixScopedName(genLabel, 's') shouldBe genLabel
prefixScopedName("main.start.$genLabel", 'v') shouldBe "p8b_main.p8s_start.$genLabel"
}
// --- typePrefixChar tests ---
test("typePrefixChar maps all StNodeTypes correctly") {
typePrefixChar(StNodeType.BLOCK) shouldBe 'b'
typePrefixChar(StNodeType.SUBROUTINE) shouldBe 's'
typePrefixChar(StNodeType.EXTSUB) shouldBe 's'
typePrefixChar(StNodeType.LABEL) shouldBe 'l'
typePrefixChar(StNodeType.STATICVAR) shouldBe 'v'
typePrefixChar(StNodeType.MEMVAR) shouldBe 'v'
typePrefixChar(StNodeType.CONSTANT) shouldBe 'c'
typePrefixChar(StNodeType.BUILTINFUNC) shouldBe 's'
typePrefixChar(StNodeType.MEMORYSLAB) shouldBe 'v'
typePrefixChar(StNodeType.STRUCT) shouldBe 't'
typePrefixChar(StNodeType.STRUCTINSTANCE) shouldBe 'i'
}
// --- full program prefixing tests ---
test("prefixSymbols prefixes a simple program") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
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)
sub.add(varX)
block.add(sub)
program.add(block)
val options = basicTestOptions()
val st = SymbolTableMaker(program, options).make()
val updatedSt = prefixSymbols(program, options, st)
// AST names should be prefixed
block.name shouldBe "p8b_main"
sub.name shouldBe "p8s_start"
varX.name shouldBe "p8v_x"
// symbol table should have prefixed entries
updatedSt.lookup("p8b_main") shouldNotBe null
updatedSt.lookup("p8b_main.p8s_start") shouldNotBe null
updatedSt.lookup("p8b_main.p8s_start.p8v_x") shouldNotBe null
}
test("prefixSymbols handles subroutine parameters") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
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)
sub.add(param)
block.add(sub)
program.add(block)
val options = basicTestOptions()
val st = SymbolTableMaker(program, options).make()
prefixSymbols(program, options, st)
param.name shouldBe "p8v_arg"
}
test("prefixSymbols handles constants") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
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)
program.add(block)
val options = basicTestOptions()
val st = SymbolTableMaker(program, options).make()
prefixSymbols(program, options, st)
constant.name shouldBe "p8c_SIZE"
}
test("prefixSymbols handles labels") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
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)
sub.add(label)
block.add(sub)
program.add(block)
val options = basicTestOptions()
val st = SymbolTableMaker(program, options).make()
prefixSymbols(program, options, st)
label.name shouldBe "p8l_mylabel"
}
test("prefixSymbols handles generated labels without prefixing them") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
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)
sub.add(genLabel)
block.add(sub)
program.add(block)
val options = basicTestOptions()
val st = SymbolTableMaker(program, options).make()
prefixSymbols(program, options, st)
genLabel.name shouldBe "p8_label_gen_42"
}
test("prefixSymbols handles struct declarations") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val struct = PtStructDecl("Point", listOf(
PtStructField(DataType.UBYTE, "x", null),
PtStructField(DataType.UBYTE, "y", null)
), Position.DUMMY)
block.add(struct)
program.add(block)
val options = basicTestOptions()
val st = SymbolTableMaker(program, options).make()
prefixSymbols(program, options, st)
struct.name shouldBe "p8t_Point"
}
test("prefixSymbols handles asmsub") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
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)
program.add(block)
val options = basicTestOptions()
val st = SymbolTableMaker(program, options).make()
prefixSymbols(program, options, st)
asmsub.name shouldBe "p8s_routine"
}
test("prefixSymbols handles memmapped variables") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
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)
program.add(block)
val options = basicTestOptions()
val st = SymbolTableMaker(program, options).make()
prefixSymbols(program, options, st)
memvar.name shouldBe "p8v_reg"
}
// --- noSymbolPrefixing tests ---
test("noSymbolPrefixing blocks keep their names intact") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
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)
libBlock.add(libSub)
program.add(libBlock)
val options = basicTestOptions()
val st = SymbolTableMaker(program, options).make()
prefixSymbols(program, options, st)
libBlock.name shouldBe "lib" // no prefix
libSub.name shouldBe "func" // no prefix
}
test("references from user block to noSymbolPrefixing block are not prefixed") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
// Library block with no symbol prefixing
val libOptions = PtBlock.Options(noSymbolPrefixing = true)
val libBlock = PtBlock("lib", false, SourceCode.Generated("test"), libOptions, Position.DUMMY)
val libSub = PtSub("print", emptyList(), emptyList(), Position.DUMMY)
libBlock.add(libSub)
program.add(libBlock)
// User block (regular)
val userBlock = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val userSub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
userBlock.add(userSub)
program.add(userBlock)
val options = basicTestOptions()
// Now add a function call referencing the library sub - this must happen AFTER making the st
// because the st is needed for lookup during prefixing
val call = PtFunctionCall("lib.print", false, false, emptyArray(), Position.DUMMY)
userSub.add(call)
// Rebuild st after adding the call
val st2 = SymbolTableMaker(program, options).make()
prefixSymbols(program, options, st2)
// The call site should NOT be prefixed (lib has noSymbolPrefixing)
call.name shouldBe "lib.print"
}
test("references from noSymbolPrefixing block to regular block get prefixed") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
// Regular user block
val userBlock = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val userSub = PtSub("helper", emptyList(), emptyList(), Position.DUMMY)
userBlock.add(userSub)
program.add(userBlock)
// Library block with no symbol prefixing
val libOptions = PtBlock.Options(noSymbolPrefixing = true)
val libBlock = PtBlock("lib", false, SourceCode.Generated("test"), libOptions, Position.DUMMY)
val libSub = PtSub("start", emptyList(), emptyList(), Position.DUMMY)
val call = PtFunctionCall("main.helper", false, false, emptyArray(), Position.DUMMY)
libSub.add(call)
libBlock.add(libSub)
program.add(libBlock)
val options = basicTestOptions()
val st = SymbolTableMaker(program, options).make()
prefixSymbols(program, options, st)
// The call site from lib to main should get prefixed (new node, get from parent)
val newCall = libSub.children.find { it is PtFunctionCall } as PtFunctionCall
newCall.name shouldBe "p8b_main.p8s_helper"
}
test("identifier references are prefixed according to their target type") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
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)
sub.add(varX)
// Add an identifier reference to the variable
val idRef = PtIdentifier("main.start.x", DataType.UBYTE, Position.DUMMY)
sub.add(idRef)
block.add(sub)
program.add(block)
val options = basicTestOptions()
val st = SymbolTableMaker(program, options).make()
prefixSymbols(program, options, st)
// The identifier reference should have been prefixed (new node, get from parent)
val newIdRef = sub.children.find { it is PtIdentifier } as PtIdentifier
newIdRef.name shouldBe "p8b_main.p8s_start.p8v_x"
}
test("prefixSymbols exception for unknown identifier") {
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
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)
sub.add(idRef)
block.add(sub)
program.add(block)
val options = basicTestOptions()
val st = SymbolTableMaker(program, options).make()
shouldThrow<AssemblyError> {
prefixSymbols(program, options, st)
}
}
})
private fun basicTestOptions(): CompilationOptions {
val target = prog8.code.target.VMTarget()
return CompilationOptions.builder(target)
.output(OutputType.RAW)
.zeropage(ZeropageType.DONTUSE)
.floats(true)
.compilerVersion("99.99")
.build()
}
+30 -1
View File
@@ -628,11 +628,40 @@ class TestVmCodeGen: FunSpec({
val st = SymbolTableMaker(program, options).make()
val errors = ErrorReporterForTests()
val result = codegen.generate(program, st, options, errors) as VmAssemblyProgram
val irChunks = (result.irProgram.blocks.first().children.single() as IRSubroutine).chunks
val irSub = result.irProgram.blocks.first().children.filterIsInstance<IRSubroutine>().single()
val irChunks = irSub.chunks
irChunks.size shouldBe 1
irChunks[0].instructions.size shouldBe 1
val callInstr = irChunks[0].instructions[0]
callInstr.opcode shouldBe Opcode.CALL
callInstr.address shouldBe 0x5000u.toAddress()
}
test("ir codegen for target 'virtual' produces no prefixed names") {
val codegen = VmCodeGen(false)
val program = PtProgram("test", DummyMemsizer, DummyStringEncoder)
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))
block.add(sub)
program.add(block)
val options = getTestOptions()
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 not be prefixed
val irBlock = irProg.blocks.first()
irBlock.label shouldBe "main"
// Subroutine label should be scoped but not prefixed
val irSub = irBlock.children.single() as IRSubroutine
irSub.label shouldBe "main.start"
// Variable in IR symbol table should not have prefixed scoped name
val irVar = irProg.st.allVariables().single()
irVar.name shouldBe "main.start.x"
}
})
-16
View File
@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
<orderEntry type="module" module-name="codeCore" />
<orderEntry type="module" module-name="compilerAst" />
<orderEntry type="library" name="michael.bull.kotlin.result.jvm" level="project" />
</component>
</module>
@@ -66,51 +66,49 @@ class ConstExprEvaluator {
return NumericLiteral.optimalNumeric(left.type, null, result.toDouble(), left.position)
}
private fun bitwiseXor(left: NumericLiteral, right: NumericLiteral): NumericLiteral {
if(right.type.isIntegerOrBool) {
val leftDt = left.type
if(leftDt.isByteOrBool)
return NumericLiteral(BaseDataType.UBYTE, ((left.number.toInt() xor right.number.toInt()) and 255).toDouble(), left.position)
else if(leftDt.isInteger) {
if (leftDt == BaseDataType.UWORD || leftDt == BaseDataType.WORD)
return NumericLiteral(BaseDataType.UWORD, ((left.number.toInt() xor right.number.toInt()) and 65535).toDouble(), left.position)
else if (leftDt == BaseDataType.LONG)
return NumericLiteral.optimalNumeric((left.number.toInt() xor right.number.toInt()).toDouble(), left.position)
}
}
throw ExpressionError("cannot calculate $left ^ $right", left.position)
}
private fun bitwiseOr(left: NumericLiteral, right: NumericLiteral): NumericLiteral {
if(right.type.isIntegerOrBool) {
val leftDt = left.type
if(leftDt.isByteOrBool)
return NumericLiteral(BaseDataType.UBYTE, ((left.number.toInt() or right.number.toInt()) and 255).toDouble(), left.position)
else if(leftDt.isInteger) {
if (leftDt == BaseDataType.UWORD || leftDt == BaseDataType.WORD)
return NumericLiteral(BaseDataType.UWORD, ((left.number.toInt() or right.number.toInt()) and 65535).toDouble(), left.position)
else if (leftDt == BaseDataType.LONG)
return NumericLiteral.optimalNumeric((left.number.toInt() or right.number.toInt()).toDouble(), left.position)
}
if(left.type.isIntegerOrBool && right.type.isIntegerOrBool) {
val resultType = bitwiseResultType(left.type, right.type)
val result = (left.number.toInt() or right.number.toInt()) and bitwiseMask(resultType)
return NumericLiteral(resultType, result.toDouble(), left.position)
}
throw ExpressionError("cannot calculate $left | $right", left.position)
}
private fun bitwiseAnd(left: NumericLiteral, right: NumericLiteral): NumericLiteral {
if(right.type.isIntegerOrBool) {
val leftDt = left.type
if(leftDt.isByteOrBool)
return NumericLiteral(BaseDataType.UBYTE, ((left.number.toInt() and right.number.toInt()) and 255).toDouble(), left.position)
else if(leftDt.isInteger) {
if (leftDt == BaseDataType.UWORD || leftDt == BaseDataType.WORD)
return NumericLiteral(BaseDataType.UWORD, ((left.number.toInt() and right.number.toInt()) and 65535).toDouble(), left.position)
else if (leftDt == BaseDataType.LONG)
return NumericLiteral.optimalNumeric((left.number.toInt() and right.number.toInt()).toDouble(), left.position)
}
if(left.type.isIntegerOrBool && right.type.isIntegerOrBool) {
val resultType = bitwiseResultType(left.type, right.type)
val result = (left.number.toInt() and right.number.toInt()) and bitwiseMask(resultType)
return NumericLiteral(resultType, result.toDouble(), left.position)
}
throw ExpressionError("cannot calculate $left & $right", left.position)
}
private fun bitwiseXor(left: NumericLiteral, right: NumericLiteral): NumericLiteral {
if(left.type.isIntegerOrBool && right.type.isIntegerOrBool) {
val resultType = bitwiseResultType(left.type, right.type)
val result = (left.number.toInt() xor right.number.toInt()) and bitwiseMask(resultType)
return NumericLiteral(resultType, result.toDouble(), left.position)
}
throw ExpressionError("cannot calculate $left ^ $right", left.position)
}
private fun bitwiseResultType(type1: BaseDataType, type2: BaseDataType): BaseDataType {
val width = if (type1.largerSizeThan(type2)) type1 else type2
return when {
width.isByteOrBool -> BaseDataType.UBYTE
width.isWord -> BaseDataType.UWORD
width == BaseDataType.LONG -> BaseDataType.LONG
else -> BaseDataType.UBYTE
}
}
private fun bitwiseMask(type: BaseDataType): Int = when(type) {
BaseDataType.UBYTE, BaseDataType.BYTE -> 0xFF
BaseDataType.UWORD, BaseDataType.WORD -> 0xFFFF
else -> 0xFFFFFFFF.toInt()
}
private fun logicalAnd(left: NumericLiteral, right: NumericLiteral): NumericLiteral =
NumericLiteral.fromBoolean(left.asBooleanValue and right.asBooleanValue, left.position)
@@ -355,33 +353,10 @@ class ConstExprEvaluator {
}
private fun evalString(func: FunctionCallExpression, args: List<NumericLiteral>): NumericLiteral? {
return when(func.target.nameInSource[1]) {
"isdigit" -> {
val char = args[0].number.toInt()
NumericLiteral.fromBoolean(char in 48..57, func.position)
}
"isupper" -> {
// shifted petscii has 2 ranges that contain the upper case letters... 97-122 and 193-218
val char = args[0].number.toInt()
NumericLiteral.fromBoolean(char in 97..122 || char in 193..218, func.position)
}
"islower" -> {
val char = args[0].number.toInt()
NumericLiteral.fromBoolean(char in 65..90, func.position)
}
"isletter" -> {
val char = args[0].number.toInt()
NumericLiteral.fromBoolean(char in 65..90 || char in 97..122 || char in 193..218, func.position)
}
"isspace" -> {
val char = args[0].number.toInt()
NumericLiteral.fromBoolean(char in setOf(32, 13, 9, 10, 141, 160), func.position)
}
"isprint" -> {
val char = args[0].number.toInt()
NumericLiteral.fromBoolean(char in 32..127 || char>=160, func.position)
}
else -> null
return if (func.target.nameInSource[1] == "isdigit") {
val char = args[0].number.toInt()
NumericLiteral.fromBoolean(char in 48..57, func.position)
}
else null
}
}
@@ -21,10 +21,11 @@ class ConstantFoldingOptimizer(private val program: Program, private val errors:
}
override fun before(memread: DirectMemoryRead, parent: Node): Iterable<AstModification> {
// @( &thing ) --> thing (but only if thing is a byte type!)
// @( &thing ) --> thing (but only if thing is unsigned byte or bool, this prevents sign extension errors)
val addrOf = memread.addressExpression as? AddressOf
if(addrOf!=null) {
if(addrOf.identifier?.inferType(program)?.isBytes==true)
val it = addrOf.identifier?.inferType(program)
if(it?.isUnsignedByte==true || it?.isBool==true)
return listOf(AstReplaceNode(memread, addrOf.identifier!!, parent))
}
return noModifications
@@ -32,6 +32,12 @@ class VarConstantValueTypeAdjuster(
private fun isConstantInitializer(expr: Expression): Boolean = getConstantInitializer(expr) != null
private fun isMemorySlabInit(expr: Expression): Boolean =
expr is MemorySlabRef || (expr is IFunctionCall && expr.isMemoryCall)
private fun requiresZp(decl: VarDecl): Boolean =
decl.zeropage == ZeropageWish.REQUIRE_ZEROPAGE
override fun after(decl: VarDecl, parent: Node): Iterable<AstModification> {
if(decl.parent is AnonymousScope)
@@ -47,95 +53,95 @@ class VarConstantValueTypeAdjuster(
}
// replace variables by constants, if possible
if(options.optimize) {
if (decl.sharedWithAsm || decl.type != VarDeclType.VAR || decl.origin != VarDeclOrigin.USERCODE
|| !(decl.datatype.isNumericOrBool || decl.datatype.isPointer))
return noModifications
if (decl.value != null && !isConstantInitializer(decl.value!!))
return noModifications
val usages = callGraph.usages(decl)
val (writes, reads) = usages
.partition {
it is InlineAssembly // can't really tell if it's written to or only read, assume the worst
|| it.parent is AssignTarget
|| it.parent is ForLoop
|| (it.parent as? IFunctionCall)?.target?.nameInSource?.singleOrNull() in InplaceModifyingBuiltinFunctions
}
// If the address of the variable is taken, we cannot replace it with a constant.
if (reads.any { it.parent is AddressOf })
return noModifications
var singleAssignment: Assignment? = null
val singleWrite=writes.singleOrNull()
if(singleWrite!=null) {
singleAssignment = singleWrite.parent as? Assignment
if (decl.sharedWithAsm || decl.type != VarDeclType.VAR || decl.origin != VarDeclOrigin.USERCODE
|| !(decl.datatype.isNumericOrBool || decl.datatype.isPointer))
return noModifications
if (decl.value != null && !isConstantInitializer(decl.value!!))
return noModifications
if (decl.value != null && isMemorySlabInit(decl.value!!))
return noModifications // a pointer to a memory slab is usually accessed often and/or in ways that benefit more from the pointer being an actual variable (preferably in zeropage)
val usages = callGraph.usages(decl)
val (writes, reads) = usages
.partition {
it is InlineAssembly // can't really tell if it's written to or only read, assume the worst
|| it.parent is AssignTarget
|| it.parent is ForLoop
|| (it.parent as? IFunctionCall)?.target?.nameInSource?.singleOrNull() in InplaceModifyingBuiltinFunctions
}
// If the address of the variable is taken, we cannot replace it with a constant.
if (reads.any { it.parent is AddressOf })
return noModifications
var singleAssignment: Assignment? = null
val singleWrite=writes.singleOrNull()
if(singleWrite!=null) {
singleAssignment = singleWrite.parent as? Assignment
if(singleAssignment==null) {
singleAssignment = singleWrite.parent.parent as? Assignment
if(singleAssignment==null) {
singleAssignment = singleWrite.parent.parent as? Assignment
if(singleAssignment==null) {
// we could be part of a multi-assign
if(singleWrite.parent is AssignTarget && singleWrite.parent.parent is AssignTarget)
singleAssignment = singleWrite.parent.parent.parent as? Assignment
}
// we could be part of a multi-assign
if(singleWrite.parent is AssignTarget && singleWrite.parent.parent is AssignTarget)
singleAssignment = singleWrite.parent.parent.parent as? Assignment
}
}
if (singleAssignment == null) {
if (writes.isEmpty()) {
if(reads.isEmpty()) {
if(decl.names.size>1) {
errors.info("unused variable '${decl.name}'", decl.position)
} else {
// variable is never used AT ALL so we just remove it altogether
if ("ignore_unused" !in decl.definingBlock.options())
errors.info("removing unused variable '${decl.name}'", decl.position)
return listOf(AstRemove(decl, parent as IStatementContainer))
}
}
val declValue = if (decl.value != null) getConstantInitializer(decl.value!!) else null
if (declValue != null && canBeMadeConst(decl, usages)) {
// variable is never written to, so it can be replaced with a constant, IF the value is a constant
errors.info("variable '${decl.name}' is never written to and was made const", decl.position)
val const = VarDecl.builder(decl.datatype, decl.position)
.copyFrom(decl)
.type(VarDeclType.CONST)
.value(declValue)
.build()
decl.value = null
return listOf(
AstReplaceNode(decl, const, parent)
)
}
if (singleAssignment == null) {
if (writes.isEmpty()) {
if(reads.isEmpty()) {
if(decl.names.size>1) {
errors.info("unused variable '${decl.name}'", decl.position)
} else {
// variable is never used AT ALL so we just remove it altogether
if ("ignore_unused" !in decl.definingBlock.options())
errors.info("removing unused variable '${decl.name}'", decl.position)
return listOf(AstRemove(decl, parent as IStatementContainer))
}
}
} else {
if (singleAssignment.origin == AssignmentOrigin.VARINIT && isConstantInitializer(singleAssignment.value) && canBeMadeConst(decl, usages)) {
if(reads.isEmpty()) {
if(decl.names.size>1) {
errors.info("unused variable '${decl.name}'", decl.position)
} else {
// variable is never used AT ALL so we just remove it altogether, including the single assignment
if("ignore_unused" !in decl.definingBlock.options())
errors.info("removing unused variable '${decl.name}'", decl.position)
return listOf(
AstRemove(decl, parent as IStatementContainer),
AstRemove(singleAssignment, singleAssignment.parent as IStatementContainer)
)
}
}
// variable only has a single write, and it is the initialization value, so it can be replaced with a constant, but only IF the value is a constant
val declValue = if (decl.value != null) getConstantInitializer(decl.value!!) else null
if (declValue != null && !requiresZp(decl) && canBeMadeConst(decl, usages)) {
// variable is never written to, so it can be replaced with a constant, IF the value is a constant
errors.info("variable '${decl.name}' is never written to and was made const", decl.position)
val const = VarDecl.builder(decl.datatype, decl.position)
.copyFrom(decl)
.type(VarDeclType.CONST)
.value(singleAssignment.value)
.value(declValue)
.build()
decl.value = null
return listOf(
AstReplaceNode(decl, const, parent),
AstRemove(singleAssignment, singleAssignment.parent as IStatementContainer)
AstReplaceNode(decl, const, parent)
)
}
}
} else {
if (singleAssignment.origin == AssignmentOrigin.VARINIT && isConstantInitializer(singleAssignment.value) && !isMemorySlabInit(singleAssignment.value) && !requiresZp(decl) && canBeMadeConst(decl, usages)) {
if(reads.isEmpty()) {
if(decl.names.size>1) {
errors.info("unused variable '${decl.name}'", decl.position)
} else {
// variable is never used AT ALL so we just remove it altogether, including the single assignment
if("ignore_unused" !in decl.definingBlock.options())
errors.info("removing unused variable '${decl.name}'", decl.position)
return listOf(
AstRemove(decl, parent as IStatementContainer),
AstRemove(singleAssignment, singleAssignment.parent as IStatementContainer)
)
}
}
// variable only has a single write, and it is the initialization value, so it can be replaced with a constant, but only IF the value is a constant
errors.info("variable '${decl.name}' is never written to and was made const", decl.position)
val const = VarDecl.builder(decl.datatype, decl.position)
.copyFrom(decl)
.type(VarDeclType.CONST)
.value(singleAssignment.value)
.build()
return listOf(
AstReplaceNode(decl, const, parent),
AstRemove(singleAssignment, singleAssignment.parent as IStatementContainer)
)
}
}
return noModifications
@@ -148,10 +154,11 @@ class VarConstantValueTypeAdjuster(
val p = it.parent
val gp = p.parent
it is PtrDereference || it is ArrayIndexedPtrDereference
|| p is ArrayIndexedExpression || p is DirectMemoryRead || p is DirectMemoryWrite
|| gp is DirectMemoryRead || gp is DirectMemoryWrite
|| p is ArrayIndexedExpression || p is DirectMemoryRead || p is DirectMemoryWrite
|| gp is DirectMemoryRead || gp is DirectMemoryWrite
}
if (derefCount > 2) return false
if (derefCount > 0 && (decl.zeropage==ZeropageWish.PREFER_ZEROPAGE || decl.zeropage==ZeropageWish.REQUIRE_ZEROPAGE)) return false
}
return true
}
@@ -340,6 +347,12 @@ internal class ConstantIdentifierReplacer(
override fun before(addressOf: AddressOf, parent: Node): Iterable<AstModification> {
val constValue = addressOf.constValue(program)
if(constValue!=null) {
// don't replace &constVar (without array index); leave it for AstChecker to reject as invalid address-of.
// But still allow &constArray[idx] (those are valid address-of operations).
val target = addressOf.identifier?.targetVarDecl()
if(target?.type==VarDeclType.CONST && addressOf.arrayIndex==null) {
return noModifications
}
return listOf(AstReplaceNode(addressOf, constValue, parent))
}
return noModifications
@@ -348,7 +361,7 @@ internal class ConstantIdentifierReplacer(
override fun after(identifier: IdentifierReference, parent: Node): Iterable<AstModification> {
// replace identifiers that refer to const value, with the value itself
// if it's a simple type and if it's not a left hand side variable
if (identifier.parent is AssignTarget || identifier.parent is Alias)
if (identifier.parent is AssignTarget || identifier.parent is Alias || identifier.parent is AddressOf)
return noModifications
var forloop = identifier.parent as? ForLoop
if (forloop == null)
@@ -12,7 +12,7 @@ import kotlin.math.floor
import kotlin.math.log2
class ExpressionSimplifier(private val program: Program, private val errors: IErrorReporter) : AstWalker() {
class ExpressionSimplifier(private val program: Program, private val errors: IErrorReporter, private val options: CompilationOptions) : AstWalker() {
override fun after(typecast: TypecastExpression, parent: Node): Iterable<AstModification> {
val mods = mutableListOf<AstModification>()
@@ -177,7 +177,8 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
}
// X <= Y-1 ---> X<Y , X >= Y+1 ---> X>Y
if(leftDt.isInteger && rightDt.isInteger) {
// Only for signed integers to avoid unsigned wraparound issues.
if(leftDt.isSignedInteger && rightDt.isSignedInteger) {
val rightExpr = expr.right as? BinaryExpression
if(rightExpr!=null && rightExpr.right.constValue(program)?.number==1.0) {
if (expr.operator == "<=" && rightExpr.operator == "-") {
@@ -463,12 +464,12 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
when {
leftVal?.number==0.0 -> expr.right
rightVal?.number==0.0 -> expr.left
rightIDt.isBytes && rightVal?.number==255.0 -> NumericLiteral(BaseDataType.UBYTE, 255.0, rightVal.position)
rightIDt.isWords && rightVal?.number==65535.0 -> NumericLiteral(BaseDataType.UWORD, 65535.0, rightVal.position)
rightIDt.isLong && rightVal?.number==-1.0 -> NumericLiteral(BaseDataType.LONG, -1.0, rightVal.position)
leftIDt.isBytes && leftVal?.number==255.0 -> NumericLiteral(BaseDataType.UBYTE, 255.0, leftVal.position)
leftIDt.isWords && leftVal?.number==65535.0 -> NumericLiteral(BaseDataType.UWORD, 65535.0, leftVal.position)
leftIDt.isLong && leftVal?.number==-1.0 -> NumericLiteral(BaseDataType.LONG, -1.0, leftVal.position)
leftIDt.isBytes && rightIDt.isBytes && rightVal?.number==255.0 -> NumericLiteral(BaseDataType.UBYTE, 255.0, rightVal.position)
leftIDt.isWords && rightIDt.isWords && rightVal?.number==65535.0 -> NumericLiteral(BaseDataType.UWORD, 65535.0, rightVal.position)
leftIDt.isLong && rightIDt.isLong && rightVal?.number==-1.0 -> NumericLiteral(BaseDataType.LONG, -1.0, rightVal.position)
leftIDt.isBytes && rightIDt.isBytes && leftVal?.number==255.0 -> NumericLiteral(BaseDataType.UBYTE, 255.0, leftVal.position)
leftIDt.isWords && rightIDt.isWords && leftVal?.number==65535.0 -> NumericLiteral(BaseDataType.UWORD, 65535.0, leftVal.position)
leftIDt.isLong && rightIDt.isLong && leftVal?.number==-1.0 -> NumericLiteral(BaseDataType.LONG, -1.0, leftVal.position)
else -> null
}
}
@@ -476,12 +477,12 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
when {
leftVal?.number==0.0 -> expr.right
rightVal?.number==0.0 -> expr.left
rightIDt.isBytes && rightVal?.number==255.0 -> PrefixExpression("~", expr.left, expr.left.position)
rightIDt.isWords && rightVal?.number==65535.0 -> PrefixExpression("~", expr.left, expr.left.position)
rightIDt.isLong && rightVal?.number==-1.0 -> PrefixExpression("~", expr.left, expr.left.position)
leftIDt.isBytes && leftVal?.number==255.0 -> PrefixExpression("~", expr.right, expr.right.position)
leftIDt.isWords && leftVal?.number==65535.0 -> PrefixExpression("~", expr.right, expr.right.position)
leftIDt.isLong && leftVal?.number==-1.0 -> PrefixExpression("~", expr.right, expr.right.position)
leftIDt.isBytes && rightIDt.isBytes && rightVal?.number==255.0 -> PrefixExpression("~", expr.left, expr.left.position)
leftIDt.isWords && rightIDt.isWords && rightVal?.number==65535.0 -> PrefixExpression("~", expr.left, expr.left.position)
leftIDt.isLong && rightIDt.isLong && rightVal?.number==-1.0 -> PrefixExpression("~", expr.left, expr.left.position)
leftIDt.isBytes && rightIDt.isBytes && leftVal?.number==255.0 -> PrefixExpression("~", expr.right, expr.right.position)
leftIDt.isWords && rightIDt.isWords && leftVal?.number==65535.0 -> PrefixExpression("~", expr.right, expr.right.position)
leftIDt.isLong && rightIDt.isLong && leftVal?.number==-1.0 -> PrefixExpression("~", expr.right, expr.right.position)
else -> null
}
}
@@ -489,12 +490,12 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
when {
leftVal?.number==0.0 -> constFalse
rightVal?.number==0.0 -> constFalse
rightIDt.isBytes && rightVal?.number==255.0 -> expr.left
rightIDt.isWords && rightVal?.number==65535.0 -> expr.left
rightIDt.isLong && rightVal?.number==-1.0 -> expr.left
leftIDt.isBytes && leftVal?.number==255.0 -> expr.right
leftIDt.isWords && leftVal?.number==65535.0 -> expr.right
leftIDt.isLong && leftVal?.number==-1.0 -> expr.right
leftIDt.isBytes && rightIDt.isBytes && rightVal?.number==255.0 -> expr.left
leftIDt.isWords && rightIDt.isWords && rightVal?.number==65535.0 -> expr.left
leftIDt.isLong && rightIDt.isLong && rightVal?.number==-1.0 -> expr.left
leftIDt.isBytes && rightIDt.isBytes && leftVal?.number==255.0 -> expr.right
leftIDt.isWords && rightIDt.isWords && leftVal?.number==65535.0 -> expr.right
leftIDt.isLong && rightIDt.isLong && leftVal?.number==-1.0 -> expr.right
else -> null
}
}
@@ -679,23 +680,23 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
return null
// absorption laws: a or (a and b) --> a, a and (a or b) --> a
if(expr.operator=="or" && rightB.operator=="and") {
if(expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) {
if((expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) && !expr.right.hasSideEffects(options.compTarget)) {
return expr.left
}
}
else if(expr.operator=="and" && rightB.operator=="or") {
if(expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) {
if((expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) && !expr.right.hasSideEffects(options.compTarget)) {
return expr.left
}
}
else if(expr.operator=="or" && rightB.operator=="or") {
if(expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) {
if((expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) && !expr.left.hasSideEffects(options.compTarget)) {
// a or (a or b) -> a or b
return expr.right
}
}
else if(expr.operator=="and" && rightB.operator=="and") {
if(expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) {
if((expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) && !expr.left.hasSideEffects(options.compTarget)) {
// a and (a and b) -> a and b
return expr.right
}
@@ -707,23 +708,23 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
return null
// absorption laws: (a and b) or a --> a, (a or b) and a --> a
if(expr.operator=="or" && leftB.operator=="and") {
if(expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) {
if((expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) && !expr.left.hasSideEffects(options.compTarget)) {
return expr.right
}
}
else if(expr.operator=="and" && leftB.operator=="or") {
if(expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) {
if((expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) && !expr.left.hasSideEffects(options.compTarget)) {
return expr.right
}
}
else if(expr.operator=="or" && leftB.operator=="or") {
if(expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) {
if((expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) && !expr.right.hasSideEffects(options.compTarget)) {
// (a or b) or a -> a or b
return expr.left
}
}
else if(expr.operator=="and" && leftB.operator=="and") {
if(expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) {
if((expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) && !expr.right.hasSideEffects(options.compTarget)) {
// (a and b) or a -> a and b
return expr.left
}
@@ -844,7 +845,7 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
}
private fun optimizeAdd(expr: BinaryExpression, leftVal: NumericLiteral?, rightVal: NumericLiteral?): Expression? {
if(expr.left.isSameAs(expr.right)) {
if(expr.left.isSameAs(expr.right) && !expr.left.hasSideEffects(options.compTarget)) {
// optimize X+X into X *2
expr.operator = "*"
expr.right = NumericLiteral.optimalInteger(2, expr.right.position)
@@ -879,7 +880,7 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
}
private fun optimizeSub(expr: BinaryExpression, leftVal: NumericLiteral?, rightVal: NumericLiteral?): Expression? {
if(expr.left.isSameAs(expr.right)) {
if(expr.left.isSameAs(expr.right) && !expr.left.hasSideEffects(options.compTarget)) {
// optimize X-X into 0
return NumericLiteral.optimalInteger(0, expr.position)
}
@@ -924,12 +925,12 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
val cv = rightVal?.number?.toInt()?.toDouble()
when (expr.operator) {
"%" -> {
if (cv == 1.0) {
if (cv == 1.0 && !expr.left.hasSideEffects(options.compTarget)) {
val idt = expr.inferType(program)
if(!idt.isKnown)
throw FatalAstException("unknown dt")
return NumericLiteral(idt.getOrUndef().base, 0.0, expr.position)
} else if (cv in powersOfTwoFloat) {
} else if (cv in powersOfTwoFloat && expr.left.inferType(program).getOrUndef().isUnsignedInteger) {
expr.operator = "&"
expr.right = NumericLiteral.optimalInteger(cv!!.toInt()-1, expr.position)
return null
@@ -969,7 +970,7 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
}
256.0 -> {
when(leftDt.base) {
BaseDataType.UBYTE -> return NumericLiteral(BaseDataType.UBYTE, 0.0, expr.position)
BaseDataType.UBYTE -> if (!expr.left.hasSideEffects(options.compTarget)) return NumericLiteral(BaseDataType.UBYTE, 0.0, expr.position) else return null
BaseDataType.BYTE -> return null // is either 0 or -1 we cannot tell here
BaseDataType.UWORD, BaseDataType.WORD -> {
// just use: msb(value) as type
@@ -992,11 +993,11 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
}
if (leftDt.isUnsignedByte) {
if (abs(rightConst.number) >= 256.0) {
if (abs(rightConst.number) >= 256.0 && !expr.left.hasSideEffects(options.compTarget)) {
return NumericLiteral(BaseDataType.UBYTE, 0.0, expr.position)
}
} else if (leftDt.isUnsignedWord) {
if (abs(rightConst.number) >= 65536.0) {
if (abs(rightConst.number) >= 65536.0 && !expr.left.hasSideEffects(options.compTarget)) {
return NumericLiteral(BaseDataType.UBYTE, 0.0, expr.position)
}
}
@@ -1006,8 +1007,9 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
// left value is a constant, see if we can optimize
when (leftVal.number) {
0.0 -> {
// 0
return NumericLiteral(leftVal.type, 0.0, expr.position)
// 0 (only if not float and no side effects)
if (!expr.inferType(program).isFloat && !expr.right.hasSideEffects(options.compTarget))
return NumericLiteral(leftVal.type, 0.0, expr.position)
}
}
}
@@ -1030,8 +1032,11 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
return PrefixExpression("-", leftValue, expr.position)
}
0.0 -> {
// 0
return NumericLiteral(rightConst.type, 0.0, expr.position)
// 0 (only if not float and no side effects)
if (!expr.inferType(program).isFloat && !leftValue.hasSideEffects(options.compTarget))
return NumericLiteral(rightConst.type, 0.0, expr.position)
else
return null
}
1.0 -> {
// left
@@ -1,6 +1,7 @@
package prog8.optimizer
import prog8.ast.IBuiltinFunctions
import prog8.ast.ParentSentinel
import prog8.ast.Program
import prog8.code.core.CompilationOptions
import prog8.code.core.IErrorReporter
@@ -25,6 +26,7 @@ fun Program.constantFold(errors: IErrorReporter, options: CompilationOptions) {
optimizer.visit(this)
var tries=0
while (errors.noErrors() && optimizer.applyModifications() > 0 && tries++ < 100000) {
optimizer.linkAffectedParents(namespace)
optimizer.visit(this)
}
require(tries<100000) { "endless loop in constantfold" }
@@ -32,13 +34,14 @@ fun Program.constantFold(errors: IErrorReporter, options: CompilationOptions) {
if (errors.noErrors()) {
replacer.visit(this)
replacer.applyModifications()
replacer.linkAffectedParents(namespace)
}
}
}
}
if(errors.noErrors())
modules.forEach { it.linkParents(namespace) } // re-link in final configuration
namespace.linkParents(ParentSentinel) // re-link in final configuration
}
@@ -49,8 +52,7 @@ fun Program.optimizeStatements(errors: IErrorReporter,
val optimizer = StatementOptimizer(this, errors, functions, options)
optimizer.visit(this)
val optimizationCount = optimizer.applyModifications()
modules.forEach { it.linkParents(this.namespace) } // re-link in final configuration
optimizer.linkAffectedParents(this.namespace)
return optimizationCount
}
@@ -61,11 +63,15 @@ fun Program.inlineSubroutines(options: CompilationOptions): Int {
val inliner = Inliner(this, options)
inliner.visit(this)
return inliner.applyModifications()
val mods = inliner.applyModifications()
inliner.linkAffectedParents(namespace)
return mods
}
fun Program.simplifyExpressions(errors: IErrorReporter) : Int {
val opti = ExpressionSimplifier(this, errors)
fun Program.simplifyExpressions(errors: IErrorReporter, options: CompilationOptions) : Int {
val opti = ExpressionSimplifier(this, errors, options)
opti.visit(this)
return opti.applyModifications()
val mods = opti.applyModifications()
opti.linkAffectedParents(namespace)
return mods
}
+241 -81
View File
@@ -1,9 +1,6 @@
package prog8.optimizer
import prog8.ast.IFunctionCall
import prog8.ast.IStatementContainer
import prog8.ast.Node
import prog8.ast.Program
import prog8.ast.*
import prog8.ast.expressions.*
import prog8.ast.statements.*
import prog8.ast.walk.*
@@ -14,10 +11,28 @@ import prog8.code.target.VMTarget
private fun isEmptyReturn(stmt: Statement): Boolean = stmt is Return && stmt.values.isEmpty()
private class CannotInlineException : Exception()
/**
* The Inliner performs subroutine inlining on the AST.
*
* It supports both manual inlining (subroutines marked with the 'inline' keyword)
* and automatic inlining for very simple subroutines.
*
* Current restrictions and considerations:
* - Automatic inlining is currently restricted to subroutines with ZERO parameters.
* - Inlining subroutines with parameters is complicated because it requires careful
* parameter substitution to avoid side effects and name collisions.
* - Handling parameterized inlining can easily lead to infinite loops or recursion
* in the optimizer if not done VERY CAREFULLY (e.g. self-referential subroutines).
* - Subroutines must have a single functional statement in their body to be auto-inlined.
* - Multi-return value subroutines are not yet supported for auto-inlining.
*/
class Inliner(private val program: Program, private val options: CompilationOptions): AstWalker() {
class DetermineInlineSubs(val program: Program): IAstVisitor {
inner class DetermineInlineSubs(val program: Program): IAstVisitor {
private val modifications = mutableListOf<AstModification>()
private val visitedSubroutines = java.util.Collections.newSetFromMap(java.util.IdentityHashMap<Subroutine, Boolean>())
init {
visit(program)
@@ -25,35 +40,62 @@ class Inliner(private val program: Program, private val options: CompilationOpti
modifications.clear()
}
private fun isRecursive(sub: Subroutine): Boolean {
var recursive = false
val visitor = object : IAstVisitor {
override fun visit(functionCallExpr: FunctionCallExpression) {
if (!recursive && functionCallExpr.target.targetSubroutine() == sub) recursive = true
if (!recursive) super.visit(functionCallExpr)
}
override fun visit(functionCallStatement: FunctionCallStatement) {
if (!recursive && functionCallStatement.target.targetSubroutine() == sub) recursive = true
if (!recursive) super.visit(functionCallStatement)
}
}
sub.statements.forEach { if (!recursive) it.accept(visitor) }
return recursive
}
override fun visit(subroutine: Subroutine) {
if (!visitedSubroutines.add(subroutine)) return
fun isBodyInlineable(stmt: Return): Boolean {
if (stmt.values.isEmpty())
return true
return stmt.values.all { value ->
when (value) {
is NumericLiteral -> true
is IdentifierReference -> {
makeFullyScoped(value)
true
}
is FunctionCallExpression -> {
if (value.args.size <= 1 && value.args.all {
it is NumericLiteral || it is IdentifierReference
}) {
makeFullyScoped(value)
true
} else {
false
if (isSimpleReturnExpression(value)) {
val walker = object : AstWalker() {
override fun after(identifier: IdentifierReference, parent: Node): Iterable<AstModification> {
makeFullyScoped(identifier)
return noModifications
}
}
else -> false
value.accept(walker, stmt)
true
} else if (value is FunctionCallExpression) {
if (value.args.size <= 1 && value.args.all {
it is NumericLiteral || it is IdentifierReference
}) {
makeFullyScoped(value)
true
} else {
false
}
} else {
false
}
}
}
fun isBodyInlineable(stmt: Assignment): Boolean {
// Never auto-inline a sub that assigns directly to one of its parameters.
// Substituting the call argument into the assignment target would either
// crash (literal argument has no identifier to assign to) or change semantics
// (assigning to the caller's variable instead of the local parameter copy).
val targetName = stmt.target.identifier?.nameInSource?.lastOrNull()
if (targetName != null && subroutine.parameters.any { it.name == targetName })
return false
return if (stmt.value.isSimple) {
val targetInline =
if (stmt.target.identifier != null) {
@@ -106,28 +148,16 @@ class Inliner(private val program: Program, private val options: CompilationOpti
stmt.target is NumericLiteral
}
if (!subroutine.isAsmSubroutine) {
// NOTE: We allow subroutines with ANY number of parameters to be considered for inlining.
// For void calls where the body doesn't use the parameters, inlining is trivial -
// we just remove the call entirely (no parameter substitution needed).
// For calls that return values or use parameters in the body, we need simple arguments
// (NumericLiteral or IdentifierReference) for parameter substitution to work.
// See canInlineAtCallSite() for the argument simplicity check.
val containsSubsOrVariables = subroutine.statements.any {
it is Subroutine || (it is VarDecl && it.origin != VarDeclOrigin.SUBROUTINEPARAM)
}
if (!subroutine.isAsmSubroutine && subroutine.parameters.size <= 1 && subroutine.parameters.all { it.registerOrPair == null }) {
val containsSubsOrVariables = subroutine.statements.any { it is Subroutine || (it is VarDecl && it.origin != VarDeclOrigin.SUBROUTINEPARAM) }
if (!containsSubsOrVariables) {
// For subroutines with parameters, there will be a VarDecl(SUBROUTINEPARAM) per parameter
// plus the body statement(s)
val expectedMaxStmts = subroutine.parameters.size + 1 // param VarDecls + 1 body statement
val hasOnlyBodyStatements = subroutine.statements.size == expectedMaxStmts ||
(subroutine.statements.size == expectedMaxStmts + 1 && subroutine.statements.lastOrNull()?.let(::isEmptyReturn) == true)
val functionalStatements = subroutine.statements.filter { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
val hasOnlyBodyStatements = functionalStatements.size == 1 ||
(functionalStatements.size == 2 && functionalStatements.lastOrNull()?.let(::isEmptyReturn) == true)
if (hasOnlyBodyStatements) {
if (subroutine !== program.entrypoint) {
// Find the first non-parameter statement (the actual body)
val bodyStmt = subroutine.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
val bodyStmt = functionalStatements.firstOrNull()
val isAutoInlineable =
when (val stmt = bodyStmt) {
is Return -> isBodyInlineable(stmt)
@@ -136,16 +166,15 @@ class Inliner(private val program: Program, private val options: CompilationOpti
is Jump -> isBodyInlineable(stmt)
else -> false
}
if (isAutoInlineable)
if (isAutoInlineable && !isRecursive(subroutine))
subroutine.inline = true
}
}
if (subroutine.inline && subroutine.statements.size > 1) {
if (subroutine.inline && functionalStatements.size > 1) {
// Remove trailing return if it's empty and there's another body statement
val bodyStmts = subroutine.statements.filter { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
if (bodyStmts.size > 1 && subroutine.statements.lastOrNull()?.let(::isEmptyReturn) == true) {
subroutine.statements.removeLast()
if (functionalStatements.lastOrNull()?.let(::isEmptyReturn) == true) {
subroutine.statements.remove(functionalStatements.last())
}
}
}
@@ -154,7 +183,9 @@ class Inliner(private val program: Program, private val options: CompilationOpti
}
private fun makeFullyScoped(identifier: IdentifierReference) {
if (identifier.nameInSource.singleOrNull() in program.builtinFunctions.names) return
identifier.targetStatement()?.let { target ->
if (target is VarDecl && target.origin == VarDeclOrigin.SUBROUTINEPARAM) return@let
val scoped = (target as INamedStatement).scopedName
val scopedIdent = IdentifierReference(scoped, identifier.position)
modifications += AstReplaceNode(identifier, scopedIdent, identifier.parent)
@@ -223,18 +254,32 @@ class Inliner(private val program: Program, private val options: CompilationOpti
private fun canInlineAtCallSiteWithReason(sub: Subroutine, fcall: IFunctionCall): Pair<Boolean, String> {
if (!sub.inline)
return false to "subroutine not marked as inlineable"
// Check parameter argument complexity
if (sub.parameters.isNotEmpty()) {
// Only allow inlining for void statement calls, not expression context
if (fcall !is FunctionCallStatement || !fcall.void)
return false to "expression context calls with parameters not supported yet"
// Only inline if all arguments are simple (literals or identifiers)
if (!fcall.args.all { it is NumericLiteral || it is IdentifierReference }) {
val complexArgs = fcall.args.filterNot { it is NumericLiteral || it is IdentifierReference }
return false to "complex arguments (${complexArgs.size}x): ${complexArgs.map { it::class.simpleName }.joinToString(", ")}"
if (sub.isAsmSubroutine && sub.parameters.isNotEmpty()) {
return false to "asmsub with parameters cannot be inlined"
}
if (sub.parameters.size > 1) {
return false to "subroutines with more than 1 parameter cannot be inlined in this compiler version"
}
if (sub.parameters.any { it.registerOrPair != null }) {
return false to "subroutines with register-mapped parameters cannot be inlined"
}
if (sub.parameters.size == 1) {
val arg = fcall.args.first()
if (!arg.isSimple) {
return false to "argument is too complex to inline (might have side effects)"
}
// Refuse to inline a sub that assigns directly to its parameter.
// Substituting the argument into the target would crash for a literal
// argument, or wrongly modify the caller's variable for a variable argument
// (parameters are passed by value, so the sub only modifies its local copy).
val paramName = sub.parameters[0].name
val bodyStmt = sub.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
if ((bodyStmt as? Assignment)?.target?.identifier?.nameInSource?.lastOrNull() == paramName)
return false to "cannot inline subroutine that assigns to its parameter"
}
if (options.compTarget.name != VMTarget.NAME) {
@@ -279,13 +324,12 @@ class Inliner(private val program: Program, private val options: CompilationOpti
fun inlineFunctionBody(toInline: Return): Iterable<AstModification> {
// call site is an expression, so we have to have a Return here in the inlined sub to provide the values
// note that we don't have to process any args, because we are currently only inlining parameterless subroutines.
return if(toInline.values.size==1 && functionCallExpr!==toInline.values[0]) {
// println(">>> INLINER: INLINED expression '${sub.name}' at ${functionCallExpr.position} (return value substituted)")
sub.hasBeenInlined =true
listOf(AstReplaceNode(functionCallExpr, toInline.values[0].copy(), parent))
}
else
return if (toInline.values.size == 1 && functionCallExpr !== toInline.values[0]) {
val substitutedReturn = substituteParameters(sub, functionCallExpr, toInline.values[0]) as? Expression
?: return noModifications
sub.hasBeenInlined = true
listOf(AstReplaceNode(functionCallExpr, substitutedReturn, parent))
} else
noModifications
}
@@ -303,17 +347,18 @@ class Inliner(private val program: Program, private val options: CompilationOpti
private fun possiblyInlineFunctioncallStmt(sub: Subroutine, origNode: Node, parent: Node): Iterable<AstModification> {
fun possiblyShortCircuitFunctionCall(toInline: Return): Iterable<AstModification> {
val functionCalls = toInline.values.filterIsInstance<FunctionCallExpression>()
// Substitute parameters in the return values first
val substitutedReturn = substituteParameters(sub, origNode as IFunctionCall, toInline) as? Return
?: return noModifications
val functionCalls = substitutedReturn.values.filterIsInstance<FunctionCallExpression>()
if (functionCalls.isEmpty()) {
// No function calls in the return values - the void call has no side effects and can be removed.
// println(">>> INLINER: INLINED void call to '${sub.name}' at ${functionCallStatement.position} (removed, no side effects)")
sub.hasBeenInlined = true
return listOf(AstRemove(origNode as Statement, parent as IStatementContainer))
}
// There are function calls in the return values - convert each to a void statement.
// println(">>> INLINER: INLINED void call to '${sub.name}' at ${functionCallStatement.position} (converted ${functionCalls.size} inner call(s) to void)")
sub.hasBeenInlined = true
val voidStatements: MutableList<Statement> = functionCalls.map { fcall ->
@@ -331,22 +376,17 @@ class Inliner(private val program: Program, private val options: CompilationOpti
}
fun possiblyInlineFunctionBody(toInline: Statement): Iterable<AstModification> {
// For void calls with parameters, we already verified in canInlineAtCallSiteWithReason()
// that the parameter is unused in the body (so no substitution needed).
// For expression context calls with parameters, those are rejected in canInlineAtCallSiteWithReason()
// so we never reach here.
// Just copy the statement - parameters are unused so no substitution needed.
val inlinedStatement = toInline.copy()
// println(">>> INLINER: INLINED '${sub.name}' at ${functionCallStatement.position} (body inserted, ${sub.parameters.size} param(s))")
return if(origNode !== toInline) {
val inlinedStatement = substituteParameters(sub, origNode as IFunctionCall, toInline) as? Statement
?: return noModifications
return if (origNode !== toInline) {
sub.hasBeenInlined = true
listOf(AstReplaceNode(origNode, inlinedStatement, parent))
} else
noModifications
}
if(sub.inline && sub.parameters.size >= 0) {
if(sub.inline) {
// Get the first non-parameter statement (skip VarDecl with SUBROUTINEPARAM origin)
val bodyStmt = sub.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
val hasOnlyBodyAndReturn = bodyStmt != null && (
@@ -361,7 +401,6 @@ class Inliner(private val program: Program, private val options: CompilationOpti
sub.hasBeenInlined=true
listOf(AstReplaceNode(origNode, bodyStmt.copy(), parent))
} else {
// note that we don't have to process any args, because we only inline parameterless subroutines.
when (val toInline = bodyStmt) {
is Return -> possiblyShortCircuitFunctionCall(toInline)
else -> possiblyInlineFunctionBody(toInline)
@@ -371,6 +410,19 @@ class Inliner(private val program: Program, private val options: CompilationOpti
return noModifications
}
private fun isSimpleReturnExpression(expr: Expression): Boolean {
return when (expr) {
is NumericLiteral -> true
is IdentifierReference -> true
is PrefixExpression -> isSimpleReturnExpression(expr.expression)
is BinaryExpression ->
(expr.left is NumericLiteral || expr.left is IdentifierReference) &&
(expr.right is NumericLiteral || expr.right is IdentifierReference)
else -> false
}
}
/**
* Substitutes parameter references in a statement with argument values.
*/
@@ -382,22 +434,26 @@ class Inliner(private val program: Program, private val options: CompilationOpti
val sub = fcall.target.targetStatement(program.builtinFunctions) as? Subroutine
?: return noModifications
// Only handle parameterless subroutines with simple returns
if (!sub.inline || sub.parameters.isNotEmpty() || !canInlineAtCallSite(sub, fcall))
// Only handle inlining if sub is marked as such and call site is okay
if (!sub.inline || !canInlineAtCallSite(sub, fcall))
return noModifications
val toInline = sub.statements.firstOrNull() as? Return ?: return noModifications
val toInline = sub.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM } as? Return ?: return noModifications
// Check return count matches target count
if (toInline.values.size != multiTargets.size)
return noModifications
// Only allow simple return values (literals or identifiers, no function calls)
if (!toInline.values.all { it is NumericLiteral || it is IdentifierReference })
// Only allow simple return values to prevent code bloat on 6502
if (!toInline.values.all { isSimpleReturnExpression(it) })
return noModifications
// Substitute parameters in all return values
val substitutedReturn = substituteParameters(sub, fcall, toInline) as? Return
?: return noModifications
// Create multiple single assignments, skipping void targets (they discard the value)
val newAssignments = multiTargets.zip(toInline.values)
val newAssignments = multiTargets.zip(substitutedReturn.values)
.filter { (target, _) -> !target.void }
.map { (target, value) ->
Assignment(
@@ -420,4 +476,108 @@ class Inliner(private val program: Program, private val options: CompilationOpti
val scope = AnonymousScope(newAssignments.toMutableList(), assignment.position)
return listOf(AstReplaceNode(assignment, scope, parent))
}
private fun substituteParameters(sub: Subroutine, fcall: IFunctionCall, node: Node): Node? {
val paramVarDecls = sub.parameters.map { param ->
sub.statements.filterIsInstance<VarDecl>().find { it.origin == VarDeclOrigin.SUBROUTINEPARAM && it.name == param.name }
?: throw FatalAstException("parameter ${param.name} not found in subroutine ${sub.name}")
}
val paramMap: Map<VarDecl, Expression> = paramVarDecls.zip(fcall.args).associate { it.first to it.second }
fun substitute(n: Node): Node {
if (n is IdentifierReference) {
val target = n.targetStatement(program.builtinFunctions)
if (target is VarDecl) {
val arg = paramMap[target]
if (arg != null) return arg.copy()
}
}
return when (n) {
is BinaryExpression -> BinaryExpression(substitute(n.left) as Expression, n.operator, substitute(n.right) as Expression, n.position)
is PrefixExpression -> PrefixExpression(n.operator, substitute(n.expression) as Expression, n.position)
is DirectMemoryRead -> DirectMemoryRead(substitute(n.addressExpression) as Expression, n.position)
is ArrayIndexedExpression -> ArrayIndexedExpression(
n.plainarrayvar?.let {
val subId = substitute(it)
if (subId !is IdentifierReference) throw CannotInlineException()
subId
},
n.nestedArray?.let { substitute(it) as ArrayIndexedExpression },
n.pointerderef?.let { substitute(it) as PtrDereference },
substitute(n.indexer) as ArrayIndex,
n.position
)
is ArrayIndex -> ArrayIndex(substitute(n.indexExpr) as Expression, n.position)
is Return -> Return(n.values.map { substitute(it) as Expression }.toTypedArray(), n.position)
is Assignment -> {
val newTarget = n.target.copy()
newTarget.identifier = n.target.identifier?.let {
val subId = substitute(it)
if (subId !is IdentifierReference) throw CannotInlineException()
subId
}
newTarget.memoryAddress = n.target.memoryAddress?.let { DirectMemoryWrite(substitute(it.addressExpression) as Expression, it.position) }
newTarget.arrayindexed = n.target.arrayindexed?.let { substitute(it) as ArrayIndexedExpression }
newTarget.pointerDereference = n.target.pointerDereference?.let { substitute(it) as PtrDereference }
newTarget.arrayIndexedDereference = n.target.arrayIndexedDereference?.let {
ArrayIndexedPtrDereference(
it.chain.map { (name, index) -> name to index?.let { substitute(it) as ArrayIndex } },
it.derefLast,
it.position
)
}
Assignment(
newTarget,
substitute(n.value) as Expression,
n.origin,
n.position
)
}
is FunctionCallExpression -> FunctionCallExpression(
substitute(n.target).let {
if (it !is IdentifierReference) throw CannotInlineException()
it
},
n.args.map { substitute(it) as Expression }.toMutableList(),
n.position
)
is FunctionCallStatement -> FunctionCallStatement(
substitute(n.target).let {
if (it !is IdentifierReference) throw CannotInlineException()
it
},
n.args.map { substitute(it) as Expression }.toMutableList(),
n.void,
n.position
)
is AnonymousScope -> AnonymousScope(
n.statements.map { substitute(it) as Statement }.toMutableList(),
n.position
)
is Jump -> Jump(substitute(n.target) as Expression, n.position)
is TypecastExpression -> TypecastExpression(substitute(n.expression) as Expression, n.type, n.implicit, n.position)
is AddressOf -> AddressOf(
n.identifier?.let {
val subId = substitute(it)
if (subId !is IdentifierReference) throw CannotInlineException()
subId
},
n.arrayIndex?.let { substitute(it) as ArrayIndex },
n.dereference?.let { substitute(it) as PtrDereference },
n.msb,
n.typed,
n.position
)
else -> n.copy()
}
}
return try {
substitute(node)
} catch (_: CannotInlineException) {
null
}
}
}
@@ -313,30 +313,21 @@ class StatementOptimizer(private val program: Program,
val rNum = (rExpr.right as? NumericLiteral)?.number
if(rNum!=null) {
if (op1 == "+" || op1 == "-") {
if (op2 == "+") {
// A = A +/- B + N ---> A = A +/- B ; A = A + N
val expr2 = BinaryExpression(binExpr.left, binExpr.operator, rExpr.left, binExpr.position)
val addConstant = Assignment(
assignment.target.copy(),
BinaryExpression(binExpr.left.copy(), "+", rExpr.right, rExpr.position),
AssignmentOrigin.OPTIMIZER, assignment.position
)
return listOf(
AstReplaceNode(binExpr, expr2, binExpr.parent),
AstInsert.after(assignment, addConstant, parent as IStatementContainer))
} else if (op2 == "-") {
// A = A +/- B - N ---> A = A +/- B ; A = A - N
val expr2 = BinaryExpression(binExpr.left, binExpr.operator, rExpr.left, binExpr.position)
val subConstant = Assignment(
assignment.target.copy(),
BinaryExpression(binExpr.left.copy(), "-", rExpr.right, rExpr.position),
AssignmentOrigin.OPTIMIZER, assignment.position
)
return listOf(
AstReplaceNode(binExpr, expr2, binExpr.parent),
AstInsert.after(assignment, subConstant, parent as IStatementContainer))
}
if ((op1 == "+" || op1 == "-") && (op2 == "+" || op2 == "-")) {
// A = A op1 (B op2 N) ---> A = A op1 B ; A = A secondOp N
// only valid for additive operators (op2 must be + or -).
// secondOp keeps the combined expression equivalent:
// A+(B+N)=A+N, A+(B-N)=A-N, A-(B+N)=A-N, A-(B-N)=A+N
val secondOp = if (op1 == "+") op2 else if (op2 == "+") "-" else "+"
val expr2 = BinaryExpression(binExpr.left, binExpr.operator, rExpr.left, binExpr.position)
val opConstant = Assignment(
assignment.target.copy(),
BinaryExpression(binExpr.left.copy(), secondOp, rExpr.right, binExpr.position),
AssignmentOrigin.OPTIMIZER, assignment.position
)
return listOf(
AstReplaceNode(binExpr, expr2, binExpr.parent),
AstInsert.after(assignment, opConstant, parent as IStatementContainer))
}
}
}
@@ -366,7 +357,7 @@ class StatementOptimizer(private val program: Program,
}
override fun after(assignment: Assignment, parent: Node): Iterable<AstModification> {
if(assignment.target isSameAs assignment.value) {
if(assignment.target isSameAs assignment.value && !assignment.target.hasSideEffects(options.compTarget) && !assignment.value.hasSideEffects(options.compTarget)) {
// remove assignment to self
return listOf(AstRemove(assignment, parent as IStatementContainer))
}
@@ -380,7 +371,7 @@ class StatementOptimizer(private val program: Program,
if(bexpr!=null) {
val rightCv = bexpr.right.constValue(program)?.number
if (rightCv != null && assignment.target isSameAs bexpr.left) {
if (rightCv != null && assignment.target isSameAs bexpr.left && !assignment.target.hasSideEffects(options.compTarget) && !bexpr.left.hasSideEffects(options.compTarget)) {
// assignments of the form: X = X <operator> <expr>
// remove assignments that have no effect (such as X=X+0)
// optimize/rewrite some other expressions
@@ -255,7 +255,7 @@ class UnusedCodeRemover(private val program: Program,
}
override fun after(assignment: Assignment, parent: Node): Iterable<AstModification> {
if(assignment.target isSameAs assignment.value)
if(assignment.target isSameAs assignment.value && !assignment.target.hasSideEffects(options.compTarget) && !assignment.value.hasSideEffects(options.compTarget))
return listOf(AstRemove(assignment, parent as IStatementContainer))
return noModifications
}
+5 -6
View File
@@ -1,8 +1,8 @@
plugins {
id("application")
kotlin("jvm")
id("com.gradleup.shadow") version "9.3.2"
id("com.peterabeles.gversion") version "1.10.3"
id("com.gradleup.shadow") version "9.6.1"
id("com.peterabeles.gversion") version "1.11.0"
}
dependencies {
@@ -12,7 +12,6 @@ dependencies {
implementation(project(":compilerAst"))
implementation(project(":codeGenCpu6502"))
implementation(project(":codeGenIntermediate"))
implementation(project(":codeGenExperimental"))
implementation(project(":intermediate"))
implementation(project(":virtualmachine"))
implementation("org.jetbrains.kotlinx:kotlinx-cli:0.3.6")
@@ -22,8 +21,7 @@ dependencies {
testImplementation(testFixtures(project(":codeCore")))
testImplementation(project(":intermediate"))
testImplementation("io.kotest:kotest-runner-junit5")
testImplementation("io.kotest:kotest-framework-datatest")
testImplementation("com.github.irmen:ksim65:v2.0")
testImplementation("com.github.irmen:ksim65:v2.1")
}
// Exclude transitive antlr4 dependency (we only need it in parser module)
@@ -43,6 +41,7 @@ application {
tasks.shadowJar {
archiveBaseName.set("prog8c")
archiveVersion.set(version.toString())
duplicatesStrategy = DuplicatesStrategy.WARN
// minimize()
}
@@ -52,7 +51,7 @@ gversion {
className = "Version"
language = "kotlin"
debug = false
annotate = true
annotate = ""
}
tasks.build {
-29
View File
@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
<orderEntry type="library" name="jetbrains.kotlinx.cli.jvm" level="project" />
<orderEntry type="library" name="michael.bull.kotlin.result.jvm" level="project" />
<orderEntry type="module" module-name="codeCore" />
<orderEntry type="module" module-name="simpleAst" />
<orderEntry type="module" module-name="compilerAst" />
<orderEntry type="module" module-name="codeOptimizers" />
<orderEntry type="module" module-name="codeGenCpu6502" />
<orderEntry type="module" module-name="codeGenExperimental" />
<orderEntry type="module" module-name="codeGenIntermediate" />
<orderEntry type="module" module-name="virtualmachine" />
<orderEntry type="module" module-name="intermediate" />
<orderEntry type="library" scope="TEST" name="io.kotest.runner.junit5.jvm" level="project" />
<orderEntry type="library" scope="TEST" name="io.kotest.framework.datatest" level="project" />
<orderEntry type="library" scope="TEST" name="github.irmen.ksim65" level="project" />
</component>
</module>
+14 -10
View File
@@ -25,15 +25,16 @@ smallringbuffer {
}
sub free() -> ubyte {
return 255-fill
return 255 - fill
}
sub isfull() -> bool {
return fill>=254
; returns true if less than 2 bytes of space remaining (to allow putw)
return fill >= 254
}
sub isempty() -> bool {
return fill<=1
return fill == 0
}
sub put(ubyte value) {
@@ -92,11 +93,12 @@ smallstack {
}
sub isfull() -> bool {
return sp==0
; returns true if less than 2 bytes of space remaining (to allow push_w)
return sp <= 1
}
sub isempty() -> bool {
return sp==255
return sp == 255
}
sub push_b(ubyte value) {
@@ -150,11 +152,12 @@ stack {
}
sub isfull() -> bool {
return sp==0
; returns true if less than 2 bytes of space remaining (to allow push_w)
return sp == 0 or sp > 8191
}
sub isempty() -> bool {
return sp==8191
return sp == 8191
}
sub push_b(ubyte value) {
@@ -207,15 +210,16 @@ ringbuffer {
}
sub free() -> uword {
return 8191-fill
return 8192-fill
}
sub isempty() -> bool {
return fill==0
return fill == 0
}
sub isfull() -> bool {
return fill>=8191
; returns true if less than 2 bytes of space remaining (to allow putw)
return fill >= 8191
}
sub put(ubyte value) {
+2
View File
@@ -534,6 +534,8 @@ sys {
const word MAX_WORD = 32767
const uword MIN_UWORD = 0
const uword MAX_UWORD = 65535
const long MIN_LONG = -2147483648
const long MAX_LONG = 2147483647
; MIN_FLOAT and MAX_FLOAT are defined in the floats module if imported
+1 -1
View File
@@ -14,7 +14,7 @@ txt {
const ubyte DEFAULT_WIDTH = 40
const ubyte DEFAULT_HEIGHT = 25
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly ofcourse. Note: takes a PETSCII encoded character.
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly of course. Note: takes a PETSCII encoded character.
sub bell() {
+40
View File
@@ -539,3 +539,43 @@ swap_floats .proc
bpl -
rts
.pend
; Wrappers for the ROM's bare T-variant math routines (FADDT, FMULTT, FDIVT, FPWRT).
; These ROM entries skip CONUPK which normally sets the Z flag and arisgn byte.
; Callers that have not just come through CONUPK must set these up themselves
; or the result can be wrong (heisenbug with some values).
; The corresponding entries on the X16 (via its KERNAL jump table) do this correctly,
; so these wrappers are only needed on C64 and PET32.
FADDT_NZ .proc
lda FAC_ADDR+5 ; facsgn
eor FAC_ADDR+$0D ; argsgn
sta FAC_ADDR+$0E ; arisgn
lda FAC_ADDR+0 ; facexp to set Z flag
jmp FADDT
.pend
FMULTT_NZ .proc
lda FAC_ADDR+5 ; facsgn
eor FAC_ADDR+$0D ; argsgn
sta FAC_ADDR+$0E ; arisgn
lda FAC_ADDR+0 ; facexp to set Z flag
jmp FMULTT
.pend
FDIVT_NZ .proc
lda FAC_ADDR+5 ; facsgn
eor FAC_ADDR+$0D ; argsgn
sta FAC_ADDR+$0E ; arisgn
lda FAC_ADDR+0 ; facexp to set Z flag
jmp FDIVT
.pend
FPWRT_NZ .proc
lda FAC_ADDR+5 ; facsgn
eor FAC_ADDR+$0D ; argsgn
sta FAC_ADDR+$0E ; arisgn
lda FAC_ADDR+0 ; facexp to set Z flag
jmp FPWRT
.pend
+5 -5
View File
@@ -51,15 +51,15 @@ extsub $bae2 = MUL10() clobbers(A,X,Y) ; fac1 *= 10
extsub $bafe = DIV10() clobbers(A,X,Y) ; fac1 /= 10 , CAUTION: result is always positive! You have to fix sign manually!
extsub $bc5b = FCOMP(uword mflpt @ AY) clobbers(X,Y) -> ubyte @ A ; A = compare fac1 to mflpt in A/Y, 0=equal 1=fac1 is greater, 255=fac1 is less than
extsub $b86a = FADDT() clobbers(A,X,Y) ; fac1 += fac2
extsub $b86a = FADDT() clobbers(A,X,Y) ; fac1 += fac2 ; WARNING: bare entry, needs Z flag=facexp + arisgn; use FADDT_NZ wrapper
extsub $b867 = FADD(uword mflpt @ AY) clobbers(A,X,Y) ; fac1 += mflpt value from A/Y
extsub $b853 = FSUBT() clobbers(A,X,Y) ; fac1 = fac2-fac1 mind the order of the operands
extsub $b853 = FSUBT() clobbers(A,X,Y) ; fac1 = fac2-fac1 mind the order of the operands ; (FSUBT does NOT need the Z flag hack, it already does the setup internally)
extsub $b850 = FSUB(uword mflpt @ AY) clobbers(A,X,Y) ; fac1 = mflpt from A/Y - fac1
extsub $ba2b = FMULTT() clobbers(A,X,Y) ; fac1 *= fac2
extsub $ba2b = FMULTT() clobbers(A,X,Y) ; fac1 *= fac2 ; WARNING: bare entry, needs Z flag=facexp + arisgn; use FMULTT_NZ wrapper
extsub $ba28 = FMULT(uword mflpt @ AY) clobbers(A,X,Y) ; fac1 *= mflpt value from A/Y
extsub $bb12 = FDIVT() clobbers(A,X,Y) ; fac1 = fac2/fac1 mind the order of the operands
extsub $bb12 = FDIVT() clobbers(A,X,Y) ; fac1 = fac2/fac1 mind the order of the operands ; WARNING: bare entry, needs Z flag=facexp + arisgn; use FDIVT_NZ wrapper
extsub $bb0f = FDIV(uword mflpt @ AY) clobbers(A,X,Y) ; fac1 = mflpt in A/Y / fac1
extsub $bf7b = FPWRT() clobbers(A,X,Y) ; fac1 = fac2 ** fac1
extsub $bf7b = FPWRT() clobbers(A,X,Y) ; fac1 = fac2 ** fac1 ; WARNING: bare entry, needs Z flag=facexp + arisgn; use FPWRT_NZ wrapper
extsub $bf78 = FPWR(uword mflpt @ AY) clobbers(A,X,Y) ; fac1 = fac2 ** mflpt from A/Y
extsub $bd7e = FINLOG(byte value @A) clobbers (A, X, Y) ; fac1 += signed byte in A
+19 -4
View File
@@ -349,16 +349,28 @@ hline_filled_right .byte 0, %10000000, %11000000, %11100000, %11110000, %1111
sub disc(uword xcenter, ubyte ycenter, ubyte radius) {
; Warning: NO BOUNDS CHECKS. Make sure circle fits in the screen.
; Midpoint algorithm, filled.
; Overdraws horizontal lines unfortunately.
if radius==0
return
ubyte @zp yy = 0
word decisionOver2 = (1 as word)-radius
ubyte pendingRadius
ubyte pendingWidth
bool hasPending = false
while radius>=yy {
horizontal_line(xcenter-radius, ycenter+yy, radius*2+1)
horizontal_line(xcenter-radius, ycenter-yy, radius*2+1)
horizontal_line(xcenter-yy, ycenter+radius, yy*2+1)
horizontal_line(xcenter-yy, ycenter-radius, yy*2+1)
if hasPending and pendingRadius != radius {
if pendingRadius != pendingWidth {
horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*2+1)
horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*2+1)
}
hasPending = false
}
if not hasPending {
pendingRadius = radius
hasPending = true
}
pendingWidth = yy
yy++
if decisionOver2>=0 {
radius--
@@ -367,6 +379,10 @@ hline_filled_right .byte 0, %10000000, %11000000, %11100000, %11110000, %1111
decisionOver2 += yy*$0002
decisionOver2++
}
if hasPending and pendingRadius != pendingWidth {
horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*2+1)
horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*2+1)
}
}
@@ -446,4 +462,3 @@ _y_lookup_hi .byte >_plot_y_values
}
+2
View File
@@ -509,6 +509,8 @@ sys {
const word MAX_WORD = 32767
const uword MIN_UWORD = 0
const uword MAX_UWORD = 65535
const long MIN_LONG = -2147483648
const long MAX_LONG = 2147483647
; MIN_FLOAT and MAX_FLOAT are defined in the floats module if imported
+1 -1
View File
@@ -13,7 +13,7 @@ txt {
const ubyte DEFAULT_WIDTH = 40
const ubyte DEFAULT_HEIGHT = 25
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly ofcourse. Note: takes a PETSCII encoded character.
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly of course. Note: takes a PETSCII encoded character.
sub bell() {
+13 -9
View File
@@ -27,11 +27,12 @@ smallringbuffer {
}
sub isfull() -> bool {
return fill>=254
; returns true if less than 2 bytes of space remaining (to allow putw)
return fill >= 254
}
sub isempty() -> bool {
return fill<=1
return fill == 0
}
sub put(ubyte value) {
@@ -90,11 +91,12 @@ smallstack {
}
sub isfull() -> bool {
return sp==0
; returns true if less than 2 bytes of space remaining (to allow push_w)
return sp <= 1
}
sub isempty() -> bool {
return sp==255
return sp == 255
}
sub push_b(ubyte value) {
@@ -150,11 +152,12 @@ stack {
}
sub isfull() -> bool {
return sp<=$a001
; returns true if less than 2 bytes of space remaining (to allow push_w)
return sp <= $a000
}
sub isempty() -> bool {
return sp==$bfff
return sp == $bfff
}
sub push_b(ubyte value) {
@@ -227,15 +230,16 @@ ringbuffer {
}
sub free() -> uword {
return $1fff-fill
return 8192-fill
}
sub isempty() -> bool {
return fill<=1
return fill == 0
}
sub isfull() -> bool {
return fill>=8191
; returns true if less than 2 bytes of space remaining (to allow putw)
return fill >= 8191 or head == $bfff
}
sub put(ubyte value) {
Loaded 100 of 334 files, more files were not shown because too many files have changed in this diff. Show more