Compare commits

...
100 Commits
Author SHA1 Message Date
Irmen de Jong 5702e5aa2a 14.0-beta2 release 2026-10-10 13:25:11 +02:00
Irmen de Jong 592637856f m68k: cleaner recursive defer errors 2026-10-10 02:02:25 +02:00
Irmen de Jong 233ca21492 m68k: frameable inline asm, word-sized sys.exit, and sys.die alerts 2026-10-10 00:19:22 +02:00
Irmen de Jong 1a84616363 update skills 2026-10-09 20:06:56 +02:00
Irmen de Jong d32b70bb75 m68k: tear down frames on external conditional branches 2026-10-08 20:50:39 +02:00
Irmen de Jong 73241b0261 m68k: tear down frames before absolute jumps 2026-10-08 20:41:35 +02:00
Irmen de Jong e515dee561 m68k: suggest @shared for locals referenced from inline assembly 2026-10-08 19:08:27 +02:00
Irmen de Jong 3d78415088 better vm error for %asm 2026-10-08 04:37:19 +02:00
Irmen de Jong 0189903a63 m68k: pin @shared variables to static storage 2026-10-08 04:17:40 +02:00
Irmen de Jong 9c9552174e m68k: stackful coroutines fixed with per-task stacks, new %option noframe 2026-10-08 01:59:50 +02:00
Irmen de Jong be61d37fd1 update gradle wrapper 2026-10-08 00:30:07 +02:00
Irmen de Jong 70bb199e12 m68k: pop frame on jumps leaving subroutine; reject dispatch to subs with parameters 2026-10-07 23:46:48 +02:00
Irmen de Jong 09737187ad fix %jmptable crashes on expression and unqualified entries 2026-10-07 22:25:23 +02:00
Irmen de Jong de39533fba qemu68k: new recursive maze solver example 2026-10-07 19:06:16 +02:00
Irmen de Jong 8bc2b177a0 m68k: made textelite native recursion 2026-10-07 19:06:16 +02:00
Irmen de Jong 09eba995ae m68k: storing or returning the address of a variable in call stack frame is now an error 2026-10-07 19:06:16 +02:00
Irmen de Jong b739d7b663 m68k: print call arg names in asm comments 2026-10-07 19:06:16 +02:00
Irmen de Jong 5e709fca7e m68k: stack-frame zero-clear coalescing 2026-10-07 19:06:16 +02:00
Irmen de Jong cc8df33418 m68k: plan for optimized stack frame zeroing 2026-10-07 19:06:16 +02:00
Irmen de Jong 125af552ff amiga: added recursive fractal-tree example 2026-10-07 19:06:16 +02:00
Irmen de Jong 29254e892b m68k: no longer print recursion warnings 2026-10-07 19:06:16 +02:00
Irmen de Jong a1a48405b0 m68k: fix a5 frame pointer register clobbering 2026-10-07 19:06:16 +02:00
Irmen de Jong acc8705ea8 bugfix for m68k stack memory model 2026-10-07 19:06:16 +02:00
Irmen de Jong bbff472457 doc cleanup 2026-10-07 19:06:16 +02:00
Irmen de Jong 353b0c84b7 slice 5 for m68k stack memory model
(slice 4 was skipped)
2026-10-07 19:06:16 +02:00
Irmen de Jong 329b3df4a3 slice 3 for m68k stack memory model 2026-10-07 19:06:16 +02:00
Irmen de Jong fe90ca572d slice 2 for m68k stack mememory model 2026-10-07 19:06:16 +02:00
Irmen de Jong 26bbd6c1fe vertical slice prototype for m68k stack mememory model 2026-10-07 19:06:16 +02:00
Irmen de Jong 822cfd4f35 tweak textelite 2026-10-07 02:24:17 +02:00
Irmen de Jong 0807d07ee5 cleanups 2026-10-07 02:00:31 +02:00
Irmen de Jong 2e3aa76a2f m68k: improve subroutine inlining 2026-10-07 01:17:14 +02:00
Irmen de Jong 3beb565c84 m68k targets: add sys.stack_size() function that gives remaining stack size in bytes 2026-10-07 00:39:11 +02:00
Irmen de Jong b1642f4f7a m68k: fix some subtle memset/memclear errors 2026-10-06 22:28:40 +02:00
Irmen de Jong 1f8b42fae5 make user docs and some code comments less convoluted 2026-10-05 20:00:32 +02:00
Irmen de Jong 13828ea932 fix defer stack-save ordering for multi-value returns 2026-10-04 07:55:51 +02:00
Irmen de Jong e550143ed0 amiga: added dos.read_file helper routine to read complete files 2026-10-04 04:46:33 +02:00
Irmen de Jong 47e6066a36 fix sizeof for large structs 2026-10-04 04:44:08 +02:00
Irmen de Jong 266f4ab380 amiga: fixed lowlevel-timer example 2026-10-04 02:53:35 +02:00
Irmen de Jong e5954542dd mention stack alignment rule in m68k skill 2026-10-04 02:31:23 +02:00
Irmen de Jong 8c2bc15b41 Fix IR peephole crash on lsigb.l + ext.b with mismatched register types
regression was introduced in commit 2ce7a63c1e
2026-10-03 14:01:56 +02:00
Irmen de Jong c9a7dc05d9 better error message 2026-10-03 02:08:57 +02:00
Irmen de Jong 9d023c9568 fix .p8ir file system case (in)sensitivity fragility 2026-10-02 23:41:42 +02:00
Irmen de Jong a1ce45e409 doc updates 2026-10-02 23:24:07 +02:00
Irmen de Jong 09d469714b whole enum aliasing (by aliasing each member) 2026-10-02 22:53:25 +02:00
Irmen de Jong 38db571f20 implemented -o / --output to specify specific output file 2026-10-02 22:33:43 +02:00
Irmen de Jong 5f3b587af1 docs
outputfile todo
2026-10-02 21:28:18 +02:00
Irmen de Jong e74c34a5ab fix builtins rejecting a cx16 virtual register as their result register 2026-10-01 02:54:53 +02:00
Irmen de Jong 2b156ec652 amiga: add lowlevel.library timer example 2026-09-30 22:56:29 +02:00
Irmen de Jong e38f71fd2d keep routines referenced from struct instance initializers, so that unusedcoderemover doesn't strip them 2026-09-30 21:33:26 +02:00
Irmen de Jong 6c107379df fix broken struct-array initializer prefixing for address-of arguments
fixes #246
2026-09-30 21:15:32 +02:00
Irmen de Jong 99f29cb9b9 fix broken min/max rewrite when the target aliases an operand.
fixes #247
2026-09-30 21:15:32 +02:00
Irmen de Jong d6ea0782e2 amiga: added lowlevel.library support 2026-09-30 19:23:32 +02:00
Irmen de Jong bdee93dc8f eliminate redundant lea for repeated indexed array access 2026-09-28 21:51:03 +02:00
Irmen de Jong 3d675a460e don't fold signed division by a power of two into a shift 2026-09-28 21:22:01 +02:00
Irmen de Jong bd1747926c slightly optimize long multiplication 2026-09-28 02:29:25 +02:00
Irmen de Jong cff7732189 fix unsound index-reload peephole and extend it to two indexed accesses 2026-09-28 00:48:00 +02:00
Irmen de Jong fa966086e9 corrections 2026-09-28 00:39:57 +02:00
Irmen de Jong ec4b47ac0d support custom m68k targets: select the codegen by CpuType and allow a per-target link.ld 2026-09-27 11:50:33 +02:00
Irmen de Jong 5222562493 move sqr range check into prog8_math.square instead of inlining it at each call site 2026-09-27 11:49:49 +02:00
Irmen de Jong c8cda7947a 6502 optimization for uword * ubyte 2026-09-26 05:15:41 +02:00
Irmen de Jong ffe855f295 x*x correctness fix 2026-09-26 04:57:40 +02:00
Irmen de Jong 8a66dabf6d adjust benchmark weights and bump benchmark version number 2026-09-26 04:49:28 +02:00
Irmen de Jong b8c975704a 6502 byte shift optimization for const multiplications 2026-09-26 03:25:25 +02:00
Irmen de Jong 2ef0ddd42c 6502 word shift optimization for const multiplications 2026-09-26 02:45:57 +02:00
Irmen de Jong 9a83b7455a possible 6502 codegen optimization opportunities 2026-09-25 23:58:59 +02:00
Irmen de Jong 9fce07565c some romable tests, fix romable callfar crash and update stale VM label address test 2026-09-25 22:40:36 +02:00
Irmen de Jong d703d678b4 fix VM label-valued initializers 2026-09-25 02:22:19 +02:00
Irmen de Jong 90a7c41586 fix IR symbol prefixing 2026-09-25 02:04:43 +02:00
Irmen de Jong b1a9ba929d remove ARRAY_POINTER and make pointer arrays target-independent 2026-09-25 01:18:32 +02:00
Irmen de Jong 75ab3ccfbd amiga1200: added ahi library and demo programs 2026-09-25 00:03:50 +02:00
Irmen de Jong c031737da0 fixup docs about amiga targets (missing amiga1200 mostly) 2026-09-24 22:31:03 +02:00
Irmen de Jong 2f905ef6b6 update reg design 2026-09-24 01:23:33 +02:00
Irmen de Jong 4cbf93da85 fix Status flag extraction clobbers a register return value 2026-09-24 01:00:49 +02:00
Irmen de Jong 976b64185d fix struct size truncation to 256 bytes on targets that allow larger structs (m68k) 2026-09-24 00:22:23 +02:00
Irmen de Jong 2050fcb4ee m68k: emit struct field definitions in the asm 2026-09-23 23:55:57 +02:00
Irmen de Jong 69525ae67f 6502 codegen: emit _size symbol for struct and union memory size 2026-09-23 23:14:11 +02:00
Irmen de Jong 4eaba62f1e 6502 codegen: inline clamp() on word/uword when having compile-time constant bounds 2026-09-23 22:54:57 +02:00
Irmen de Jong bf696bcf5e fix multiple status flag returns in new codegens 2026-09-23 03:10:59 +02:00
Irmen de Jong 34bbb46514 fix a few new6502 codegen bugs 2026-09-23 01:27:40 +02:00
Irmen de Jong f086918e6f add union types: structs where every field is at offset 0, thus saving memory 2026-09-23 00:07:44 +02:00
Irmen de Jong 521f7ffbe6 tweak amiga audio 2026-09-22 22:49:09 +02:00
Irmen de Jong 2d070aaec4 Fix 6502 pointer-field augmented assignment clobbering scratch pointer
Fixes #245
2026-09-21 22:51:11 +02:00
Irmen de Jong 044a4a1789 amiga: added trackdisk device support 2026-09-21 00:13:17 +02:00
Irmen de Jong 6de6a81d4b amiga: added asyncio.library support 2026-09-20 05:12:10 +02:00
Irmen de Jong 815f4109a5 cx16/gfx_lores: optimize text() by setting VERA address once per character 2026-09-19 22:06:05 +02:00
Irmen de Jong a49f080ca7 cx16/gfx_hires: optimize text() by hoisting color dispatch and using local tables 2026-09-19 19:04:20 +02:00
Irmen de Jong ac4ae0440f vera64 restructure 2026-09-19 17:50:30 +02:00
Irmen de Jong 773f1f2292 more vera64 code 2026-09-19 16:25:52 +02:00
Irmen de Jong efee8bde68 added preliminary VERA64 support library for the c64 target
vera64 = a board for the c64 containing a VERA graphics adapter
2026-09-19 00:14:11 +02:00
Irmen de Jong ddfdd9097d fix adpcm private symbols 2026-09-18 22:40:59 +02:00
Irmen de Jong ef64e2f9a4 fix typed pointer arithmetic p2-p1 to yield the number of elements. fixes #244 2026-09-18 21:22:07 +02:00
Irmen de Jong 2f6ab002d8 amiga: adpcmbench, m68k adpcm decoder optimized further, fix wavc output byte order format 2026-09-18 03:46:00 +02:00
Irmen de Jong ba76fbfe85 fix code generation for "return <void function call>" 2026-09-17 22:39:53 +02:00
Irmen de Jong 7f3b2e8aa6 m68k asm versions of adpcm decoder routines, now byte at a time instead of nibble 2026-09-17 21:53:13 +02:00
Irmen de Jong f254fc2b62 m68k asm versions of adpcm decoder routines 2026-09-17 02:53:48 +02:00
Irmen de Jong 457d55b392 fix potential endless loop in wavfile library, also made a correct implementation of it for m68k targets (little/big endianness problem resolved) 2026-09-17 00:04:34 +02:00
Irmen de Jong b21a984dbb even more optimized adpcm decoding routine 2026-09-16 19:04:49 +02:00
Irmen de Jong 7427e6ddad changed adpcm decoder by a table driven one, almost twice as fast now 2026-09-16 05:03:13 +02:00
Irmen de Jong 192e0086f9 update plans 2026-09-16 00:52:40 +02:00
Irmen de Jong c8972c9561 add jdk option --enable-native-access=ALL-UNNAMED to manifest and launcher scripts to suppress newer JDK warning about JNI/JNA calling native libraries 2026-09-14 23:10:12 +02:00
259 changed files with 20442 additions and 2710 deletions

No files matched your search

+61
View File
@@ -125,6 +125,55 @@ Avoid self-modifying code unless it is explicitly required. It cannot run
from ROM and complicates debugging and tooling. Prefer lookup tables, RAM
vectors, or alternative algorithms.
## Reference
### Primary: 6502 Family CPU Reference
<https://www.pagetable.com/c64ref/6502/?cpu=65c02&tab=2> is the reference to reach for.
It covers the whole 6502 family and lets you **select the CPU variant**, which matters
because the parts genuinely differ. The variants are `6502`, `6502rorbug`, `65dtv02`,
`65c02`, `r65c02` (Rockwell), `65c02s`, `65ce02` and `65c816`. Change the `cpu=` query
parameter to switch; for Prog8, use `cpu=65c02` when working on cx16 and `cpu=6502` for
c64, c128 and pet32.
Per instruction it gives the operation, the opcodes, the byte length, the cycle count
and which status flags are affected, with tabs for opcodes, instructions, addressing
modes and a full opcode table (switchable between 4-4 and 3-3-2 layout). It can also
show the NMOS undocumented opcodes, and each part lists what it is based on, so a
variant's inherited instructions are visible.
Cycle counts use a compact notation worth knowing: a trailing `t` means +1 if a branch
is taken, `p` means +1 if a page is crossed, and `d` means +1 when the D (decimal) flag
is set. So `d0 2+t+p` for `bne` is 2 cycles, 3 if taken, 4 if taken across a page.
Consult it whenever an exact figure is needed rather than recalled: the byte length or
cycle cost of an instruction, which flags it clobbers, whether an addressing mode exists
on the target CPU, or whether an instruction belongs to a different part at all. Typical
reasons are costing a peephole or code-size tradeoff, checking a hand-written routine,
and confirming a mnemonic is valid before using it.
Do not assume a timing from memory or from another part. Cycle counts are exactly the
kind of detail that is misremembered, and the difference matters when it is the basis of
an optimization claim. Read the number off the reference for the specific CPU being
targeted.
### Secondary: WDC 65C02 reference
<https://cx16.dk/65c02/reference.html> is a 65C02-only reference. It is worth having
because it states each instruction's effect in a single formula (for example
`ADC: A,Z,C,N = A+M+C`), which makes the flag behaviour quick to check, and it lists
every status bit per instruction including the ones not affected. Use it for flag
semantics; use the primary reference for timings and for anything CPU-specific, since
it covers the 65C02 only and will not answer a question about the NMOS 6502.
### Neither reference covers the assembler
Both document the CPU, not 64tass. For whether 64tass accepts a mnemonic or a given
syntax, assemble it: an instruction outside the selected CPU set fails with
`error: general syntax` rather than being silently ignored. Note also that 64tass has
no `w65c02s`; its CPU selection is `--m65xx` (default), `--m65c02` and `--m65ce02`, and
Prog8 emits `.cpu 'w65c02'`.
## Tools
Prog8 invokes 64tass with `--ascii --case-sensitive --long-branch -Wall` plus
@@ -137,3 +186,15 @@ the target output option such as `--cbm-prg`. For manual assembly:
For debugging, add `--vice-labels --labels=labels.txt` and/or
`--list=listing.txt`.
To check an instruction's encoding or size, assemble it in isolation and read the
output bytes. The 2-byte load address comes first, so `stx $7b` / `ldy $7b` below
shows as `86 7b a4 7b` - two instructions, four bytes:
```bash
printf '.cpu %s\n* = $0801\nlbl\tstx\t$7b\n\tldy\t$7b\n' "'w65c02'" > sz.asm
64tass --ascii --case-sensitive --cbm-prg -o sz.prg sz.asm && xxd sz.prg
```
Note that a 64tass label takes no trailing colon; `lbl:` is a syntax error reported as
`error: label required`.
+34
View File
@@ -63,6 +63,17 @@ when both operands are registers.
is at offset `+3`.
- Word and long accesses must be even-aligned on 68000. 68020 permits
misalignment but it is slower.
- **Byte `A7` auto-update steps by 2, not 1.** If A7 (== SP, the stack pointer) is the address
register and the operand length for post-increment or post-decrement is one byte, A7 is
decremented/incremented by 2 instead of 1, to maintain a word-aligned stack. This is the
specified behaviour of all the 680x0, not a 68000 quirk, so it is identical everywhere and
safe to rely on. Word and long operands step by 2 and 4.
```asm
move.b d0,-(sp) ; A7 steps by 2, so SP stays word-aligned
...
addq.l #2, sp ; and the matching pop is #2, not #1
```
Do not "fix" this to an explicit `subq.l #1,sp`: that leaves SP odd and cause crash.
- The 68000 (`amiga500`) has only word multiply/divide and no scaled indexing,
bit-field, or 32-bit multiply/divide instructions. The 68020 (`qemu68k`)
adds those features and `extb.l`.
@@ -82,8 +93,31 @@ when both operands are registers.
pointer.
- `D0-D1` and `A0-A1` are scratch registers. Preserve registers required by
the surrounding calling convention, especially `D2-D7` and `A2-A6`.
- Never clobber `A5`: on every M68K target it is the Prog8 stack frame pointer
(locals at negative offsets, parameters at positive offsets). Overwriting it
corrupts the frame of the running subroutine and breaks unwinding.
- Never clobber `A6` on the Amiga targets (`amiga500`, `amiga1200`): it holds
the AmigaOS library base pointer that library calls depend on. Prog8 itself
leaves `A6` free for that reason.
- `move.l d0,-(sp)` pushes and `move.l (sp)+,d0` pops.
### Prog8 M68K stack ABI
- Normal framed subroutines use `link a5,#-N` and `unlk a5`; locals are at
negative `A5` offsets and incoming parameters are at positive offsets.
- Arguments use one padded 4-byte slot each. The caller pushes arguments
left-to-right, right-justifies byte and word values in their slots, and
removes the complete argument area after the call. Parameter `i` of `N`
arguments is at `8 + 4 * (N - 1 - i)` from the callee's `A5` when `i` is zero-based.
- A normal call preserves the caller's frame. A `goto` or conditional branch
leaving the current subroutine must execute `unlk a5` on the taken path before
transferring control.
- The built-in Prog8 `call(address)` is a zero-argument indirect call. It does
not marshal stack arguments. `asmsub` and `extsub` keep their explicit
register-annotated ABI, separate from normal framed calls.
- Do not push byte arguments directly with `move.b ..., -(sp)`: reserve a full
4-byte slot and store the byte at offset `3(sp)`.
## Prog8 Integration
Assembly can appear in `%asm {{ }}` blocks or `asmsub` routines. `asmsub`
+49 -3
View File
@@ -34,6 +34,9 @@ repeating them here:
- Use `%zeropage basicsafe` in test programs when a clean return is needed.
- Find library routines with `prog8c -libsearch <regex>` or extract them with
`prog8c -libdump <dir>`.
- Options accept single-dash or double-dash (`-target` == `--target`). Use
`-out <dir>` to place output in a directory, or `-o <file>` to name the exact
final program artifact (all other output files then land beside it).
For CX16 emulator programs, use `%encoding iso`, call `txt.iso()` in `start`,
and finish with `sys.poweroff_system()`. CBM targets use PETSCII by default;
@@ -57,9 +60,24 @@ libraries.
- M68000 targets are big-endian and use 32-bit pointers. Load the `m68k-coder`
skill for assembly details.
- `float` requires `%import floats`.
- Variables are statically allocated and zero-initialized. There is no normal
call stack for locals, so recursion overwrites locals unless an explicit
hardware or software stack is used. Iterative rewrites are preferred.
- On 6502 and virtual targets, variables are statically allocated and
zero-initialized. There is no normal call stack for locals, so recursion
overwrites locals unless an explicit hardware or software stack is used.
Iterative rewrites are preferred. Arrays are cleared once at startup here,
not on every subroutine entry.
- On the M68000 targets (`amiga500`, `amiga1200`, `qemu68k`) ordinary subroutine
locals and parameters live in an activation record on the CPU stack, so true
recursion and re-entrancy work. `link a5,#-N` / `unlk a5`, frame pointer `A5`,
locals at negative offsets, parameters at positive offsets. Block-level
variables, globals, `@shared`, memory slabs, and objects needing a permanent
external address stay static. Clean locals are zeroed on every call
including arrays, so the compiler warns for framed arrays over 8 bytes; use
`@dirty` when you assign first, otherwise you read stack garbage.
- Normal M68K calls use uniform 4-byte, right-justified argument slots. The caller
pushes arguments left-to-right and removes the complete argument area after the
call. Framed parameters start at positive `A5` offsets, with the first parameter
at `8 + 4 * (parameter_count - 1)`; locals use negative offsets. See
`docs/source/technical.rst` for the full convention and static fallback rules.
- `memory(name, size)` reserves static memory and returns a `pointer`.
- `str` and arrays have compile-time byte budgets. On 6502 targets the usual
limit is 256 bytes; M68000 targets allow up to 32768 bytes. Use `memory()`
@@ -106,10 +124,16 @@ Use `if_cs`, `if_cc`, `if_z`, and `if_nz` for direct CPU-flag branches.
- Subroutines can return zero, one, or multiple values:
`a, b = routine()` or `void routine()`.
- On M68K, ordinary calls preserve the caller's active frame. A `goto` or branch
that leaves the current subroutine must tear that frame down before transferring
control. The built-in `call(address)` is a zero-argument indirect call; it does
not marshal stack arguments.
- Avoid `private` unless requested. Prog8 symbols are public by default.
- `asmsub` bodies contain only one `%asm {{ ... }}` node. Parameters are type
checked and documented, but assembly must use the mapped registers. Declare
every modified hardware register in `clobbers`.
- On M68K targets never clobber `A5`, the stack frame pointer. On the Amiga
targets never clobber `A6` either, it holds the AmigaOS library base.
- `extsub` maps a signature to a fixed external address and has no body. Use
it for ROM, kernel, drivers, or binary-library routines.
- For inline assembly, load `asm6502-coder` for 64tass syntax, target-specific
@@ -128,6 +152,12 @@ Use `if_cs`, `if_cc`, `if_z`, and `if_nz` for direct CPU-flag branches.
## Verification
- A full `gradle build` is CPU- and IO-heavy so lower its priority on Unix like systems. Prefix
it with `nice`, e.g. `nice gradle build --console=plain`, and use `nice -n 19` for
maximum politeness. Targeted runs (`gradle :compiler:test --tests "..."`) are
cheaper but still worth lowering the priority of.
- Always pass `--console=plain`; the fancy console output interferes with agent
output parsing.
- Use the `virtual` target for behavioral tests when possible:
`prog8c -target virtual -emu program.p8`.
- Use `-check` for syntax and semantic checks without output generation.
@@ -135,6 +165,22 @@ Use `if_cs`, `if_cc`, `if_z`, and `if_nz` for direct CPU-flag branches.
- For IR execution, use `-vmtrace` when control flow needs inspection.
- Do not modify a correct test program to work around a compiler crash. Reduce
the case and fix the compiler instead.
- `gradle build` already runs the full test suite; do not run a separate
`gradle test` before it, to avoid a lengthy double run.
- `TestCompilerOnExamples` asserts exact output sizes per example. Any change to
the code generator shifts them, so expect a handful of
`expected:<N> but was:<M>` failures with no stack trace pointing at the cause.
Attribute each one before editing: rebuild with the change reverted and compare
the two generated `.asm` files, so the size delta is demonstrably yours and not a
side effect. Then update the expected size. A delta of a few bytes from removed
`ldy`/`lda` instructions is normal; a large or erratic delta means something else
changed and needs investigating.
- When running `gradle` tests, grep for `SUCCESSFUL` to check if all tests
passed, and `FAILED` to check for failures. Gradle does not print individual
`PASSED` lines by default.
- Do not write compiler unit tests that depend on the `vasm` assembler: it is
not always installed (CI only installs `64tass`). For m68k codegen tests
(`amiga500`, `qemu68k` targets) pass `writeAssembly = true, assemble = false`
to the `compileText`/`compileFile` helpers. This still generates the codegen
AST to assert on, but skips assembling, so no vasm is needed. If a test's
assertions cannot be made from the AST alone, drop the test.
+1
View File
@@ -14,6 +14,7 @@ build/
out/
output/
gradle/
Libs/
.gradle/
.kotlin/
.git/
+4 -1
View File
@@ -45,4 +45,7 @@ compiler/lib/
.gradle
sd*.img
*.d64
AsyncIO/
AsyncIO.info
m68k-amigaos-ahi/
Libs/
-1
View File
@@ -4,7 +4,6 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="LOCAL" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
+32 -9
View File
@@ -13,17 +13,23 @@
## CHANGE / EDIT MODE
- Never implement features yourself when possible - use sub-agents!
- Use a subagent for larger chunks of work
- Identify changes from the plan that can be implemented in parallel, and use sub-agents to implement the features efficiently
- When using sub-agents to implement features, act as a coordinator only
- Use the best model for the task - premium models for complex tasks (like coding) and mid-tier models for simpler tasks, like documentation
- After completing features (large or small), always run commands like lint, type check and next build to check code quality
- After completing features (large or small), run the relevant quality commands (lint, type check, build) to check code quality.
Only compiler/standard-library changes require a build: that means Kotlin sources (`.kt`), the grammar (`.g4`), or embedded
standard library files (`compiler/res/prog8lib/**/*.p8` and `**/*.asm`). For changes limited to `examples/**`, `docs/**`, or
other non-compiler files, verify with the target compiler directly instead (for example `prog8c -target <target> <file>`), and
skip the build entirely
(exception: if the change is only whitespace or code comments, a build or test run is not necessary)
- `gradle build` already runs the full test suite; don't run a separate full `gradle test` before it, to avoid lengthy double test runs (targeted `--tests` filters to find failing tests are fine)
## Project Overview
- This project is a compiler for the Prog8 programming language.
- Prog8 is a programming language primarily targeting 8-bit retro systems with the 6502 CPU, such as the Commodore 64, Commodore 128, and Commander X16.
- The compiler has a 6502 code generator backend, and an IR code generator.
- The IR code is meant to be used in a new machine specific code generator backend (primarily 6502 but maybe 68000 as well later)
- The IR code is used by machine-specific code generator backends, including 6502 and M68K.
- The compiler includes a simple 'virtual machine' that can execute the IR code directly via interpretation.
- Prog8 source files have .p8 extension – these are *not* LUA or PICO-8 source files in this case!
- Prog8 source files are a "module" that can contain one or more "blocks". They can also import other modules, from internal library files or from source files on the filesystem.
@@ -55,7 +61,8 @@ Source → parseMainModule() → processAst() → optimizeAst() → postprocessA
### Target Differences
- **CPU instruction set differences**: Only the CommanderX16 target (cx16) can use 65C02 instructions such as STZ. The other targets (C64, C128, PET32) can only use original 6502 instructions.
- When writing or understanding 6502/65C02 assembly code, load the `asm6502-coder` skill for 64tass syntax and conventions.
- When writing or understanding M68K assembly code (amiga500, qemu68k targets), load the `m68k-coder` skill for vasm mot syntax and conventions.
- When writing or understanding M68K assembly code (amiga500, amiga1200, qemu68k targets), load the `m68k-coder` skill for vasm mot syntax and conventions.
- M68K ordinary subroutine locals and parameters use `A5`-based stack frames. Normal calls pass padded 4-byte argument slots on the stack, and the caller cleans up; see `docs/source/technical.rst` for the complete convention and frame eligibility rules.
## DEBUGGING TIP: Use `-noopt` to isolate problems
@@ -127,7 +134,19 @@ Steps when encountering a compiler crash:
- **NO EMOJI in user documentation**. Do not use emoji or decorative unicode symbols in documentation files. Functional unicode symbols are acceptable when they serve a clear purpose (e.g., → for arrows, ± for plus-minus, × for multiplication, ° for degrees). Avoid decorative emoji like ❌ ✅ ⚠️ 🎉 etc.
### Code Style Guidelines
**Minimal comments when making changes**: When modifying existing code, add only essential comments that explain *why* a change was made or document non-obvious behavior. **Do not add verbose comments** that restate what the code does; let the code speak for itself. Existing extensive comments should be preserved, but new changes should have minimal commentary.
**Code comments**: Comments are the exception, not the norm. A comment earns its place only by recording
something the code itself cannot say: the reason for a non-obvious choice, or a constraint that
a well-meaning future edit would otherwise violate.
- Inline comment inside a function body: 1 line, never more.
- KDoc on a declaration: 3 lines, unless a genuine non-obvious contract cannot be stated shorter.
- Never restate the identifier, the type, or what the following line does.
- Prefer a good name or a well-named function over a comment that explains it.
Before keeping any comment, delete it mentally and ask what the reader loses. If nothing, it goes.
Keep existing comments as they are; do not expand, reword, or reformat them while editing
nearby lines - unless that would cause inconsistency.
**Kotlin string `$` handling**: The `$` character is special in Kotlin string literals (string interpolation). If you need a literal `$` character, use Kotlin's `$$"..."` interpolation prefix instead of backslash escaping (`\$`) or `${'$'}`. The `$$"..."` syntax is a fairly recent Kotlin feature that avoids nasty escaping. For example:
```kotlin
@@ -198,10 +217,12 @@ When the task involves writing or understanding `.p8` (Prog8 source) or `.p8ir`
- This compiles AND installs, but still skips running tests (faster than `gradle build`)
### When to run full build with tests
- **Before final verification**: Always run `gradle build` to ensure all tests pass
- **Before final verification**: Run `gradle build` to ensure all tests pass - but only if the change actually touched the
compiler or standard library (`.kt`, `.g4`, or `compiler/res/prog8lib/**`). For `examples/**` or `docs/**`-only changes,
verify with `prog8c` directly instead and skip the build
- **After major refactoring**: Run `gradle build` to catch regressions
- **When debugging test failures**: Use `gradle test --tests "*TestName*"` for specific tests
- **When working on a standalone code gen module** (one that reads `.p8ir` files): Run `gradle :<module>:test --console=plain` instead of full `gradle build`. Compiler unit tests are irrelevant for IR-level changes. Do run full `gradle build` before final verification.
- **When working on a standalone code gen module** (one that reads `.p8ir` files): Run `gradle :<module>:test --console=plain` instead of full `gradle build`. Compiler unit tests are irrelevant for IR-level changes. Do run full `gradle build` before final verification if the change touched compiler sources.
### Build command summary
@@ -218,13 +239,15 @@ When the task involves writing or understanding `.p8` (Prog8 source) or `.p8ir`
- the prog8c compiler executable can be found in the compiler/build/install/prog8c/bin folder (this is already added to the shell's path)
- **the `-check` switch performs a quick syntax/semantic check only; it will NOT produce any output files (no .prg, .asm, etc.)**. Use it only for fast error checking during development.
- **the `-noopt` switch DISABLES all optimizations** - useful for debugging to determine if a problem is caused by the optimizer. **Optimizations are ENABLED by default** (no flag needed).
- **prog8c uses single-dash command line options** (e.g., `-target`, `-noopt`, `-check`), NOT double-dash (`--target` is invalid).
- **prog8c options accept single-dash or double-dash** (e.g., `-target` and `--target` are equivalent).
- **the `-printast1` switch prints out the internal Compiler AST** after parsing and semantic analysis.
- **the `-printast2` switch prints out the optimized Simple AST** just before it goes to the code generator. This is useful for debugging optimizer issues.
- **the `-out outdir` switch sets an alternative output directory** for compiled files (.prg, .asm, .list, etc.). **By default, output files are written to the same directory as the source file**.
- **the `-out outdir` switch sets an alternative output directory** for compiled files (.prg, .asm, .list, etc.). **By default, output files are written to the current working directory**.
- **the `-o <file>` switch names the exact final program artifact** (also spelled `--output`); all other output files go in the same directory with the same stem. Cannot be combined with `-out` or with multiple source files.
- **Other useful flags**: `-quiet` (suppress messages), `-warnimplicitcasts` (warn on implicit type widening), `-daemon` (keep a background compiler process alive — must be passed on every invocation)
### Compilation Output Files
Names follow the source file name, or the stem of the `-o` file when that option is used.
- `*.prg` - The final compiled program file for the target system (e.g., Commander X16)
- `*.asm` - Generated assembly code from the Prog8 source
- `*.list` - Generated full assembly listing file from the Prog8 source
+20 -3
View File
@@ -4,6 +4,12 @@
;
; As the X16 computer is a more or less fixed system, it's not very useful
; to benchmark the computer itself with.
;
; The individual scores are iteration counts, and they differ by more than an
; order of magnitude between benchmarks (maze does ~3000, game of life ~180).
; Summing the raw counts therefore let one or two benchmarks dominate the total
; and hide regressions in all the others, so the total is a weighted sum - see
; benchmark_weight below.
%import textio
@@ -26,7 +32,15 @@ main {
str[20] benchmark_names
uword[20] benchmark_score
str version = "12.1"
; Weight per benchmark, in the same order as the benchmarks are run below. The total is the
; weighted sum of the raw iteration counts.
; The raw counts differ by more than an order of magnitude (maze ~3064, game of life ~180), so
; in an unweighted sum the two heaviest - maze solver and text-elite - were 57% of the total
; while game of life was 2%. A regression in a small benchmark was therefore nearly invisible.
; Their weights are scaled down so that every benchmark contributes roughly comparably; the
; other eight are left at 256 (i.e. unweighted).
uword[10] benchmark_weight = [ 64, 256, 256, 256, 256, 256, 256, 64, 256, 256 ]
str version = "14.0"
sub start() {
@@ -96,11 +110,14 @@ main {
txt.print(")\n\n")
do {
; Weighted contribution. Done in long arithmetic on purpose: score*weight overflows a
; uword (3064*256 is 784384, which wraps to 63488 and would corrupt the total).
uword weighted = ((benchmark_score[benchmark_number] as long) * benchmark_weight[benchmark_number] / 256) as uword
txt.spc()
txt.print_uw(benchmark_score[benchmark_number])
txt.print_uw(weighted)
txt.column(6)
txt.print(benchmark_names[benchmark_number])
total_score += benchmark_score[benchmark_number]
total_score += weighted
txt.nl()
benchmark_number++
} until benchmark_names[benchmark_number]==0
@@ -1,10 +1,7 @@
package prog8.code.assembly
import prog8.code.GENERATED_LABEL_PREFIX
import prog8.code.core.CompilationOptions
import prog8.code.core.ICompilationTarget
import prog8.code.core.IErrorReporter
import prog8.code.core.OutputType
import prog8.code.core.*
import prog8.code.target.C128Target
import prog8.code.target.C64Target
import prog8.code.target.PETTarget
@@ -13,25 +10,20 @@ import java.nio.file.Path
class AssemblyProgram6502(
override val name: String,
outputDir: Path,
private val options: CompilationOptions,
private val compTarget: ICompilationTarget) : IAssemblyProgram {
override val irInstructionCount: Int = 0
override val irChunkCount: Int = 0
override val irRegisterCount: Int = 0
private val assemblyFile = outputDir.resolve("$name.asm")
private val prgFile = outputDir.resolve("$name.prg") // CBM prg executable program
private val xexFile = outputDir.resolve("$name.xex") // Atari xex executable program
private val binFile = outputDir.resolve("$name.bin")
private val viceMonListFile = outputDir.resolve(C64Target.viceMonListName(name))
private val listFile = outputDir.resolve("$name.list")
override fun assemble(options: CompilationOptions, errors: IErrorReporter): Boolean {
val assemblerCommand: List<String>
val outs = OutputFiles.of(options, name)
fun addRemainingOptions(command: MutableList<String>, program: Path, assembly: Path): List<String> {
// keep additionalAssemblerOptions BEFORE --output: a target config may contain its own --output
if(options.compTarget.additionalAssemblerOptions.isNotEmpty())
command.addAll(options.compTarget.additionalAssemblerOptions)
@@ -44,7 +36,7 @@ class AssemblyProgram6502(
// CBM machines .prg generation.
val command = mutableListOf("64tass", "--cbm-prg", "--ascii", "--case-sensitive", "--long-branch",
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=$viceMonListFile")
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=${outs.viceMonList()}")
if(options.warnSymbolShadowing)
command.add("-Wshadow")
@@ -55,10 +47,10 @@ class AssemblyProgram6502(
command.add("--quiet")
if(options.asmListfile) {
command.add("--list=$listFile")
command.add("--list=${outs.list()}")
}
assemblerCommand = addRemainingOptions(command, prgFile, assemblyFile)
assemblerCommand = addRemainingOptions(command, outs.programFile, outs.asm())
if(!options.quiet)
println("\nCreating prg for target ${compTarget.name}.")
}
@@ -66,7 +58,7 @@ class AssemblyProgram6502(
// Atari800XL .xex generation.
val command = mutableListOf("64tass", "--atari-xex", "--case-sensitive", "--long-branch",
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=$viceMonListFile")
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=${outs.viceMonList()}")
if(options.warnSymbolShadowing)
command.add("-Wshadow")
@@ -77,16 +69,16 @@ class AssemblyProgram6502(
command.add("--quiet")
if(options.asmListfile)
command.add("--list=$listFile")
command.add("--list=${outs.list()}")
assemblerCommand = addRemainingOptions(command,xexFile, assemblyFile)
assemblerCommand = addRemainingOptions(command, outs.programFile, outs.asm())
if(!options.quiet)
println("\nCreating xex for target ${compTarget.name}.")
}
OutputType.RAW -> {
// Neo6502/headerless raw program generation.
val command = mutableListOf("64tass", "--nostart", "--case-sensitive", "--long-branch",
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=$viceMonListFile")
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=${outs.viceMonList()}")
if(options.warnSymbolShadowing)
command.add("-Wshadow")
@@ -97,9 +89,9 @@ class AssemblyProgram6502(
command.add("--quiet")
if(options.asmListfile)
command.add("--list=$listFile")
command.add("--list=${outs.list()}")
assemblerCommand = addRemainingOptions(command, binFile, assemblyFile)
assemblerCommand = addRemainingOptions(command, outs.programFile, outs.asm())
if(!options.quiet)
println("\nCreating raw binary for target ${compTarget.name}.")
}
@@ -107,7 +99,7 @@ class AssemblyProgram6502(
// CBM machines library (.bin) generation (with or without 2 byte load address header depending on the compilation target machine)
val command = mutableListOf("64tass", "--ascii", "--case-sensitive", "--long-branch",
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=$viceMonListFile")
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=${outs.viceMonList()}")
if(options.warnSymbolShadowing)
command.add("-Wshadow")
@@ -118,7 +110,7 @@ class AssemblyProgram6502(
command.add("--quiet")
if(options.asmListfile)
command.add("--list=$listFile")
command.add("--list=${outs.list()}")
if(compTarget.name in listOf(C64Target.NAME, C128Target.NAME, PETTarget.NAME)) {
if(!options.quiet)
@@ -130,7 +122,7 @@ class AssemblyProgram6502(
command.add("--nostart") // should be headerless bin, because basic has problems doing a normal LOAD"lib",8,1 - need to use BLOAD
}
assemblerCommand = addRemainingOptions(command, binFile, assemblyFile)
assemblerCommand = addRemainingOptions(command, outs.programFile, outs.asm())
}
else -> error("Unsupported output type: ${compTarget.defaultOutputType}")
}
@@ -159,16 +151,16 @@ class AssemblyProgram6502(
val result = process.waitFor()
if (result == 0) {
removeGeneratedLabelsFromMonlist()
generateBreakpointList()
removeGeneratedLabelsFromMonlist(outs)
generateBreakpointList(outs)
}
return result==0
}
private fun removeGeneratedLabelsFromMonlist() {
private fun removeGeneratedLabelsFromMonlist(outs: OutputFiles) {
val pattern = Regex("""al (\w+) \S+$GENERATED_LABEL_PREFIX.+?""")
val lines = viceMonListFile.toFile().readLines()
viceMonListFile.toFile().outputStream().bufferedWriter().use {
val lines = outs.viceMonList().toFile().readLines()
outs.viceMonList().toFile().outputStream().bufferedWriter().use {
for (line in lines) {
if(pattern.matchEntire(line)==null)
it.write(line+"\n")
@@ -176,11 +168,11 @@ class AssemblyProgram6502(
}
}
private fun generateBreakpointList() {
private fun generateBreakpointList(outs: OutputFiles) {
// builds list of breakpoints, appends to monitor list file
val breakpoints = mutableListOf<String>()
val pattern = Regex("""al (\w+) \S+_prog8_breakpoint_\d+.?""") // gather breakpoints by the source label that's generated for them
for (line in viceMonListFile.toFile().readLines()) {
for (line in outs.viceMonList().toFile().readLines()) {
val match = pattern.matchEntire(line)
if (match != null)
breakpoints.add("break $" + match.groupValues[1])
@@ -189,7 +181,7 @@ class AssemblyProgram6502(
breakpoints.add(0, "; breakpoint list now follows")
breakpoints.add(1, "; $num breakpoints have been defined")
breakpoints.add(2, "del")
viceMonListFile.toFile().appendText(breakpoints.joinToString("\n") + "\n")
outs.viceMonList().toFile().appendText(breakpoints.joinToString("\n") + "\n")
}
}
@@ -24,7 +24,7 @@ class CallConvention(val params: List<ParamConvention>, val returns: ReturnConve
class FParam(val name: String, vararg val possibleDatatypes: BaseDataType)
private val IterableDatatypes = arrayOf(BaseDataType.STR, BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW, BaseDataType.ARRAY_POINTER)
private val IterableDatatypes = arrayOf(BaseDataType.STR, BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW)
private val IntegerDatatypes = arrayOf(BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.LONG)
private val NumericDatatypes = arrayOf(BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
@@ -36,7 +36,8 @@ class CompilationOptions(val output: OutputType,
var quiet: Boolean = false,
var profilingInstrumentation: Boolean = false,
var symbolDefs: Map<String, String> = emptyMap(),
var privateSymbols: Boolean = false
var privateSymbols: Boolean = false,
var outputFile: Path? = null
) {
init {
compTarget.initializeMemoryAreas(this)
@@ -77,6 +78,7 @@ class CompilationOptions(val output: OutputType,
private var profilingInstrumentation: Boolean = false
private var newCodegen: Boolean = false
private var symbolDefs: Map<String, String> = emptyMap()
private var outputFile: Path? = null
fun output(output: OutputType) = apply { this.output = output }
fun launcher(launcher: CbmPrgLauncherType) = apply { this.launcher = launcher }
@@ -107,6 +109,7 @@ class CompilationOptions(val output: OutputType,
fun profilingInstrumentation(profilingInstrumentation: Boolean) = apply { this.profilingInstrumentation = profilingInstrumentation }
fun symbolDefs(symbolDefs: Map<String, String>) = apply { this.symbolDefs = symbolDefs }
fun newCodegen(newCodegen: Boolean) = apply { this.newCodegen = newCodegen }
fun outputFile(outputFile: Path?) = apply { this.outputFile = outputFile }
fun build(): CompilationOptions {
return CompilationOptions(
@@ -115,7 +118,7 @@ class CompilationOptions(val output: OutputType,
includeSourcelines, dumpVariables, dumpSymbols, newCodegen, varsHighBank, varsGolden,
breakpointCpuInstruction, ignoreFootguns, outputDir, quiet,
profilingInstrumentation, symbolDefs
)
).apply { this.outputFile = this@Builder.outputFile }
}
}
}
+103 -94
View File
@@ -1,6 +1,8 @@
package prog8.code.core
import java.util.*
import java.util.Objects
/**
@@ -20,7 +22,6 @@ enum class BaseDataType {
ARRAY_SPLITW, // pass by reference, split word layout, subtype is the element type (restricted to word types)
POINTER, // typed pointer, subtype is whatever type is pointed to
STRUCT_INSTANCE, // the actual instance of a struct (not directly supported in the language yet, but we need its type)
ARRAY_POINTER, // array of pointers (uwords or longs), subtype is whatever type each element points to
UNDEFINED;
@@ -91,11 +92,10 @@ val BaseDataType.isSigned get() = this in setOf(BaseDataType.BYTE, BaseDataType.
val BaseDataType.isUnsigned get() = !this.isSigned
val BaseDataType.isSignedInteger get() = this.isSigned && this.isInteger
val BaseDataType.isUnsignedInteger get() = this.isUnsigned && this.isInteger
val BaseDataType.isArray get() = this == BaseDataType.ARRAY || this == BaseDataType.ARRAY_SPLITW || this == BaseDataType.ARRAY_POINTER
val BaseDataType.isArray get() = this == BaseDataType.ARRAY || this == BaseDataType.ARRAY_SPLITW
val BaseDataType.isPointer get() = this == BaseDataType.POINTER
val BaseDataType.isStructInstance get() = this == BaseDataType.STRUCT_INSTANCE
val BaseDataType.isPointerArray get() = this == BaseDataType.ARRAY_POINTER
val BaseDataType.isIterable get() = this in setOf(BaseDataType.STR, BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW, BaseDataType.ARRAY_POINTER)
val BaseDataType.isIterable get() = this in setOf(BaseDataType.STR, BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW)
val BaseDataType.isPassByRef get() = this.isIterable && !this.isPointer
val BaseDataType.isPassByValue get() = !this.isIterable || this.isPointer
@@ -126,14 +126,15 @@ interface ISubType {
class DataType private constructor(
val base: BaseDataType,
val sub: BaseDataType?,
val pointeeSub: BaseDataType?,
var subType: ISubType?,
var subTypeFromAntlr: List<String>? = null
) {
init {
when {
base.isPointerArray -> {
require(sub!=null || subType!=null || subTypeFromAntlr!=null)
base.isArray && sub == BaseDataType.POINTER -> {
require(pointeeSub != null || subType != null || subTypeFromAntlr != null)
}
base.isArray -> {
require(sub != null)
@@ -155,7 +156,7 @@ class DataType private constructor(
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is DataType) return false
if (base != other.base || sub != other.sub) return false
if (base != other.base || sub != other.sub || pointeeSub != other.pointeeSub) return false
val st1 = subType
val st2 = other.subType
if (st1 == st2) return true
@@ -163,7 +164,7 @@ class DataType private constructor(
return st1.sameas(st2)
}
override fun hashCode(): Int = Objects.hash(base, sub, subType)
override fun hashCode(): Int = Objects.hash(base, sub, pointeeSub, subType)
fun setActualSubType(actualSubType: ISubType) {
subType = actualSubType
@@ -176,73 +177,72 @@ class DataType private constructor(
companion object {
val UBYTE = DataType(BaseDataType.UBYTE, null, null)
val BYTE = DataType(BaseDataType.BYTE, null, null)
val UWORD = DataType(BaseDataType.UWORD, null, null)
val WORD = DataType(BaseDataType.WORD, null, null)
val LONG = DataType(BaseDataType.LONG, null, null)
val FLOAT = DataType(BaseDataType.FLOAT, null, null)
val BOOL = DataType(BaseDataType.BOOL, null, null)
val STR = DataType(BaseDataType.STR, BaseDataType.UBYTE, null)
val UNDEFINED = DataType(BaseDataType.UNDEFINED, null, null)
val UBYTE = DataType(BaseDataType.UBYTE, null, null, null)
val BYTE = DataType(BaseDataType.BYTE, null, null, null)
val UWORD = DataType(BaseDataType.UWORD, null, null, null)
val WORD = DataType(BaseDataType.WORD, null, null, null)
val LONG = DataType(BaseDataType.LONG, null, null, null)
val FLOAT = DataType(BaseDataType.FLOAT, null, null, null)
val BOOL = DataType(BaseDataType.BOOL, null, null, null)
val STR = DataType(BaseDataType.STR, BaseDataType.UBYTE, null, null)
val UNDEFINED = DataType(BaseDataType.UNDEFINED, null, null, null)
private val simpletypes = mapOf(
BaseDataType.UBYTE to DataType(BaseDataType.UBYTE, null, null),
BaseDataType.BYTE to DataType(BaseDataType.BYTE, null, null),
BaseDataType.UWORD to DataType(BaseDataType.UWORD, null, null),
BaseDataType.WORD to DataType(BaseDataType.WORD, null, null),
BaseDataType.LONG to DataType(BaseDataType.LONG, null, null),
BaseDataType.FLOAT to DataType(BaseDataType.FLOAT, null, null),
BaseDataType.BOOL to DataType(BaseDataType.BOOL, null, null),
BaseDataType.STR to DataType(BaseDataType.STR, BaseDataType.UBYTE, null),
BaseDataType.POINTER to DataType(BaseDataType.POINTER, null, null),
BaseDataType.UNDEFINED to DataType(BaseDataType.UNDEFINED, null, null)
BaseDataType.UBYTE to DataType(BaseDataType.UBYTE, null, null, null),
BaseDataType.BYTE to DataType(BaseDataType.BYTE, null, null, null),
BaseDataType.UWORD to DataType(BaseDataType.UWORD, null, null, null),
BaseDataType.WORD to DataType(BaseDataType.WORD, null, null, null),
BaseDataType.LONG to DataType(BaseDataType.LONG, null, null, null),
BaseDataType.FLOAT to DataType(BaseDataType.FLOAT, null, null, null),
BaseDataType.BOOL to DataType(BaseDataType.BOOL, null, null, null),
BaseDataType.STR to DataType(BaseDataType.STR, BaseDataType.UBYTE, null, null),
BaseDataType.POINTER to DataType(BaseDataType.POINTER, null, null, null),
BaseDataType.UNDEFINED to DataType(BaseDataType.UNDEFINED, null, null, null)
)
fun forDt(dt: BaseDataType): DataType {
if(dt.isStructInstance)
return DataType(BaseDataType.STRUCT_INSTANCE, null, null)
return DataType(BaseDataType.STRUCT_INSTANCE, null, null, null)
return simpletypes.getValue(dt)
}
fun arrayFor(elementDt: BaseDataType, memsizer: IMemSizer): DataType {
// wether or not a word-array should be split-words, is determined later
require(!elementDt.isPointer) { "use other array constructor for arrays of pointers" }
// On 32-bit targets (m68k, virtual), str arrays become arrays of LONG (4-byte pointers)
// via pointerBaseType. On 6502 targets they become arrays of UWORD (2-byte pointers).
require(elementDt != BaseDataType.POINTER) { "use arrayOfPointersTo for arrays of pointers" }
val actualElementDt = if(elementDt==BaseDataType.STR) {
memsizer.pointerBaseType
} else elementDt
if(actualElementDt.isNumericOrBool)
return DataType(BaseDataType.ARRAY, actualElementDt, null)
return DataType(BaseDataType.ARRAY, actualElementDt, null, null)
else
throw NoSuchElementException("invalid basic element dt $elementDt")
}
fun splitWordArrayFor(elementDt: BaseDataType): DataType {
require(elementDt.isWord) { "split word array element type must be word" }
return DataType(BaseDataType.ARRAY_SPLITW, elementDt, null)
return DataType(BaseDataType.ARRAY_SPLITW, elementDt, null, null)
}
fun arrayOfPointersTo(sub: BaseDataType): DataType = DataType(BaseDataType.ARRAY_POINTER, sub, null)
fun arrayOfPointersTo(structType: ISubType?): DataType = DataType(BaseDataType.ARRAY_POINTER, null, structType)
fun arrayOfPointersTo(sub: BaseDataType): DataType =
DataType(BaseDataType.ARRAY, BaseDataType.POINTER, sub, null)
fun arrayOfPointersTo(structType: ISubType?): DataType =
DataType(BaseDataType.ARRAY, BaseDataType.POINTER, BaseDataType.STRUCT_INSTANCE, structType)
fun arrayOfPointersFromAntlrTo(sub: BaseDataType?, identifier: List<String>?): DataType =
DataType(BaseDataType.ARRAY_POINTER, sub, null, identifier)
DataType(BaseDataType.ARRAY, BaseDataType.POINTER, sub ?: BaseDataType.STRUCT_INSTANCE, null, identifier)
// array of struct instances: sub is STRUCT_INSTANCE, subType carries the resolved struct
fun arrayOfStructs(structType: ISubType): DataType = DataType(BaseDataType.ARRAY, BaseDataType.STRUCT_INSTANCE, structType)
fun arrayOfStructs(structType: ISubType): DataType = DataType(BaseDataType.ARRAY, BaseDataType.STRUCT_INSTANCE, null, structType)
fun arrayOfStructsFromAntlr(struct: List<String>): DataType =
DataType(BaseDataType.ARRAY, BaseDataType.STRUCT_INSTANCE, null, subTypeFromAntlr = struct)
DataType(BaseDataType.ARRAY, BaseDataType.STRUCT_INSTANCE, null, null, subTypeFromAntlr = struct)
fun pointer(base: BaseDataType): DataType = DataType(BaseDataType.POINTER, base, null)
fun pointer(base: BaseDataType): DataType = DataType(BaseDataType.POINTER, base, null, null)
fun pointer(dt: DataType): DataType = if(dt.isBasic)
DataType(BaseDataType.POINTER, dt.base, null)
DataType(BaseDataType.POINTER, dt.base, null, null)
else
DataType(BaseDataType.POINTER, null, dt.subType, dt.subTypeFromAntlr)
fun pointer(structType: ISubType): DataType = DataType(BaseDataType.POINTER, null, structType)
fun pointerFromAntlr(identifier: List<String>): DataType = DataType(BaseDataType.POINTER, null, null, identifier)
fun structInstance(type: ISubType?): DataType = DataType(BaseDataType.STRUCT_INSTANCE, sub=null, type)
fun structInstanceFromAntlr(struct: List<String>): DataType = DataType(BaseDataType.STRUCT_INSTANCE, null, null, subTypeFromAntlr = struct)
DataType(BaseDataType.POINTER, null, null, dt.subType, dt.subTypeFromAntlr)
fun pointer(structType: ISubType): DataType = DataType(BaseDataType.POINTER, null, null, structType)
fun pointerFromAntlr(identifier: List<String>): DataType = DataType(BaseDataType.POINTER, null, null, null, identifier)
fun structInstance(type: ISubType?): DataType = DataType(BaseDataType.STRUCT_INSTANCE, sub=null, pointeeSub=null, type)
fun structInstanceFromAntlr(struct: List<String>): DataType = DataType(BaseDataType.STRUCT_INSTANCE, null, null, null, subTypeFromAntlr = struct)
}
@@ -267,29 +267,30 @@ class DataType private constructor(
arrayOfStructsFromAntlr(subTypeFromAntlr!!)
}
if(!target.cpu.is6502) {
return if (base.isPointer)
pointerArray()
else
arrayFor(base, target)
}
if(base.isPointer)
return pointerArray()
if(!target.cpu.is6502)
return arrayFor(base, target)
return if (splitwords && (base == BaseDataType.UWORD || base == BaseDataType.WORD || base == BaseDataType.STR))
splitWordArrayFor(base)
else if(base.isPointer)
pointerArray()
else
arrayFor(base, target)
}
fun elementType(): DataType =
when {
isPointerArray -> DataType(BaseDataType.POINTER, sub, subType)
isPointerArray -> {
if (subType != null) DataType(BaseDataType.POINTER, null, null, subType)
else if (subTypeFromAntlr != null) DataType(BaseDataType.POINTER, null, null, null, subTypeFromAntlr)
else DataType(BaseDataType.POINTER, pointeeSub!!, null, null)
}
base.isArray || base==BaseDataType.STR -> {
if (sub == BaseDataType.STRUCT_INSTANCE && subType != null)
return DataType(BaseDataType.STRUCT_INSTANCE, null, subType)
return DataType(BaseDataType.STRUCT_INSTANCE, null, null, subType)
val dt = forDt(sub!!)
if (subType != null) DataType(BaseDataType.POINTER, null, subType) else dt
if (subType != null) DataType(BaseDataType.POINTER, null, null, subType) else dt
}
else -> throw IllegalArgumentException("not an array")
}
@@ -307,7 +308,7 @@ class DataType private constructor(
return untypedPointerType
if (isArray) {
val isSplit = base == BaseDataType.ARRAY_SPLITW ||
(base == BaseDataType.ARRAY_POINTER && mem.memorySize(BaseDataType.POINTER) <= 2)
(isPointerArray && mem.memorySize(BaseDataType.POINTER) <= 2)
if (msb || isSplit)
return pointer(BaseDataType.UBYTE)
val elementDt = elementType()
@@ -326,8 +327,8 @@ class DataType private constructor(
return when {
isUnsignedWord -> forDt(BaseDataType.UBYTE)
sub!=null -> forDt(sub)
subType!=null -> DataType(BaseDataType.STRUCT_INSTANCE, null, subType)
subTypeFromAntlr!=null -> DataType(BaseDataType.STRUCT_INSTANCE, null, null, subTypeFromAntlr)
subType!=null -> DataType(BaseDataType.STRUCT_INSTANCE, null, null, subType)
subTypeFromAntlr!=null -> DataType(BaseDataType.STRUCT_INSTANCE, null, null, null, subTypeFromAntlr)
else -> throw IllegalArgumentException("cannot dereference this pointer type")
}
}
@@ -342,6 +343,12 @@ class DataType private constructor(
BaseDataType.UBYTE -> "ubyte[]"
BaseDataType.UWORD -> "uword[]"
BaseDataType.LONG -> "long[]"
BaseDataType.POINTER -> {
val pointee = if (subType != null) subType!!.scopedNameString
else if (subTypeFromAntlr != null) "$subTypeFromAntlr"
else pointeeSub!!.name.lowercase()
"^^$pointee[]"
}
BaseDataType.STRUCT_INSTANCE -> if (subType != null) "${subType!!.scopedNameString}[]" else "$subTypeFromAntlr[]"
else -> throw IllegalArgumentException("invalid sub type")
}
@@ -356,9 +363,6 @@ class DataType private constructor(
BaseDataType.POINTER -> {
if(sub!=null) "^^${sub.name.lowercase()}" else if(subType!=null) "^^${subType!!.scopedNameString}" else "^^${subTypeFromAntlr}"
}
BaseDataType.ARRAY_POINTER -> {
if(sub!=null) "^^${sub.name.lowercase()}[] (split)" else if (subType!=null) "^^${subType!!.scopedNameString}[] (split)" else "^^${subTypeFromAntlr}[] (split)"
}
BaseDataType.STRUCT_INSTANCE -> {
sub?.name?.lowercase() ?: if (subType!=null) subType!!.scopedNameString else "$subTypeFromAntlr"
}
@@ -390,14 +394,6 @@ class DataType private constructor(
else -> "?????"
}
}
BaseDataType.ARRAY_POINTER -> {
when {
sub!=null -> "^^${sub.name.lowercase()}["
subType!=null -> "^^${subType!!.scopedNameString}["
subTypeFromAntlr!=null -> "^^${subTypeFromAntlr!!.joinToString(".")}["
else -> "????? ["
}
}
BaseDataType.ARRAY -> {
when(sub) {
BaseDataType.UBYTE -> "ubyte["
@@ -407,6 +403,12 @@ class DataType private constructor(
BaseDataType.WORD -> "@nosplit word["
BaseDataType.LONG -> "long["
BaseDataType.FLOAT -> "float["
BaseDataType.POINTER -> {
val pointee = if (subType != null) "^^${subType!!.scopedNameString}["
else if (subTypeFromAntlr != null) "^^${subTypeFromAntlr!!.joinToString(".")}["
else "^^${pointeeSub!!.name.lowercase()}["
pointee
}
BaseDataType.STRUCT_INSTANCE -> if (subType != null) "${subType!!.scopedNameString}[" else "$subTypeFromAntlr["
else -> throw IllegalArgumentException("invalid sub type")
}
@@ -430,12 +432,21 @@ class DataType private constructor(
BaseDataType.BOOL -> targetType.base == BaseDataType.BOOL
BaseDataType.UBYTE -> targetType.base in setOf(BaseDataType.UBYTE, BaseDataType.WORD, BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.FLOAT)
BaseDataType.BYTE -> targetType.base in setOf(BaseDataType.BYTE, BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
BaseDataType.UWORD -> targetType.base in setOf(BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.FLOAT, BaseDataType.POINTER, BaseDataType.ARRAY_POINTER)
BaseDataType.UWORD -> targetType.base in setOf(BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.FLOAT, BaseDataType.POINTER) || targetType.isPointerArray
BaseDataType.WORD -> targetType.base in setOf(BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
BaseDataType.LONG -> targetType.base in setOf(BaseDataType.LONG, BaseDataType.FLOAT, BaseDataType.POINTER, BaseDataType.ARRAY_POINTER)
BaseDataType.LONG -> targetType.base in setOf(BaseDataType.LONG, BaseDataType.FLOAT, BaseDataType.POINTER) || targetType.isPointerArray
BaseDataType.FLOAT -> targetType.base in arrayOf(BaseDataType.FLOAT)
BaseDataType.STR -> targetType.base in setOf(BaseDataType.STR, BaseDataType.UWORD, BaseDataType.LONG) || (targetType.isPointer && targetType.sub==BaseDataType.UBYTE)
BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW -> targetType.base in setOf(BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW) && targetType.sub == sub
BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW -> {
if (isPointerArray) {
if (!targetType.isPointerArray) false
else if (subType != null && targetType.subType != null) subType!!.sameas(targetType.subType!!)
else if (subType == null && targetType.subType == null) pointeeSub == targetType.pointeeSub
else false
} else {
targetType.base in setOf(BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW) && targetType.sub == sub
}
}
BaseDataType.POINTER -> {
when {
targetType.base == BaseDataType.UWORD || targetType.base == BaseDataType.LONG -> true
@@ -443,7 +454,6 @@ class DataType private constructor(
else -> false
}
}
BaseDataType.ARRAY_POINTER -> targetType.base in setOf(BaseDataType.ARRAY_POINTER, targetType.base == BaseDataType.ARRAY) && targetType.sub == sub
BaseDataType.STRUCT_INSTANCE -> false // we cannot deal with actual struct instances yet in any shape or form (only getting fields from it)
BaseDataType.UNDEFINED -> false
}
@@ -457,13 +467,12 @@ class DataType private constructor(
* Note: for pointer types, size() doesn't return the size of the pointer itself
* but the size of the thing it points to.
*/
fun size(memsizer: IMemSizer): Int = if(sub!=null) {
memsizer.memorySize(sub)
} else if(subType!=null) {
subType!!.memsize(memsizer)
} else {
memsizer.memorySize(base)
}
fun size(memsizer: IMemSizer): Int = when {
base == BaseDataType.ARRAY && sub == BaseDataType.POINTER -> memsizer.memorySize(BaseDataType.POINTER)
sub!=null -> memsizer.memorySize(sub)
subType!=null -> subType!!.memsize(memsizer)
else -> memsizer.memorySize(base)
}
// ============================================================================
// DataType Properties
@@ -492,16 +501,16 @@ class DataType private constructor(
val isPointerToByte = base.isPointer && sub?.isByteOrBool==true
val isPointerToWord = base.isPointer && sub?.isWord==true
val isStructInstance = base.isStructInstance
val isPointerArray = base.isPointerArray
val isBoolArray = base.isArray && !base.isPointerArray && sub == BaseDataType.BOOL
val isByteArray = base.isArray && !base.isPointerArray && (sub == BaseDataType.UBYTE || sub == BaseDataType.BYTE)
val isUnsignedByteArray = base.isArray && !base.isPointerArray && sub == BaseDataType.UBYTE
val isSignedByteArray = base.isArray && !base.isPointerArray && sub == BaseDataType.BYTE
val isWordArray = base.isArray && !base.isPointerArray && (sub == BaseDataType.UWORD || sub == BaseDataType.WORD)
val isUnsignedWordArray = base.isArray && !base.isPointerArray && sub == BaseDataType.UWORD
val isSignedWordArray = base.isArray && !base.isPointerArray && sub == BaseDataType.WORD
val isLongArray = base.isArray && sub == BaseDataType.LONG
val isFloatArray = base.isArray && !base.isPointerArray && sub == BaseDataType.FLOAT
val isPointerArray = base == BaseDataType.ARRAY && sub == BaseDataType.POINTER
val isBoolArray = base == BaseDataType.ARRAY && sub == BaseDataType.BOOL
val isByteArray = base == BaseDataType.ARRAY && (sub == BaseDataType.UBYTE || sub == BaseDataType.BYTE)
val isUnsignedByteArray = base == BaseDataType.ARRAY && sub == BaseDataType.UBYTE
val isSignedByteArray = base == BaseDataType.ARRAY && sub == BaseDataType.BYTE
val isWordArray = (base == BaseDataType.ARRAY || base == BaseDataType.ARRAY_SPLITW) && (sub == BaseDataType.UWORD || sub == BaseDataType.WORD)
val isUnsignedWordArray = (base == BaseDataType.ARRAY || base == BaseDataType.ARRAY_SPLITW) && sub == BaseDataType.UWORD
val isSignedWordArray = (base == BaseDataType.ARRAY || base == BaseDataType.ARRAY_SPLITW) && sub == BaseDataType.WORD
val isLongArray = base == BaseDataType.ARRAY && sub == BaseDataType.LONG
val isFloatArray = base == BaseDataType.ARRAY && sub == BaseDataType.FLOAT
val isString = base == BaseDataType.STR
val isBool = base == BaseDataType.BOOL
val isFloat = base == BaseDataType.FLOAT
@@ -514,7 +523,7 @@ class DataType private constructor(
fun isSplitWordArray(memsizer: IMemSizer): Boolean {
return if(base== BaseDataType.ARRAY_SPLITW)
true
else if(base==BaseDataType.ARRAY_POINTER)
else if(base==BaseDataType.ARRAY && sub==BaseDataType.POINTER)
memsizer.POINTER_MEM_SIZE<=2u
else false
}
@@ -87,13 +87,25 @@ interface ICompilationTarget: IStringEncoding, IMemSizer {
val defaultLauncherType: CbmPrgLauncherType
val supportsBankedCalls: Boolean
/**
* True when the code generator allocates subroutine locals and per-activation intermediate values
* in a stack frame instead of static memory, so a subroutine on a call-graph cycle can have more
* than one live activation. False means two live activations would share the same storage.
*/
val supportsStackFrames: Boolean
fun initializeMemoryAreas(compilerOptions: CompilationOptions)
fun getFloatAsmBytes(num: Number): String
fun convertFloatToBytes(num: Double): List<UByte>
fun convertBytesToFloat(bytes: List<UByte>): Double
fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean)
/**
* Launches an emulator to run the compiled program artifact.
* [programFile] is the exact artifact path including its extension; launchers must use it
* verbatim, without stripping or appending one.
*/
fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean)
fun isIOAddress(address: UInt): Boolean
override fun encodeString(str: String, encoding: Encoding): List<UByte>
@@ -0,0 +1,80 @@
package prog8.code.core
import prog8.code.target.VMTarget
import java.nio.file.Path
import kotlin.io.path.extension
import kotlin.io.path.name
/** VICE/box16 monitor (symbol) list file name for an artifact stem. */
fun viceMonListName(stem: String) = "$stem.vice-mon-list"
/**
* Artifact stem of a user-supplied `-o` output file name.
* A dot at index 0 is not an extension separator: ".pgz" keeps its whole name.
* Only applied to `-o`; the legacy program stem (source name without extension) is used verbatim.
*/
fun outputStemOf(name: Path): String {
val filename = name.name
val dotIndex = filename.lastIndexOf('.')
return if (dotIndex <= 0) filename else filename.substring(0, dotIndex)
}
/**
* The final program artifact path: [CompilationOptions.outputFile] verbatim when set,
* otherwise [defaultName] plus the extension for [CompilationOptions.output], in [CompilationOptions.outputDir].
* RAW and LIBRARY both use ".bin"; AMIGAHUNK has no extension.
* On the virtual target the artifact is the IR file, so ".p8ir" is always enforced on -o.
*/
fun CompilationOptions.programFile(defaultName: String): Path {
// VMTarget's defaultOutputType = PRG is misleading: its artifact is the .p8ir file, never PRG.
// The VM identifies the IR by its extension, so -o is not verbatim here: ".p8ir" is enforced.
// Only the exact lowercase extension is recognized (same rule as -vm in CompilerMain).
if(compTarget is VMTarget) {
val ir = outputFile ?: return outputDir.resolve("$defaultName.p8ir")
return if (ir.extension=="p8ir") ir else ir.resolveSibling("${ir.name}.p8ir")
}
outputFile?.let { return it }
val filename = when(output) {
OutputType.PRG -> "$defaultName.prg"
OutputType.XEX -> "$defaultName.xex"
OutputType.RAW -> "$defaultName.bin"
OutputType.LIBRARY -> "$defaultName.bin"
OutputType.ELF -> "$defaultName.elf"
OutputType.AMIGAHUNK -> defaultName
}
return outputDir.resolve(filename)
}
/**
* Output artifact naming.
* Invariant: [asm] must resolve into the same directory as `CompilationOptions.outputDir`,
* because 64tass resolves `.binary` includes relative to the generated `.asm`.
*/
class OutputFiles(val programFile: Path, val stem: String, val dir: Path) {
fun asm() = dir.resolve("$stem.asm")
fun list() = dir.resolve("$stem.list")
fun viceMonList() = dir.resolve(viceMonListName(stem))
fun binFile() = dir.resolve("$stem.bin")
fun ir() = dir.resolve("$stem.p8ir")
fun bankedCalls() = dir.resolve("$stem.bankedcalls")
fun obj() = dir.resolve("$stem.o")
fun linkScript() = dir.resolve("$stem.link.ld")
companion object {
/**
* Builds the output file set. Without `-o` the stem is [defaultName] verbatim
* (outputStemOf is only applied to the `-o` name). With `-o` the stem is derived
* from the resolved artifact name, so it also covers the virtual target appending
* ".p8ir". [dir] is the program file's parent, or [CompilationOptions.outputDir]
* for a bare -o filename.
*/
fun of(options: CompilationOptions, defaultName: String): OutputFiles {
val programFile = options.programFile(defaultName)
val stem = options.outputFile?.let { outputStemOf(programFile) } ?: defaultName
val dir = programFile.parent ?: options.outputDir
return OutputFiles(programFile, stem, dir)
}
}
}
@@ -0,0 +1,80 @@
package prog8.code.cpu6502
// Horner ("shift-add") expansion of a multiply by a small compile-time constant, for the 6502 and
// 65C02 backends. The runtime routines prog8_math.multiply_words / multiply_bytes cost the same
// whatever the constant is (~442 and ~130 cycles), while this expansion costs O(bit length), so
// for a small constant inlining wins. It generalises the hand-written prog8_math.mul_word_N /
// mul_byte_N routines, which are themselves unrolled Horner chains (mul_word_3 is "AY = AY*2 + AY",
// one Horner step for 11).
//
// The sequences live here rather than in either backend because the register bookkeeping is easy to
// get subtly wrong and the two backends must not drift apart.
//
// The gate is on generated size, not on the constant's magnitude: cycles alone would argue for
// expanding nearly every multiply and bloating every program. Budgets are the max instruction count
// the expansion may take; the call sequence each replaces is 6 instructions.
private const val WORD_BUDGET = 32
private const val BYTE_BUDGET = 24
// --- byte ---
// Contract: A holds the multiplicand. Result in A. Only A and one scratch byte are live, so there
// is no second register to keep in sync (unlike the word case, where a bare doubling leaves the
// high byte stale in Y).
fun byteShiftAddExpansion(value: Int): List<String>? {
if (value < 3 || value > 255)
return null
val bitlen = 31 - Integer.numberOfLeadingZeros(value)
val popcount = Integer.bitCount(value)
if (1 + (bitlen-1) + 2*(popcount-1) > BYTE_BUDGET)
return null
val result = ArrayList<String>(1 + 3*bitlen)
result.add("sta P8ZP_SCRATCH_REG") // keep x for the conditional adds
for (bit in (bitlen-1) downTo 0) {
result.add("asl a") // r = r*2
if (value and (1 shl bit) != 0) { // r += x
result.add("clc")
result.add("adc P8ZP_SCRATCH_REG")
}
}
return result
}
// --- word ---
// Contract on entry: A = r.lo, Y = r.hi, P8ZP_SCRATCH_W1 = r, P8ZP_SCRATCH_W2 = x (all 16-bit,
// little-endian: byte 0 is the low byte). On exit: A = result.lo, Y = result.hi.
//
// Two things this sequence must get right, both learned the hard way:
// * the add step must store the new high byte back to W1+1 as well as to Y. Writing it only to Y
// leaves W1+1 stale, and the next step's 'rol P8ZP_SCRATCH_W1+1' would then double the wrong
// high byte.
// * a trailing bare doubling (any even constant) updates A and W1+1 but not Y, so Y needs an
// explicit reload at the end. It cannot be 'tay', because at that point A holds the low byte.
fun wordShiftAddExpansion(value: Int): List<String>? {
if (value < 3)
return null
val bitlen = 31 - Integer.numberOfLeadingZeros(value)
val popcount = Integer.bitCount(value)
if (1 + 2*(bitlen-1) + 7*(popcount-1) > WORD_BUDGET)
return null
val result = ArrayList<String>(2 + 9*bitlen)
for (bit in (bitlen-1) downTo 0) {
result.add("asl a") // r = r*2
result.add("rol P8ZP_SCRATCH_W1+1")
if (value and (1 shl bit) != 0) { // r += x
result.add("clc")
result.add("adc P8ZP_SCRATCH_W2")
result.add("sta P8ZP_SCRATCH_W1")
result.add("lda P8ZP_SCRATCH_W1+1")
result.add("adc P8ZP_SCRATCH_W2+1")
result.add("sta P8ZP_SCRATCH_W1+1")
result.add("tay")
result.add("lda P8ZP_SCRATCH_W1")
}
}
if (value and 1 == 0)
result.add("ldy P8ZP_SCRATCH_W1+1")
return result
}
@@ -2,6 +2,7 @@ package prog8.code.target
import prog8.code.core.*
import prog8.code.target.encodings.Encoder
import prog8.code.target.zp.M68kZeropage
import java.nio.file.Path
@@ -11,6 +12,7 @@ class Amiga1200Target: ICompilationTarget,
override val name = NAME
override val supportsBankedCalls = true
override val supportsStackFrames = true
override val defaultEncoding = Encoding.ISO
override val libraryPath = null
override val customLauncher = emptyList<String>()
@@ -54,7 +56,7 @@ class Amiga1200Target: ICompilationTarget,
TODO("convert bytes to float")
}
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
if(selectedEmulator!=1) {
System.err.println("The amiga1200 target only supports the main emulator (Amitools's vamos).")
return
@@ -64,12 +66,11 @@ class Amiga1200Target: ICompilationTarget,
CpuType.M68020 -> "68020"
else -> error("invalid cpu type")
}
val exeFile = programNameWithPath.resolveSibling("${programNameWithPath.fileName}")
val cmd =
listOf(
"vamos",
"--cpu", cpuStr,
exeFile.toString(),
programFile.toString(),
)
if(!quiet)
println("Launching Amitools's Vamos...")
@@ -88,20 +89,6 @@ class Amiga1200Target: ICompilationTarget,
address in 0xe80000u..0xefffffu // Zorro II autoconfig space
override fun initializeMemoryAreas(compilerOptions: CompilationOptions) {
zeropage = Amiga1200Zeropage(compilerOptions)
zeropage = M68kZeropage(compilerOptions)
}
}
private class Amiga1200Zeropage(options: CompilationOptions): Zeropage(options) {
override val SCRATCH_B1: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_REG: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_W1: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_W2: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_PTR: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
}
@@ -2,6 +2,7 @@ package prog8.code.target
import prog8.code.core.*
import prog8.code.target.encodings.Encoder
import prog8.code.target.zp.M68kZeropage
import java.nio.file.Path
@@ -11,6 +12,7 @@ class Amiga500Target: ICompilationTarget,
override val name = NAME
override val supportsBankedCalls = true
override val supportsStackFrames = true
override val defaultEncoding = Encoding.ISO
override val libraryPath = null
override val customLauncher = emptyList<String>()
@@ -58,7 +60,10 @@ class Amiga500Target: ICompilationTarget,
33 to "AmigaGuide",
34 to "ARexx",
35 to "BattClock",
36 to "BattMem"
36 to "BattMem",
// from here, non-standard libraries are listed
100 to "AsyncIO",
101 to "AHI", // ahi.device base (amiga1200 target only, 68020+, used by the low-level ahi.* LVOs)
)
}
@@ -91,7 +96,7 @@ class Amiga500Target: ICompilationTarget,
TODO("convert bytes to float")
}
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
if(selectedEmulator!=1) {
System.err.println("The amiga500 target only supports the main emulator (Amitools's vamos).")
return
@@ -101,19 +106,18 @@ class Amiga500Target: ICompilationTarget,
CpuType.M68020 -> "68020"
else -> error("invalid cpu type")
}
val exeFile = programNameWithPath.resolveSibling("${programNameWithPath.fileName}")
val cmd =
listOf(
"vamos",
"--cpu", cpuStr,
exeFile.toString(),
programFile.toString(),
)
if(!quiet)
println("Launching Amitools's Vamos...")
val pb = ProcessBuilder(cmd).inheritIO()
try {
pb.start().waitFor()
} catch(e: Exception) {
} catch(_: Exception) {
System.err.println("Cannot find 'vamos' (Amitools's Amiga emulator). Install it from: https://github.com/cnvogelg/amitools")
}
}
@@ -125,20 +129,6 @@ class Amiga500Target: ICompilationTarget,
address in 0xe80000u..0xefffffu // Zorro II autoconfig space
override fun initializeMemoryAreas(compilerOptions: CompilationOptions) {
zeropage = AmigaZeropage(compilerOptions)
zeropage = M68kZeropage(compilerOptions)
}
}
private class AmigaZeropage(options: CompilationOptions): Zeropage(options) {
override val SCRATCH_B1: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_REG: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_W1: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_W2: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_PTR: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
}
+6 -5
View File
@@ -12,6 +12,7 @@ class C128Target: ICompilationTarget,
override val name = NAME
override val supportsBankedCalls = true
override val supportsStackFrames = false
override val defaultEncoding = Encoding.PETSCII
override val libraryPath = null
override val customLauncher = emptyList<String>()
@@ -28,7 +29,7 @@ class C128Target: ICompilationTarget,
override val FLOAT_MAX_POSITIVE = Mflpt5.FLOAT_MAX_POSITIVE
override val FLOAT_MAX_NEGATIVE = Mflpt5.FLOAT_MAX_NEGATIVE
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE.toUInt()
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE
override val POINTER_MEM_SIZE = 2u
override val ARRAY_SIZE_LIMIT = 256u
override val PROGRAM_LOAD_ADDRESS = 0x1c01u
@@ -54,7 +55,7 @@ class C128Target: ICompilationTarget,
return m5.toDouble()
}
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
if(selectedEmulator!=1) {
System.err.println("The c128 target only supports the main emulator (Vice).")
return
@@ -63,9 +64,9 @@ class C128Target: ICompilationTarget,
if(!quiet)
println("\nStarting C-128 emulator x128...")
val viceMonlist = C64Target.viceMonListName(programNameWithPath.toString())
val cmdline = listOf("x128", "-silent", "-moncommands", viceMonlist,
"-autostartprgmode", "1", "-autostart-warp", "-autostart", "${programNameWithPath}.prg")
val viceMonlist = programFile.resolveSibling(viceMonListName(outputStemOf(programFile)))
val cmdline = listOf("x128", "-silent", "-moncommands", viceMonlist.toString(),
"-autostartprgmode", "1", "-autostart-warp", "-autostart", programFile.toString())
val processb = ProcessBuilder(cmdline)
if(!quiet)
processb.inheritIO()
+5 -6
View File
@@ -13,6 +13,7 @@ class C64Target: ICompilationTarget,
override val name = NAME
override val supportsBankedCalls = true
override val supportsStackFrames = false
override val defaultEncoding = Encoding.PETSCII
override val libraryPath = null
override val customLauncher = emptyList<String>()
@@ -22,8 +23,6 @@ class C64Target: ICompilationTarget,
companion object {
const val NAME = "c64"
fun viceMonListName(baseFilename: String) = "$baseFilename.vice-mon-list"
}
@@ -58,7 +57,7 @@ class C64Target: ICompilationTarget,
return m5.toDouble()
}
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
if(selectedEmulator!=1) {
System.err.println("The c64 target only supports the main emulator (Vice).")
return
@@ -68,9 +67,9 @@ class C64Target: ICompilationTarget,
if(!quiet)
println("\nStarting C-64 emulator $emulator...")
val viceMonlist = viceMonListName(programNameWithPath.toString())
val cmdline = listOf(emulator, "-silent", "-moncommands", viceMonlist,
"-autostartprgmode", "1", "-autostart-warp", "-autostart", "${programNameWithPath}.prg")
val viceMonlist = programFile.resolveSibling(viceMonListName(outputStemOf(programFile)))
val cmdline = listOf(emulator, "-silent", "-moncommands", viceMonlist.toString(),
"-autostartprgmode", "1", "-autostart-warp", "-autostart", programFile.toString())
val processb = ProcessBuilder(cmdline)
if(!quiet)
processb.inheritIO()
@@ -4,6 +4,7 @@ import prog8.code.core.*
import prog8.code.source.ImportFileSystem.expandTilde
import prog8.code.target.encodings.Encoder
import prog8.code.target.zp.ConfigurableZeropage
import prog8.code.target.zp.M68kZeropage
import java.io.IOException
import java.nio.file.Path
import java.util.*
@@ -18,6 +19,7 @@ class ConfigFileTarget(
override val supportsBankedCalls: Boolean,
override val defaultEncoding: Encoding,
override val cpu: CpuType,
override val supportsStackFrames: Boolean,
override val PROGRAM_LOAD_ADDRESS: UInt,
override val PROGRAM_MEMTOP_ADDRESS: UInt,
override val BSSHIGHRAM_START: UInt,
@@ -109,9 +111,12 @@ class ConfigFileTarget(
CpuType.valueOf("CPU$cpuString")
}
val ioAddresses = parseAddressRanges("io_regions", props)
val zpFullsafe = parseAddressRanges("zp_fullsafe", props)
val zpKernalsafe = parseAddressRanges("zp_kernalsafe", props)
val zpBasicsafe = parseAddressRanges("zp_basicsafe", props)
// The m68k has no zero page, so the zeropage settings below don't apply to an m68k
// target and are not required in its config file.
fun zpSetting(name: String) = if(cpuType.is68k) 0u else props.getInteger(name)
val zpFullsafe = if(cpuType.is68k) emptyList() else parseAddressRanges("zp_fullsafe", props)
val zpKernalsafe = if(cpuType.is68k) emptyList() else parseAddressRanges("zp_kernalsafe", props)
val zpBasicsafe = if(cpuType.is68k) emptyList() else parseAddressRanges("zp_basicsafe", props)
val libraryPath = expandTilde(Path(props.getString("library")))
if(!libraryPath.isDirectory())
@@ -140,6 +145,7 @@ class ConfigFileTarget(
props.getProperty("supports_banked_calls", "false").toBoolean(),
Encoding.entries.first { it.prefix==props.getString("encoding") },
cpuType,
props.getProperty("supports_stack_frames", cpuType.is68k.toString()).toBoolean(),
props.getInteger("load_address"),
props.getInteger("memtop"),
props.getInteger("bss_highram_start"),
@@ -153,12 +159,12 @@ class ConfigFileTarget(
if(assemblerOptionsStr=="") emptyList() else assemblerOptionsStr.split(" "),
varsAddress,
ioAddresses,
props.getInteger("zp_scratch_b1"),
props.getInteger("zp_scratch_reg"),
props.getInteger("zp_scratch_w1"),
props.getInteger("zp_scratch_w2"),
props.getInteger("zp_scratch_ptr"),
props.getInteger("virtual_registers"),
zpSetting("zp_scratch_b1"),
zpSetting("zp_scratch_reg"),
zpSetting("zp_scratch_w1"),
zpSetting("zp_scratch_w2"),
zpSetting("zp_scratch_ptr"),
zpSetting("virtual_registers"),
zpFullsafe,
zpKernalsafe,
zpBasicsafe,
@@ -178,14 +184,15 @@ class ConfigFileTarget(
override fun getFloatAsmBytes(num: Number) = TODO("floats")
override fun convertFloatToBytes(num: Double): List<UByte> = TODO("floats")
override fun convertBytesToFloat(bytes: List<UByte>): Double = TODO("floats")
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
throw IllegalArgumentException("Custom compiler target cannot automatically launch an emulator. Do this manually.")
}
override fun isIOAddress(address: UInt): Boolean = ioAddresses.any { address in it }
override fun initializeMemoryAreas(compilerOptions: CompilationOptions) {
zeropage = ConfigurableZeropage(
// the m68k has no zero page, so the zp scratch addresses in the config file don't apply there
zeropage = if(cpu.is68k) M68kZeropage(compilerOptions) else ConfigurableZeropage(
zpScratchB1, zpScratchReg, zpScratchW1, zpScratchW2, zpScratchPtr,
virtualregistersStart,
zpBasicsafe,
+5 -4
View File
@@ -12,6 +12,7 @@ class Cx16Target: ICompilationTarget,
override val name = NAME
override val supportsBankedCalls = true
override val supportsStackFrames = false
override val defaultEncoding = Encoding.PETSCII
override val libraryPath = null
override val customLauncher = emptyList<String>()
@@ -28,7 +29,7 @@ class Cx16Target: ICompilationTarget,
override val FLOAT_MAX_POSITIVE = Mflpt5.FLOAT_MAX_POSITIVE
override val FLOAT_MAX_NEGATIVE = Mflpt5.FLOAT_MAX_NEGATIVE
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE.toUInt()
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE
override val POINTER_MEM_SIZE = 2u
override val ARRAY_SIZE_LIMIT = 256u
override val PROGRAM_LOAD_ADDRESS = 0x0801u
@@ -54,7 +55,7 @@ class Cx16Target: ICompilationTarget,
return m5.toDouble()
}
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
val emulator: String
val extraArgs: List<String>
@@ -65,7 +66,7 @@ class Cx16Target: ICompilationTarget,
}
2 -> {
emulator = "box16"
extraArgs = listOf("-sym", C64Target.viceMonListName(programNameWithPath.toString()))
extraArgs = listOf("-sym", programFile.resolveSibling(viceMonListName(outputStemOf(programFile))).toString())
}
else -> {
System.err.println("Cx16 target only supports x16emu and box16 emulators.")
@@ -76,7 +77,7 @@ class Cx16Target: ICompilationTarget,
if(!quiet)
println("\nStarting Commander X16 emulator $emulator...")
val cmdline = listOf(emulator, "-scale", "2", "-rtc", "-run", "-prg", "${programNameWithPath}.prg") + extraArgs
val cmdline = listOf(emulator, "-scale", "2", "-rtc", "-run", "-prg", programFile.toString()) + extraArgs
val processb = ProcessBuilder(cmdline)
if(!quiet)
processb.inheritIO()
+6 -5
View File
@@ -12,6 +12,7 @@ class PETTarget: ICompilationTarget,
override val name = NAME
override val supportsBankedCalls = false
override val supportsStackFrames = false
override val defaultEncoding = Encoding.PETSCII
override val libraryPath = null
override val customLauncher = emptyList<String>()
@@ -27,7 +28,7 @@ class PETTarget: ICompilationTarget,
override val FLOAT_MAX_POSITIVE = Mflpt5.FLOAT_MAX_POSITIVE
override val FLOAT_MAX_NEGATIVE = Mflpt5.FLOAT_MAX_NEGATIVE
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE.toUInt()
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE
override val POINTER_MEM_SIZE = 2u
override val ARRAY_SIZE_LIMIT = 256u
override val PROGRAM_LOAD_ADDRESS = 0x0401u
@@ -53,7 +54,7 @@ class PETTarget: ICompilationTarget,
return m5.toDouble()
}
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
if(selectedEmulator!=1) {
System.err.println("The pet target only supports the main emulator (Vice).")
return
@@ -62,9 +63,9 @@ class PETTarget: ICompilationTarget,
if(!quiet)
println("\nStarting PET emulator...")
val viceMonlist = C64Target.viceMonListName(programNameWithPath.toString())
val cmdline = listOf("xpet", "-model", "4032", "-ramsize", "32", "-videosize", "40", "-silent", "-moncommands", viceMonlist,
"-autostartprgmode", "1", "-autostart-warp", "-autostart", "${programNameWithPath}.prg")
val viceMonlist = programFile.resolveSibling(viceMonListName(outputStemOf(programFile)))
val cmdline = listOf("xpet", "-model", "4032", "-ramsize", "32", "-videosize", "40", "-silent", "-moncommands", viceMonlist.toString(),
"-autostartprgmode", "1", "-autostart-warp", "-autostart", programFile.toString())
val processb = ProcessBuilder(cmdline)
if(!quiet)
processb.inheritIO()
+16 -42
View File
@@ -2,6 +2,7 @@ package prog8.code.target
import prog8.code.core.*
import prog8.code.target.encodings.Encoder
import prog8.code.target.zp.M68kZeropage
import java.nio.file.Path
/**
@@ -15,6 +16,7 @@ class Qemu68kTarget: ICompilationTarget,
override val name = NAME
override val supportsBankedCalls = false
override val supportsStackFrames = true
override val defaultEncoding = Encoding.ISO
override val libraryPath = null
override val customLauncher = emptyList<String>()
@@ -55,42 +57,27 @@ class Qemu68kTarget: ICompilationTarget,
TODO("convert bytes to float")
}
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
if(selectedEmulator!=1) {
System.err.println("The qemu68k target only supports the main emulator (Qemu).")
return
}
val elfFile = programNameWithPath.resolveSibling("${programNameWithPath.fileName}.elf")
val binFile = programNameWithPath.resolveSibling("${programNameWithPath.fileName}.bin")
val isElf = elfFile.toFile().exists()
val cpuStr = this.cpu.toString().lowercase()
val cmd = if (isElf) {
listOf(
"qemu-system-m68k",
"-M", "virt",
"-cpu", cpuStr,
"-m", "1M",
"-kernel", elfFile.toString(),
"-nographic"
)
} else if (binFile.toFile().exists()) {
val loadAddr = PROGRAM_LOAD_ADDRESS.toInt()
listOf(
"qemu-system-m68k",
"-M", "virt",
"-cpu", cpuStr,
"-m", "1M",
"-device", "loader,file=${binFile},addr=0x${loadAddr.toString(16)},cpu-num=0",
"-nographic"
)
} else {
System.err.println("No .elf or .bin file found for ${programNameWithPath.fileName}")
if(!programFile.toFile().exists()) {
System.err.println("No program file found: $programFile")
return
}
val cpuStr = this.cpu.toString().lowercase()
val cmd = listOf(
"qemu-system-m68k",
"-M", "virt",
"-cpu", cpuStr,
"-m", "1M",
"-kernel", programFile.toString(),
"-nographic"
)
if(!quiet) {
val launchMsg = if (isElf) "ELF" else "raw binary"
println("Launching QEMU (press Ctrl-A X to exit)... (from $launchMsg)")
println("Launching QEMU (press Ctrl-A X to exit)...")
}
val pb = ProcessBuilder(cmd).inheritIO()
try {
@@ -110,16 +97,3 @@ class Qemu68kTarget: ICompilationTarget,
}
}
private class M68kZeropage(options: CompilationOptions): Zeropage(options) {
override val SCRATCH_B1: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_REG: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_W1: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_W2: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_PTR: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
}
+11 -9
View File
@@ -3,9 +3,7 @@ package prog8.code.target
import prog8.code.core.*
import prog8.code.target.encodings.Encoder
import java.nio.file.Path
import kotlin.io.path.extension
import kotlin.io.path.isReadable
import kotlin.io.path.name
import kotlin.io.path.readText
class VMTarget: ICompilationTarget,
@@ -14,6 +12,7 @@ class VMTarget: ICompilationTarget,
override val name = NAME
override val supportsBankedCalls = false
override val supportsStackFrames = false
override val defaultEncoding = Encoding.ISO
override val libraryPath = null
override val customLauncher = emptyList<String>()
@@ -71,21 +70,24 @@ class VMTarget: ICompilationTarget,
return Double.fromBits(b0 or b1 or b2 or b3 or b4 or b5 or b6 or b7)
}
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
launchEmulatorWithTrace(programNameWithPath, quiet, traceEnabled = false)
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
launchEmulatorWithTrace(programFile, quiet, traceEnabled = false)
}
fun launchEmulatorWithTrace(programNameWithPath: Path, quiet: Boolean, traceEnabled: Boolean) {
/**
* Runs the given .p8ir file in the embedded virtual machine.
* The caller must pass the exact path; no fallback probing happens here, the -vm CLI flow resolves a bare name itself.
*/
fun launchEmulatorWithTrace(programFile: Path, quiet: Boolean, traceEnabled: Boolean) {
if(!quiet)
println("\nStarting Virtual Machine...")
// to not have external module dependencies in our own module, we launch the virtual machine via reflection
val vm = Class.forName("prog8.vm.VmRunner").getDeclaredConstructor().newInstance() as IVirtualMachineRunner
val withExt = if(programNameWithPath.extension=="p8ir") programNameWithPath else programNameWithPath.resolveSibling("${programNameWithPath.name}.p8ir")
if(withExt.isReadable())
vm.runProgram(withExt.readText(), quiet, traceEnabled)
if(programFile.isReadable())
vm.runProgram(programFile.readText(), quiet, traceEnabled)
else
throw java.nio.file.NoSuchFileException(withExt.name, null, "not a .p8ir file")
throw java.nio.file.NoSuchFileException(programFile.toString(), null, "not a .p8ir file")
}
override fun isIOAddress(address: UInt): Boolean = false
@@ -0,0 +1,21 @@
package prog8.code.target.zp
import prog8.code.core.CompilationOptions
import prog8.code.core.Zeropage
/**
* The m68k has no zero page, so there are no scratch locations to hand out.
* Any attempt to use one is a bug in the code generator.
*/
class M68kZeropage(options: CompilationOptions): Zeropage(options) {
override val SCRATCH_B1: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_REG: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_W1: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_W2: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
override val SCRATCH_PTR: UInt
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
}
@@ -21,6 +21,8 @@ object DummyMemsizer : IMemSizer {
BaseDataType.UWORD, BaseDataType.WORD -> numElements * 2
BaseDataType.LONG -> numElements * 4
BaseDataType.FLOAT -> numElements * 5
BaseDataType.POINTER -> numElements * 2
BaseDataType.STRUCT_INSTANCE -> numElements * (dt.subType?.memsize(this) ?: throw IllegalArgumentException("struct instance array has unresolved subtype"))
else -> throw IllegalArgumentException("invalid sub type")
}
}
@@ -30,6 +30,8 @@ class TestDataType: FunSpec({
BaseDataType.UWORD, BaseDataType.WORD -> numElements * 2
BaseDataType.LONG -> numElements * 4
BaseDataType.FLOAT -> numElements * 5
BaseDataType.POINTER -> numElements * 2
BaseDataType.STRUCT_INSTANCE -> numElements * (dt.subType?.memsize(this) ?: throw IllegalArgumentException("struct instance array has unresolved subtype"))
else -> throw IllegalArgumentException("invalid sub type")
}
}
@@ -93,10 +95,15 @@ class TestDataType: FunSpec({
test("isArray extension") {
BaseDataType.ARRAY.isArray shouldBe true
BaseDataType.ARRAY_SPLITW.isArray shouldBe true
BaseDataType.ARRAY_POINTER.isArray shouldBe true
BaseDataType.UBYTE.isArray shouldBe false
}
test("isPointerArray property") {
DataType.arrayOfPointersTo(BaseDataType.UBYTE).isPointerArray shouldBe true
DataType.arrayOfPointersTo(BaseDataType.WORD).isPointerArray shouldBe true
DataType.arrayFor(BaseDataType.UBYTE, tgt).isPointerArray shouldBe false
}
test("isPointer extension") {
BaseDataType.POINTER.isPointer shouldBe true
BaseDataType.UBYTE.isPointer shouldBe false
@@ -19,7 +19,7 @@ import prog8.code.target.zp.PETZeropage
/**
* Comprehensive tests for Zeropage free list configurations across all targets.
* Tests for Zeropage free list configurations across all targets.
*
* Tests verify that for each target and ZeropageType combination:
* - The exact list of free locations is correct
@@ -6,6 +6,8 @@ import prog8.code.assembly.AssemblyProgram6502
import prog8.code.assembly.IAssemblyProgram
import prog8.code.ast.*
import prog8.code.core.*
import prog8.code.cpu6502.byteShiftAddExpansion
import prog8.code.cpu6502.wordShiftAddExpansion
import prog8.code.source.ImportFileSystem
import prog8.code.source.SourceCode
import prog8.code.target.Cx16Target
@@ -45,8 +47,36 @@ class AsmGen6502Internal (
preassignedCallSiteIds: Map<PtAsmSub, UByte> = emptyMap()
) {
internal val optimizedByteMultiplications = setOf(3,5,6,7,9,10,11,12,13,14,15,20,25,40,50,80,100)
internal val optimizedWordMultiplications = setOf(3,5,6,7,9,10,12,15,20,25,40,50,80,100,320,640)
// 40, 50, 80 and 100 are deliberately absent: prog8_math's mul_byte_40/50/80/100 are wrong.
// They mask the multiplicand with `and #7` or `and #3` and index a small table, which only
// works if x*k mod 256 has that period. The true period is 256/gcd(k,256), i.e. 32, 128, 16
// and 64 entries respectively, so the tables are far too small (e.g. mul_byte_100 returns
// (x&3)*100, giving 0 for x=4 where the answer is 144). The generic expansion below computes
// them correctly in 8-11 instructions.
internal val optimizedByteMultiplications = setOf(3,5,6,7,9,10,11,12,13,14,15,20,25)
// 320 and 640 are deliberately absent: mul_word_320/640 compute (low byte of x)*320/*640 and
// ignore the multiplicand's high byte, so they are wrong for any x >= 256 (the comment
// "msb in Y doesn't matter" is false). The generic expansion handles them in 27-29 instructions.
internal val optimizedWordMultiplications = setOf(3,5,6,7,9,10,12,15,20,25,40,50,80,100)
// Thin wrappers over the shared expansion in codeCore, so this backend and codeGenNew6502 emit
// an identical sequence. Works on the raw bit pattern, exactly like the hand-written
// prog8_math.mul_word_N / mul_byte_N routines, so signed and unsigned agree because both
// truncate mod 2^n and no sign extension is applied afterwards.
internal fun emitWordShiftAdd(value: Int): Boolean {
val expansion = wordShiftAddExpansion(value) ?: return false
out(" sta P8ZP_SCRATCH_W2 | sty P8ZP_SCRATCH_W2+1") // x
out(" sta P8ZP_SCRATCH_W1 | sty P8ZP_SCRATCH_W1+1") // r = x
expansion.forEach { out(" $it") }
return true
}
internal fun emitByteShiftAdd(value: Int): Boolean {
val expansion = byteShiftAddExpansion(value) ?: return false
expansion.forEach { out(" $it") }
return true
}
internal val loopEndLabels = ArrayDeque<String>()
private val zeropage = options.compTarget.zeropage
private val allocator = VariableAllocator(symbolTable, options, errors, program)
@@ -80,7 +110,7 @@ class AsmGen6502Internal (
programGen.generate()
if(errors.noErrors()) {
val output = options.outputDir.resolve("${program.name}.asm")
val output = OutputFiles.of(options, program.name).asm()
val asmLines = assembly.toMutableList()
if(options.compTarget.name==Cx16Target.NAME) {
scanInvalid65816instructions(asmLines)
@@ -100,7 +130,7 @@ class AsmGen6502Internal (
if(options.dumpVariables)
dumpVariables()
return AssemblyProgram6502(program.name, options.outputDir, options.compTarget)
return AssemblyProgram6502(program.name, options, options.compTarget)
} else {
errors.report()
return null
@@ -287,12 +317,12 @@ class AsmGen6502Internal (
return Pair("#<$label", "#>$label")
} else if (e.value != null) {
val value = e.value!!.toInt()
return Pair("#<${(value and 0xff).toHex()}", "#>${((value shr 8) and 0xff).toHex()}")
return Pair("#<${value.toHex()}", "#>${value.toHex()}")
}
}
if (e is PtNumber) {
val value = e.number.toInt()
return Pair("#<${(value and 0xff).toHex()}", "#>${((value shr 8) and 0xff).toHex()}")
return Pair("#<${value.toHex()}", "#>${value.toHex()}")
}
if (e is PtAddressOf && e.identifier != null && !e.isFromArrayElement && e.dereference == null) {
var symbol = asmVariableName(e.identifier!!)
@@ -1982,6 +2012,16 @@ $repeatLabel""")
return
}
if(sub.signature.returns.isEmpty()) {
// a void subroutine may return the result of a void function call (e.g. "return voidfunc(args)"):
// perform the call for its side effects, then return
val fcall = ret.children.single() as? PtFunctionCall
?: throw AssemblyError("void subroutine can only return the result of a void function call ${ret.position}")
functioncallAsmGen.translateFunctionCall(fcall)
out(" rts")
return
}
val returnDt = sub.signature.returns.single()
if (returnDt.isNumericOrBool || returnDt.isPointer) {
assignExpressionToRegister(returnvalue, returnRegs.single().first.registerOrPair!!, returnDt.isSigned)
@@ -2016,14 +2056,19 @@ $repeatLabel""")
val tgt = AsmAssignTarget(TargetStorageKind.REGISTER, this, targetDt, null, it.first.position, register = it.second.first.registerOrPair!!)
assignExpressionTo(it.first as PtExpression, tgt)
}
// if the float is not the first value, assign it now: the float load (MOVFM) clobbers A/Y,
// and the first non-float value is passed in AY, so it must be loaded after the float
if(floatIdx > 0 && floatAssign != null) {
assignExpressionToRegister(floatAssign.first as PtExpression, floatAssign.second.first.registerOrPair!!, false)
}
if(nonFloatAssigns.isNotEmpty()) {
nonFloatAssigns.first().also {
assignExpressionToRegister(it.first as PtExpression, it.second.first.registerOrPair!!, (it.first as PtExpression).type.isSigned)
}
}
// Assign float return LAST (so FAC1 has the correct value when we RTS)
if(floatAssign != null) {
if(floatIdx == 0 && floatAssign != null) {
assignExpressionToRegister(floatAssign.first as PtExpression, floatAssign.second.first.registerOrPair!!, false)
}
}
@@ -2048,11 +2093,17 @@ $repeatLabel""")
throw AssemblyError("%asmbinary inside non-library/non-filesystem module not yet supported")
val sourcePath = Path(incbin.definingBlock()!!.source.origin)
val includedPath = sourcePath.resolveSibling(incbin.file)
val pathForAssembler = options.outputDir // #54: 64tass needs the path *relative to the .asm file*
.sanitize()
.relativize(includedPath.sanitize())
.normalize() // avoid assembler warnings (-Wportable; only some, not all)
.toString().replace('\\', '/')
val pathForAssembler = try {
options.outputDir // #54: 64tass needs the path *relative to the .asm file*
.sanitize()
.relativize(includedPath.sanitize())
.normalize() // avoid assembler warnings (-Wportable; only some, not all)
.toString().replace('\\', '/')
} catch (_: IllegalArgumentException) {
// Path.relativize throws when the two paths have different roots (different drive/volume)
errors.err("cannot determine relative path for %asmbinary file '${incbin.file}' (at ${includedPath}): it must be on the same drive/volume as the generated assembly in ${options.outputDir}", incbin.position)
return
}
out(" .binary \"$pathForAssembler\" $offset $length")
}
@@ -49,7 +49,7 @@ internal fun optimizeAssembly(lines: MutableList<String>, machine: ICompilationT
var linesByFourteen = getLinesBy(pretrimmed, lines, 14)
linesByFourteen = runPass(optimizeSameAssignments(linesByFourteen, machine, symbolTable), 14, linesByFourteen)
linesByFourteen = runPass(optimizeSamePointerIndexingAndUselessBeq(linesByFourteen), 14, linesByFourteen)
linesByFourteen = runPass(optimizeSamePointerIndexingAndUselessBeq(linesByFourteen, machine, symbolTable), 14, linesByFourteen)
linesByFourteen = runPass(optimizeAddWordToSameVariableOrExtraRegisterLoadInWordStore(linesByFourteen), 14, linesByFourteen)
if(!modified)
@@ -412,7 +412,96 @@ private fun optimizeSameAssignments(
return mods
}
private fun optimizeSamePointerIndexingAndUselessBeq(linesByFourteen: Sequence<List<TrimmedLine>>): List<Modification> {
private const val NO_MEMORY_WRITE = ""
private const val UNKNOWN_WRITE_TARGET = "\u0000"
/** Mnemonics that can change memory (direct stores plus read-modify-write). */
private val memoryWritingMnemonics = setOf(
"sta", "stx", "sty", "stz", "inc", "dec", "asl", "lsr", "rol", "ror",
"and", "ora", "eor", "adc", "sbc", "bit"
)
/** Control transfers that can reach the code after them with a different Y, or change it. */
private val flowMnemonics = setOf(
"jsr", "rts", "rti", "jmp", "bra", "brk",
"bcc", "bcs", "beq", "bmi", "bne", "bpl", "bvc", "bvs", "bbs", "bbr"
)
private fun TrimmedLine.mnemonic(): String =
instruction.trimStart().takeWhile { !it.isWhitespace() }.lowercase()
/** True if the line carries a label, with or without an instruction following it. */
private fun TrimmedLine.hasLabel(): Boolean {
if (haslabelPretrimmed(trimmed)) return true
return trimmed.isNotEmpty() && (trimmed[0] == '+' || trimmed[0] == '-')
}
/** The memory operand this instruction writes, with any +offset and ,index suffix stripped.
* Returns [NO_MEMORY_WRITE] for immediate and implied forms, and [UNKNOWN_WRITE_TARGET] for
* indirect forms, where the destination cannot be determined statically. */
private fun TrimmedLine.memoryWriteBase(): String {
val op = instruction.extractOperandTrimmed()
if (op.isEmpty() || op.startsWith("#") || op == "a") return NO_MEMORY_WRITE
if (op.startsWith("(")) return UNKNOWN_WRITE_TARGET
return op.removeSuffix(",y").removeSuffix(",x").substringBefore('+')
}
/** True if this line may write to [base]. Conservative: an indirect store is assumed to. */
private fun TrimmedLine.mayWriteBase(base: String): Boolean {
if (mnemonic() !in memoryWritingMnemonics) return false
val target = memoryWriteBase()
if (target == NO_MEMORY_WRITE) return false
if (target == UNKNOWN_WRITE_TARGET) return true
return target.equals(base, ignoreCase = true)
}
/** True if [operand] is safe to read more than once: an immediate, or memory that is not an
* IO location (an IO read can return a different value without the CPU having written it). */
private fun isNonVolatileIndexOperand(indexLine: TrimmedLine, machine: ICompilationTarget, symbolTable: SymbolTable): Boolean {
val operand = indexLine.instruction.extractOperandTrimmed()
if (operand.isEmpty() || operand.startsWith("#")) return true
val addr = getAddressArg(indexLine.instruction, symbolTable)
return if (addr == null) !looksLikeIOAddress(operand, machine)
else !machine.isIOAddress(addr)
}
/** Decides whether the index reload at [reloadIdx] inside [window] can be deleted.
*
* The reload is redundant only if Y still holds the value it would have loaded. That requires
* the reload to load the very same operand as the original index load, and none of the
* following in between:
* - modifies Y
* - writes the index operand itself (so `ldy i / lda arr,y / inc i / ldy i / sta arr,y` keeps
* its reload: without it the store lands one element earlier)
* - is a call or control transfer, since a callee can clobber Y and another path can arrive
* with a different Y
* - carries a label, for the same reason
* The index operand must also not be a volatile IO location.
*/
private fun canDropIndexReload(
window: List<TrimmedLine>,
reloadIdx: Int,
indexLine: TrimmedLine,
machine: ICompilationTarget,
symbolTable: SymbolTable
): Boolean {
if (reloadIdx <= 1) return false
val operand = indexLine.instruction.extractOperandTrimmed()
// the reload has to load the same index, or Y holds a different one and is not redundant
if (window[reloadIdx].instruction.extractOperandTrimmed() != operand) return false
val base = if (operand.startsWith("#") || operand.isEmpty()) NO_MEMORY_WRITE
else operand.removeSuffix(",y").removeSuffix(",x").substringBefore('+')
for (idx in 1 until reloadIdx) {
val line = window[idx]
if (line.instruction.modifiesYRegister()) return false
if (line.mnemonic() in flowMnemonics) return false
if (line.hasLabel()) return false
if (base != NO_MEMORY_WRITE && line.mayWriteBase(base)) return false
}
return isNonVolatileIndexOperand(indexLine, machine, symbolTable)
}
private fun optimizeSamePointerIndexingAndUselessBeq(linesByFourteen: Sequence<List<TrimmedLine>>, machine: ICompilationTarget, symbolTable: SymbolTable): List<Modification> {
// Optimize same pointer indexing where for instance we load and store to the same ptr index in Y
// if Y isn't modified in between we can omit the second LDY:
@@ -436,20 +525,38 @@ private fun optimizeSamePointerIndexingAndUselessBeq(linesByFourteen: Sequence<L
val secondvalue = f2.extractOperandTrimmed()
val fourthvalue = f4.extractOperandTrimmed()
val fifthvalue = f5.extractOperandTrimmed()
if(!f3.modifiesYRegister() && firstvalue==fourthvalue && secondvalue==fifthvalue && secondvalue.endsWith(",y") && fifthvalue.endsWith(",y")) {
if(secondvalue==fifthvalue && secondvalue.endsWith(",y") && fifthvalue.endsWith(",y") &&
canDropIndexReload(lines, 3, lines[0], machine, symbolTable)) {
mods.add(Modification(lines[3].index, true, null))
}
}
if(f1.startsWith("ldy ") && f2.startsWith("lda ") && f5.startsWith("ldy ") && f6.startsWith("sta ")) {
val firstvalue = f1.extractOperandTrimmed()
val secondvalue = f2.extractOperandTrimmed()
val fifthvalue = f5.extractOperandTrimmed()
val sixthvalue = f6.extractOperandTrimmed()
if(!f3.modifiesYRegister() && !f4.modifiesYRegister() && firstvalue==fifthvalue && secondvalue==sixthvalue && secondvalue.endsWith(",y") && sixthvalue.endsWith(",y")) {
if(secondvalue==sixthvalue && secondvalue.endsWith(",y") && sixthvalue.endsWith(",y") &&
canDropIndexReload(lines, 4, lines[0], machine, symbolTable)) {
mods.add(Modification(lines[4].index, true, null))
}
}
// Relaxed variant: two consecutive indexed accesses, not necessarily a matched load/store
// pair. Only the index reload has to be the same, and nothing in between may disturb it.
// ldy V
// lda A,y
// ... <-- does not modify Y, does not write V, no call or label
// ldy V <-- can be removed
// lda B,y
if(f1.startsWith("ldy ") && f2.startsWith("lda ") && f4.startsWith("ldy ") && f5.startsWith("lda ") &&
f2.endsWith(",y") && f5.endsWith(",y") &&
canDropIndexReload(lines, 3, lines[0], machine, symbolTable)) {
mods.add(Modification(lines[3].index, true, null))
}
if(f1.startsWith("ldy ") && f2.startsWith("lda ") && f5.startsWith("ldy ") && f6.startsWith("lda ") &&
f2.endsWith(",y") && f6.endsWith(",y") &&
canDropIndexReload(lines, 4, lines[0], machine, symbolTable)) {
mods.add(Modification(lines[4].index, true, null))
}
/*
beq +
@@ -20,6 +20,25 @@ import prog8.codegen.cpu6502.assignment.*
translateFunctioncall(fcall, null, discardResult = true)
}
/**
* Several builtins can only produce their result in a CPU register, and which one that is
* depends on the width of the result: the byte-returning ones (lsb/msb) take a single register,
* the word-returning ones (lsw/msw) and popw take a register pair, and the builtin picks the
* specific register. [hardwareReg] is the register to fall back on when the caller has no
* preference, or when it asked for something the builtin cannot deliver to directly.
*
* The caller's requested register is honoured when it is a real CPU register. A CX16 virtual
* register (for example an asmsub parameter declared as @R0) is not: those are memory locations,
* so the builtin produces its result in [hardwareReg] and the caller's assignment then stores it
* into the virtual register. Passing the virtual register through would make the builtin reject
* it as an invalid result register.
*
* (funcPeekW and funcMkword already handle virtual registers themselves.)
*/
private fun withResultRegister(requested: RegisterOrPair?, hardwareReg: RegisterOrPair, compute: (RegisterOrPair) -> Array<RegisterOrPair>): Array<RegisterOrPair> =
if (requested != null && requested !in Cx16VirtualRegisters) compute(requested)
else compute(hardwareReg)
private fun translateFunctioncall(fcall: PtFunctionCall, firstReturnRegister: RegisterOrPair?, discardResult: Boolean): Array<RegisterOrPair> {
// Note: returns the actual register(s) that the return value is in, so the caller should still make sure to place it into the actual correct destination.
@@ -31,12 +50,12 @@ import prog8.codegen.cpu6502.assignment.*
val sscope = fcall.definingISub()
return when (fcall.name) {
"msw" -> funcMsw(fcall, firstReturnRegister ?: RegisterOrPair.AY)
"lsw" -> funcLsw(fcall, firstReturnRegister ?: RegisterOrPair.AY)
"msb" -> funcMsb(fcall, false, firstReturnRegister ?: RegisterOrPair.A)
"msb__long" -> funcMsb(fcall, true, firstReturnRegister ?: RegisterOrPair.A)
"lsb" -> funcLsb(fcall, false, firstReturnRegister ?: RegisterOrPair.A)
"lsb__long" -> funcLsb(fcall,true, firstReturnRegister ?: RegisterOrPair.A)
"msw" -> withResultRegister(firstReturnRegister, RegisterOrPair.AY) { funcMsw(fcall, it) }
"lsw" -> withResultRegister(firstReturnRegister, RegisterOrPair.AY) { funcLsw(fcall, it) }
"msb" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcMsb(fcall, false, it) }
"msb__long" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcMsb(fcall, true, it) }
"lsb" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcLsb(fcall, false, it) }
"lsb__long" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcLsb(fcall, true, it) }
"mkword" -> funcMkword(fcall, firstReturnRegister ?: RegisterOrPair.AY)
"mklong", "mklong2" -> funcMklong(fcall, firstReturnRegister ?: RegisterOrPair.R14R15)
"clamp__byte", "clamp__ubyte" -> funcClamp(fcall)
@@ -98,8 +117,8 @@ import prog8.codegen.cpu6502.assignment.*
"pushw" -> funcPushW(fcall)
"pushl" -> funcPushL(fcall)
"pushf" -> funcPushF(fcall)
"pop" -> funcPop(firstReturnRegister ?: RegisterOrPair.A)
"popw" -> funcPopW(firstReturnRegister ?: RegisterOrPair.AY)
"pop" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcPop(it) }
"popw" -> withResultRegister(firstReturnRegister, RegisterOrPair.AY) { funcPopW(it) }
"popl" -> funcPopL()
"popf" -> funcPopF()
else -> throw AssemblyError("sizeof must have been replaced with a constant ${fcall.position}")
@@ -247,11 +266,15 @@ import prog8.codegen.cpu6502.assignment.*
}
BaseDataType.UWORD -> {
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.AY)
// prog8_math.square does its own range check and falls back to the general
// multiply_words for values outside -255..255, so the fast path stays valid.
asmgen.out(" jsr prog8_math.square") // result is in AY
return arrayOf(RegisterOrPair.AY)
}
BaseDataType.LONG -> {
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.R14R15, true)
// prog8_math.square_long does its own range check and falls back to the general
// multiply_longs when the long doesn't fit in a signed 16-bit word.
asmgen.out(" jsr prog8_math.square_long") // result is in R14:R15
return arrayOf(RegisterOrPair.R14R15)
}
@@ -426,7 +449,7 @@ import prog8.codegen.cpu6502.assignment.*
.byte $constBank""")
} else {
if(asmgen.options.romable)
TODO("non-const callfar (jsrfar) yet that's usable in ROM ${fcall.position}")
asmgen.errors.err("non-const callfar (jsrfar) has no ROMable code generation", fcall.position)
// self-modifying code: set jsrfar arguments
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A) // bank
asmgen.out(" sta (++)+0")
@@ -483,7 +506,7 @@ import prog8.codegen.cpu6502.assignment.*
.byte $constBank""")
} else {
if(asmgen.options.romable)
TODO("non-const callfar2 (jsrfar) yet that's usable in ROM ${fcall.position}")
asmgen.errors.err("non-const callfar2 (jsrfar) has no ROMable code generation", fcall.position)
// self-modifying code: set jsrfar arguments
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A) // bank
asmgen.out(" sta (++)+0")
@@ -2065,6 +2088,154 @@ import prog8.codegen.cpu6502.assignment.*
}
}
private fun funcClampWordConst(minExpr: PtExpression, maxExpr: PtExpression, minOp: Pair<String, String>, maxOp: Pair<String, String>, signed: Boolean) {
// value is already in AY on entry, result in AY
val minVal = (asmgen.unwrapCasts(minExpr) as? PtNumber)?.number?.toInt()
val maxVal = (asmgen.unwrapCasts(maxExpr) as? PtNumber)?.number?.toInt()
val typeMin = if (signed) -32768 else 0
val typeMax = if (signed) 32767 else 65535
val oneSided = minVal != null && minVal == typeMin || maxVal != null && maxVal == typeMax
val fullRange = minVal != null && maxVal != null && minVal == typeMin && maxVal == typeMax
when {
fullRange -> {
// no-op: the value in AY is already the clamped result
}
signed && minVal == typeMin -> {
// upper clamp only
emitSignedWordClampProlog()
emitSignedWordUpperCompare(maxOp)
emitWordLoadValueFromW1()
}
signed && maxVal == typeMax -> {
// lower clamp only
emitSignedWordClampProlog()
emitSignedWordLowerCompare(minOp)
}
signed -> {
emitSignedWordClampProlog()
emitSignedWordUpperCompare(maxOp)
emitSignedWordLowerCompare(minOp)
}
!oneSided -> {
// uword, two-sided clamp
emitWordUpperClamp(maxOp)
emitWordLowerClamp(minOp)
}
minVal == typeMin -> {
// uword, upper clamp only
emitWordUpperClamp(maxOp)
}
else -> {
// uword, lower clamp only
emitWordLowerClamp(minOp)
}
}
}
private fun emitSignedWordClampProlog() {
asmgen.out(listOf(
" sta P8ZP_SCRATCH_W1",
" sty P8ZP_SCRATCH_W1+1"
).joinToString("\n"))
}
private fun emitWordLoadValueFromW1() {
asmgen.out(listOf(
" lda P8ZP_SCRATCH_W1",
" ldy P8ZP_SCRATCH_W1+1"
).joinToString("\n"))
}
// signed compare of max/min (16-bit immediates) against the running value in P8ZP_SCRATCH_W1.
// The result of the two-sided compare is the high byte of (bound - value), with the
// overflow-compensated sign bit set, mirroring the func_clamp_word sequence.
private fun emitSignedWordUpperCompare(maxOp: Pair<String, String>) {
// if value > max: running value becomes max
val keep = asmgen.makeLabel("clampkeep")
val done = asmgen.makeLabel("clampdone")
asmgen.out(listOf(
" ldy ${maxOp.second}",
" lda ${maxOp.first}",
" cmp P8ZP_SCRATCH_W1",
" tya",
" sbc P8ZP_SCRATCH_W1+1",
" bvc $keep",
" eor #\$80",
keep,
" bpl $done",
" lda ${maxOp.first}",
" ldy ${maxOp.second}",
" sta P8ZP_SCRATCH_W1",
" sty P8ZP_SCRATCH_W1+1",
done
).joinToString("\n"))
}
private fun emitSignedWordLowerCompare(minOp: Pair<String, String>) {
// if value < min: result becomes min, otherwise the running value (in W1)
val keep = asmgen.makeLabel("clampkeep")
val set = asmgen.makeLabel("clampset")
val done = asmgen.makeLabel("clampdone")
asmgen.out(listOf(
" ldy ${minOp.second}",
" lda ${minOp.first}",
" cmp P8ZP_SCRATCH_W1",
" tya",
" sbc P8ZP_SCRATCH_W1+1",
" bvc $keep",
" eor #\$80",
keep,
" bpl $set",
" lda P8ZP_SCRATCH_W1",
" ldy P8ZP_SCRATCH_W1+1",
" jmp $done",
set,
" lda ${minOp.first}",
" ldy ${minOp.second}",
done
).joinToString("\n"))
}
private fun emitWordUpperClamp(maxOp: Pair<String, String>) {
// uword: clamp the value in AY down to maxOp (result = min(value, max))
val keep = asmgen.makeLabel("clampkeep")
val set = asmgen.makeLabel("clampset")
asmgen.out(listOf(
" cpy ${maxOp.second}",
" bcc $keep",
" bne $set",
" cmp ${maxOp.first}",
" bcc $keep",
" beq $keep",
set,
" lda ${maxOp.first}",
" ldy ${maxOp.second}",
keep
).joinToString("\n"))
}
private fun emitWordLowerClamp(minOp: Pair<String, String>) {
// uword: clamp the value in AY up to minOp (result = max(value, min))
val keep = asmgen.makeLabel("clampkeep")
val set = asmgen.makeLabel("clampset")
val done = asmgen.makeLabel("clampdone")
asmgen.out(listOf(
" cpy ${minOp.second}",
" bcc $set",
" bne $keep",
" cmp ${minOp.first}",
" bcc $set",
" beq $keep",
keep,
" jmp $done",
set,
" lda ${minOp.first}",
" ldy ${minOp.second}",
done
).joinToString("\n"))
}
private fun funcClamp(fcall: PtFunctionCall): Array<RegisterOrPair> {
val signed = fcall.type.isSigned
val zpCheck = { name: String -> asmgen.isZpVar(name) }
@@ -2099,6 +2270,17 @@ import prog8.codegen.cpu6502.assignment.*
return arrayOf(RegisterOrPair.A)
}
fcall.type.isWord -> {
val minOp = asmgen.getStaticAddressLowHigh(fcall.args[1])
val maxOp = asmgen.getStaticAddressLowHigh(fcall.args[2])
val minIsConst = asmgen.unwrapCasts(fcall.args[1]) !is PtIdentifier
val maxIsConst = asmgen.unwrapCasts(fcall.args[2]) !is PtIdentifier
if (minOp != null && maxOp != null && minIsConst && maxIsConst) {
// both bounds are compile-time constants (numeric literals or fixed label addresses):
// emit a small inline clamp instead of evaluating bounds into scratch + jsr func_clamp_word/uword
assignAsmGen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.AY, signed) // value
funcClampWordConst(fcall.args[1], fcall.args[2], minOp, maxOp, signed)
return arrayOf(RegisterOrPair.AY)
}
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck) && fcall.args[2].isSimple(zpCheck)) {
assignAsmGen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W1", fcall.args[1].type) // minimum
assignAsmGen.assignExpressionToVariable(fcall.args[2], "P8ZP_SCRATCH_W2", fcall.args[2].type) // maximum
@@ -396,24 +396,27 @@ internal class ProgramAndVarsGen(
asmgen.out("; struct types")
symboltable.allStructTypes().distinctBy { it.name }.forEach { structtype ->
val paramFields = structtype.fields.filter { !it.isArray }
val structargs = paramFields.indices.joinToString(",") { "f$it" }
asmgen.out("${structtype.scopedNameString} .struct $structargs\n")
var paramIdx = 0
for(field in structtype.fields) {
val type = asmTypeString(field.type)
if(field.isArray) {
val anonymous = List(field.arraySize!!) { "?" }.joinToString(",")
asmgen.out("p8v_${field.name} $type $anonymous")
} else {
asmgen.out("p8v_${field.name} $type \\f$paramIdx")
paramIdx++
}
if(structtype.isUnion) {
// NB: .union takes no constructor parameters, unlike .struct
asmgen.out("${structtype.scopedNameString} .union\n")
emitStructDefFields(structtype) { "?" }
asmgen.out(" .endunion\n")
asmgen.out("${structtype.scopedNameString}_size=${structtype.size}\n")
} else {
val paramFields = structtype.fields.filter { !it.isArray }
val structargs = paramFields.indices.joinToString(",") { "f$it" }
asmgen.out("${structtype.scopedNameString} .struct $structargs\n")
var paramIdx = 0
emitStructDefFields(structtype) { "\\f${paramIdx++}" }
asmgen.out(" .endstruct\n")
asmgen.out("${structtype.scopedNameString}_size=${structtype.size}\n")
}
asmgen.out(" .endstruct\n")
}
val (instancesNoInit, instances) = symboltable.allStructInstances().partition { it.initialValues.isEmpty() }
val (instancesNoInit, instances) = symboltable.allStructInstances().partition {
val structtype = symboltable.lookup(it.structName) as StStruct
it.initialValues.isEmpty() || structtype.isUnion
}
asmgen.out("; struct instances without initialization values, as BSS zeroed at startup\n")
asmgen.out(" .section BSS\n")
asmgen.out("${StStructInstanceBlockName}_bss .block\n")
@@ -454,6 +457,27 @@ internal class ProgramAndVarsGen(
asmgen.out(" .send STRUCTINSTANCES\n")
}
private fun emitStructDefFields(structtype: StStruct, scalarValue: () -> String) {
for(field in structtype.fields) {
val effectiveDt = if(field.isArray) field.type.elementType() else field.type
if(effectiveDt.isFloat) {
val floatSize = compTarget.FLOAT_MEM_SIZE.toInt()
if(field.isArray)
asmgen.out("p8v_${field.name} .fill ${floatSize*field.arraySize!!}, ?")
else
asmgen.out("p8v_${field.name} .fill $floatSize, ${scalarValue()}")
} else {
val type = asmTypeString(field.type)
if(field.isArray) {
val anonymous = List(field.arraySize!!) { "?" }.joinToString(",")
asmgen.out("p8v_${field.name} $type $anonymous")
} else {
asmgen.out("p8v_${field.name} $type ${scalarValue()}")
}
}
}
}
internal fun translateAsmSubroutine(sub: PtAsmSub) {
if(sub.inline) {
return // subroutine gets inlined at call site.
@@ -799,9 +823,6 @@ internal class ProgramAndVarsGen(
asmgen.out("${variable.name}\t.fill $amount * $elementSize ; array of $amount ${variable.dt.elementType()} ")
}
dt.isPointer -> asmgen.out("${variable.name}\t.word ?") // a pointer is just an uword address
dt.isPointerArray -> {
TODO("pointers are not supported yet for uninitialized array ${variable.astNode?.position}")
}
else -> {
throw AssemblyError("weird dt")
}
@@ -136,6 +136,12 @@ internal class VariableAllocator(private val symboltable: SymbolTable,
private fun computeUsageScores(vars: Collection<StStaticVariable>): Map<String, Int> {
val scores = mutableMapOf<String, Int>()
// Note: by the time codegen runs, the simple-AST identifier names are already fully scoped
// AND symbol-prefixed ("p8b_maze.p8s_solve.p8v_cx"), so a plain lookup by PtIdentifier.name
// correctly matches the scopedNameString keys used here. This looks like a name/scope
// mismatch bug but is not: resolving identifiers to their enclosing scope instead (or
// keying this map by the bare variable name) silently makes every score zero, which
// degenerates the sort in allocateZeropageVariables() into a stable no-op.
val varInfo = vars.associateBy { it.scopedNameString }
var loopDepth = 0
@@ -68,10 +68,15 @@ internal class AssignmentAsmGen(
// Check if we need to save A register before extracting status flags
// (flag extraction functions like assignCarryFlagResult overwrite A)
val hasByteInA = registersResults.any { (ret, _) ->
// A holds the low byte of a byte/bool return in @A, and of a word return in @AX/@AY
val byteInA = registersResults.any { (ret, _) ->
ret.type.isByteOrBool && ret.register.registerOrPair == RegisterOrPair.A
}
if(hasByteInA && statusFlagResults.isNotEmpty()) {
val wordLowByteInA = registersResults.any { (ret, _) ->
ret.type.isWord && ret.register.registerOrPair in setOf(RegisterOrPair.AX, RegisterOrPair.AY)
}
val hasAInResult = byteInA || wordLowByteInA
if(hasAInResult && statusFlagResults.isNotEmpty()) {
asmgen.out(" pha") // Save A (contains return value) before flag extraction
}
@@ -101,7 +106,7 @@ internal class AssignmentAsmGen(
}
// Restore A if we saved it for flag extraction
if(hasByteInA && statusFlagResults.isNotEmpty()) {
if(hasAInResult && statusFlagResults.isNotEmpty()) {
asmgen.out(" pla")
}
@@ -2,6 +2,8 @@ package prog8.codegen.cpu6502.assignment
import prog8.code.ast.*
import prog8.code.core.*
import prog8.code.cpu6502.byteShiftAddExpansion
import prog8.code.cpu6502.wordShiftAddExpansion
import prog8.codegen.cpu6502.AsmGen6502Internal
import prog8.codegen.cpu6502.VariableAllocator
import prog8.codegen.cpu6502.toLongHex
@@ -119,6 +121,96 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
}
}
private fun inplaceModificationPointerField(target: AsmAssignTarget, operator: String, value: AsmAssignSource) {
// the pointer to the target field is materialized in the shared scratch pointer
// P8ZP_SCRATCH_PTR because the pointer variable isn't in the zeropage (or the
// deref chain is too complex). Evaluating the source value may clobber that
// scratch pointer, so we operate on a temporary variable and preserve the
// address on the hardware stack, mirroring inplaceModificationIndexedPointerField.
val elementDt = target.datatype
if(!elementDt.isByteOrBool && !elementDt.isWord && !elementDt.isLong)
throw AssemblyError("inplace modification of pointer field for type $elementDt not supported ${target.position}")
val ptrTarget = PtrTarget(target)
val (zpPtrVar, offset) = ptrgen.deref(ptrTarget.pointer)
require(zpPtrVar=="P8ZP_SCRATCH_PTR")
val elementTemp = asmgen.createTempVarReused(elementDt.base, false, target.origAstTarget ?: target.pointer!!)
if(elementDt.isByteOrBool) {
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
sta $elementTemp""")
} else if(elementDt.isWord) {
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
sta $elementTemp
iny
lda ($zpPtrVar),y
sta $elementTemp+1""")
} else {
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
sta $elementTemp
iny
lda ($zpPtrVar),y
sta $elementTemp+1
iny
lda ($zpPtrVar),y
sta $elementTemp+2
iny
lda ($zpPtrVar),y
sta $elementTemp+3""")
}
// save the computed address on the hardware stack so later code can't clobber it
asmgen.out("""
lda $zpPtrVar
pha
lda $zpPtrVar+1
pha""")
// reuse the existing variable-target logic to modify the temp
val tempTarget = AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, elementDt, null, target.position, variableAsmName = elementTemp)
inplaceModification(tempTarget, operator, value)
// restore the address and store the temp back
asmgen.out("""
pla
sta $zpPtrVar+1
pla
sta $zpPtrVar""")
if(elementDt.isByteOrBool) {
asmgen.out("""
ldy #$offset
lda $elementTemp
sta ($zpPtrVar),y""")
} else if(elementDt.isWord) {
asmgen.out("""
ldy #$offset
lda $elementTemp
sta ($zpPtrVar),y
iny
lda $elementTemp+1
sta ($zpPtrVar),y""")
} else {
asmgen.out("""
ldy #$offset
lda $elementTemp
sta ($zpPtrVar),y
iny
lda $elementTemp+1
sta ($zpPtrVar),y
iny
lda $elementTemp+2
sta ($zpPtrVar),y
iny
lda $elementTemp+3
sta ($zpPtrVar),y""")
}
}
private fun inplaceModification(target: AsmAssignTarget, operator: String, value: AsmAssignSource) {
// the asm-gen code can deal with situations where you want to assign a byte into a word.
@@ -611,7 +703,19 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
}
}
}
TargetStorageKind.POINTER -> ptrgen.inplaceModification(PtrTarget(target), operator, value)
TargetStorageKind.POINTER -> {
// if the effective target address ends up in the shared scratch pointer
// P8ZP_SCRATCH_PTR, evaluating the source value could clobber it. In that
// case operate on a protected temporary value instead.
val usesOffsetPointer = target.datatype.isFloat || operator in setOf("*", "/", "%")
val needsProtectedPtr = ptrgen.derefUsesScratchPointer(target.pointer!!, usesOffsetPointer)
if (value.kind in setOf(SourceStorageKind.EXPRESSION, SourceStorageKind.REGISTER)
&& needsProtectedPtr
&& (target.datatype.isByteOrBool || target.datatype.isWord || target.datatype.isLong))
inplaceModificationPointerField(target, operator, value)
else
ptrgen.inplaceModification(PtrTarget(target), operator, value)
}
TargetStorageKind.REGISTER -> throw AssemblyError("no asm gen for reg in-place modification")
TargetStorageKind.VOID -> { /* do nothing */ }
}
@@ -2143,6 +2247,11 @@ $shortcutLabel:""")
"*" -> {
if(value in asmgen.optimizedByteMultiplications)
asmgen.out(" lda $name | jsr prog8_math.mul_byte_$value | sta $name")
else if(byteShiftAddExpansion(value)!=null) {
asmgen.out(" lda $name")
asmgen.emitByteShiftAdd(value)
asmgen.out(" sta $name")
}
else
asmgen.out(" lda $name | ldy #$value | jsr prog8_math.multiply_bytes | sta $name")
}
@@ -2454,8 +2563,7 @@ $shortcutLabel:""")
if(value in asmgen.optimizedWordMultiplications) {
asmgen.out(" lda $lsb | ldy $msb | jsr prog8_math.mul_word_$value | sta $lsb | sty $msb")
} else {
if(block?.options?.veraFxMuls==true) {
// cx16 verafx hardware mul
if(block?.options?.veraFxMuls==true) { // cx16 verafx hardware mul
asmgen.out("""
lda $lsb
ldy $msb
@@ -2469,6 +2577,11 @@ $shortcutLabel:""")
sta $lsb
sty $msb""")
}
else if(wordShiftAddExpansion(value)!=null) {
asmgen.out(" lda $lsb | ldy $msb")
asmgen.emitWordShiftAdd(value)
asmgen.out(" sta $lsb | sty $msb")
}
else
asmgen.out("""
lda $lsb
@@ -3,6 +3,8 @@ package prog8.codegen.cpu6502.assignment
import prog8.code.StExtSub
import prog8.code.ast.*
import prog8.code.core.*
import prog8.code.cpu6502.byteShiftAddExpansion
import prog8.code.cpu6502.wordShiftAddExpansion
import prog8.codegen.cpu6502.AsmGen6502Internal
import prog8.codegen.cpu6502.toLongHex
import kotlin.math.log2
@@ -412,6 +414,26 @@ internal class BinaryOpAssignmentsGen(
asmgen.out(" jsr verafx.muls16")
assignmentAsmGen.assignRegisterpairWord(target, RegisterOrPair.AY)
return true
} else if (asmgen.unwrapCasts(expr.left).type.isUnsignedByte || asmgen.unwrapCasts(expr.right).type.isUnsignedByte) {
// One operand is an unsigned byte (possibly under an implicit widening cast,
// hence unwrapCasts). The general path would zero-extend it into a full word
// and run 16 shift-add iterations even though only 8 bits are significant, so
// use the 16x8 routine instead. isUnsignedByte matters: a signed byte is
// sign-extended into A/Y and multiply_ubyte_word deliberately ignores the high
// byte, so it would compute the unsigned product instead.
//
// multiply_ubyte_word wants the 8-bit value in A and the word in the variable.
// assignWordOperandsToAYAndVar(left, right, var) puts `left` in AY and `right`
// in the variable, so the byte has to be passed as the `left` argument. Only
// the low 16 bits of the product are wanted and that is commutative mod 2^16,
// so swapping the operands is safe. (Prog8 rejects mixed signedness, so the
// other operand here is always an unsigned word.)
if (asmgen.unwrapCasts(expr.left).type.isUnsignedByte)
asmgen.assignWordOperandsToAYAndVar(expr.left, expr.right, "prog8_math.multiply_ubyte_word.multiplier")
else
asmgen.assignWordOperandsToAYAndVar(expr.right, expr.left, "prog8_math.multiply_ubyte_word.multiplier")
asmgen.out(" jsr prog8_math.multiply_ubyte_word")
assignmentAsmGen.assignRegisterpairWord(target, RegisterOrPair.AY)
} else {
asmgen.assignWordOperandsToAYAndVar(expr.right, expr.left, "prog8_math.multiply_words.multiplier")
asmgen.out(" jsr prog8_math.multiply_words")
@@ -449,6 +471,8 @@ internal class BinaryOpAssignmentsGen(
repeat(shifts) { asmgen.out(" asl a") }
}
}
else if(byteShiftAddExpansion(value)!=null)
asmgen.emitByteShiftAdd(value)
else
asmgen.out(" ldy #$value | jsr prog8_math.multiply_bytes")
assignmentAsmGen.assignRegisterByte(target, CpuRegister.A, false, true)
@@ -485,6 +509,9 @@ internal class BinaryOpAssignmentsGen(
sta cx16.r0
sty cx16.r0+1
jsr verafx.muls16""")
} else if(wordShiftAddExpansion(value)!=null) {
assignmentAsmGen.assignExpressionToRegister(expr.left, RegisterOrPair.AY, expr.type.isSigned)
asmgen.emitWordShiftAdd(value)
} else {
asmgen.assignWordOperandsToAYAndVar(expr.right, expr.left, "prog8_math.multiply_words.multiplier")
asmgen.out(" jsr prog8_math.multiply_words")
@@ -701,13 +728,13 @@ internal class BinaryOpAssignmentsGen(
if(target.kind==TargetStorageKind.VARIABLE) {
asmgen.assignExpressionTo(expr.left, target)
require(expr.right.type.isByte)
asmgen.out(" lda #<${target.asmVarname} | ldy #>${target.asmVarname}")
asmgen.out(" ldx #$shifts")
if (expr.operator == "<<") {
asmgen.out(" jsr prog8_lib.long_shiftleftX_inplace")
} else {
asmgen.out(" jsr prog8_lib.long_shiftrightX_inplace")
}
// The shift count is a compile-time constant here and the value is already in the
// target variable, so shift it in place directly. prog8_lib's long_shift*X_inplace
// takes a pointer and shifts through (zp),y inside a loop, which costs ~67 cycles
// per bit; inplacemodificationLongWithLiteralval already unrolls the small counts
// with plain asl/rol on the 4 bytes, and its '>>' keeps the same arithmetic
// (sign-preserving) semantics as the library routine.
assignmentAsmGen.augmentableAsmGen.inplacemodificationLongWithLiteralval(target.asmVarname, expr.operator, shifts)
return true
}
@@ -290,7 +290,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
return "P8ZP_SCRATCH_PTR" to 0u
}
else
return pointer.startpointer.name to field.second
return pointer.startpointer.name to field.second.toUByte()
}
else {
// have to copy it to temp zp var
@@ -300,7 +300,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
return "P8ZP_SCRATCH_PTR" to 0u
}
else
return "P8ZP_SCRATCH_PTR" to field.second
return "P8ZP_SCRATCH_PTR" to field.second.toUByte()
}
}
}
@@ -333,10 +333,22 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
updateScratchPointer()
return "P8ZP_SCRATCH_PTR" to 0u
} else
return "P8ZP_SCRATCH_PTR" to fieldinfo.second
return "P8ZP_SCRATCH_PTR" to fieldinfo.second.toUByte()
}
}
internal fun derefUsesScratchPointer(pointer: PtPointerDeref, addOffsetToPointer: Boolean=false): Boolean {
// determine if deref() would need to keep the effective address in the shared
// scratch pointer P8ZP_SCRATCH_PTR, which a subsequent evaluation of a source
// expression could then clobber. Only if it doesn't, may the pointer itself (if
// located in the zeropage) keep being used for the read-modify-write.
if(pointer.chain.isEmpty())
return !allocator.isZpVar(pointer.startpointer.name)
if(pointer.chain.size > 1 || addOffsetToPointer)
return true
return !(allocator.isZpVar(pointer.startpointer.name) && !pointer.derefLast)
}
internal fun indexedEffectiveAddress(target: IndexedPtrTarget): String {
val eltSize = asmgen.program.memsizer.memorySize(target.elementDt, null)
// generic: evaluate index as word, scale by element size, add field-corrected base pointer
@@ -946,7 +958,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
val right = binExpr.right as? PtIdentifier
require(binExpr.operator=="." && left!=null && right!=null) {"invalid dereference expression ${binExpr.position}"}
val field: Pair<DataType, UByte>
val field: Pair<DataType, Int>
var extraFieldOffset = 0
if(left.type.isStructInstance) {
@@ -977,7 +989,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
field = struct.getField(right.name, asmgen.program.memsizer)
}
val offset = extraFieldOffset + field.second.toInt()
val offset = extraFieldOffset + field.second
// TODO avoid always using a zp scratch reg below if the only thing we dereferenced above is a pointer variable that is already in zeropage itself
if(offset>=256) {
// add field offset to pointer if it doesn't fit in the index register
+109 -1
View File
@@ -9,7 +9,15 @@ import prog8.code.core.BaseDataType
import prog8.code.core.DataType
import prog8.code.target.C64Target
import prog8.code.target.VMTarget
import prog8.codegen.cpu6502.*
import prog8.codegen.cpu6502.hasInstr
import prog8.codegen.cpu6502.haslabel
import prog8.codegen.cpu6502.isBranch
import prog8.codegen.cpu6502.isLoadReg
import prog8.codegen.cpu6502.isStoreReg
import prog8.codegen.cpu6502.isStoreRegOrZero
import prog8.codegen.cpu6502.keeplabel
import prog8.codegen.cpu6502.modifiesYRegister
import prog8.codegen.cpu6502.optimizeAssembly
class TestAsmOptimizer: FunSpec({
@@ -262,6 +270,106 @@ class TestAsmOptimizer: FunSpec({
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop")
}
// --- optimizeSamePointerIndexing: guards on the index reload ---
fun padded(vararg body: String): MutableList<String> =
(body.toList() + List(14 - body.size) { " nop" }).toMutableList()
/** Same, but one line shorter, for cases where the optimizer is expected to drop a line. */
fun paddedAfterDrop(vararg body: String): MutableList<String> =
(body.toList() + List(13 - body.size) { " nop" }).toMutableList()
test("samePointerIndexing: KEEPS reload when the index variable is written in between") {
val lines = padded(" ldy i", " lda arr,y", " inc i", " ldy i", " sta arr,y", " rts")
optimize(lines)
lines shouldBe padded(" ldy i", " lda arr,y", " inc i", " ldy i", " sta arr,y", " rts")
}
test("samePointerIndexing: KEEPS reload when the index variable is decremented in between") {
val lines = padded(" ldy i", " lda arr,y", " dec i", " ldy i", " sta arr,y", " rts")
optimize(lines)
lines shouldBe padded(" ldy i", " lda arr,y", " dec i", " ldy i", " sta arr,y", " rts")
}
test("samePointerIndexing: KEEPS reload across a jsr") {
val lines = padded(" ldy #0", " lda (ptr),y", " jsr someSub", " ldy #0", " sta (ptr),y", " rts")
optimize(lines)
lines shouldBe padded(" ldy #0", " lda (ptr),y", " jsr someSub", " ldy #0", " sta (ptr),y", " rts")
}
test("samePointerIndexing: KEEPS reload across a label") {
val lines = padded(" ldy #0", " lda (ptr),y", "someLabel:", " ldy #0", " sta (ptr),y", " rts")
optimize(lines)
lines shouldBe padded(" ldy #0", " lda (ptr),y", "someLabel:", " ldy #0", " sta (ptr),y", " rts")
}
test("samePointerIndexing: KEEPS reload when an indirect store may alias the index") {
val lines = padded(" ldy i", " lda arr,y", " sta (vptr),y", " ldy i", " lda arr2,y")
optimize(lines)
lines shouldBe padded(" ldy i", " lda arr,y", " sta (vptr),y", " ldy i", " lda arr2,y")
}
test("samePointerIndexing: KEEPS reload when an intervening instruction modifies Y") {
val lines = padded(" ldy #0", " lda (ptr),y", " tay", " ldy #0", " sta (ptr),y", " rts")
optimize(lines)
lines shouldBe padded(" ldy #0", " lda (ptr),y", " tay", " ldy #0", " sta (ptr),y", " rts")
}
test("samePointerIndexing: KEEPS reload for an IO index operand") {
val lines = padded($$" ldy c64.$d020", " lda arr,y", " lda arr2,y", $$" ldy c64.$d020", " lda arr3,y")
optimize(lines)
lines shouldBe padded($$" ldy c64.$d020", " lda arr,y", " lda arr2,y", $$" ldy c64.$d020", " lda arr3,y")
}
test("samePointerIndexing: still drops reload when a store to an unrelated var intervenes") {
val lines = padded(" ldy i", " lda arr,y", " sta other", " ldy i", " sta arr,y", " rts")
optimize(lines)
lines shouldBe paddedAfterDrop(" ldy i", " lda arr,y", " sta other", " sta arr,y", " rts")
}
test("samePointerIndexing: KEEPS reload when the two ldy load DIFFERENT immediates") {
// regression: seed[1] then seed[0] - the reload selects a different element
val lines = padded(" ldy #1", " lda seed_msb,y", " sta x", " ldy #0", " lda seed_msb,y", " sta y", " rts")
optimize(lines)
lines shouldBe padded(" ldy #1", " lda seed_msb,y", " sta x", " ldy #0", " lda seed_msb,y", " sta y", " rts")
}
test("samePointerIndexing: KEEPS reload when the two ldy load different variables") {
val lines = padded(" ldy i", " lda arr,y", " sta x", " ldy j", " lda arr,y", " sta y", " rts")
optimize(lines)
lines shouldBe padded(" ldy i", " lda arr,y", " sta x", " ldy j", " lda arr,y", " sta y", " rts")
}
test("samePointerIndexing: KEEPS reload when the two ldy differ by an offset") {
val lines = padded(" ldy w+0", " lda arr,y", " sta x", " ldy w+1", " lda arr,y", " sta y", " rts")
optimize(lines)
lines shouldBe padded(" ldy w+0", " lda arr,y", " sta x", " ldy w+1", " lda arr,y", " sta y", " rts")
}
test("samePointerIndexing: matched load/store arm also requires the same index") {
val lines = padded(" ldy i", " lda arr,y", " clc", " ldy j", " sta arr,y", " rts")
optimize(lines)
lines shouldBe padded(" ldy i", " lda arr,y", " clc", " ldy j", " sta arr,y", " rts")
}
test("samePointerIndexing: relaxed arm drops reload for two indexed loads") {
val lines = padded(" ldy sp", " lda cx,y", " sta cxv", " ldy sp", " lda cy,y", " sta cyv", " rts")
optimize(lines)
lines shouldBe paddedAfterDrop(" ldy sp", " lda cx,y", " sta cxv", " lda cy,y", " sta cyv", " rts")
}
test("samePointerIndexing: relaxed arm does not fire when Y is modified") {
val lines = padded(" ldy sp", " lda cx,y", " dey", " ldy sp", " lda cy,y", " rts")
optimize(lines)
lines shouldBe padded(" ldy sp", " lda cx,y", " dey", " ldy sp", " lda cy,y", " rts")
}
test("samePointerIndexing: relaxed arm keeps reload when index is written in between") {
val lines = padded(" ldy i", " lda arr,y", " inc i", " ldy i", " lda arr2,y", " rts")
optimize(lines)
lines shouldBe padded(" ldy i", " lda arr,y", " inc i", " ldy i", " lda arr2,y", " rts")
}
// --- optimizeAddWordToSameVariableOrExtraRegisterLoadInWordStore ---
test("optimizeAddWordToSameVariable: optimizes P8ZP_SCRATCH_PTR += AY") {
@@ -33,9 +33,10 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
if (extsub.returns.size == assignmentTargets.size) {
// Targets and values match. Assign all the things. Skip 'void' targets.
// We need to handle both FP registers (FAC1/FAC2) and regular CPU registers
// Status flag returns MUST be processed first, because their branch-based
// code needs to read CPU flags immediately after the call, before any
// LOADHR/STOREM instructions clobber them.
// Status flag returns must be read before anything that clobbers the CPU status
// bits. When the call also has non-flag register returns, those are processed
// first (they may clobber the scratch register used by the flag extraction),
// protected by an outer PUSHST/POPST so the flags stay intact for the extraction.
val fpRegs = funcCall.multipleResultFpRegs.toMutableList()
val cpuRegs = funcCall.multipleResultRegs.toMutableList()
@@ -59,8 +60,40 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
}
}
flagPairs.forEach(::processPair)
otherPairs.forEach(::processPair)
fun isVoidTarget(pair: Pair<Pair<StExtSubParameter, PtNode>, Int>) =
(pair.first.second as PtAssignTarget).void
// When multiple status flags are returned in one call, the branch-based
// extraction of one flag clobbers the CPU status bits that a subsequent
// flag still needs to read. Wrap every flag pair's handling in
// PUSHST/POPST so each one sees the original status right after the call.
val wrapStatusFlags = flagPairs.count { !isVoidTarget(it) } > 1
fun processFlagPairs() {
flagPairs.forEach { pair ->
if (wrapStatusFlags && !isVoidTarget(pair)) {
result += IRCodeChunk(null, null).also { it += IRInstructions.simple(Opcode.PUSHST) }
processPair(pair)
result += IRCodeChunk(null, null).also { it += IRInstructions.simple(Opcode.POPST) }
} else {
processPair(pair)
}
}
}
val hasStatusFlags = flagPairs.any { !isVoidTarget(it) }
val hasOtherResults = otherPairs.any { !isVoidTarget(it) }
if (hasStatusFlags && hasOtherResults) {
// The non-flag register returns are read before the flag extraction,
// so the extraction's scratch register cannot clobber them. The flag
// bits are preserved across the register loads/stores with PUSHST/POPST.
result += IRCodeChunk(null, null).also { it += IRInstructions.simple(Opcode.PUSHST) }
otherPairs.forEach(::processPair)
result += IRCodeChunk(null, null).also { it += IRInstructions.simple(Opcode.POPST) }
processFlagPairs()
} else {
processFlagPairs()
otherPairs.forEach(::processPair)
}
} else {
throw AssemblyError("number of values and targets don't match")
}
@@ -222,7 +255,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
val (addressReg, fieldOffset) = codeGen.evaluatePointerAddressIntoReg(inplaceInstrs, pointerDeref)
val oldvalueReg = codeGen.registers.next(targetDt)
if((augAssign.operator=="+=" || augAssign.operator=="-=") && value.asConstInteger()==1 || value.asConstInteger()==2) {
if((augAssign.operator=="+=" || augAssign.operator=="-=") && (value.asConstInteger()==1 || value.asConstInteger()==2)) {
// INC/DEC optimization instead of ADD/SUB
loadfield(inplaceInstrs, addressReg, fieldOffset, targetDt, oldvalueReg)
@@ -346,20 +379,20 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
private fun loadfield(
inplaceInstrs: MutableList<IRCodeChunkBase>,
addressReg: Int,
fieldOffset: UByte,
fieldOffset: Int,
targetDt: IRDataType,
oldvalueReg: Int
) {
if (targetDt == IRDataType.FLOAT) {
addInstr(
inplaceInstrs,
IRInstructions.loadMemory(Opcode.LOADI, targetDt, oldvalueReg, IRMemory.indirect(addressReg, fieldOffset.toInt())),
IRInstructions.loadMemory(Opcode.LOADI, targetDt, oldvalueReg, IRMemory.indirect(addressReg, fieldOffset)),
null
)
} else {
addInstr(
inplaceInstrs,
IRInstructions.loadMemory(Opcode.LOADI, targetDt, oldvalueReg, IRMemory.indirect(addressReg, fieldOffset.toInt())),
IRInstructions.loadMemory(Opcode.LOADI, targetDt, oldvalueReg, IRMemory.indirect(addressReg, fieldOffset)),
null
)
}
@@ -785,26 +818,26 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
val offset = eltSize * constIndex
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, offset), null)
} else {
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, eltSize, true, targetArray.splitWords)
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, true)
result += code
if(eltSize!=1)
result += codeGen.multiplyByConst(DataType.UWORD, indexReg, eltSize)
addInstr(result, IRInstructions.binary(Opcode.ADDR, IRDataType.POINTER, pointerReg, indexReg), null)
}
codeGen.storeValueAtPointersLocation(result, pointerReg, 0u, targetIdent.type.dereference(), true, -1)
codeGen.storeValueAtPointersLocation(result, pointerReg, 0, targetIdent.type.dereference(), true, -1)
} else {
if(constIndex!=null) {
val offset = eltSize * constIndex
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, offset), null)
} else {
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, eltSize, true, targetArray.splitWords)
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, true)
result += code
if(eltSize!=1)
result += codeGen.multiplyByConst(DataType.UWORD, indexReg, eltSize)
addInstr(result, IRInstructions.binary(Opcode.ADDR, IRDataType.POINTER, pointerReg, indexReg), null)
}
val realValueReg = if(targetDt == IRDataType.FLOAT) valueFpRegister else valueRegister
codeGen.storeValueAtPointersLocation(result, pointerReg, 0u, targetIdent.type.dereference(), false, realValueReg)
codeGen.storeValueAtPointersLocation(result, pointerReg, 0, targetIdent.type.dereference(), false, realValueReg)
}
}
@@ -819,7 +852,6 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
valueFpRegister: Int
) {
val fixedIndex = targetArray.index.asConstInteger()
val arrayLength = codeGen.symbolTable.getLength(variable)
if(zero) {
if(fixedIndex!=null) {
val chunk = IRCodeChunk(null, null).also {
@@ -832,7 +864,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
}
result += chunk
} else {
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, eltSize, codeGen.wordArrayIndex, targetArray.splitWords)
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, codeGen.wordArrayIndex)
result += code
val scaleZX = if(targetArray.splitWords) 1 else eltSize
result += IRCodeChunk(null, null).also {
@@ -853,7 +885,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
}
result += chunk
} else {
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, eltSize, codeGen.wordArrayIndex, targetArray.splitWords)
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, codeGen.wordArrayIndex)
result += code
result += IRCodeChunk(null, null).also {
it += IRInstructions.storeMemory(Opcode.STOREX, targetDt, valueFpRegister, IRMemory.indexed(variable, indexReg, codeGen.options.compTarget.indexRegType, scale=eltSize))
@@ -874,7 +906,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
}
result += chunk
} else {
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, eltSize, codeGen.wordArrayIndex, targetArray.splitWords)
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, codeGen.wordArrayIndex)
result += code
result += IRCodeChunk(null, null).also {
if(targetArray.splitWords) {
@@ -922,7 +954,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
}
} else {
// index is an expression
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, eltSize, true, targetArray.splitWords)
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, true)
result += code
if(eltSize!=1)
result += codeGen.multiplyByConst(DataType.UWORD, indexReg, eltSize)
@@ -3030,7 +3062,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
private fun tryFoldStructArrayDirectWrite(addressExpr: PtExpression, zero: Boolean, valueReg: Int, targetDt: IRDataType, result: MutableList<IRCodeChunkBase>): Boolean {
val info = extractStructArrayIndexInfo(addressExpr, codeGen) ?: return false
val (idxCode, indexReg) = codeGen.loadIndexReg(info.idxExpr, info.structSize, codeGen.wordArrayIndex, false)
val (idxCode, indexReg) = codeGen.loadIndexReg(info.idxExpr, codeGen.wordArrayIndex)
result += idxCode
if(zero) {
result += IRCodeChunk(null, null).also {
@@ -876,7 +876,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
if(offset > 0)
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerTr.resultReg, offset), null)
} else {
val (code, indexWordReg) = codeGen.loadIndexReg(target.index, eltSize, true, false)
val (code, indexWordReg) = codeGen.loadIndexReg(target.index, true)
result += code
if(eltSize!=1)
result += codeGen.multiplyByConst(DataType.UWORD, indexWordReg, eltSize)
@@ -946,7 +946,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
it += IRInstructions.storeZero(Opcode.STOREZX, IRDataType.BYTE, IRMemory.indexed(targetVariable.name, offsetReg, codeGen.options.compTarget.indexRegType))
}
} else {
val (code, indexReg) = codeGen.loadIndexReg(target.index, eltSize, codeGen.wordArrayIndex, false)
val (code, indexReg) = codeGen.loadIndexReg(target.index, codeGen.wordArrayIndex)
result += code
result += IRCodeChunk(null, null).also {
val offset = byteOffset(eltSize)
@@ -964,7 +964,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
it += IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, valueTr.resultReg, IRMemory.indexed(targetVariable.name, offsetReg, codeGen.options.compTarget.indexRegType))
}
} else {
val (code, indexReg) = codeGen.loadIndexReg(target.index, eltSize, codeGen.wordArrayIndex, false)
val (code, indexReg) = codeGen.loadIndexReg(target.index, codeGen.wordArrayIndex)
result += code
result += IRCodeChunk(null, null).also {
val offset = byteOffset(eltSize)
@@ -985,7 +985,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val info = extractStructArrayIndexInfo(call.args[0], codeGen) ?: return null
val isZero = codeGen.isZero(call.args[1])
val result = mutableListOf<IRCodeChunkBase>()
val (idxCode, indexReg) = codeGen.loadIndexReg(info.idxExpr, info.structSize, codeGen.wordArrayIndex, false)
val (idxCode, indexReg) = codeGen.loadIndexReg(info.idxExpr, codeGen.wordArrayIndex)
result += idxCode
return if(isZero) {
result += IRCodeChunk(null, null).also {
@@ -186,7 +186,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
}
result += IRCodeChunk(null, null).also {
it += IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, firstField.second.toInt())
it += IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, firstField.second)
if (firstField.first.isPointer) {
// get the address stored in the pointer and use that for the rest of the chain
// LOADI has an exception to allo reg1 and reg2 to be the same, so we can avoid using extra temporary registers and LOADs
@@ -214,19 +214,19 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
// For inline array fields (not pointer fields), return the address (ptr+offset) instead of loading from it
if(deref.type.isArray && !deref.derefLast) {
if(offset > 0u)
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, offset.toInt()), null)
if(offset > 0)
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, offset), null)
return ExpressionCodeResult(result, IRDataType.POINTER, pointerReg, -1)
}
return if(deref.type.isFloat) {
val resultReg = codeGen.registers.next(IRDataType.FLOAT)
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, IRDataType.FLOAT, resultReg, IRMemory.indirect(pointerReg, offset.toInt())), null)
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, IRDataType.FLOAT, resultReg, IRMemory.indirect(pointerReg, offset)), null)
ExpressionCodeResult(result, IRDataType.FLOAT, -1, resultReg)
} else {
val irdt = codeGen.irType(deref.type)
val resultReg = codeGen.registers.next(irdt)
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, irdt, resultReg, IRMemory.indirect(pointerReg, offset.toInt())), null)
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, irdt, resultReg, IRMemory.indirect(pointerReg, offset)), null)
ExpressionCodeResult(result, irdt, resultReg, -1)
}
}
@@ -436,7 +436,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
result += pointerTr.chunks
val (instructions, offset) = traverseRestOfDerefChainToCalculateFinalAddress(expr.dereference!!, pointerTr.resultReg)
result += instructions
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerTr.resultReg, offset.toInt()), null)
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerTr.resultReg, offset), null)
return ExpressionCodeResult(result, vmDt, pointerTr.resultReg, -1)
}
}
@@ -645,7 +645,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
}
fun indexByExpression() {
val (code, indexReg) = codeGen.loadIndexReg(arrayIx.index, eltSize, codeGen.wordArrayIndex, arrayIx.splitWords)
val (code, indexReg) = codeGen.loadIndexReg(arrayIx.index, codeGen.wordArrayIndex)
result += code
val scale = if(arrayIx.splitWords) 1 else eltSize
if(vmDt==IRDataType.FLOAT) {
@@ -681,7 +681,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, memOffset), null)
}
else {
val (code, indexWordReg) = codeGen.loadIndexReg(index, eltSize, true, false)
val (code, indexWordReg) = codeGen.loadIndexReg(index, true)
result += code
if(eltSize!=1)
result += codeGen.multiplyByConst(DataType.UWORD, indexWordReg, eltSize)
@@ -931,9 +931,6 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
}
}
}
BaseDataType.ARRAY_POINTER -> {
TODO("typecast to array of pointers $valueDt -> ${cast.type} ${cast.position}")
}
else -> throw AssemblyError("weird cast value type ${cast.position}")
}
@@ -1876,7 +1873,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
val right = binExpr.right as? PtIdentifier
require(binExpr.operator=="." && left!=null && right!=null) {"invalid dereference expression ${binExpr.position}"}
val result = mutableListOf<IRCodeChunkBase>()
val field: Pair<DataType, UByte>
val field: Pair<DataType, Int>
val pointerReg: Int
var extraFieldOffset = 0
@@ -1886,7 +1883,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
val struct = left.type.subType!! as StStruct
field = struct.getField(right.name, codeGen.program.memsizer)
val fieldVmDt = codeGen.irType(field.first)
val fieldOffset = field.second.toInt()
val fieldOffset = field.second
if(left.variable!=null) {
val constindex = left.index as? PtNumber
if(constindex!=null) {
@@ -1898,7 +1895,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
} else if(left.variable!!.type.isArray) {
// variable index: fold to single LOADX with scale=structSize, disp=fieldOffset
// only for known array symbols (not generic pointer derefs)
val (chunks, indexReg) = codeGen.loadIndexReg(left.index, struct.size.toInt(), codeGen.wordArrayIndex, false)
val (chunks, indexReg) = codeGen.loadIndexReg(left.index, codeGen.wordArrayIndex)
result += chunks
if (fieldVmDt == IRDataType.FLOAT) {
val fpReg = codeGen.registers.next(IRDataType.FLOAT)
@@ -1914,7 +1911,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
val pointerTr = translateExpression(left.variable!!)
result += pointerTr.chunks
pointerReg = pointerTr.resultReg
val (chunks, indexReg) = codeGen.loadIndexReg(left.index, struct.size.toInt(), true, false)
val (chunks, indexReg) = codeGen.loadIndexReg(left.index, true)
result += chunks
if(struct.size.toInt()!=1)
result += codeGen.multiplyByConst(DataType.UWORD, indexReg, struct.size.toInt())
@@ -1940,7 +1937,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
// add field offset to pointer and load the value into the result register
val fieldVmDt = codeGen.irType(field.first)
val fieldOffset = field.second.toInt() + extraFieldOffset
val fieldOffset = field.second + extraFieldOffset
var resultFpReg = -1
var resultReg = -1
if (fieldVmDt == IRDataType.FLOAT)
@@ -1955,14 +1952,14 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
return ExpressionCodeResult(result, fieldVmDt, resultReg, resultFpReg)
}
internal fun traverseRestOfDerefChainToCalculateFinalAddress(targetPointerDeref: PtPointerDeref, pointerReg: Int): Pair<IRCodeChunks, UByte> {
internal fun traverseRestOfDerefChainToCalculateFinalAddress(targetPointerDeref: PtPointerDeref, pointerReg: Int): Pair<IRCodeChunks, Int> {
// returns instructions to calculate the pointer address, and the offset into the struct it points to
// so that LOADI and STOREFIELD opcodes can be used instead of having an explicit extra ADD
val result = mutableListOf<IRCodeChunkBase>()
if(targetPointerDeref.chain.isEmpty())
return result to 0u // nothing to do; there's no deref chain
return result to 0 // nothing to do; there's no deref chain
var struct: StStruct? = null
if(targetPointerDeref.startpointer.type.subType!=null)
@@ -1978,11 +1975,11 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
val field = targetPointerDeref.chain.last()
val fieldinfo = struct!!.getField(field, codeGen.program.memsizer)
if(fieldinfo.second>0u) {
if(fieldinfo.second>0) {
if(targetPointerDeref.derefLast) {
require(fieldinfo.first.isPointer)
result += updatePointerFromField(fieldinfo, pointerReg)
return result to 0u
return result to 0
} else {
return result to fieldinfo.second
}
@@ -1992,13 +1989,13 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
// LOADI has an exception to allow reg1 and reg2 to be the same, so we can avoid using extra temporary registers and LOADs
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, IRDataType.POINTER, pointerReg, IRMemory.indirect(pointerReg, 0)), null)
}
return result to 0u
return result to 0
}
private fun updatePointerFromField(fieldinfo: Pair<DataType, UByte>, pointerReg: Int): IRCodeChunk {
private fun updatePointerFromField(fieldinfo: Pair<DataType, Int>, pointerReg: Int): IRCodeChunk {
return IRCodeChunk(null, null).also {
// LOADI has an exception to allow reg1 and reg2 to be the same, so we can avoid using extra temporary registers and LOADs
it += IRInstructions.loadMemory(Opcode.LOADI, IRDataType.POINTER, pointerReg, IRMemory.indirect(pointerReg, fieldinfo.second.toInt()))
it += IRInstructions.loadMemory(Opcode.LOADI, IRDataType.POINTER, pointerReg, IRMemory.indirect(pointerReg, fieldinfo.second))
}
}
@@ -168,7 +168,7 @@ class IRCodeGen(
replacements += IRStStaticVariable(
variable.name, variable.dt, null, variable.length,
variable.zpwish, variable.align, variable.dirty,
variable.inBss, variable.readonly
variable.inBss, variable.readonly, variable.shared
)
}
is IRVariableInitializer.Str, is IRVariableInitializer.Array -> {
@@ -274,7 +274,8 @@ class IRCodeGen(
variable.align,
variable.dirty,
variable.inBss,
variable.readonly
variable.readonly,
variable.shared
)
irProg.st.add(clearedVar)
@@ -1895,7 +1896,14 @@ class IRCodeGen(
}
val value = ret.children.singleOrNull()
if(value==null) {
if(ret.numReturnValues()==0) {
// a void subroutine may return the result of a void function call (e.g. "return voidfunc(args)"):
// translate the call for its side effects, then return void
val callValue = value as? PtExpression
if(callValue!=null)
result += expressionEval.translateExpression(callValue).chunks
addInstr(result, IRInstructions.returnVoid(), null)
} else if(value==null) {
addInstr(result, IRInstructions.returnVoid(), null)
} else {
value as PtExpression
@@ -1942,7 +1950,7 @@ class IRCodeGen(
irBlock += chunk
}
is PtSub -> {
val sub = IRSubroutine(child.name, translateParameters(child.signature.children), child.signature.returns, child.position)
val sub = IRSubroutine(child.name, translateParameters(child.signature.children), child.signature.returns, child.position, noframe = child.noframe)
for (subchild in child.children) {
translateNode(subchild).forEach { sub += it }
}
@@ -2174,7 +2182,7 @@ class IRCodeGen(
return chunk
}
internal fun evaluatePointerAddressIntoReg(result: MutableList<IRCodeChunkBase>, deref: PtPointerDeref): Pair<Int, UByte> {
internal fun evaluatePointerAddressIntoReg(result: MutableList<IRCodeChunkBase>, deref: PtPointerDeref): Pair<Int, Int> {
// calculates the pointer address and returns the register it's in + remaining offset into the struct (so that LOADI/STOREFIELD instructions can be used)
val pointerTr = expressionEval.translateExpression(deref.startpointer)
result += pointerTr.chunks
@@ -2183,8 +2191,8 @@ class IRCodeGen(
return pointerTr.resultReg to offset
}
internal fun storeValueAtPointersLocation(result: MutableList<IRCodeChunkBase>, addressReg: Int, offset: UByte, type: DataType, valueIsZero: Boolean, existingValueRegister: Int) {
if(offset<=0u) {
internal fun storeValueAtPointersLocation(result: MutableList<IRCodeChunkBase>, addressReg: Int, offset: Int, type: DataType, valueIsZero: Boolean, existingValueRegister: Int) {
if(offset<=0) {
val irdt = irType(type)
val instr = if(type.isFloat) {
if (valueIsZero) IRInstructions.storeZero(Opcode.STOREZI, IRDataType.FLOAT, IRMemory.indirect(addressReg, 0))
@@ -2201,17 +2209,17 @@ class IRCodeGen(
val valueRegister = existingValueRegister
val irdt = irType(type)
if(valueIsZero && valueRegister<0) {
addInstr(result, IRInstructions.storeZero(Opcode.STOREZI, irdt, IRMemory.indirect(addressReg, offset.toInt())), null)
addInstr(result, IRInstructions.storeZero(Opcode.STOREZI, irdt, IRMemory.indirect(addressReg, offset)), null)
} else {
val instr = if (type.isFloat)
IRInstructions.storeMemory(Opcode.STOREI, IRDataType.FLOAT, valueRegister, IRMemory.indirect(addressReg, offset.toInt()))
IRInstructions.storeMemory(Opcode.STOREI, IRDataType.FLOAT, valueRegister, IRMemory.indirect(addressReg, offset))
else
IRInstructions.storeMemory(Opcode.STOREI, irdt, valueRegister, IRMemory.indirect(addressReg, offset.toInt()))
IRInstructions.storeMemory(Opcode.STOREI, irdt, valueRegister, IRMemory.indirect(addressReg, offset))
addInstr(result, instr, null)
}
}
internal fun loadIndexReg(index: PtExpression, itemsize: Int, wordIndex: Boolean, arrayIsSplitWords: Boolean): Pair<IRCodeChunks, Int> {
internal fun loadIndexReg(index: PtExpression, wordIndex: Boolean): Pair<IRCodeChunks, Int> {
// returns the code to load the Index into the register, which is also returned.
// The returned register is always canonicalized to the expected index width
// (WORD on 32-bit targets, BYTE on 8-bit) so that LOADX/STOREX/STOREZX never
@@ -1158,13 +1158,18 @@ jump p8_label_gen_2
mods.add(Mod(i+1, IRInstructions.binaryImmediate(Opcode.AND, IRDataType.LONG, outer.requireDest().registerNumber, 0xffff)))
mods.add(Mod(i, IRInstructions.move(IRDataType.LONG, outer.requireDest().registerNumber, inner.requireSrcA().registerNumber)))
}
// truncate-long/word-to-byte followed by zero-extend byte-to-word == mask with $ff
// truncate-long/word-to-byte followed by zero-extend byte-to-word == mask with $ff.
// This is only valid when the source and destination have the same type, because
// the rewritten loadr would otherwise access the source register with a different
// type than its definition (e.g. long source to word dest).
inner.opcode==Opcode.LSIGB && inner.type in setOf(IRDataType.WORD, IRDataType.LONG)
&& outer.opcode==Opcode.EXT && outer.type==IRDataType.BYTE
&& mid!=null && mid==outer.srcA?.register && inner.srcA!=null && outer.dest!=null
&& readCounts.getValue(mid)==1 -> {
mods.add(Mod(i+1, IRInstructions.binaryImmediate(Opcode.AND, inner.type!!, outer.requireDest().registerNumber, 0xff)))
mods.add(Mod(i, IRInstructions.move(inner.type!!, outer.requireDest().registerNumber, inner.requireSrcA().registerNumber)))
&& readCounts.getValue(mid)==1
&& inner.type == outer.requireDest().type -> {
val resultType = outer.requireDest().type
mods.add(Mod(i+1, IRInstructions.binaryImmediate(Opcode.AND, resultType, outer.requireDest().registerNumber, 0xff)))
mods.add(Mod(i, IRInstructions.move(resultType, outer.requireDest().registerNumber, inner.requireSrcA().registerNumber)))
}
else -> continue
}
@@ -8,6 +8,18 @@ class IRUnusedCodeRemover(
private val irprog: IRProgram,
private val errors: IErrorReporter
) {
/**
* The symbols that struct instance initializers take the address of. These are plain data
* references: nothing ever calls them, so without this the code they point at is considered
* unused and removed, leaving a dangling reference in the instance initializer.
* The same reasoning applies to the array initializers of variables, which are handled
* separately at each of the use sites below.
*/
private fun structInstanceInitRefs(): Sequence<IRStSymbolicReference.Symbol> =
irprog.st.allStructInstances()
.flatMap { instance -> instance.values.asSequence().map { it.value } }
.filterIsInstance<IRStSymbolicReference.Symbol>()
fun optimize(): Int {
var numRemoved = removeUnusedSubroutines() + removeUnusedAsmSubroutines()
@@ -52,6 +64,14 @@ class IRUnusedCodeRemover(
return // symbol occurs in an initializer value (address-of this symbol)_
}
}
irprog.st.allStructInstances().forEach { instance ->
if(instance.values.any { fieldValue ->
val ref = fieldValue.value
ref is IRStSymbolicReference.Symbol && ref.name.startsWith(blockLabel)
})
return // symbol occurs in a struct instance initializer value
}
}
val blockStructs = irprog.st.allStructDefs().filter { it.name.startsWith(prefix) }
@@ -221,6 +241,11 @@ class IRUnusedCodeRemover(
}
}
// ... and likewise the ones referenced in struct instance initializers:
structInstanceInitRefs()
.filter { irprog.st.lookup(it.name) == null }
.forEach { ref -> irprog.getChunkWithLabel(ref.name).let { reachable += it } }
fun grow() {
val new = mutableSetOf<IRCodeChunkBase>()
reachable.forEach {
@@ -277,6 +302,11 @@ class IRUnusedCodeRemover(
}
}
// ... and likewise the chunks referenced in struct instance initializers:
structInstanceInitRefs()
.filter { irprog.st.lookup(it.name) == null }
.forEach { ref -> allLabeledChunks[ref.name]?.let { linkedChunks += it } }
irprog.foreachCodeChunk { chunk ->
chunk.next?.let { next -> linkedChunks += next }
chunk.instructions.forEach {
@@ -10,7 +10,7 @@ import prog8.intermediate.*
*/
private class StToIrConverter(val romable: Boolean) {
fun convert(struct: StStruct): IRStStructDef =
IRStStructDef(struct.scopedNameString, makeIrStructFieldsFrom(struct.fields), struct.size)
IRStStructDef(struct.scopedNameString, makeIrStructFieldsFrom(struct.fields), struct.size, struct.isUnion)
private fun makeIrStructFieldsFrom(fields: List<PtStructField>): List<IRStStructField> =
fields.map { IRStStructField(it.type, it.name, it.arraySize) }
@@ -76,9 +76,9 @@ private class StToIrConverter(val romable: Boolean) {
val name = if('.' in variable.name) variable.name else variable.scopedNameString
val hasInit = initValue != null
val inBss = !hasInit || (romable && hasInit)
val inBss = !hasInit || romable
val readonly = romable && hasInit
return IRStStaticVariable(name, variable.dt, initValue, variable.length, variable.zpwish, variable.align, variable.dirty, inBss, readonly)
return IRStStaticVariable(name, variable.dt, initValue, variable.length, variable.zpwish, variable.align, variable.dirty, inBss, readonly, variable.shared)
}
fun convert(variable: StMemVar): IRStMemVar {
File diff suppressed because it is too large. Load diff
@@ -196,7 +196,7 @@ fun prefixSymbols(program: PtProgram, options: CompilationOptions, st: SymbolTab
}
internal fun prefixScopedName(name: String, type: Char): String {
internal fun prefixScopedName(name: String, type: Char, st: SymbolTable? = null): String {
if('.' !in name) {
if(name.startsWith(GENERATED_LABEL_PREFIX))
return name
@@ -206,7 +206,11 @@ internal fun prefixScopedName(name: String, type: Char): String {
val firstPrefixed = "p8b_${parts[0]}"
val lastPart = parts.last()
val lastPrefixed = if(lastPart.startsWith(GENERATED_LABEL_PREFIX)) lastPart else "p8${type}_$lastPart"
val inbetweenPrefixed = parts.drop(1).dropLast(1).map{ "p8s_$it" }
val inbetweenPrefixed = parts.drop(1).dropLast(1).mapIndexed { index, part ->
val scopedName = parts.take(index + 2).joinToString(".")
val middleType = st?.lookup(scopedName)?.type ?: StNodeType.SUBROUTINE
"p8${typePrefixChar(middleType)}_$part"
}
val prefixed = listOf(firstPrefixed) + inbetweenPrefixed + listOf(lastPrefixed)
return prefixed.joinToString(".")
}
@@ -229,7 +233,7 @@ internal fun typePrefixChar(targetNt: StNodeType): Char {
private fun PtVariable.prefixVariableOrThis(parent: PtNode, st: SymbolTable): PtVariable {
name = prefixScopedName(name, 'v')
name = prefixScopedName(name, 'v', st)
if(value==null)
return this
@@ -256,13 +260,25 @@ private fun PtVariable.prefixVariableOrThis(parent: PtNode, st: SymbolTable): Pt
is PtFunctionCall if elt.builtin -> {
if (elt.name != "prog8_lib_structalloc")
throw AssemblyError("weird array value element $elt")
else
else {
// The struct initializer's arguments are not reachable via children, so
// collectRefs never visits them; prefix the address-of ones here.
elt.children.forEachIndexed { idx, arg ->
if (arg is PtAddressOf && arg.definingBlock()?.options?.noSymbolPrefixing != true) {
val newAddr = PtAddressOf(arg.type, false, arg.position)
newAddr.add(arg.identifier!!.prefixIdentifierOrThis(newAddr, st))
if (arg.arrayIndexExpr != null)
newAddr.add(arg.arrayIndexExpr!!)
elt.setChild(idx, newAddr)
}
}
newValue.add(elt)
}
}
else -> throw AssemblyError("weird array value element $elt")
}
}
val result = PtVariable(name, type, isSplitWordArray, zeropage, align, dirty, newValue, arraySize, position)
val result = PtVariable(name, type, isSplitWordArray, zeropage, align, dirty, newValue, arraySize, position, shared)
result.parent = parent
result
}
@@ -290,7 +306,7 @@ private fun PtIdentifier.prefixIdentifierOrThis(parent: PtNode, st: SymbolTable)
}
val prefixType = typePrefixChar(targetNt)
val newName = prefixScopedName(name, prefixType)
val newName = prefixScopedName(name, prefixType, st)
val node = PtIdentifier(newName, type, position)
node.parent = parent
return node
@@ -6,12 +6,9 @@ import prog8.code.assembly.IAssemblyProgram
import prog8.code.ast.PtProgram
import prog8.code.core.CompilationOptions
import prog8.code.core.IErrorReporter
import prog8.code.core.OutputFiles
import prog8.codegen.intermediate.IRCodeGen
import prog8.intermediate.IRDataType
import prog8.intermediate.IRFileWriter
import prog8.intermediate.IRProgram
import prog8.intermediate.VirtualRegister
import prog8.intermediate.dumpVariables
import prog8.intermediate.*
class VmCodeGen(val retainSSA: Boolean,
private val preassignedCallSiteIds: Map<String, UByte> = emptyMap()
@@ -54,8 +51,9 @@ internal class VmAssemblyProgram(
get() = irProgram.countUsedRegisters()
override fun assemble(options: CompilationOptions, errors: IErrorReporter): Boolean {
// the VM reads the IR file from disk.
IRFileWriter(irProgram, null).write()
// the VM reads the IR file from disk; for the virtual target the IR file is the program artifact.
val programFile = OutputFiles.of(options, irProgram.name).programFile
IRFileWriter(irProgram, programFile).write()
return true
}
}
@@ -514,6 +514,51 @@ class TestIRPeepholeOpt: FunSpec({
instr[2].requireImmediateInt() shouldBe 42
}
test("collapse lsigb.w + ext.b into loadr.w + and.w") {
val irProg = makeIRProgram(listOf(
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.WORD, 7, IRMemory.direct("src")),
IRInstructions.binary(Opcode.LSIGB, IRDataType.WORD, 8, 7),
IRInstructions.binary(Opcode.EXT, IRDataType.BYTE, 9, 8),
IRInstructions.binary(Opcode.ADDR, IRDataType.WORD, 2, 9),
IRInstructions.returnVoid()
))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.any { it.opcode == Opcode.LSIGB } shouldBe false
instr.any { it.opcode == Opcode.EXT && it.requireDest().register == VirtualRegister.int(9) } shouldBe false
instr.any {
it.opcode == Opcode.LOADR &&
it.type == IRDataType.WORD &&
it.requireDest().register == VirtualRegister.int(9) &&
it.requireSrcA().register == VirtualRegister.int(7)
} shouldBe true
instr.any {
it.opcode == Opcode.AND &&
it.type == IRDataType.WORD &&
it.requireDest().register == VirtualRegister.int(9) &&
it.requireImmediateInt() == 0xff
} shouldBe true
}
test("do not collapse lsigb.l + ext.b when source and dest types differ") {
// lsigb.l produces a byte from a long; ext.b widens it to word. Rewriting this as
// loadr + and would require accessing the long source as word, which conflicts with
// the register type system. The optimizer must leave the original sequence intact.
val irProg = makeIRProgram(listOf(
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.LONG, 7, IRMemory.direct("src")),
IRInstructions.binary(Opcode.LSIGB, IRDataType.LONG, 8, 7),
IRInstructions.binary(Opcode.EXT, IRDataType.BYTE, 9, 8),
IRInstructions.binary(Opcode.ADDR, IRDataType.WORD, 2, 9),
IRInstructions.returnVoid()
))
val opt = IRPeepholeOptimizer(irProg, false)
opt.optimize(true, ErrorReporterForTests())
val instr = irProg.chunks().single().instructions
instr.any { it.opcode == Opcode.LSIGB } shouldBe true
instr.any { it.opcode == Opcode.EXT && it.requireDest().register == VirtualRegister.int(9) } shouldBe true
}
test("coalesce redundant LOADX/STOREX to same index is removed") {
val irProg = makeIRProgram(listOf(
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 10, indexRegType)),
@@ -34,6 +34,17 @@ class TestSymbolPrefixer : FunSpec({
prefixScopedName("lib.module.func", 's') shouldBe "p8b_lib.p8s_module.p8s_func"
}
test("prefixScopedName uses the symbol type for middle components") {
val program = PtProgram("test", VMTarget())
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
val struct = PtStructDecl("Point", listOf(PtStructField(DataType.UBYTE, "x", null)), Position.DUMMY)
block.add(struct)
program.add(block)
val st = SymbolTableMaker(program, basicTestOptions()).make()
prefixScopedName("main.Point.field", 'v', st) shouldBe "p8b_main.p8t_Point.p8v_field"
}
test("prefixScopedName generated label is not prefixed") {
val genLabel = "p8_label_gen_42"
prefixScopedName(genLabel, 's') shouldBe genLabel
+10
View File
@@ -53,6 +53,16 @@ Passes the linker script (see below).
## The linker script (`link.ld`)
The compiler resolves the linker script for a target in this order:
1. `link.ld` in the target's library directory (the `library` setting). This is how a custom
target, defined outside of the compiler, supplies its own memory layout.
2. `/prog8lib/<targetname>/link.ld`, for targets built into the compiler.
3. `/prog8lib/qemu68k/link.ld` as the fallback, which is the script shown below.
Only the `ELF` output type links at all. The `AMIGAHUNK` output type (used by the built-in
Amiga targets) assembles straight to a hunk executable and ignores the linker script.
```
ENTRY(_start)
SECTIONS
+177 -13
View File
@@ -38,13 +38,13 @@ internal fun IRInstruction.forwardableImmediateFor(register: VirtualRegister, ty
* It may contain registers that are NOT call arguments.
* For every call argument register, the loaded immediate value is forwarded
* directly into the argument's hardware register instead of going through the
* register file (move #imm,dX instead of move p8_regfile+N,dX, and
* fmovecr or a const-pool load into fpX instead of moving through p8_fregfile).
* register file. That is move #imm,dX instead of move p8_regfile+N,dX, and
* fmovecr or a const-pool load into fpX instead of moving through p8_fregfile.
*
* [deadRegisters] are the registers from [loads] that are not read anywhere
* else in the subroutine (their only use is the forwarded call argument);
* their register-file stores are omitted entirely. Note that this can also
* remove stores for registers that are not call arguments at all.
* else in the subroutine; their only use is the forwarded call argument, so
* their register-file stores are omitted entirely. This can also remove
* stores for registers that are not call arguments at all.
*
* The read check covers the subroutine's whole flattened instruction list
* rather than only the part after the call (see `isRegisterReadElsewhere`),
@@ -103,7 +103,7 @@ internal fun nativeFloatConst(value: Double): String? = when (value) {
* instead. Return values are passed via virtual registers mapped back to the caller's result register.
*/
internal class AsmGen(val program: IRProgram, internal val target: ICompilationTarget) {
internal class AsmGen(val program: IRProgram, internal val target: ICompilationTarget, internal val errors: IErrorReporter? = null) {
private val output = StringBuilder()
private val regsUsed by lazy { program.registersUsed() }
internal val cpu get() = target.cpu
@@ -111,6 +111,10 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
companion object {
const val REGFILE_LABEL = "p8_regfile"
const val FLOAT_REGFILE_LABEL = "p8_fregfile"
// frames larger than this are reported with an INFO message; deep recursion multiplies the frame per call
const val LARGE_FRAME_SIZE = 512
// consecutive STOREZM LONG frame stores from which a dbra clear loop is smaller than straight clr.l
const val ZERO_CLEAR_LOOP_THRESHOLD = 16
}
init {
@@ -281,6 +285,38 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
output.appendLine(code)
}
// === stack frame context ===
// true while emitting a subroutine that has a frame (local area and/or incoming argument
// area); the return translation emits `unlk a5` before each rts when this is set.
internal var frameActive = false
// the code chunks of the subroutine currently being emitted, to tell a jump that stays
// inside it apart from one that leaves it
private var currentSubChunks: Set<IRCodeChunkBase> = emptySet()
// Virtual registers of the subroutine being emitted, by register number -> frame offset.
// A re-entrant subroutine (one that can have two live activations) keeps them per-activation in
// its stack frame; every other subroutine uses the flat program-static register file.
private var frameVregSlots: Map<Int, Int> = emptyMap()
private fun frameVregAddr(reg: Int): String? = frameVregSlots[reg]?.let { "$it(a5)" }
/** emits the frame teardown for the subroutine currently being translated, if it has a frame */
internal fun emitFrameUnlk() {
if (frameActive)
emitLine("unlk a5")
}
/**
* true when a jump to this label lands outside the subroutine currently being emitted.
* An unresolvable label counts as inside: without the chunk map we cannot tell, and guessing
* 'outside' would tear down frames of jumps that stay in the subroutine.
*/
internal fun labelIsOutsideCurrentSub(labelName: String): Boolean {
val target = program.resolveCodeTarget(CodeReference.Label(labelName)) ?: return false
return target !in currentSubChunks
}
// === virtual register file layout (1, 2 or 4 bytes per slot depending on type, word-aligned) ===
private data class RegFileLayout(val offsets: Map<Int, Int>, val totalSize: Int)
@@ -328,6 +364,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
}
fun regAddr(reg: Int): String {
frameVregAddr(reg)?.let { return it }
val offset = regFileLayout.offsets[reg] ?: error("register r$reg has no layout info")
return "$REGFILE_LABEL+$offset"
}
@@ -335,9 +372,11 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
private fun regNumForOffset(offset: Int): Int? =
regFileLayout.offsets.entries.find { it.value == offset }?.key
// Resolve a register-file address string ("p8_regfile" or "p8_regfile+N")
// back to a virtual register number, or null if it is not a regfile slot.
// Resolve a register-file address string ("p8_regfile" or "p8_regfile+N", or a frame slot
// like "-8(a5)" for a per-activation virtual register) back to a virtual register number,
// or null if it is not a register-file slot.
fun regNumForAddress(address: String): Int? {
frameVregSlots.entries.firstOrNull { "$it.value(a5)" == address }?.let { return it.key }
if (!address.startsWith(REGFILE_LABEL))
return null
val suffix = address.removePrefix(REGFILE_LABEL)
@@ -350,6 +389,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
}
fun regAddrByte(reg: Int, byteOffset: Int): String {
frameVregSlots[reg]?.let { return "${it + byteOffset}(a5)" }
val offset = regFileLayout.offsets[reg] ?: error("register r$reg has no layout info")
return "$REGFILE_LABEL+${offset + byteOffset}"
}
@@ -380,6 +420,10 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
fun loadPointerToA0(reg: Int) {
// load a 32-bit pointer from the register file into a0
frameVregAddr(reg)?.let {
emitLine("movea.l $it, a0")
return
}
val offset = regFileLayout.offsets[reg] ?: error("register r$reg has no layout info")
val addr = if (offset == 0) REGFILE_LABEL else "$REGFILE_LABEL+$offset"
emitLine("movea.l $addr, a0")
@@ -454,6 +498,12 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
private fun resolveMemoryBase(base: AddressBase, displacement: Int): String {
val resolved = when (base) {
// combine slot offset and reference displacement into a single a5-relative displacement
is AddressBase.FrameSlot -> {
val total = base.offset + displacement
if (total !in -32768..32767) error("frame displacement $total out of 16-bit range")
return "$total(a5)"
}
is AddressBase.Symbol -> resolveSymbolRef(base.name)
is AddressBase.Absolute -> base.address.value.toHex()
}
@@ -535,6 +585,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
// === FPU helpers (M680x0 with 68881/68882) ===
fun floatRegFileAddr(reg: RegisterNum): String {
frameVregAddr(reg.value)?.let { return it }
val offset = floatRegFileLayout.offsets[reg.value] ?: error("float register fr${reg.value} has no layout info")
return "$FLOAT_REGFILE_LABEL+$offset"
}
@@ -577,7 +628,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
}
val options = program.options
val asmFile = options.outputDir.resolve("${program.name}.asm")
val asmFile = OutputFiles.of(options, program.name).asm()
try {
if (options.optimize) {
val asmLines = output.toString().lines().toMutableList()
@@ -638,6 +689,9 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
// emit all Prog8 constants as vasm symbols for inline asm use
emitConstants()
// emit struct/union type sizes and member offsets as vasm symbols for inline asm use
emitStructDefs()
// Set up stack pointer and jump to program start
emitLabel("prog8_program_start")
@@ -725,6 +779,14 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
val entrypointNames = setOf("p8b_main.p8s_start", "main.start")
if(sub.label in entrypointNames)
emitLine("bsr run_global_inits")
frameActive = sub.hasFrame
frameVregSlots = sub.frameVregSlots
currentSubChunks = mutableSetOf<IRCodeChunkBase>().also { set -> sub.forEachChunk { set.add(it) } }
if (sub.hasFrame) {
emitRaw("; stack frame: ${sub.frameSize} bytes of locals, ${sub.incomingSize} bytes of arguments")
emitLine(if (sub.frameSize > 0) "link a5,#-${sub.frameSize}" else "link a5,#0")
reportFrameSize(sub)
}
val livenessInstructions = sub.chunks.filterIsInstance<IRCodeChunk>().flatMap { it.instructions }
val deadStoreSuppressionAllowed = canSuppressDeadStores(sub)
var instructionOffset = 0
@@ -756,6 +818,9 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
}
emitRaw("; End of subroutine: $subLabel")
emitRaw("")
frameActive = false
frameVregSlots = emptyMap()
currentSubChunks = emptySet()
}
private fun emitAsmSubroutine(sub: IRAsmSubroutine) {
@@ -775,6 +840,16 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
private fun unscopedName(scopedName: String): String =
scopedName.substringAfterLast('.')
/** emits an informational message for unusually large frames; deep recursion multiplies the frame per call */
private fun reportFrameSize(sub: IRSubroutine) {
if (sub.frameSize > LARGE_FRAME_SIZE)
errors?.info("subroutine '${userFacingName(sub.label)}' uses a ${sub.frameSize} byte stack frame; deep recursion multiplies this per call", sub.position)
}
/** the scoped IR label without its internal scope markers, for use in user-facing messages */
private fun userFacingName(scopedName: String): String =
scopedName.split('.').joinToString(".") { it.removePrefix("p8b_").removePrefix("p8s_") }
// Dead-store suppression checks whether a register is read anywhere else in the
// subroutine (see isRegisterReadElsewhere), which is independent of control
// flow and therefore sound regardless of branch topology (loops, indirect
@@ -876,8 +951,18 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
}
}
}
for (index in chunk.instructions.indices) {
var index = 0
while (index < chunk.instructions.size) {
val insn = chunk.instructions[index]
// Long ascending STOREZM LONG runs lower to a dbra loop; short runs keep clr.l.
val zeroRun = frameZeroRunLength(chunk.instructions, index)
if (zeroRun >= ZERO_CLEAR_LOOP_THRESHOLD) {
val mem = insn.memory as MemoryReference.Direct
val slot = mem.base as AddressBase.FrameSlot
emitFrameZeroLoop(slot.offset + mem.displacement, zeroRun)
index += zeroRun
continue
}
val clobbersLoopCounter = preserveLoopCounter && mayClobberLoopCounter(insn)
if (clobbersLoopCounter)
emitLine("move.w d7,-(sp)", "save loop counter around call")
@@ -888,9 +973,57 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
)
if (clobbersLoopCounter)
emitLine("move.w (sp)+,d7", "restore loop counter after call")
index++
}
}
/**
* Counts consecutive STOREZM LONG instructions on ascending frame-slot displacements
* starting at [instructions[from]], stepping by one longword with nothing interleaved.
* Returns 0 when [instructions[from]] is not such a store.
*/
private fun frameZeroRunLength(instructions: List<IRInstruction>, from: Int): Int {
val first = instructions[from]
if (first.opcode != Opcode.STOREZM || first.type != IRDataType.LONG) return 0
val firstMem = first.memory as? MemoryReference.Direct ?: return 0
if (firstMem.base !is AddressBase.FrameSlot) return 0
var expected = (firstMem.base as AddressBase.FrameSlot).offset + firstMem.displacement
var count = 0
var index = from
while (index < instructions.size) {
val insn = instructions[index]
if (insn.opcode != Opcode.STOREZM || insn.type != IRDataType.LONG) break
val mem = insn.memory as? MemoryReference.Direct ?: break
val slot = mem.base as? AddressBase.FrameSlot ?: break
if (slot.offset + mem.displacement != expected) break
count++
expected += 4
index++
}
return count
}
/**
* Lowers [longwords] consecutive longword zero-clears at [startDisp](a5)
* to a dbra loop using scratch d0/d1/a0 while preserving d7.
* Assumes scratch registers are free at chunk boundaries (true at prologues).
*/
private fun emitFrameZeroLoop(startDisp: Int, longwords: Int) {
invalidateD0Cache() // moveq clobbers d0
emitLine("moveq #0, d0")
emitLine("lea $startDisp(a5), a0")
val iterations = longwords - 1
if (iterations in 0..127)
emitLine("moveq #$iterations, d1")
else
emitLine("move.w #$iterations, d1")
val loopLabel = makeLabel("zeroloop")
emitLabel(loopLabel)
emitLine("move.l d0,(a0)+")
emitLine("dbra d1,$loopLabel")
invalidateD0Cache()
}
private fun mayClobberLoopCounter(insn: IRInstruction): Boolean {
if (insn.opcode in setOf(Opcode.CALL, Opcode.CALLI, Opcode.CALLFAR, Opcode.CALLFARVB, Opcode.SYSCALL))
return true
@@ -962,7 +1095,10 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
forwardedImmediateCall: ImmediateCallOptimization? = null,
suppressRegfileStore: Boolean = false
) {
emitRaw(" ; $insn")
// No IR echo for zero-clear stores: prologue clear runs would drown in
// "; storezm" noise while the lowered clr lines are self-explanatory.
if (insn.opcode != Opcode.STOREZM)
emitRaw(" ; $insn")
when (insn.opcode) {
Opcode.NOP -> {}
Opcode.BREAKPOINT -> emitLine("illegal")
@@ -1036,7 +1172,10 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
} else {
emptyList<IRStStaticVariable>() to initdVars
}
val structInstancesWithInit: List<IRStStructInstance> = program.st.allStructInstances().filter { it.values.isNotEmpty() }.toList()
val structInstancesWithInit: List<IRStStructInstance> = program.st.allStructInstances().filter {
val def = program.st.lookup(it.structName) as IRStStructDef
it.values.isNotEmpty() && !def.isUnion
}.toList()
val (chipramStructs: List<IRStStructInstance>, normalStructs: List<IRStStructInstance>) =
if (chipramBlocks.isNotEmpty()) {
structInstancesWithInit.partition { it.name.substringBefore('.') in chipramBlocks }
@@ -1332,6 +1471,28 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
emitRaw("")
}
// Emit the size and member offsets of all struct/union types as vasm 'equ' symbols,
// so inline asm can reference them by name (parity with the 64tass-based backends).
// Offsets use the same packed layout (no alignment padding) that the compiler's own
// member-access codegen uses: a plain fold over the target's memory sizes.
private fun emitStructDefs() {
val structs = program.st.allStructDefs().toList()
if (structs.isEmpty()) return
emitRaw("; struct type definitions")
for (sd in structs) {
val base = fixNameSymbols(sd.name)
emitRaw("${base}_size equ ${sd.size}")
var offset = 0
for (field in sd.fields) {
emitRaw("${base}.p8v_${field.name} equ $offset")
if (!sd.isUnion)
offset += target.memorySize(field.type, field.arraySize ?: 1)
}
emitRaw("")
}
emitRaw("")
}
private fun emitBssSection() {
val chipramBlocks = if (target.name in setOf("amiga500", "amiga1200")) chipramBlockLabels() else emptySet()
@@ -1344,7 +1505,10 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
} else {
emptyList<IRStStaticVariable>() to allBssVars
}
val allStructsNoInit: List<IRStStructInstance> = program.st.allStructInstances().filter { it.values.isEmpty() }.toList()
val allStructsNoInit: List<IRStStructInstance> = program.st.allStructInstances().filter {
val def = program.st.lookup(it.structName) as IRStStructDef
it.values.isEmpty() || def.isUnion
}.toList()
val (chipramStructsNoInit: List<IRStStructInstance>, normalStructsNoInit: List<IRStStructInstance>) =
if (chipramBlocks.isNotEmpty()) {
allStructsNoInit.partition { it.name.substringBefore('.') in chipramBlocks }
@@ -61,8 +61,91 @@ internal fun optimizeAssembly(lines: MutableList<String>) {
if (fuseMods.isNotEmpty()) {
applyModifications(fuseMods, lines, pretrimmed)
}
// M5: drop a `lea base,a0` when a0 already holds that same base.
// NOT REGFILE-DEPENDENT: a0 is a real CPU register, so this pass is independent of the
// p8_regfile layout and of the planned register allocator.
val leaMods = optimizeRedundantLea(pretrimmed)
if (leaMods.isNotEmpty()) {
applyModifications(leaMods, lines, pretrimmed)
}
}
/**
* M5: remove `lea base,a0` instructions that recompute a base a0 is already known to hold.
*
* Indexed array access is emitted as `lea base,a0` followed by `(a0,d0.w*2)`. The lea itself is
* required - absolute-indexed addressing (`base(a0,d0.w*2)`) does not exist on the m68k - but in
* a sequence of accesses to the same array the base is reloaded several times over.
*
* A tracked value is dropped (a0 treated as unknown) whenever the scan sees:
* - a label: incoming control flow may not be the fallthrough path
* - any control transfer: a branch may reach elsewhere, and a call clobbers a0 (it is a
* scratch register in the m68k calling convention)
* - any instruction that names a0 outside a parenthesised address, i.e. writes it
*
* Anything else - including pure reads such as `move.w (a0,d0.w),d1` - leaves it untouched.
*/
private fun optimizeRedundantLea(allPretrimmed: List<String>): List<Modification> {
// Unconditional control flow, calls and returns end the tracked value: control leaves via a
// path where a0's contents are unknown (and a call clobbers a0, a scratch register in the
// m68k calling convention). The whole DBcc family is included as loop control.
// Plain condition-code branches (bne, bge, ...) are deliberately NOT in this set: they do not
// modify a0, and their target is always a label, which resets the tracked value below. So the
// fallthrough keeps the tracked value and the branch target is re-entered conservatively.
val controlTransfers = setOf(
"bra", "bsr", "jmp", "jsr", "rts", "rtr", "rte", "trap",
"dbcc", "dbcs", "dbhi", "dbhs", "dblo", "dbls", "dbne", "dbnz", "dbeq", "dbz",
"dbmi", "dbpl", "dbge", "dblt", "dbgt", "dble", "dbvc", "dbvs", "dbtst", "dbset",
"dbclr", "dbchg", "dbf", "dbra"
)
// A lea whose destination is a0, and the value it leaves there.
fun leaBase(instr: String): String? {
if (!instr.startsWith("lea ") && !instr.startsWith("lea\t"))
return null
val operands = instr.substringAfter(' ').substringBefore(';')
val parts = operands.split(',', limit = 2)
if (parts.size != 2 || parts[1].trim() != "a0")
return null
return parts[0].trim()
}
val filtered = allPretrimmed.withIndex()
.filter { it.value.isNotBlank() && !it.value.trimStart().startsWith(';') }
.map { TrimmedLine(it.value.trimStart(), it.value.trimStart(), it.index) }
val mods = mutableListOf<Modification>()
var a0Base: String? = null
for (line in filtered) {
val instr = line.instruction
if (hasLabel(line.value)) {
// fallthrough into a label is fine, but the label may also be entered from
// elsewhere, so the tracked value is no longer known to hold
a0Base = null
continue
}
val mnemonic = instr.substringBefore(' ').substringBefore('\t').lowercase()
if (mnemonic in controlTransfers) {
a0Base = null
continue
}
val base = leaBase(instr.lowercase())
if (base != null) {
if (base == a0Base)
mods.add(Modification(line.index, remove = true, replacement = null))
else
a0Base = base
continue
}
// Any non-parenthesised mention of a0 means it is written
if (instr.replace(parenthesised, "").contains("a0", ignoreCase = true))
a0Base = null
}
return mods
}
/** Matches a parenthesised group, for stripping addressing modes. */
private val parenthesised = Regex("""\([^)]*\)""")
/** A single edit to apply: either remove a line (keeping its label if present) or replace it. */
private class Modification(val lineIndex: Int, val remove: Boolean, val replacement: String?)
@@ -1,24 +1,35 @@
package prog8.codegen.m68k
import prog8.code.assembly.IAssemblyProgram
import prog8.code.core.CompilationOptions
import prog8.code.core.CpuType
import prog8.code.core.IErrorReporter
import prog8.code.core.OutputType
import prog8.code.core.*
import prog8.code.target.Amiga1200Target
import prog8.code.target.Amiga500Target
import prog8.code.target.Qemu68kTarget
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.readText
class AssemblyProgramM68k(override val name: String, private val outputDir: Path) : IAssemblyProgram {
class AssemblyProgramM68k(override val name: String) : IAssemblyProgram {
override val irInstructionCount: Int = 0
override val irChunkCount: Int = 0
override val irRegisterCount: Int = 0
private val assemblyFile = outputDir.resolve("$name.asm")
private fun elfFile(outs: OutputFiles): Path = outs.programFile
fun elfFile(): Path = outputDir.resolve("$name.elf")
/**
* A target supplies its own memory layout as a "link.ld" file. A target defined by a
* config file (i.e. living outside of the compiler) keeps it in its library directory, so
* that is looked at first. Targets built into the compiler are looked up as a
* /prog8lib/<targetname>/link.ld resource. Anything else falls back to the qemu68k layout.
*/
private fun resolveLinkerScript(target: ICompilationTarget): String {
target.libraryPath?.resolve("link.ld")?.takeIf { Files.isReadable(it) }?.let { return it.readText() }
val targetResource = "/prog8lib/${target.name}/link.ld"
AssemblyProgramM68k::class.java.getResource(targetResource)?.let { return it.readText() }
return AssemblyProgramM68k::class.java.getResource("/prog8lib/${Qemu68kTarget.NAME}/link.ld")?.readText()
?: error("cannot find $targetResource resource, and no qemu68k fallback either")
}
private fun runProcess(command: List<String>, quiet: Boolean, tool: String? = null): Boolean {
val proc = ProcessBuilder(command).redirectErrorStream(true)
@@ -55,12 +66,13 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
else -> error("invalid cpu type for m68k codegen ${options.compTarget.cpu}")
}
val assemblerCpu = if(options.compTarget.name == "amiga500" && options.floats) "68020" else cpu
val outs = OutputFiles.of(options, name)
when(options.output) {
OutputType.ELF -> {
// Step 1: assemble to ELF object file
val objFile = outputDir.resolve("$name.o")
val listFile = outputDir.resolve("$name.list")
val objFile = outs.obj()
val listFile = outs.list()
val assembleCmd = mutableListOf(
"vasmm68k_mot",
"-m$assemblerCpu",
@@ -71,7 +83,7 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
"-ldots",
"-spaces",
"-o", objFile.toString(),
assemblyFile.toString()
outs.asm().toString()
)
if (options.asmListfile)
assembleCmd.addAll(listOf("-L", listFile.toString()))
@@ -80,14 +92,12 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
if (!runProcess(assembleCmd, options.quiet, "vasm"))
return false
// clean up any leftover ELF/obj files from previous builds
Files.deleteIfExists(outputDir.resolve("$name.bin"))
Files.deleteIfExists(outs.binFile())
// Step 2: write linker script and link to ELF executable
val linkScript = outputDir.resolve("$name.link.ld")
val elfFile = elfFile()
val resourceUrl = AssemblyProgramM68k::class.java.getResource("/prog8lib/qemu68k/link.ld")
?: error("cannot find /prog8lib/qemu68k/link.ld resource")
Files.writeString(linkScript, resourceUrl.readText())
val linkScript = outs.linkScript()
val elfFile = elfFile(outs)
Files.writeString(linkScript, resolveLinkerScript(options.compTarget))
val linkCmd = listOf(
"vlink",
@@ -105,8 +115,8 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
}
OutputType.AMIGAHUNK -> {
// Step 1: assemble directly to AmigaHunk executable file
val exefile = outputDir.resolve(name)
val listfile = outputDir.resolve("$name.list")
val exefile = outs.programFile
val listfile = outs.list()
val assembleCmd = when(options.compTarget) {
is Amiga500Target -> {
// amiga 500 with kickstart 1.3
@@ -122,7 +132,7 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
"-spaces",
"-nosym", // no debug symbols
"-o", exefile.toString(),
assemblyFile.toString()
outs.asm().toString()
).also { cmd ->
if (options.asmListfile) {
cmd.add("-L")
@@ -146,7 +156,7 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
"-spaces",
"-nosym", // no debug symbols
"-o", exefile.toString(),
assemblyFile.toString()
outs.asm().toString()
).also { cmd ->
if (options.asmListfile) {
cmd.add("-L")
@@ -28,40 +28,46 @@ import prog8.intermediate.requireSrcB
import prog8.intermediate.requireTarget
internal fun AsmGen.translateBranch(insn: IRInstruction) {
val label: String = when (val target = insn.requireTarget()) {
val target = insn.requireTarget()
val label: String = when (target) {
is CodeReference.Label -> fixNameSymbols(target.name)
is CodeReference.Absolute -> target.address.toHex()
is CodeReference.Indirect -> error("branch needs a static target")
}
val leavesSubroutine = when (target) {
is CodeReference.Label -> labelIsOutsideCurrentSub(target.name)
is CodeReference.Absolute -> true
is CodeReference.Indirect -> false
}
when (insn.opcode) {
Opcode.BSTCC -> emitBranch("bcc", label)
Opcode.BSTCS -> emitBranch("bcs", label)
Opcode.BSTEQ -> emitBranch("beq", label)
Opcode.BSTNE -> emitBranch("bne", label)
Opcode.BSTNEG -> emitBranch("bmi", label)
Opcode.BSTPOS -> emitBranch("bpl", label)
Opcode.BSTVC -> emitBranch("bvc", label)
Opcode.BSTVS -> emitBranch("bvs", label)
Opcode.BSTCC -> emitBranch("bcc", label, leavesSubroutine)
Opcode.BSTCS -> emitBranch("bcs", label, leavesSubroutine)
Opcode.BSTEQ -> emitBranch("beq", label, leavesSubroutine)
Opcode.BSTNE -> emitBranch("bne", label, leavesSubroutine)
Opcode.BSTNEG -> emitBranch("bmi", label, leavesSubroutine)
Opcode.BSTPOS -> emitBranch("bpl", label, leavesSubroutine)
Opcode.BSTVC -> emitBranch("bvc", label, leavesSubroutine)
Opcode.BSTVS -> emitBranch("bvs", label, leavesSubroutine)
// Unsigned integer comparison branches
Opcode.BGT -> cmpBranchUnsignedImm(insn, label, "bhi")
Opcode.BGE -> cmpBranchUnsignedImm(insn, label, "bhs")
Opcode.BLT -> cmpBranchUnsignedImm(insn, label, "blo")
Opcode.BLE -> cmpBranchUnsignedImm(insn, label, "bls")
Opcode.BGT -> cmpBranchUnsignedImm(insn, label, "bhi", leavesSubroutine)
Opcode.BGE -> cmpBranchUnsignedImm(insn, label, "bhs", leavesSubroutine)
Opcode.BLT -> cmpBranchUnsignedImm(insn, label, "blo", leavesSubroutine)
Opcode.BLE -> cmpBranchUnsignedImm(insn, label, "bls", leavesSubroutine)
Opcode.BGTR -> cmpBranchUnsignedReg(insn, label, "bhi")
Opcode.BGER -> cmpBranchUnsignedReg(insn, label, "bhs")
Opcode.BGTR -> cmpBranchUnsignedReg(insn, label, "bhi", leavesSubroutine)
Opcode.BGER -> cmpBranchUnsignedReg(insn, label, "bhs", leavesSubroutine)
// BLTR doesn't exist in IR — uses BGTR with swapped operands
// Signed integer comparison branches
Opcode.BGTS -> cmpBranchSignedImm(insn, label, "bgt")
Opcode.BGES -> cmpBranchSignedImm(insn, label, "bge")
Opcode.BLTS -> cmpBranchSignedImm(insn, label, "blt")
Opcode.BLES -> cmpBranchSignedImm(insn, label, "ble")
Opcode.BGTS -> cmpBranchSignedImm(insn, label, "bgt", leavesSubroutine)
Opcode.BGES -> cmpBranchSignedImm(insn, label, "bge", leavesSubroutine)
Opcode.BLTS -> cmpBranchSignedImm(insn, label, "blt", leavesSubroutine)
Opcode.BLES -> cmpBranchSignedImm(insn, label, "ble", leavesSubroutine)
Opcode.BGTSR -> cmpBranchSignedReg(insn, label, "bgt")
Opcode.BGESR -> cmpBranchSignedReg(insn, label, "bge")
Opcode.BGTSR -> cmpBranchSignedReg(insn, label, "bgt", leavesSubroutine)
Opcode.BGESR -> cmpBranchSignedReg(insn, label, "bge", leavesSubroutine)
// BLTSR doesn't exist in IR — uses BGTSR with swapped operands
else -> error("Unknown branch opcode: ${insn.opcode}")
@@ -70,7 +76,7 @@ internal fun AsmGen.translateBranch(insn: IRInstruction) {
// === Unsigned comparisons: register vs immediate ===
private fun AsmGen.cmpBranchUnsignedImm(insn: IRInstruction, label: String, branchOp: String) {
private fun AsmGen.cmpBranchUnsignedImm(insn: IRInstruction, label: String, branchOp: String, leavesSubroutine: Boolean) {
val type = insn.type ?: IRDataType.BYTE
val reg = insn.requireSrcA().intNumber
val imm = insn.requireImmediateInt()
@@ -81,24 +87,24 @@ private fun AsmGen.cmpBranchUnsignedImm(insn: IRInstruction, label: String, bran
emitLine("tst$s ${regAddr(reg)}")
else
emitLine("cmpi$s #$imm, ${regAddr(reg)}")
emitBranch(branchOp, label)
emitBranch(branchOp, label, leavesSubroutine)
}
// === Unsigned comparisons: register vs register ===
private fun AsmGen.cmpBranchUnsignedReg(insn: IRInstruction, label: String, branchOp: String) {
private fun AsmGen.cmpBranchUnsignedReg(insn: IRInstruction, label: String, branchOp: String, leavesSubroutine: Boolean) {
val type = insn.type ?: IRDataType.BYTE
val left = insn.requireSrcA().intNumber
val right = insn.requireSrcB().intNumber
val s = dtSuffix(type)
emitLoadD0(left, type)
emitLine("cmp$s ${regAddr(right)}, d0")
emitBranch(branchOp, label)
emitBranch(branchOp, label, leavesSubroutine)
}
// === Signed comparisons: register vs immediate ===
private fun AsmGen.cmpBranchSignedImm(insn: IRInstruction, label: String, branchOp: String) {
private fun AsmGen.cmpBranchSignedImm(insn: IRInstruction, label: String, branchOp: String, leavesSubroutine: Boolean) {
val type = insn.type ?: IRDataType.BYTE
val reg = insn.requireSrcA().intNumber
val imm = insn.requireImmediateInt()
@@ -109,22 +115,41 @@ private fun AsmGen.cmpBranchSignedImm(insn: IRInstruction, label: String, branch
emitLine("tst$s ${regAddr(reg)}")
else
emitLine("cmpi$s #$imm, ${regAddr(reg)}")
emitBranch(branchOp, label)
emitBranch(branchOp, label, leavesSubroutine)
}
// === Signed comparisons: register vs register ===
private fun AsmGen.cmpBranchSignedReg(insn: IRInstruction, label: String, branchOp: String) {
private fun AsmGen.cmpBranchSignedReg(insn: IRInstruction, label: String, branchOp: String, leavesSubroutine: Boolean) {
val type = insn.type ?: IRDataType.BYTE
val left = insn.requireSrcA().intNumber
val right = insn.requireSrcB().intNumber
val s = dtSuffix(type)
emitLoadD0(left, type)
emitLine("cmp$s ${regAddr(right)}, d0")
emitBranch(branchOp, label)
emitBranch(branchOp, label, leavesSubroutine)
}
private fun AsmGen.emitBranch(branchOp: String, label: String) {
private val inverseBranchOps = mapOf(
"bcc" to "bcs", "bcs" to "bcc",
"beq" to "bne", "bne" to "beq",
"bmi" to "bpl", "bpl" to "bmi",
"bvc" to "bvs", "bvs" to "bvc",
"bhi" to "bls", "bls" to "bhi",
"bhs" to "blo", "blo" to "bhs",
"bgt" to "ble", "ble" to "bgt",
"bge" to "blt", "blt" to "bge")
private fun AsmGen.emitBranch(branchOp: String, label: String, leavesSubroutine: Boolean) {
invalidateD0Cache()
emitLine("$branchOp $label")
if (!leavesSubroutine || !frameActive) {
emitLine("$branchOp $label")
return
}
val skipLabel = makeLabel("branch_keep_frame")
emitLine("${inverseBranchOps.getValue(branchOp)} $skipLabel")
emitFrameUnlk()
emitLine("jmp $label")
emitLabel(skipLabel)
}
@@ -26,8 +26,19 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
Opcode.JUMP -> {
invalidateD0Cache()
when (val t = insn.requireTarget()) {
is CodeReference.Label -> emitLine("bra ${fixNameSymbols(t.name)}") // PC-relative branch; vasm picks the optimal size and falls back to jmp if out of range
is CodeReference.Absolute -> emitLine("jmp ${t.address.value.toHex()}")
is CodeReference.Label -> {
// a jump to a label in another subroutine ends this activation, so our frame has to be gone
// before we leave: whatever runs next returns to our caller, not to us
if (frameActive && labelIsOutsideCurrentSub(t.name))
emitFrameUnlk()
emitLine("bra ${fixNameSymbols(t.name)}") // PC-relative branch; vasm picks the optimal size and falls back to jmp if out of range
}
is CodeReference.Absolute -> {
// a fixed address can't be resolved to a chunk of this subroutine, so assume it
// leaves for good: whatever runs there returns to our caller, not to us
emitFrameUnlk()
emitLine("jmp ${t.address.value.toHex()}")
}
is CodeReference.Indirect -> error("JUMP needs target")
}
}
@@ -36,6 +47,8 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
val ref = insn.requireTarget() as? CodeReference.Indirect ?: error("JUMPI needs an indirect target register")
val reg = ref.pointer.intNumber
invalidateD0Cache()
// a dispatch jump leaves this subroutine for good: the target's rts must find our caller's return address, so our frame has to be gone first
emitFrameUnlk()
if(program.options.compTarget.cpu >= CpuType.M68020) {
// 68020+ supports memory-indirect addressing: fetch the target address from memory directly.
emitLine("jmp ([${regAddr(reg)}])")
@@ -99,8 +112,10 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
// In multi-assign context (returns.size > 1), also skip slot-based returns:
// the IR generates LOADHR for them.
// In single-return context, process slot returns normally.
// LIMITATION: multiple status flag returns in one multi-assign are not supported
// (codegen limitation: first flag extraction clobbers flags before second can be read).
// Multiple status flag returns in one multi-assign are protected by the IR:
// it wraps each flag's extraction in PUSHST/POPST so they don't clobber each other.
// On the 68000 cpu this fails, because it cannot save/restore the CCR with a
// nonprivileged instruction (compile for 68010 or higher to use this).
val isMultiReturn = callSite.results.size > 1
for(ret in callSite.results) {
if (ret.location is CallLocation.StatusFlag)
@@ -129,6 +144,7 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
Opcode.RETURN -> {
invalidateD0Cache()
emitFrameUnlk()
emitLine("rts")
}
@@ -142,6 +158,7 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
val reg = insn.requireIntSourceA().intNumber
emitLoadD0(reg, type)
}
emitFrameUnlk()
emitLine("rts")
}
@@ -154,6 +171,7 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
emitLine("moveq #$value, d0")
else
emitLine("move$s #$value, d0")
emitFrameUnlk()
emitLine("rts")
}
@@ -178,21 +196,18 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
// === Status flag stack ops via Scc ===
Opcode.PUSHST -> {
// Move CCR to D0 (low byte), then push as byte.
// Save CCR to the stack. The 68010+ memory form keeps the CCR save
// register-neutral (no data register is clobbered) and word-aligned.
if(program.options.compTarget.cpu == CpuType.M68000)
error("the 68000 cpu cannot save/restore the status bits using a nonprivileged instruction. This is required to implement the 'PUSHST/POPST' IR opcodes. Compile for 68010 or higher cpu or change the code such that PUSHST/POPST are no longer used (sometimes complicated rol/ror operations need it)")
invalidateD0Cache()
emitLine("move ccr, d0")
emitLine("move.b d0, -(sp)")
emitLine("move ccr, -(sp)")
}
Opcode.POPST -> {
// Pop byte into D0, then restore CCR
// Restore CCR from the stack (register-neutral on 68010+).
if(program.options.compTarget.cpu == CpuType.M68000)
error("the 68000 cpu cannot save/restore the status bits using a nonprivileged instruction. This is required to implement the 'PUSHST/POPST' IR opcodes. Compile for 68010 or higher cpu or change the code such that PUSHST/POPST are no longer used (sometimes complicated rol/ror operations need it)")
invalidateD0Cache()
emitLine("move.b (sp)+, d0")
emitLine("move d0, ccr")
emitLine("move (sp)+, ccr")
}
// === Flag manipulation ===
@@ -634,16 +649,19 @@ private fun AsmGen.translateCall(fnLabel: String, callSite: CallSite, forwardedI
// Emit non-constant arguments first (from regfile), then constant immediates
// just before the BSR/JSR to avoid clobbering the constant registers while
// evaluating non-const arguments (e.g. address calculations that may use D0/A0 as scratch).
val (forwarded, nonForwarded) = if (forwardedImmediateCall != null) {
callSite.arguments.partition { it.source.register in forwardedImmediateCall.loads }
} else {
emptyList<CallArgument>() to callSite.arguments
// Parameter names are resolved by argument index so each stack push can be
// annotated with the parameter it fills; unknown callees get no annotation.
val paramNames = program.allSubs().find { it.label == fnLabel }?.parameters?.map { it.name } ?: emptyList()
fun paramNameFor(index: Int): String? =
paramNames.getOrNull(index)?.substringAfterLast('.')?.removePrefix("p8v_")
val forwardedRegs = forwardedImmediateCall?.loads?.keys ?: emptySet()
for ((index, arg) in callSite.arguments.withIndex()) {
if (arg.source.register in forwardedRegs) continue
translateArgument(arg, fnLabel, forwardedImmediateCall, paramNameFor(index))
}
for (arg in nonForwarded) {
translateArgument(arg, fnLabel, forwardedImmediateCall)
}
for (arg in forwarded) {
translateArgument(arg, fnLabel, forwardedImmediateCall)
for ((index, arg) in callSite.arguments.withIndex()) {
if (arg.source.register !in forwardedRegs) continue
translateArgument(arg, fnLabel, forwardedImmediateCall, paramNameFor(index))
}
// Check if this call targets an inline asmsub — emit its body directly instead of jsr
@@ -655,6 +673,20 @@ private fun AsmGen.translateCall(fnLabel: String, callSite: CallSite, forwardedI
emitLine("bsr $fnLabel")
}
// caller cleanup of the all-stack argument area:
// each argument was pushed into its own padded longword slot
val pushedSlots = callSite.arguments.count { it.location is CallLocation.FrameSlot }
if (pushedSlots > 0) {
val totalSize = pushedSlots * 4
val needsCcr = callSite.results.any { it.location is CallLocation.StatusFlag }
when {
// addq is compact but updates CCR; lea preserves CCR so status-flag returns
// remain visible to the IR's branch pattern.
totalSize in 1..8 && !needsCcr -> emitLine("addq.l #$totalSize,sp")
else -> emitLine("lea $totalSize(sp),sp")
}
}
// Move return values back to virtual registers.
// Skip status flag returns: always handled by IR's bsteq/bstneg/bstvs branch pattern.
// In multi-assign context (results.size > 1), also skip slot-based returns:
@@ -662,8 +694,10 @@ private fun AsmGen.translateCall(fnLabel: String, callSite: CallSite, forwardedI
// before the branch pattern can read them.
// In single-return expression context, process all slot returns normally
// (the IR doesn't generate LOADHR for single-return calls).
// LIMITATION: multiple status flag returns in one multi-assign (e.g. -> bool @Pz, bool @Pc)
// are not supported - codegen limitation: the first flag's extraction clobbers the state for subsequent flags.
// Multiple status flag returns in one multi-assign are protected by the IR:
// it wraps each flag's extraction in PUSHST/POPST so they don't clobber each other.
// On the 68000 cpu this fails, because it cannot save/restore the CCR with a
// nonprivileged instruction (compile for 68010 or higher to use this).
val isMultiReturn = callSite.results.size > 1
for (ret in callSite.results) {
if (ret.location is CallLocation.StatusFlag)
@@ -757,7 +791,8 @@ private fun AsmGen.emitInlineCopyLoop(iterations: Int, size: Int) {
private fun AsmGen.translateArgument(
arg: CallArgument,
fnLabel: String? = null,
forwardedImmediateCall: ImmediateCallOptimization? = null
forwardedImmediateCall: ImmediateCallOptimization? = null,
paramName: String? = null
) {
val argReg = arg.source
val forwarded = forwardedImmediateCall?.loads?.get(argReg.register)
@@ -807,6 +842,32 @@ private fun AsmGen.translateArgument(
}
}
} else {
// Stack argument: push into a padded longword slot, right-justified.
// The caller pops the whole argument area after the return, so the callee never needs to know the argument count.
// Optimizing this to use word-padding instead of long padding could be done in a future version
if (arg.location is CallLocation.FrameSlot) {
if (argReg.isFloat) {
// the FPU has no absolute-long addressing mode, so the value has to travel
// through an FPU register first (fp0 is translator scratch, never holds a live vreg).
// The store itself reserves the slot via predecrement, so no separate subq is needed.
emitLine("fmove.s ${floatRegFileAddr(argReg.floatNumber)},$FP_ACC")
emitLine("fmove.s $FP_ACC,-(sp)", paramName.orEmpty())
} else {
when (argReg.type) {
IRDataType.BYTE -> {
emitLine("subq.l #4,sp")
emitLine("move.b ${regAddr(argReg.intNumber)},3(sp)", paramName.orEmpty())
}
IRDataType.WORD -> {
emitLine("subq.l #4,sp")
emitLine("move.w ${regAddr(argReg.intNumber)},2(sp)", paramName.orEmpty())
}
// longs and pointers fill the whole slot, so reserve and store in one push
else -> emitLine("move.l ${regAddr(argReg.intNumber)},-(sp)", paramName.orEmpty())
}
}
return
}
// Store to the callee's parameter variable (if this is a named param)
val paramName = (arg.location as? CallLocation.ParameterMemory)?.name.orEmpty()
if (paramName.isNotEmpty() && fnLabel != null) {
@@ -7,6 +7,7 @@ import prog8.code.ast.PtProgram
import prog8.code.core.CompilationOptions
import prog8.code.core.IErrorReporter
import prog8.codegen.intermediate.IRCodeGen
import prog8.codegen.intermediate.StackFrameLayout
import prog8.intermediate.IRDataType
import prog8.intermediate.IRFileWriter
import prog8.intermediate.VirtualRegister
@@ -28,15 +29,18 @@ class M68kCodeGenerator(val retainSSA: Boolean): ICodeGeneratorBackend {
}
irProgram.verifyRegisterTypes(virtualRegisterTypes)
if (options.compTarget.supportsStackFrames)
StackFrameLayout(irProgram, errors).apply()
if (options.dumpVariables)
dumpVariables(irProgram)
IRFileWriter(irProgram, null).write()
val gen = AsmGen(irProgram, irProgram.options.compTarget)
val gen = AsmGen(irProgram, irProgram.options.compTarget, errors)
if (!gen.generate())
throw RuntimeException("M68k assembly generation failed")
return AssemblyProgramM68k(irProgram.name, irProgram.options.outputDir)
return AssemblyProgramM68k(irProgram.name)
}
}
+8 -2
View File
@@ -4,9 +4,11 @@ import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.main
import com.github.ajalt.clikt.parameters.arguments.argument
import prog8.code.core.CpuType
import prog8.code.core.OutputFiles
import prog8.intermediate.IRFileReader
import kotlin.io.path.Path
import kotlin.io.path.readText
import kotlin.system.exitProcess
fun main(args: Array<String>) {
val cli = M68kCli()
@@ -23,8 +25,12 @@ fun main(args: Array<String>) {
when(target.cpu) {
CpuType.M68000, CpuType.M68020 -> {
val gen = AsmGen(program, target)
gen.generate()
println("Generated assembly: ${program.name}.asm")
val ok = gen.generate()
if(!ok) {
System.err.println("Assembly failed.")
exitProcess(1)
}
println("Generated assembly: ${OutputFiles.of(program.options, program.name).asm()}")
}
else -> {
error("This code generator only works for M68000 or M68020 CPU.")
@@ -1255,7 +1255,7 @@ class TestInstructionSelectionOptimizations : FunSpec({
lines.any { it == "move.w #1234,var" } shouldBe true
}
// === extb.l gate robustification ===
// === extb.l gate hardening ===
test("M68020 sign-extends byte to long with extb.l") {
val lines = generateAsm(
@@ -1279,4 +1279,80 @@ class TestInstructionSelectionOptimizations : FunSpec({
lines.count { it == "ext.w d0" } shouldBe 1
lines.count { it == "ext.l d0" } shouldBe 1
}
test("M5 drops a lea when a0 already holds the same base") {
val lines = mutableListOf(
" lea arr,a0",
" move.w d1,(a0,d0.w)",
" lea arr,a0",
" clr.w (a0,d0.w)"
)
optimizeAssembly(lines)
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 1
lines.any { it.trimStart().startsWith("clr.w") } shouldBe true
}
test("M5 keeps a lea when a different base was loaded into a0") {
val lines = mutableListOf(
" lea arr,a0",
" move.w d1,(a0,d0.w)",
" lea other,a0",
" lea arr,a0",
" clr.w (a0,d0.w)"
)
optimizeAssembly(lines)
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 2
lines.count { it.trimStart().startsWith("lea other,a0") } shouldBe 1
}
test("M5 keeps a lea when a0 is written in between") {
val lines = mutableListOf(
" lea arr,a0",
" movea.l other,a0",
" lea arr,a0",
" clr.w (a0,d0.w)"
)
optimizeAssembly(lines)
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 2
}
test("M5 keeps a lea when a call intervenes") {
val lines = mutableListOf(
" lea arr,a0",
" bsr some.sub",
" lea arr,a0",
" clr.w (a0,d0.w)"
)
optimizeAssembly(lines)
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 2
lines.any { it.trimStart().startsWith("bsr") } shouldBe true
}
test("M5 forgets a0 across a label") {
val lines = mutableListOf(
" lea arr,a0",
"somewhere:",
" lea arr,a0",
" clr.w (a0,d0.w)"
)
optimizeAssembly(lines)
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 2
lines.any { it.trimStart().startsWith("somewhere:") } shouldBe true
}
test("M5 drops a lea across a conditional branch to a label") {
// A conditional branch does not modify a0, and its target is a label where the tracked
// value is dropped, so the fallthrough still knows a0 holds the base.
val lines = mutableListOf(
" lea arr,a0",
" move.w (a0,d0.w),d0",
" bge clamp.done",
" lea arr,a0",
" clr.w (a0,d0.w)",
"clamp.done:"
)
optimizeAssembly(lines)
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 1
lines.any { it.trimStart().startsWith("clr.w") } shouldBe true
}
})
+467
View File
@@ -0,0 +1,467 @@
package prog8tests.codegen.m68k
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.shouldBe
import prog8.code.core.*
import prog8.code.target.Qemu68kTarget
import prog8.codegen.m68k.AsmGen
import prog8.intermediate.*
import prog8tests.helpers.DummyStringEncoder
import prog8tests.helpers.ErrorReporterForTests
import kotlin.io.path.exists
import kotlin.io.path.readText
// Vertical Slice Prototype: m68k backend lowering of FrameSlot memory references
// and the link/unlk frame prologue/epilogue.
class TestStackFrameEmission : FunSpec({
val tempRoot = tempdir().toPath()
fun generateAsmWithIncoming(
frameSize: Int,
instructions: List<IRInstruction>,
incomingSize: Int = 0,
subLabel: String = "test.start",
frameVregSlots: Map<Int, Int> = emptyMap(),
errors: ErrorReporterForTests? = null
): List<String> {
val options = CompilationOptions.builder(Qemu68kTarget())
.output(OutputType.RAW)
.zeropage(ZeropageType.DONTUSE)
.floats(false)
.compilerVersion("test")
.memtopAddress(0xffffu)
.optimize(true)
.build()
val program = IRProgram("test", IRSymbolTable(), options, DummyStringEncoder)
program.options.outputDir = tempRoot
val chunk = IRCodeChunk(subLabel, null)
chunk.instructions.addAll(instructions)
val params = if (incomingSize > 0)
List(incomingSize / 4) { IRSubroutine.IRParam("p8v_p$it", DataType.UBYTE) }
else emptyList()
val sub = IRSubroutine(subLabel, params, emptyList(), Position.DUMMY, frameSize, incomingSize, frameVregSlots)
sub.chunks.add(chunk)
val block = IRBlock("test", false, IRBlock.Options(), Position.DUMMY)
block.children.add(sub)
program.blocks.add(block)
tempRoot.toFile().deleteRecursively()
tempRoot.toFile().mkdirs()
AsmGen(program, Qemu68kTarget(), errors).generate()
val asmFile = tempRoot.resolve("test.asm")
check(asmFile.exists()) { "Assembly file not written: $asmFile" }
return asmFile.readText().lines().map { it.trim() }
}
fun generateAsm(frameSize: Int, instructions: List<IRInstruction>): List<String> =
generateAsmWithIncoming(frameSize, instructions, 0, "test.start")
// builds a subroutine with a chunk per extra chunk label, for jump-target scope tests
fun sub(label: String, frameSize: Int, instructions: List<IRInstruction>, extraChunks: List<IRCodeChunkBase> = emptyList()): IRSubroutine {
val first = IRCodeChunk(label, null)
first.instructions.addAll(instructions)
val sub = IRSubroutine(label, emptyList(), emptyList(), Position.DUMMY, frameSize, 0, emptyMap())
sub.chunks.add(first)
sub.chunks.addAll(extraChunks)
return sub
}
fun generateAsmForSubs(vararg subs: IRSubroutine): List<String> {
val options = CompilationOptions.builder(Qemu68kTarget())
.output(OutputType.RAW)
.zeropage(ZeropageType.DONTUSE)
.floats(false)
.compilerVersion("test")
.memtopAddress(0xffffu)
.optimize(true)
.build()
val program = IRProgram("test", IRSymbolTable(), options, DummyStringEncoder)
program.options.outputDir = tempRoot
val block = IRBlock("test", false, IRBlock.Options(), Position.DUMMY)
block.children.addAll(subs)
program.blocks.add(block)
program.linkChunks()
tempRoot.toFile().deleteRecursively()
tempRoot.toFile().mkdirs()
AsmGen(program, Qemu68kTarget(), null).generate()
return tempRoot.resolve("test.asm").readText().lines().map { it.trim() }
}
test("frameless subroutine emits no frame setup") {
val lines = generateAsm(0, listOf(
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 1, IRMemory.direct("some.var")),
IRInstructions.simple(Opcode.RETURN)
))
lines.any { it.startsWith("link") } shouldBe false
lines.any { it.startsWith("unlk") } shouldBe false
}
test("framed subroutine emits link prologue and unlk epilogue") {
val lines = generateAsm(16, listOf(
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-1)),
IRInstructions.simple(Opcode.RETURN)
))
lines.any { it == "link a5,#-16" } shouldBe true
lines.count { it == "unlk a5" } shouldBe 1
lines.any { "-1(a5)" in it } shouldBe true
// epilogue must directly precede the subroutine's return
lines[lines.indexOf("unlk a5") + 1] shouldBe "rts"
}
test("frame slot direct references lower to displacements relative to a5") {
val lines = generateAsm(16, listOf(
IRInstructions.storeImmediate(IRDataType.WORD, 5, IRMemory.frameDirect(-4)),
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 1, IRMemory.frameDirect(-1)),
IRInstructions.memoryOpImmediate(Opcode.ADDIM, IRDataType.BYTE, IRMemory.frameDirect(-1), 3),
IRInstructions.simple(Opcode.RETURN)
))
lines.any { it.contains("-4(a5)") && it.startsWith("move.w") } shouldBe true
lines.any { it.contains("-1(a5)") } shouldBe true
lines.any { it.startsWith("addq.b") || it.startsWith("add.b") } shouldBe true
}
test("indexed frame slot access uses lea of the slot base plus index") {
val lines = generateAsm(16, listOf(
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.BYTE, 1, IRMemory.frameIndexed(-8, 3, IRDataType.WORD, scale = 1)),
IRInstructions.storeZero(Opcode.STOREZX, IRDataType.BYTE, IRMemory.frameIndexed(-8, 3, IRDataType.WORD, scale = 1)),
IRInstructions.simple(Opcode.RETURN)
))
lines.any { it.startsWith("lea") && "-8(a5)" in it } shouldBe true
lines.count { it.contains("(a0,d0.w)") } shouldBe 2
}
test("frame displacement combines slot offset and reference displacement") {
val lines = generateAsm(16, listOf(
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-8, 4)),
IRInstructions.simple(Opcode.RETURN)
))
// -8 slot + 4 displacement = -4(a5), a single combined displacement
lines.any { it.contains("-4(a5)") } shouldBe true
lines.any { it.contains("-8(a5)+4") } shouldBe false
}
test("incoming-only frame still emits link and unlk") {
val lines = generateAsmWithIncoming(0, listOf(IRInstructions.simple(Opcode.RETURN)), incomingSize = 12)
lines.any { it == "link a5,#0" } shouldBe true
lines.any { it == "unlk a5" } shouldBe true
}
test("stack arguments are pushed right-justified and popped by the caller") {
val calls = listOf(
IRInstructions.call(CallSite(
target = CallTarget.Direct(CodeReference.Label("test.sub")),
arguments = listOf(
Calls.argument(1, IRDataType.BYTE, CallLocation.FrameSlot(16)),
Calls.argument(2, IRDataType.WORD, CallLocation.FrameSlot(12)),
Calls.argument(3, IRDataType.LONG, CallLocation.FrameSlot(8))
)
)),
IRInstructions.simple(Opcode.RETURN)
)
val lines = generateAsmWithIncoming(0, calls, incomingSize = 12, subLabel = "test.caller")
lines.count { it == "subq.l #4,sp" } shouldBe 2
lines.count { it.startsWith("move.b ") && it.contains(",3(sp)") } shouldBe 1
lines.count { it.startsWith("move.w ") && it.contains(",2(sp)") } shouldBe 1
lines.count { it.startsWith("move.l ") && it.contains(",-(sp)") } shouldBe 1
lines.any { it.startsWith("bsr") } shouldBe true
lines.any { it == "lea 12(sp),sp" } shouldBe true
// small argument areas use the short addq form
val smallCalls = listOf(
IRInstructions.call(CallSite(
target = CallTarget.Direct(CodeReference.Label("test.sub")),
arguments = listOf(Calls.argument(1, IRDataType.BYTE, CallLocation.FrameSlot(8)))
)),
IRInstructions.simple(Opcode.RETURN)
)
val smallLines = generateAsmWithIncoming(0, smallCalls, incomingSize = 4, subLabel = "test.caller")
smallLines.any { it == "addq.l #4,sp" } shouldBe true
}
test("status-flag results use CCR-preserving cleanup") {
val calls = listOf(
IRInstructions.call(CallSite(
target = CallTarget.Direct(CodeReference.Label("test.sub")),
arguments = listOf(Calls.argument(1, IRDataType.BYTE, CallLocation.FrameSlot(8))),
results = listOf(CallResult(null, CallLocation.StatusFlag(Statusflag.Pz)))
)),
IRInstructions.simple(Opcode.RETURN)
)
val lines = generateAsmWithIncoming(0, calls, incomingSize = 4, subLabel = "test.caller")
// a single byte argument would normally use addq.l #4,sp, but that clobbers CCR,
// so status-flag returns must use lea instead.
lines.any { it == "lea 4(sp),sp" } shouldBe true
lines.none { it == "addq.l #4,sp" } shouldBe true
}
test("float stack arguments travel through the FPU accumulator") {
val calls = listOf(
IRInstructions.call(CallSite(
target = CallTarget.Direct(CodeReference.Label("test.sub")),
arguments = listOf(Calls.argument(1, IRDataType.FLOAT, CallLocation.FrameSlot(8)))
)),
IRInstructions.simple(Opcode.RETURN)
)
val lines = generateAsmWithIncoming(0, calls, incomingSize = 4, subLabel = "test.caller")
// the 68881 has no absolute-long addressing mode: the value must pass through fp0,
// and the store itself reserves the slot via predecrement
lines.any { it.startsWith("fmove.s p8_fregfile+0,fp0") } shouldBe true
lines.any { it == "fmove.s fp0,-(sp)" } shouldBe true
lines.none { it == "subq.l #4,sp" } shouldBe true
}
test("per-activation virtual registers are addressed through the frame") {
// a re-entrant subroutine keeps its virtual registers in its own frame (slice 3), so they
// are addressed relative to a5 instead of the flat program-static register file
val lines = generateAsmWithIncoming(8, listOf(
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.WORD, 5, IRMemory.direct("main.var")),
IRInstructions.returnRegister(IRDataType.WORD, 5)
), frameVregSlots = mapOf(5 to -2, 6 to -4))
lines.any { it.startsWith("link ") } shouldBe true
lines.any { it.startsWith("move") && it.contains("-2(a5)") } shouldBe true
lines.none { it.startsWith("move") && it.contains("p8_regfile") } shouldBe true
}
test("virtual registers without a frame slot keep using the static register file") {
val lines = generateAsmWithIncoming(8, listOf(
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.WORD, 5, IRMemory.direct("main.var")),
IRInstructions.returnRegister(IRDataType.WORD, 5)
), frameVregSlots = mapOf(6 to -4))
// register 5 has no frame slot, so it stays in the static register file
lines.any { it.startsWith("move") && it.contains("p8_regfile") } shouldBe true
lines.any { it.startsWith("move") && it.contains("(a5)") && it.contains("p8_regfile") } shouldBe false
}
test("a byte-sized frame-resident register is addressed with its byte offset") {
val lines = generateAsmWithIncoming(4, listOf(
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 5, IRMemory.direct("main.var")),
IRInstructions.returnRegister(IRDataType.BYTE, 5)
), frameVregSlots = mapOf(5 to -4))
lines.any { it.startsWith("move.b") && it.contains("-4(a5)") } shouldBe true
}
test("sub-byte extraction of a frame-resident register uses a valid a5 displacement") {
// MSIGB on a long goes through regAddrByte, which must not emit "$(-4)(a5)"
val lines = generateAsmWithIncoming(4, listOf(
IRInstructions.binary(Opcode.MSIGB, IRDataType.LONG, 6, 5),
IRInstructions.returnRegister(IRDataType.BYTE, 6)
), frameVregSlots = mapOf(5 to -4))
lines.any { it.startsWith("move.b") && it.contains("-4(a5)") } shouldBe true
lines.none { it.contains("\$(-4)(a5)") } shouldBe true
}
test("framed subroutine emits a stack frame listing comment") {
val lines = generateAsmWithIncoming(24, listOf(
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-1)),
IRInstructions.simple(Opcode.RETURN)
), incomingSize = 8)
lines.any { it == "; stack frame: 24 bytes of locals, 8 bytes of arguments" } shouldBe true
}
test("incoming-only frame emits a stack frame listing comment with zero locals") {
val lines = generateAsmWithIncoming(0, listOf(IRInstructions.simple(Opcode.RETURN)), incomingSize = 8)
lines.any { it == "; stack frame: 0 bytes of locals, 8 bytes of arguments" } shouldBe true
}
test("frameless subroutine emits no stack frame listing comment") {
val lines = generateAsm(0, listOf(
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 1, IRMemory.direct("some.var")),
IRInstructions.simple(Opcode.RETURN)
))
lines.none { it.startsWith("; stack frame:") } shouldBe true
}
test("subroutine boundary markers are emitted exactly once per subroutine") {
val lines = generateAsm(16, listOf(
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-1)),
IRInstructions.simple(Opcode.RETURN)
))
lines.count { it == "; ---- Subroutine: test.start ----" } shouldBe 1
lines.count { it == "; End of subroutine: test.start" } shouldBe 1
}
test("large frames report an informational message") {
val errors = ErrorReporterForTests()
generateAsmWithIncoming(900, listOf(
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-1)),
IRInstructions.simple(Opcode.RETURN)
), subLabel = "p8b_main.p8s_worker", errors = errors)
errors.infos.any { it.contains("subroutine 'main.worker' uses a 900 byte stack frame; deep recursion multiplies this per call") } shouldBe true
}
test("small frames produce no informational message") {
val errors = ErrorReporterForTests()
generateAsmWithIncoming(24, listOf(
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-1)),
IRInstructions.simple(Opcode.RETURN)
), incomingSize = 8, errors = errors)
errors.infos shouldBe emptyList()
}
test("a long run of longword zero-clears lowers to a dbra loop") {
val stores = (0 until 17).map { i ->
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.LONG, IRMemory.frameDirect(-68 + i * 4))
}
val lines = generateAsm(68, stores + IRInstructions.simple(Opcode.RETURN))
lines.any { it == "moveq #0,d0" } shouldBe true
lines.any { it == "lea -68(a5),a0" } shouldBe true
lines.any { it == "moveq #16,d1" } shouldBe true
lines.any { it == "move.l d0,(a0)+" } shouldBe true
lines.count { it.startsWith("dbra d1,zeroloop_") } shouldBe 1
lines.none { it.startsWith("clr.l") } shouldBe true
}
test("a short run of longword zero-clears keeps straight stores") {
val stores = (0 until 15).map { i ->
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.LONG, IRMemory.frameDirect(-60 + i * 4))
}
val lines = generateAsm(60, stores + IRInstructions.simple(Opcode.RETURN))
lines.count { it.startsWith("clr.l") } shouldBe 15
lines.none { it.startsWith("dbra") } shouldBe true
}
test("an interleaved store splits the zero-clear run") {
val first = (0 until 8).map { i ->
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.LONG, IRMemory.frameDirect(-68 + i * 4))
}
val second = (0 until 8).map { i ->
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.LONG, IRMemory.frameDirect(-32 + i * 4))
}
val instructions = first +
IRInstructions.storeImmediate(IRDataType.BYTE, 0, IRMemory.frameDirect(-36)) +
second +
IRInstructions.simple(Opcode.RETURN)
val lines = generateAsm(68, instructions)
// two runs of 8 stay below the loop threshold
lines.none { it.startsWith("dbra") } shouldBe true
lines.count { it.startsWith("clr.l") } shouldBe 16
lines.any { it == "clr.b -36(a5)" } shouldBe true
}
test("dispatch jump in a framed subroutine pops the frame before jumping") {
// an on..goto leaves the subroutine for good, so the target's rts must find our caller's
// return address: leaving our frame behind would make it pop garbage
val lines = generateAsm(16, listOf(
IRInstructions.jumpIndirect(5),
IRInstructions.simple(Opcode.RETURN)
))
val unlkIdx = lines.indexOf("unlk a5")
unlkIdx shouldBe lines.indexOfFirst { it.startsWith("jmp") } - 1
}
test("dispatch jump in a frameless subroutine emits no frame teardown") {
val lines = generateAsm(0, listOf(
IRInstructions.jumpIndirect(5),
IRInstructions.simple(Opcode.RETURN)
))
lines.none { it.startsWith("unlk") } shouldBe true
}
test("jump to an absolute address pops the frame before jumping") {
// a fixed address can't be resolved to a chunk of this subroutine, so it must be assumed
// to leave for good and the frame must be gone before the target's eventual rts
val lines = generateAsm(16, listOf(
IRInstructions.jump(codeAddress(0x1234u)),
IRInstructions.simple(Opcode.RETURN)
))
val jmpIdx = lines.indexOfFirst { it.startsWith("jmp") }
(jmpIdx > 0) shouldBe true
lines[jmpIdx - 1] shouldBe "unlk a5"
}
test("jump to an absolute address in a frameless subroutine emits no frame teardown") {
val lines = generateAsm(0, listOf(
IRInstructions.jump(codeAddress(0x1234u)),
IRInstructions.simple(Opcode.RETURN)
))
lines.none { it.startsWith("unlk") } shouldBe true
}
test("conditional branch to an absolute address tears down only the taken path") {
val lines = generateAsm(16, listOf(
IRInstructions.branch(Opcode.BSTEQ, codeAddress(0x1234u)),
IRInstructions.simple(Opcode.RETURN)
))
val branchIdx = lines.indexOfFirst { it.startsWith("bne") }
val jmpIdx = lines.indexOfFirst { it.startsWith("jmp") && "$1234" in it }
(branchIdx >= 0) shouldBe true
(jmpIdx > branchIdx) shouldBe true
lines[jmpIdx - 1] shouldBe "unlk a5"
lines.count { it == "unlk a5" } shouldBe 2
}
test("conditional branch to an external non-entry label tears down only the taken path") {
val outsideInner = IRCodeChunk("test.outside.inner", null).also {
it.instructions.add(IRInstructions.simple(Opcode.RETURN))
}
val lines = generateAsmForSubs(
sub("test.start", frameSize = 16, instructions = listOf(
IRInstructions.branch(Opcode.BSTEQ, CodeReference.Label("test.outside.inner")),
IRInstructions.simple(Opcode.RETURN)
)),
sub("test.outside", frameSize = 0, instructions = listOf(IRInstructions.simple(Opcode.RETURN)),
extraChunks = listOf(outsideInner))
)
val jmpIdx = lines.indexOfFirst { it.startsWith("jmp") && "outside.inner" in it }
(jmpIdx > 0) shouldBe true
lines[jmpIdx - 1] shouldBe "unlk a5"
lines.any { it.startsWith("bne") } shouldBe true
}
test("signed comparison branch to an absolute address tears down only the taken path") {
val lines = generateAsm(16, listOf(
IRInstructions.branchImmediate(Opcode.BGTS, IRDataType.WORD, 1, 1, codeAddress(0x1234u)),
IRInstructions.simple(Opcode.RETURN)
))
lines.any { it.startsWith("cmpi.w") } shouldBe true
val branchIdx = lines.indexOfFirst { it.startsWith("ble") }
val jmpIdx = lines.indexOfFirst { it.startsWith("jmp") && "$1234" in it }
(branchIdx >= 0) shouldBe true
(jmpIdx > branchIdx) shouldBe true
lines[jmpIdx - 1] shouldBe "unlk a5"
lines.count { it == "unlk a5" } shouldBe 2
}
test("jump to a label in another subroutine pops the frame before jumping") {
// jumping out of the subroutine is a documented way to end it, so our frame must not be
// left behind for whatever runs next (and its eventual rts) to trip over
val lines = generateAsmForSubs(
sub("test.start", frameSize = 16, instructions = listOf(
IRInstructions.jump(CodeReference.Label("test.outside")),
IRInstructions.simple(Opcode.RETURN)
)),
sub("test.outside", frameSize = 0, instructions = listOf(IRInstructions.simple(Opcode.RETURN)))
)
val braIdx = lines.indexOfFirst { it.startsWith("bra") && "test.outside" in it }
(braIdx > 0) shouldBe true
lines[braIdx - 1] shouldBe "unlk a5"
}
test("jump to a label in a chunk of the same subroutine keeps the frame") {
val lines = generateAsmForSubs(
sub("test.start", frameSize = 16, instructions = listOf(
IRInstructions.jump(CodeReference.Label("test.start.second")),
IRInstructions.simple(Opcode.RETURN)
), extraChunks = listOf(
IRCodeChunk("test.start.second", null).also {
it.instructions.add(IRInstructions.storeImmediate(IRDataType.BYTE, 0, IRMemory.direct("test.var")))
}
))
)
lines.count { it == "unlk a5" } shouldBe 1 // only the return epilogue
lines[lines.indexOf("unlk a5") + 1] shouldBe "rts"
lines.any { it.startsWith("bra") && "test.start.second" in it } shouldBe true
}
test("jump to a label inside a loop body of the same subroutine keeps the frame") {
val loopBodyChunk = IRCodeChunk("test.start.loopbody", null)
loopBodyChunk.instructions.add(IRInstructions.jump(CodeReference.Label("test.start.loopbody")))
val lines = generateAsmForSubs(
sub("test.start", frameSize = 16, instructions = listOf(
IRInstructions.simple(Opcode.RETURN)
), extraChunks = listOf(IRLoopChunk("test.start.loop", 3, mutableListOf(loopBodyChunk))))
)
lines.count { it == "unlk a5" } shouldBe 1 // the jump to the loop body label must not tear the frame down
}
})
@@ -31,6 +31,7 @@
package prog8.codegen.new6502
import prog8.code.StStructInstanceBlockName
import prog8.code.core.*
import prog8.codegen.new6502.optimization.PeepholeOptimizer
import prog8.intermediate.*
@@ -116,7 +117,7 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa
emitBssSection()
val options = program.options
val asmFile = options.outputDir.resolve("${program.name}.asm")
val asmFile = OutputFiles.of(options, program.name).asm()
try {
asmFile.toFile().writeText(output.toString())
} catch (e: Exception) {
@@ -227,7 +228,25 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa
name.startsWith('>') -> ">" to name.drop(1)
else -> "" to name
}
return "$prefix${fixNameSymbols(rest)}"
val node = program.st.lookup(rest)
return when (node) {
is IRStStructInstance -> "$prefix${structInstanceRef(node, rest)}"
else -> "$prefix${fixNameSymbols(rest)}"
}
}
private fun structInstanceRef(node: IRStStructInstance, name: String): String {
// Uninitialized instances (empty initializer, or unions) are emitted into
// the prog8_struct_instances_bss block, but the IR bakes in the plain
// prog8_struct_instances prefix into every reference. Rewrite those to
// match the emitted block label.
val def = program.st.lookup(node.structName) as IRStStructDef
val fixed = fixNameSymbols(name)
return if (node.values.isEmpty() || def.isUnion) {
"${StStructInstanceBlockName}_bss.${fixed.substringAfter('.')}"
} else {
fixed
}
}
fun resolveSymbolRef(name: String): String {
@@ -235,8 +254,8 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa
return when (node) {
is IRStConstant -> constLabel(name)
is IRStStaticVariable, is IRStMemVar,
is IRStMemorySlab,
is IRStStructInstance -> fixNameSymbols(name)
is IRStMemorySlab -> fixNameSymbols(name)
is IRStStructInstance -> structInstanceRef(node, name)
else -> name // external/library symbol, keep as-is
}
}
@@ -936,34 +955,58 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa
if (structs.isEmpty()) return
emitRaw("; struct type definitions")
for (sd in structs) {
val paramFields = sd.fields.filter { it.arraySize == null }
val structargs = paramFields.indices.joinToString(",") { "f$it" }
emitRaw("${fixNameSymbols(sd.name)} .struct $structargs")
var paramIdx = 0
for (field in sd.fields) {
val type = when {
field.type.isBool || field.type.isUnsignedByte -> ".byte"
field.type.isSignedByte -> ".char"
field.type.isUnsignedWord || field.type.isPointer -> ".word"
field.type.isSignedWord -> ".sint"
field.type.isLong -> ".dint"
field.type.isFloat -> ".byte"
else -> ".byte"
if(sd.isUnion) {
// NB: .union takes no constructor parameters, unlike .struct
emitRaw("${fixNameSymbols(sd.name)} .union")
emitStructDefFields(sd) { "?" }
emitRaw(" .endunion")
emitRaw("${fixNameSymbols(sd.name)}_size=${sd.size}")
} else {
val paramFields = sd.fields.filter { it.arraySize == null }
val structargs = paramFields.indices.joinToString(",") { "f$it" }
emitRaw("${fixNameSymbols(sd.name)} .struct $structargs")
var paramIdx = 0
emitStructDefFields(sd) { "\\f${paramIdx++}" }
emitRaw(" .endstruct")
emitRaw("${fixNameSymbols(sd.name)}_size=${sd.size}")
}
emitRaw("")
}
emitRaw("")
}
private fun emitStructDefFields(sd: IRStStructDef, scalarValue: () -> String) {
for (field in sd.fields) {
val fieldName = "p8v_${field.name}"
val arrSize = field.arraySize
val effectiveDt = if(arrSize != null) field.type.elementType() else field.type
if (effectiveDt.isFloat) {
val floatSize = target.FLOAT_MEM_SIZE.toInt()
if (arrSize != null) {
emitRaw(" $fieldName .fill ${floatSize*arrSize}, ?")
} else {
emitRaw(" $fieldName .fill $floatSize, ${scalarValue()}")
}
val fieldName = "p8v_${field.name}"
val arrSize = field.arraySize
} else {
val type = structDefFieldAsmType(effectiveDt)
if (arrSize != null) {
val anon = List(arrSize) { "?" }.joinToString(",")
emitRaw(" $fieldName $type $anon")
} else {
emitRaw(" $fieldName $type \\f$paramIdx")
paramIdx++
emitRaw(" $fieldName $type ${scalarValue()}")
}
}
emitRaw(" .endstruct")
emitRaw("")
}
emitRaw("")
}
private fun structDefFieldAsmType(type: DataType): String = when {
type.isBool || type.isUnsignedByte -> ".byte"
type.isSignedByte -> ".char"
type.isUnsignedWord || type.isPointer -> ".word"
type.isSignedWord -> ".sint"
type.isLong -> ".dint"
type.isFloat -> ".byte"
else -> ".byte"
}
private fun emitFloatConstants() {
@@ -1028,7 +1071,10 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa
// struct instances without init values -> BSS (zeroed at startup)
val allInstances = program.st.allStructInstances().toList()
val (instancesNoInit, instancesWithInit) = allInstances.partition { it.values.isEmpty() }
val (instancesNoInit, instancesWithInit) = allInstances.partition {
val def = program.st.lookup(it.structName) as IRStStructDef
it.values.isEmpty() || def.isUnion
}
if (instancesNoInit.isNotEmpty()) {
emitRaw(" .section BSS")
emitRaw("prog8_struct_instances_bss .block")
@@ -1053,13 +1099,17 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa
for (fieldValue in si.values) {
when (val fv = fieldValue.value) {
is IRStSymbolicReference.Numeric -> {
val v = fv.value.toInt()
val size = when (fieldValue.dt) {
BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.BOOL -> ".byte"
BaseDataType.UWORD, BaseDataType.WORD -> ".word"
else -> ".word"
// FLOAT is 5 MFLPT bytes (6502), LONG is 4 bytes; both would
// be truncated if emitted as a 2-byte .word, corrupting the
// instance layout.
when (fieldValue.dt) {
BaseDataType.FLOAT -> emitLine(" .byte [${target.getFloatAsmBytes(fv.value)}] ; float ${fv.value}")
BaseDataType.LONG -> emitLine(" .dint ${fv.value.toLong()}")
BaseDataType.BYTE -> emitLine(" .char ${fv.value.toInt()}")
BaseDataType.UBYTE, BaseDataType.BOOL -> emitLine(" .byte ${fv.value.toInt()}")
BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.STR, BaseDataType.POINTER -> emitLine(" .word ${fv.value.toInt()}")
else -> error("unsupported datatype for struct instance field value: ${fieldValue.dt}")
}
emitLine(" $size $v")
}
is IRStSymbolicReference.Symbol -> {
emitLine(" .word ${fixNameSymbols(fv.name)}")
@@ -21,6 +21,8 @@
package prog8.codegen.new6502
import prog8.code.core.AssemblyError
import prog8.code.cpu6502.byteShiftAddExpansion
import prog8.code.cpu6502.wordShiftAddExpansion
import prog8.intermediate.*
internal fun AsmGen.translateArithmetic(insn: IRInstruction) {
@@ -722,20 +724,42 @@ internal fun AsmGen.mulImmediate(dstReg: Int, value: Int, type: IRDataType) {
when (type) {
IRDataType.BYTE -> {
emitLine("lda ${regAddrLo(dstReg)}")
emitLine("ldy #${value and 0xff}")
emitLine("jsr prog8_math.multiply_bytes")
// small constant: expand inline as a Horner shift-add instead of calling the ~130
// cycle multiply_bytes. Shared sequence with the classic 6502 backend.
val expansion = byteShiftAddExpansion(value)
if (expansion != null) {
expansion.forEach { emitLine(it) }
} else {
emitLine("ldy #${value and 0xff}")
emitLine("jsr prog8_math.multiply_bytes")
}
emitLine("sta ${regAddrLo(dstReg)}")
}
IRDataType.WORD -> {
emitLine("lda #<${value and 0xffff}")
emitLine("sta prog8_math.multiply_words.multiplier")
emitLine("lda #>${value and 0xffff}")
emitLine("sta prog8_math.multiply_words.multiplier+1")
emitLine("lda ${regAddrLo(dstReg)}")
emitLine("ldy ${regAddrHi(dstReg)}")
emitLine("jsr prog8_math.multiply_words")
emitLine("sta ${regAddrLo(dstReg)}")
emitLine("sty ${regAddrHi(dstReg)}")
val expansion = wordShiftAddExpansion(value)
if (expansion != null) {
// set up the shared sequence's contract: A = r.lo, W1 = r, W2 = x
emitLine("lda ${regAddrLo(dstReg)}")
emitLine("sta P8ZP_SCRATCH_W2")
emitLine("sta P8ZP_SCRATCH_W1")
emitLine("lda ${regAddrHi(dstReg)}")
emitLine("sta P8ZP_SCRATCH_W2+1")
emitLine("sta P8ZP_SCRATCH_W1+1")
emitLine("lda P8ZP_SCRATCH_W1")
expansion.forEach { emitLine(it) }
emitLine("sta ${regAddrLo(dstReg)}")
emitLine("sty ${regAddrHi(dstReg)}")
} else {
emitLine("lda #<${value and 0xffff}")
emitLine("sta prog8_math.multiply_words.multiplier")
emitLine("lda #>${value and 0xffff}")
emitLine("sta prog8_math.multiply_words.multiplier+1")
emitLine("lda ${regAddrLo(dstReg)}")
emitLine("ldy ${regAddrHi(dstReg)}")
emitLine("jsr prog8_math.multiply_words")
emitLine("sta ${regAddrLo(dstReg)}")
emitLine("sty ${regAddrHi(dstReg)}")
}
}
IRDataType.LONG -> {
// multiplicand (dstReg) -> cx16.r12/r13, immediate multiplier -> cx16.r14/r15
@@ -1535,6 +1559,41 @@ internal fun AsmGen.translateFloatArithmetic(insn: IRInstruction) {
emitLine("jsr floats.MOVMF")
}
Opcode.ADDIM -> {
val value = insn.requireImmediateFloat()
val constLabel = getFloatConstLabel(value)
val target = resolveAddress(insn.requireMemory())
emitLine("lda #<$target")
emitLine("ldy #>$target")
emitLine("jsr floats.MOVFM")
emitLine("lda #<$constLabel")
emitLine("ldy #>$constLabel")
emitLine("jsr floats.FADD")
emitLine("ldx #<$target")
emitLine("ldy #>$target")
emitLine("jsr floats.MOVMF")
}
Opcode.SUBIM -> {
// NOTE: FSUB does FAC1 = memory - FAC1, so we must use push/pop + FSUBT instead
val value = insn.requireImmediateFloat()
val constLabel = getFloatConstLabel(value)
val target = resolveAddress(insn.requireMemory())
emitLine("lda #<$target")
emitLine("ldy #>$target")
emitLine("jsr floats.MOVFM")
emitLine("jsr floats.pushFAC1")
emitLine("lda #<$constLabel")
emitLine("ldy #>$constLabel")
emitLine("jsr floats.MOVFM")
emitLine("sec")
emitLine("jsr floats.popFAC")
emitLine("jsr floats.FSUBT")
emitLine("ldx #<$target")
emitLine("ldy #>$target")
emitLine("jsr floats.MOVMF")
}
Opcode.INCM -> {
val target = resolveAddress(insn.requireMemory())
emitLine("lda #<$target")
@@ -494,7 +494,7 @@ internal val CallResult.hardwareSlot: CallingConventionSlot?
/**
* The slot-based arguments, in the order they must be loaded to avoid register clobbering.
* Order matches the old 6502 codegen: paired regs first (AX/AY/XY), then single regs (Y, X, A),
* then float regs, then status flags. This ensures that loading a paired register
* then float regs, then status flags. That way, loading a paired register
* (which clobbers two hardware regs) doesn't overwrite a value needed for a later argument.
* Within each group the original argument order is kept.
*/
@@ -554,8 +554,8 @@ private fun AsmGen.translateCall(fnLabel: String, site: CallSite) {
// the IR generates LOADHR for them.
// In single-return expression context, process slot returns normally
// (the IR doesn't generate LOADHR for single-return calls).
// LIMITATION: multiple status flag returns in one multi-assign (e.g. -> bool @Pz, bool @Pc)
// are not supported - codegen limitation: the first flag's extraction clobbers the state for subsequent flags.
// Multiple status flag returns in one multi-assign are protected by the IR:
// it wraps each flag's extraction in PUSHST/POPST (php/plp) so they don't clobber each other.
val isMultiReturn = site.results.size > 1
for (ret in site.results) {
if (ret.location is CallLocation.StatusFlag)
@@ -670,6 +670,8 @@ private fun AsmGen.translateArgument(arg: CallArgument, argIndex: Int = -1, fnLa
// Syscall argument - value is already in the register file,
// the syscall handler reads it from there.
}
is CallLocation.FrameSlot ->
error("stack frame argument slots are only supported on m68k targets: $location")
is CallLocation.HardwareRegister -> throw IllegalStateException("slot already handled")
}
}
@@ -1104,8 +1104,9 @@ internal fun AsmGen.copyRegisterLong(dstReg: Int, srcReg: Int) {
emitLine("bpl -")
}
/** the address of a [base] (+ [displacement], for a Symbol base only - matches the previous behavior
* of ignoring any offset that came together with a fixed numeric address) */
/** the address of a [base], plus [displacement]. The displacement is only added
* for a Symbol base; this matches the previous behavior of ignoring any offset
* that came together with a fixed numeric address. */
internal fun AsmGen.resolveAddress(base: AddressBase, displacement: Int = 0): String {
return when (base) {
is AddressBase.Symbol -> {
@@ -1113,6 +1114,7 @@ internal fun AsmGen.resolveAddress(base: AddressBase, displacement: Int = 0): St
if (displacement != 0) "$resolved+$displacement" else resolved
}
is AddressBase.Absolute -> base.address.value.toHex()
is AddressBase.FrameSlot -> error("stack frame slots are only supported on m68k targets, got $base")
}
}
@@ -41,6 +41,6 @@ class New6502CodeGenerator(val retainSSA: Boolean,
if(!success)
return null
return AssemblyProgram6502(irProgram.name, irProgram.options.outputDir, irProgram.options.compTarget)
return AssemblyProgram6502(irProgram.name, irProgram.options, irProgram.options.compTarget)
}
}
@@ -1013,11 +1013,12 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
}
in powersOfTwoFloat -> {
val numshifts = powersOfTwoFloat.indexOf(cv)
if (leftDt.isInteger) {
// division by a power of two => logical shift right
// NOTE: technically this optimization is only 100% accurate on unsigned integers
// whereas shifthing signed integers can result in an off-by-1 rounding difference
// (If you change the guard to only trigger this for unsigned integers the codegen will fall back to using division, which is slow but 100% accurate)
if (leftDt.isUnsignedInteger) {
// unsigned division by a power of two => logical shift right (exact)
// Signed integers must NOT be folded here: the shift floors toward -inf, while
// Prog8's signed '/' truncates toward zero, so x / 2^n would be off by one for
// every negative dividend that is not an exact multiple. Leaving the division in
// place lets the codegen emit a correct (slower) signed DIVS instead.
return BinaryExpression(expr.left, ">>", NumericLiteral.optimalInteger(numshifts, expr.position), expr.position)
}
}
@@ -57,11 +57,11 @@ fun Program.optimizeStatements(errors: IErrorReporter,
return optimizationCount
}
fun Program.inlineSubroutines(options: CompilationOptions): Int {
fun Program.inlineSubroutines(errors: IErrorReporter, options: CompilationOptions): Int {
// Skip inlining when optimizations are disabled (-noopt flag)
if (!options.optimize) return 0
val inliner = Inliner(this, options)
val inliner = Inliner(this, options, errors)
inliner.visit(this)
val mods = inliner.applyModifications()
inliner.linkAffectedParents(namespace)
+165 -11
View File
@@ -5,6 +5,7 @@ import prog8.ast.expressions.*
import prog8.ast.statements.*
import prog8.ast.walk.*
import prog8.code.core.CompilationOptions
import prog8.code.core.IErrorReporter
import prog8.code.core.InternalCompilerException
@@ -27,11 +28,13 @@ private class CannotInlineException : Exception()
* - Subroutines must have a single functional statement in their body to be auto-inlined.
* - Multi-return value subroutines are not yet supported for auto-inlining.
*/
class Inliner(private val program: Program, private val options: CompilationOptions): AstWalker() {
class Inliner(private val program: Program, private val options: CompilationOptions, private val errors: IErrorReporter): AstWalker() {
private val suggestInlineReported = mutableSetOf<Subroutine>()
inner class DetermineInlineSubs(val program: Program): IAstVisitor {
private val modifications = mutableListOf<AstModification>()
private val visitedSubroutines = java.util.Collections.newSetFromMap(java.util.IdentityHashMap<Subroutine, Boolean>())
private val visitedSubroutines = mutableSetOf<Subroutine>()
init {
visit(program)
@@ -238,8 +241,11 @@ class Inliner(private val program: Program, private val options: CompilationOpti
override fun after(functionCallStatement: FunctionCallStatement, parent: Node): Iterable<AstModification> {
val sub = functionCallStatement.target.targetStatement(program.builtinFunctions) as? Subroutine
if (sub == null) return noModifications
if (!sub.inline) return noModifications
if (!sub.inline) {
maybeSuggestInlineKeyword(sub)
return noModifications
}
val (canInline, _) = canInlineAtCallSiteWithReason(sub, functionCallStatement)
if (!canInline) {
@@ -258,15 +264,43 @@ class Inliner(private val program: Program, private val options: CompilationOpti
return false to "asmsub with parameters cannot be inlined"
}
if (sub.parameters.size > 1) {
return false to "subroutines with more than 1 parameter cannot be inlined in this compiler version"
}
if (sub.parameters.any { it.registerOrPair != null }) {
return false to "subroutines with register-mapped parameters cannot be inlined"
}
if (sub.parameters.size == 1) {
if (sub.parameters.size > 1) {
// Multi-parameter inlining is only supported on m68k targets (stack-based
// calling convention makes the call overhead worthwhile), and only for the
// single-return-value expression case with a tiny body and simple args.
if (!options.compTarget.cpu.is68k) {
return false to "subroutines with more than 1 parameter cannot be inlined in this compiler version"
}
if (fcall.args.size != sub.parameters.size) {
return false to "argument count does not match parameter count"
}
if (!fcall.args.all { it.isSimple }) {
return false to "argument is too complex to inline (might have side effects)"
}
// Refuse to inline a sub that assigns directly to any of its parameters.
// Substituting the argument into the target would crash for a literal
// argument, or wrongly modify the caller's variable for a variable argument
// (parameters are passed by value, so the sub only modifies its local copy).
val paramNames = sub.parameters.map { it.name }.toSet()
val bodyStmt = sub.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
if ((bodyStmt as? Assignment)?.target?.identifier?.nameInSource?.lastOrNull() in paramNames)
return false to "cannot inline subroutine that assigns to its parameter"
// v1 only supports the single-return-value expression case on this path.
// Void and multi-value statement call sites keep the previous restriction.
val returnStmt = bodyStmt as? Return
?: return false to "multi-parameter inlining requires a single return statement"
if (returnStmt.values.size != 1) {
return false to "multi-parameter inlining requires a single return value"
}
if (!isFairlySimpleReturnExpression(returnStmt.values[0], paramNames)) {
return false to "multi-parameter inline body is too complex"
}
// fall through to the 6502-independent checks below (none apply to m68k)
} else if (sub.parameters.size == 1) {
val arg = fcall.args.first()
if (!arg.isSimple) {
return false to "argument is too complex to inline (might have side effects)"
@@ -298,10 +332,52 @@ class Inliner(private val program: Program, private val options: CompilationOpti
return canInlineAtCallSiteWithReason(sub, fcall).first
}
private fun fitsM68kMultiParamInline(sub: Subroutine): Boolean {
if (!options.compTarget.cpu.is68k)
return false
if (sub.inline)
return false
if (sub.isAsmSubroutine && sub.parameters.isNotEmpty())
return false
if (sub.parameters.size <= 1)
return false
if (sub.parameters.any { it.registerOrPair != null })
return false
if (sub === program.entrypoint)
return false
if (sub.statements.any { it is Subroutine || (it is VarDecl && it.origin != VarDeclOrigin.SUBROUTINEPARAM) })
return false
val functionalCount = sub.statements.count { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
if (functionalCount != 1 && functionalCount != 2)
return false
val bodyStmt = sub.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
val returnStmt = bodyStmt as? Return ?: return false
if (returnStmt.values.size != 1)
return false
if (functionalCount == 2 && sub.statements.lastOrNull()?.let(::isEmptyReturn) != true)
return false
val paramNames = sub.parameters.map { it.name }.toSet()
if (!isFairlySimpleReturnExpression(returnStmt.values[0], paramNames))
return false
return true
}
private fun maybeSuggestInlineKeyword(sub: Subroutine) {
if (sub in suggestInlineReported)
return
if (!fitsM68kMultiParamInline(sub))
return
suggestInlineReported.add(sub)
errors.info("subroutine '${sub.name}' would likely benefit from 'inline'", sub.position)
}
override fun before(functionCallExpr: FunctionCallExpression, parent: Node): Iterable<AstModification> {
val sub = functionCallExpr.target.targetStatement(program.builtinFunctions) as? Subroutine
if (sub == null) return noModifications
if (!sub.inline) return noModifications
if (!sub.inline) {
maybeSuggestInlineKeyword(sub)
return noModifications
}
val (canInline, _) = canInlineAtCallSiteWithReason(sub, functionCallExpr)
if (!canInline) {
@@ -422,6 +498,78 @@ class Inliner(private val program: Program, private val options: CompilationOpti
}
}
/**
* m68k-only predicate for multi-parameter inlining (v1: expression case only).
* Allows any number of parameters but bounds the duplicated work instead:
* single return expression, expression-tree depth <= MAX_DEPTH, total nodes
* <= MAX_NODES, and total parameter occurrences <= MAX_PARAM_USES.
* Numeric literals and constant identifiers count as free (cost 0).
*/
private fun isFairlySimpleReturnExpression(expr: Expression, paramNames: Set<String>): Boolean {
val maxDepth = 3
val maxNodes = 12
val maxParamUses = 6
var nodes = 0
var paramUses = 0
var ok = true
fun isFreeLeaf(ident: IdentifierReference): Boolean {
// Constants count as free (usually already constant-folded to literals).
if (ident.constValue(program) != null)
return true
val target = try {
ident.targetStatement()
} catch (_: Exception) {
null
}
return target is VarDecl && target.type == VarDeclType.CONST
}
fun visit(e: Expression, depth: Int) {
if (!ok)
return
if (depth > maxDepth) {
ok = false
return
}
when (e) {
is NumericLiteral -> {
// free, costs no node budget
}
is IdentifierReference -> {
if (!isFreeLeaf(e)) {
nodes++
if (nodes > maxNodes) {
ok = false
return
}
if (e.nameInSource.lastOrNull() in paramNames) {
paramUses++
if (paramUses > maxParamUses)
ok = false
}
}
}
is PrefixExpression -> visit(e.expression, depth + 1)
is BinaryExpression -> {
nodes++
if (nodes > maxNodes) {
ok = false
return
}
visit(e.left, depth + 1)
visit(e.right, depth + 1)
}
is TypecastExpression -> visit(e.expression, depth + 1)
else -> ok = false
}
}
visit(expr, 0)
return ok
}
/**
* Substitutes parameter references in a statement with argument values.
*/
@@ -434,7 +582,11 @@ class Inliner(private val program: Program, private val options: CompilationOpti
?: return noModifications
// Only handle inlining if sub is marked as such and call site is okay
if (!sub.inline || !canInlineAtCallSite(sub, fcall))
if (!sub.inline) {
maybeSuggestInlineKeyword(sub)
return noModifications
}
if (!canInlineAtCallSite(sub, fcall))
return noModifications
val toInline = sub.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM } as? Return ?: return noModifications
@@ -477,6 +629,8 @@ class Inliner(private val program: Program, private val options: CompilationOpti
}
private fun substituteParameters(sub: Subroutine, fcall: IFunctionCall, node: Node): Node? {
if (fcall.args.size != sub.parameters.size)
return null
val paramVarDecls = sub.parameters.map { param ->
sub.statements.filterIsInstance<VarDecl>().find { it.origin == VarDeclOrigin.SUBROUTINEPARAM && it.name == param.name }
?: throw FatalAstException("parameter ${param.name} not found in subroutine ${sub.name}")
@@ -486,15 +486,19 @@ class StatementOptimizer(private val program: Program,
return listOf(AstReplaceNode(assignment, ifstmt, parent))
}
if(v2 is NumericLiteral || v2 is IdentifierReference) {
if(v2 is NumericLiteral) {
// x = min(expression, 100) -> x=expression, if x>100 x=100
// Only safe with a real constant: the assignment to x happens before the
// comparison, so comparing against anything that x could alias (such as a
// variable that happens to be the assignment target) would read the already
// overwritten value.
val assign = Assignment(assignment.target.copy(), v1, AssignmentOrigin.OPTIMIZER, assignment.position)
val ifstmt = makeMinMaxCheckAndAssignRight(assign.target.toExpression(), ">", v2, assignment.target, assignment.position)
return listOf(
AstInsert.after(assignment, ifstmt, parent as IStatementContainer),
AstReplaceNode(assignment, assign, parent)
)
} else if(v1 is NumericLiteral || v1 is IdentifierReference) {
} else if(v1 is NumericLiteral) {
// x = min(100, expression) -> x=expression, if x>100 x=100
val assign = Assignment(assignment.target.copy(), v2, AssignmentOrigin.OPTIMIZER, assignment.position)
val ifstmt = makeMinMaxCheckAndAssignRight(assign.target.toExpression(), ">", v1, assignment.target, assignment.position)
@@ -519,15 +523,17 @@ class StatementOptimizer(private val program: Program,
return listOf(AstReplaceNode(assignment, ifstmt, parent))
}
if(v2 is NumericLiteral || v2 is IdentifierReference) {
if(v2 is NumericLiteral) {
// x = max(expression, 100) -> x=expression, if x<100 x=100
// See the note in the min__ case above: the compared operand must be a
// real constant, never a variable that the assignment could have aliased.
val assign = Assignment(assignment.target.copy(), v1, AssignmentOrigin.OPTIMIZER, assignment.position)
val ifstmt = makeMinMaxCheckAndAssignRight(assign.target.toExpression(), "<", v2, assignment.target, assignment.position)
return listOf(
AstInsert.after(assignment, ifstmt, parent as IStatementContainer),
AstReplaceNode(assignment, assign, parent)
)
} else if(v1 is NumericLiteral || v1 is IdentifierReference) {
} else if(v1 is NumericLiteral) {
// x = max(100, expression) -> x=expression, if x<100 x=100
val assign = Assignment(assignment.target.copy(), v2, AssignmentOrigin.OPTIMIZER, assignment.position)
val ifstmt = makeMinMaxCheckAndAssignRight(assign.target.toExpression(), "<", v1, assignment.target, assignment.position)
+46
View File
@@ -32,20 +32,66 @@ configurations.all {
exclude(group = "org.antlr", module = "antlr4")
}
// Post-process the generated launcher scripts: pass --enable-native-access only to
// JVMs that understand it (JDK 18+), to avoid JNA restricted-method warnings on newer JDKs.
fun patchLaunchScripts(scriptDir: File) {
val unixScript = scriptDir.resolve("prog8c")
if (unixScript.canRead()) {
val marker = "eval \"set -- $("
val text = unixScript.readText()
check(marker in text) { "unexpected POSIX launch script layout for prog8c" }
val probe = $$"""if "$JAVACMD" --enable-native-access=ALL-UNNAMED -version >/dev/null 2>&1; then
JAVA_OPTS="$JAVA_OPTS --enable-native-access=ALL-UNNAMED"
fi
"""
unixScript.writeText(text.replace(marker, probe + marker))
}
val windowsScript = scriptDir.resolve("prog8c.bat")
if (windowsScript.canRead()) {
val marker = "@rem Execute prog8c"
val text = windowsScript.readText()
check(marker in text) { "unexpected Windows launch script layout for prog8c" }
val probe = $$"""@rem Prog8: add --enable-native-access on JVMs that support it (JDK 18+) to avoid JNA restricted-method warnings
"%JAVA_EXE%" --enable-native-access=ALL-UNNAMED -version >NUL 2>&1
if not errorlevel 1 set JAVA_OPTS=%JAVA_OPTS% --enable-native-access=ALL-UNNAMED
"""
windowsScript.writeText(text.replace(marker, probe + marker))
}
}
tasks.startScripts {
enabled = true
}
tasks.withType<org.gradle.jvm.application.tasks.CreateStartScripts>().configureEach {
doLast {
patchLaunchScripts(outputDir!!)
}
}
application {
mainClass.set("prog8.CompilerMainKt")
applicationName = "prog8c"
}
tasks.jar {
manifest {
attributes("Main-Class" to "prog8.CompilerMainKt")
attributes("Enable-Native-Access" to "ALL-UNNAMED")
}
}
tasks.shadowJar {
archiveBaseName.set("prog8c")
archiveVersion.set(version.toString())
duplicatesStrategy = DuplicatesStrategy.INCLUDE
mergeServiceFiles()
manifest {
attributes("Main-Class" to "prog8.CompilerMainKt")
attributes("Enable-Native-Access" to "ALL-UNNAMED")
}
filesMatching("META-INF/LICENSE") {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
+5
View File
@@ -0,0 +1,5 @@
%import shared_m68k_adpcm
adpcm {
%option ignore_unused, private_symbols
}
+377
View File
@@ -0,0 +1,377 @@
%import exec
ahi {
%option no_symbol_prefixing, ignore_unused
; Playback-only binding for ahi.device, high-level (CMD_WRITE) API.
; AHI v4 compatible: ahir_Version is 4 and only 8/16-bit sample types
; are exposed (32-bit types need version 6+).
; The unit to play on is selectable via ahi.open() (pass 0 = AHI_DEFAULT_UNIT
; for the user's default output, or 1-3 for another configured unit; AHI_NO_UNIT
; only makes sense for low-level-only access and is not supported here).
; The low-level control and audio-mode query LVOs (audio channel handles)
; are exposed as extsubs below; their hooks and requester are deliberately
; not covered.
; Requires a system with ahi.device installed (real hardware or full
; emulation); not testable under vamos. Amiga1200 target only (68020+,
; Kickstart 3+): uses CreateMsgPort/CreateIORequest for port handling.
; Struct offsets verified against NDK3.2 exec/io.h (IOStdReq) and the
; AHI v6 dev kit (Developer/Include/Asm/devices/ahi.i, AHIRequest).
; ---- constants ----
const uword AHI_VERSION = 4 ; ahir_Version preset before OpenDevice
const ubyte AHI_DEFAULT_UNIT = 0
const ubyte AHI_NO_UNIT = 255 ; low-level API only, no hardware allocated
const long AHI_INVALID_ID = $ffffffff ; audio id sentinel, not a valid mode
const long AHI_DEFAULT_ID = 0 ; AllocAudioA: use the default mode
; sample formats for ahir_Type (all V4-compatible)
const long AHIST_M8S = 0 ; mono, 8-bit signed
const long AHIST_M16S = 1 ; mono, 16-bit signed (big-endian WORDs)
const long AHIST_S8S = 2 ; stereo, 8-bit signed
const long AHIST_S16S = 3 ; stereo, 16-bit signed (big-endian WORDs)
; volume and stereo position are 16.16 Fixed
const long FULL_VOLUME = $10000
const long PAN_LEFT = 0
const long PAN_CENTER = $8000
const long PAN_RIGHT = $10000
; error codes (AHIE_*, seen in io_Error alongside exec IO errors)
const byte AHIE_OK = 0
const byte AHIE_NOMEM = 1
const byte AHIE_BADSOUNDTYPE = 2
const byte AHIE_BADSAMPLETYPE = 3
const byte AHIE_ABORTED = 4
const byte AHIE_UNKNOWN = 5
const byte AHIE_HALFDUPLEX = 6
; ---- low-level LVO API ----
; These call the device's library-style routines through its base
; (sys.AHIBase, @bank 101), which ahi.open() sets from the opened
; device's io_Device. Register assignments and offsets verified against
; the AHI dev kit ahi_lib.fd/sfd (bias 42). The synth routines need an
; AudioCtrl handle from ahi.AllocAudioA(); the query trio reads the
; audio-mode database, useful for diagnostics. Taglists are plain
; 'pointer' parameters (like the intuition bindings): build them as
; ^^utility.TagItem arrays of {tag, data} pairs, terminated by
; utility.TAG_END. Data is a pointer-or-value per the tag's semantics.
extsub @bank 101 -42 = AllocAudioA(pointer tagList @A1) -> pointer @D0 ; returns AudioCtrl, or 0 (AF_NULL) if failed
extsub @bank 101 -48 = FreeAudio(pointer audioCtrl @A2)
extsub @bank 101 -60 = ControlAudioA(pointer audioCtrl @A2, pointer tagList @A1) -> long @D0
extsub @bank 101 -66 = SetVol(uword channel @D0, long volume @D1, long pan @D2, pointer audioCtrl @A2, long flags @D3)
extsub @bank 101 -72 = SetFreq(uword channel @D0, long freq @D1, pointer audioCtrl @A2, long flags @D2)
extsub @bank 101 -78 = SetSound(uword channel @D0, uword sound @D1, long offset @D2, long length @D3, pointer audioCtrl @A2, long flags @D4)
extsub @bank 101 -90 = LoadSound(uword sound @D0, long sampletype @D1, pointer info @A0, pointer audioCtrl @A2) -> long @D0
extsub @bank 101 -96 = UnloadSound(uword sound @D0, pointer audioCtrl @A2)
extsub @bank 101 -102 = NextAudioID(long lastID @D0) -> long @D0 ; next id, or AHI_INVALID_ID at the end
extsub @bank 101 -108 = GetAudioAttrsA(long id @D0, pointer audioCtrl @A2, pointer tagList @A1) -> long @D0 ; 1=ok, 0=invalid id
extsub @bank 101 -114 = BestAudioIDA(pointer tagList @A1) -> long @D0 ; best matching audio id, AHI_INVALID_ID if none
; For ahi.BestAudioIDA(): hard requirements go in the main taglist, and
; AHIB_Dizzy points at a second taglist of soft preferences - after the
; hard ones are applied, the mode with the most of the wanted (and fewest
; of the unwanted) features wins. Without it the call hardly does what its
; name suggests, so it is worth passing.
const long AHIB_Dizzy = $800000be
; ---- low-level tag constants ----
; AHIC_* go to ahi.ControlAudioA(), AHIDB_* to ahi.GetAudioAttrsA(),
; AHIB_* to ahi.BestAudioIDA().
; Boolean AHIDB_* tags return 0/1; the R-bit string tags (driver, name,
; author, copyright, version, annotation) are OUT parameters: the device
; writes a string into the buffer you pass (size it via AHIDB_BufferLen).
; Tag base is utility.TAG_USER.
const long AHIC_Play = $80000050
const long AHIC_Record = $80000051
const long AHIC_MonitorVolume = $80000052
const long AHIC_MonitorVolume_Query = $80000053
const long AHIC_MixFreq_Query = $80000054
const long AHIC_InputGain = $80000055
const long AHIC_InputGain_Query = $80000056
const long AHIC_OutputVolume = $80000057
const long AHIC_OutputVolume_Query = $80000058
const long AHIC_Input = $80000059
const long AHIC_Input_Query = $8000005a
const long AHIC_Output = $8000005b
const long AHIC_Output_Query = $8000005c
const long AHIDB_AudioID = $80000064
const long AHIDB_Driver = $80008065 ; R
const long AHIDB_Volume = $80000067
const long AHIDB_Panning = $80000068
const long AHIDB_Stereo = $80000069
const long AHIDB_HiFi = $8000006a
const long AHIDB_PingPong = $8000006b
const long AHIDB_Name = $8000806d ; R
const long AHIDB_Bits = $8000006e
const long AHIDB_MaxChannels = $8000006f
const long AHIDB_MinMixFreq = $80000070
const long AHIDB_MaxMixFreq = $80000071
const long AHIDB_Record = $80000072
const long AHIDB_Frequencies = $80000073
const long AHIDB_FrequencyArg = $80000074
const long AHIDB_Frequency = $80000075
const long AHIDB_Author = $80008076 ; R
const long AHIDB_Copyright = $80008077 ; R
const long AHIDB_Version = $80008078 ; R
const long AHIDB_Annotation = $80008079 ; R
const long AHIDB_BufferLen = $8000007a
const long AHIDB_IndexArg = $8000007b
const long AHIDB_Index = $8000007c
const long AHIDB_Realtime = $8000007d
const long AHIDB_MaxPlaySamples = $8000007e
const long AHIDB_MaxRecordSamples = $8000007f
const long AHIDB_FullDuplex = $80000081
const long AHIDB_MinMonitorVolume = $80000082
const long AHIDB_MaxMonitorVolume = $80000083
const long AHIDB_MinInputGain = $80000084
const long AHIDB_MaxInputGain = $80000085
const long AHIDB_MinOutputVolume = $80000086
const long AHIDB_MaxOutputVolume = $80000087
const long AHIDB_Inputs = $80000088
const long AHIDB_InputArg = $80000089
const long AHIDB_Input = $8000008a
const long AHIDB_Outputs = $8000008b
const long AHIDB_OutputArg = $8000008c
const long AHIDB_Output = $8000008d
; ---- struct ----
struct AHIRequest { ; total size: 80
; struct IOStdReq base (48 bytes, same layout as exec.IOStdReq)
^^exec.Node Succ ; 0
^^exec.Node Pred ; 4
ubyte Type ; 8
byte Pri ; 9
str Name ; 10
^^exec.MsgPort ReplyPort ; 14
uword Length ; 18
pointer Device ; 20
long Unit ; 24
uword Command ; 28
ubyte Flags ; 30
byte Error ; 31
long Actual ; 32
long IOStdReq_Length ; 36
pointer Data ; 40
long Offset ; 44
; AHI extension
uword Version ; 48
uword Pad ; 50
long Private1 ; 52
long Private2 ; 56
long SampleType ; 60
long Frequency ; 64
long Volume ; 68
long Position ; 72
pointer Link ; 76
}
; Two request blocks for gapless double-buffered playback, used
; ping-pong fashion and chained via ahir_Link. Allocated with
; CreateIORequest (Kickstart 2.04+), sharing a single reply port.
^^AHIRequest Req0
^^AHIRequest Req1
private ^^exec.MsgPort port
private ubyte queued ; number of outstanding writes (0..2)
private bool play_alt ; next play uses Req1
private bool wait_alt ; next wait reaps Req1
private bool opened
; ---- high level playback interface ----
sub open(ubyte unit) -> bool {
; -- Open ahi.device for playback on the given unit (0 = default,
; 1-3 other configured units; see the header note above).
; Returns false if unavailable.
if opened
return true
port = exec.CreateMsgPort()
if port == 0
return false
Req0 = exec.CreateIORequest(port, sizeof(ahi.AHIRequest))
if Req0 == 0 {
exec.DeleteMsgPort(port)
return false
}
Req0.Version = AHI_VERSION
if exec.OpenDevice("ahi.device", unit, Req0, 0)!=0 {
exec.DeleteIORequest(Req0)
exec.DeleteMsgPort(port)
return false
}
Req1 = exec.CreateIORequest(port, sizeof(ahi.AHIRequest))
if Req1 == 0 {
exec.CloseDevice(Req0)
exec.DeleteIORequest(Req0)
exec.DeleteMsgPort(port)
return false
}
Req1.Device = Req0.Device
Req1.Unit = Req0.Unit
Req1.Version = AHI_VERSION
sys.AHIBase = Req0.Device ; base for the low-level LVO calls above
queued = 0
play_alt = false
wait_alt = false
opened = true
return true
}
sub close() {
; -- Abort pending audio, close the device, free the port and requests.
if not opened
return
stop()
exec.CloseDevice(Req0)
exec.DeleteIORequest(Req0)
exec.DeleteIORequest(Req1)
exec.DeleteMsgPort(port)
sys.AHIBase = 0
opened = false
}
sub write_sync(pointer samples, long num_bytes, long sampletype, uword frequency, long volume, long position) -> byte {
; -- Play a sample synchronously, blocks until finished. Returns io_Error.
while queued>0
wait_oldest()
num_bytes = setup(Req0, 0, samples, num_bytes, sampletype, frequency, volume, position)
if num_bytes==0
return AHIE_OK
void exec.DoIO(Req0)
return Req0.Error
}
sub play(pointer samples, long num_bytes, long sampletype, uword frequency, long volume, long position) {
; -- Play a sample asynchronously on the (mixed) output.
; If chunks were queued earlier with play_queued, those are waited for first,
; so this never reuses a request block that still has a write in flight.
while queued>0
wait_oldest()
num_bytes = setup(Req0, 0, samples, num_bytes, sampletype, frequency, volume, position)
if num_bytes==0
return
BeginIO(Req0)
queued = 1
play_alt = true
wait_alt = false
; sound now plays asynchronously.
}
sub play_queued(pointer samples, long num_bytes, long sampletype, uword frequency, long volume, long position) {
; -- Queue a sample chunk for gapless playback (ahir_Link chaining).
; Unlike play(), this may be called while the previous chunk is still playing:
; AHI starts it the moment the previous chunk ends.
; If both slots are full this waits for the oldest chunk to finish first.
if queued==2
wait_oldest()
^^AHIRequest io = Req0
pointer link = 0
if play_alt {
io = Req1
link = Req0
} else {
if queued>0
link = Req1
}
num_bytes = setup(io, link, samples, num_bytes, sampletype, frequency, volume, position)
if num_bytes==0
return
play_alt = not play_alt
BeginIO(io)
queued = queued+1
; chunk plays (or stays queued) asynchronously.
}
sub wait() {
; -- Wait for all queued audio to finish playing.
while queued>0
wait_oldest()
}
sub stop() {
; -- Abort pending playback. Only our own requests are aborted;
; CMD_FLUSH would kill other programs' audio too and must not be used here.
while queued>0 {
^^AHIRequest io = get_slot(wait_alt)
exec.AbortIO(io)
void exec.WaitIO(io)
wait_alt = not wait_alt
queued = queued-1
}
play_alt = false
wait_alt = false
}
inline sub is_playing() -> bool {
; -- True while chunks are queued but not yet reaped by wait().
; Note: a finished chunk still counts until wait() reaps it.
return queued>0
}
sub frame_size(long sampletype) -> ubyte {
; -- Bytes per sample frame for the given AHIST_ type.
when sampletype {
AHIST_M8S -> return 1
AHIST_M16S, AHIST_S8S -> return 2
else -> return 4
}
}
private sub get_slot(bool alt) -> ^^AHIRequest {
; request block taking the next play/wait (primary or alternate).
if alt
return Req1
return Req0
}
private sub wait_oldest() {
; reap the oldest outstanding write.
if queued==0
return
^^AHIRequest io = get_slot(wait_alt)
void exec.WaitIO(io)
wait_alt = not wait_alt
queued = queued-1
if queued==0 {
play_alt = false
wait_alt = false
}
}
private sub setup(^^AHIRequest io, pointer link, pointer samples, long num_bytes, long sampletype, uword frequency, long volume, long position) -> long {
; -- Fill a request block. Rounds the length down to whole sample frames
; (io_Length must be a multiple of the frame size); io_Offset must be 0.
; Returns the rounded length (0 = nothing to play).
ubyte frame = frame_size(sampletype)
num_bytes = num_bytes / frame * frame
if num_bytes==0
return 0
io.Command = exec.CMD_WRITE
io.Flags = 0
io.Data = samples
io.IOStdReq_Length = num_bytes
io.Offset = 0
io.SampleType = sampletype
io.Frequency = frequency
io.Volume = volume
io.Position = position
io.Link = link
return num_bytes
}
private asmsub BeginIO(^^AHIRequest io @A1) clobbers (D0, D1, A0, A1, A6) {
; Calls the device's BeginIO vector directly, instead of exec DoIO/SendIO,
; because those clear io_Flags.
%asm {{
move.l 20(a1), a6 ; a6 = io_Device (offset 20 in IORequest)
jsr -30(a6) ; device BeginIO vector
rts
}}
}
}
@@ -0,0 +1,5 @@
%import shared_amiga_asyncio
asyncio {
%option no_symbol_prefixing, ignore_unused
}
@@ -0,0 +1,5 @@
%import shared_amiga_lowlevel
lowlevel {
%option no_symbol_prefixing, ignore_unused
}
@@ -29,6 +29,7 @@ p8_sys_startup {
; }
sub init_system() {
%option noframe
%asm {{
; save CLI arguments
move.l d0,-(sp)
@@ -89,6 +90,7 @@ proc_CLI = 172
}
sub init_system_phase2() {
%option noframe
%asm {{
moveq #0,d0
moveq #0,d1
@@ -110,6 +112,7 @@ proc_CLI = 172
}
sub cleanup_at_exit() {
%option noframe
%asm {{
movem.l d0,-(sp) ; keep return code
@@ -124,6 +127,8 @@ proc_CLI = 172
if sys.RexxSysBase != 0 exec.CloseLibrary(sys.RexxSysBase)
if sys.IFFParseBase != 0 exec.CloseLibrary(sys.IFFParseBase)
if sys.AsyncIOBase != 0 exec.CloseLibrary(sys.AsyncIOBase)
if sys.LowLevelBase != 0 exec.CloseLibrary(sys.LowLevelBase)
if sys.UtilityBase != 0 exec.CloseLibrary(sys.UtilityBase)
if sys.IconBase != 0 exec.CloseLibrary(sys.IconBase)
if sys.IntuitionBase != 0 exec.CloseLibrary(sys.IntuitionBase)
+95 -5
View File
@@ -39,19 +39,23 @@ sys {
pointer @shared UtilityBase
pointer @shared RexxSysBase ; loaded from disk on demand
pointer @shared IFFParseBase ; loaded from disk on demand
pointer @shared AsyncIOBase ; loaded from disk on demand
pointer @shared LowLevelBase ; kickstart 2.0+, loaded from disk on demand
pointer @shared TimerBase ; opened on demand
pointer @shared AHIBase ; ahi.device base, opened on demand by ahi.open(); for the low-level ahi.* LVO calls
^^ubyte @shared arguments ; CLI argument string (null-terminated), or NULL if Workbench launch
sub reset_system() {
%option noframe
%asm {{
move.l 4.w,a6
jmp -726(a6) ; ColdReboot()
}}
}
asmsub wait(long jiffies @D1) {
; --- wait approximately the given number of jiffies (1/60th seconds)
asmsub wait(long ticks @D1) {
; --- wait approximately the given number of ticks (1/50th seconds, i.e. 20ms each)
%asm {{
move.l sys.DOSBase,a6
jmp -198(a6) ; Delay
@@ -68,17 +72,18 @@ sys {
}}
}
sub exit(ubyte returnvalue) {
asmsub exit(word returnvalue @D0) {
; -- exit the program with a return code in D0. When invoked from Prog8 via sys.exit(), all active defers in the call chain are unwound program-wide (LIFO) before system cleanup. sys.reset_system() does not run defers.
%asm {{
moveq.l #0,d0
move.b sys.exit.returnvalue,d0
and.l #$ffff,d0
move.l p8_sys_startup.orig_stackpointer,sp
jmp p8_sys_startup.cleanup_at_exit
}}
}
sub set_carry() {
%option noframe
%asm {{
; set both C (comparison carry) and X (rotate carry) bits
moveq #$11,d0
@@ -87,6 +92,7 @@ sys {
}
sub clear_carry() {
%option noframe
%asm {{
; clear C and X bits
moveq #0,d0
@@ -125,12 +131,96 @@ sys {
}}
}
asmsub stack_size() -> long @D0 {
; -- Return the amount of stack space still available, in bytes.
%asm {{
move.l 4.w,a6 ; ExecBase
move.l $114(a6),a0 ; ThisTask
move.l sp,d0
sub.l $3a(a0),d0 ; minus tc_SPLower
rts
}}
}
asmsub cpuAtLeast68020() clobbers (A6, D0) -> bool @Pz {
%asm {{
move.l 4.w,a6
move.w 296(a6),d0
andi.w #$008e,d0
seq d0
tst.b d0
rts
}}
}
sub die(uword code, str message) {
%asm {{
moveq #0,d0
move.w 14(a5),d0
lea .alert(pc),a1
lea .alert_limit(pc),a3
move.w #20,(a1)+
move.b #16,(a1)+
lea .prefix(pc),a2
.copy_prefix:
move.b (a2)+,d1
beq.s .copy_code
move.b d1,(a1)+
bra.s .copy_prefix
.copy_code:
move.w 14(a5),d2
moveq #3,d4
.copy_code_digit:
move.w d2,d3
lsr.w #8,d3
lsr.w #4,d3
andi.w #$000f,d3
cmpi.w #10,d3
bcs.s .copy_code_number
addi.w #'A'-10,d3
bra.s .copy_code_store
.copy_code_number:
addi.w #'0',d3
.copy_code_store:
move.b d3,(a1)+
lsl.w #4,d2
dbra d4,.copy_code_digit
clr.b (a1)+
move.b #1,(a1)+
move.w #20,(a1)+
move.b #32,(a1)+
.copy_message:
move.l 8(a5),a0
.copy_message_loop:
cmpa.l a3,a1
bhi.s .message_full
move.b (a0)+,d1
move.b d1,(a1)+
bne.s .copy_message_loop
clr.b (a1)
bra.s .display
.message_full:
subq.l #1,a1
clr.b (a1)
clr.b 1(a1)
.display:
moveq #48,d1
movea.l sys.IntuitionBase,a6
lea .alert(pc),a0
jsr -90(a6)
moveq #0,d0
move.w 14(a5),d0
jmp sys.exit
even
.alert:
ds.b 158
.alert_limit:
ds.b 2
.alert_end:
.prefix:
dc.b "PROGRAM DIED code $",0
; !notreached!
}}
}
}
+45 -10
View File
@@ -52,50 +52,85 @@ txt {
private long @shared fmt_value_l
private uword @shared fmt_value_w
private asmsub vprintf(str format @D1, pointer argarray @D2) {
%asm {{
move.l 4.w,a6
cmp.w #36,20(a6) ; KS 2.0 = V36
bcs.s .no_ks20
move.l sys.DOSBase,a6
jmp dos.VPrintf(a6)
.no_ks20:
jmp p8b_txt.p8s_no_ks20_alert
}}
}
private asmsub no_ks20_alert() {
; VPrintf number formatting is unavailable before kickstart 2.0
%asm {{
move.l #$00070001,d0
lea _msg(pc),a0
moveq #40,d1
move.l sys.IntuitionBase,a6
jsr -90(a6) ; DisplayAlert
moveq #1,d0
jmp sys.exit
even
_msg:
dc.w 20
dc.b 16
dc.b "textio/conv number format routines require Kickstart 2.0+",0
dc.b 0
; !notreached!
}}
}
sub print_ub(ubyte value) {
fmt_value_w = value as uword
void dos.VPrintf("%d", &fmt_value_w)
vprintf("%d", &fmt_value_w)
}
sub print_ub0(ubyte value) {
fmt_value_w = value as uword
void dos.VPrintf("%03d", &fmt_value_w)
vprintf("%03d", &fmt_value_w)
}
sub print_b(byte value) {
fmt_value_w = value as uword
void dos.VPrintf("%d", &fmt_value_w)
vprintf("%d", &fmt_value_w)
}
sub print_uw(uword value) {
fmt_value_l = value as long
void dos.VPrintf("%lu", &fmt_value_l)
vprintf("%lu", &fmt_value_l)
}
sub print_uw0(uword value) {
fmt_value_l = value as long
void dos.VPrintf("%05lu", &fmt_value_l)
vprintf("%05lu", &fmt_value_l)
}
sub print_w(word value) {
void dos.VPrintf("%d", &value)
vprintf("%d", &value)
}
sub print_l(long value) {
void dos.VPrintf("%ld", &value)
vprintf("%ld", &value)
}
sub print_ulhex(long value, bool prefix) {
void dos.VPrintf(if prefix then "$%08lx" else "%08lx", &value)
vprintf(if prefix then "$%08lx" else "%08lx", &value)
}
sub print_uwhex(uword value, bool prefix) {
void dos.VPrintf(if prefix then "$%04x" else "%04x", &value)
vprintf(if prefix then "$%04x" else "%04x", &value)
}
sub print_ubhex(ubyte value, bool prefix) {
fmt_value_w = value as uword
void dos.VPrintf(if prefix then "$%02x" else "%02x", &fmt_value_w)
vprintf(if prefix then "$%02x" else "%02x", &fmt_value_w)
}
sub print_ubbin(ubyte value, bool prefix) {
@@ -0,0 +1,36 @@
%import exec
%import shared_amiga_trackdisk
trackdisk {
%option no_symbol_prefixing, ignore_unused
sub opendevice(ubyte unit) -> bool {
; open a trackdisk unit (0-3 = DF0:-DF3:)
^^exec.MsgPort tdPort = exec.CreateMsgPort()
if tdPort == 0 return false
TrackIO = exec.CreateIORequest(tdPort, sizeof(exec.IOStdReq))
if TrackIO == 0 {
exec.DeleteMsgPort(tdPort)
return false
}
if exec.OpenDevice("trackdisk.device", unit, TrackIO, 0)==0 {
return true
}
exec.DeleteIORequest(TrackIO)
exec.DeleteMsgPort(tdPort)
TrackIO = 0
return false
}
sub closedevice() {
if TrackIO != 0 {
^^exec.MsgPort tdPort = TrackIO.ReplyPort
exec.CloseDevice(TrackIO)
exec.DeleteIORequest(TrackIO)
exec.DeleteMsgPort(tdPort)
TrackIO = 0
}
}
}
@@ -0,0 +1,5 @@
%import shared_m68k_wavfile
wavfile {
%option ignore_unused
}
+5
View File
@@ -0,0 +1,5 @@
%import shared_m68k_adpcm
adpcm {
%option ignore_unused, private_symbols
}
@@ -0,0 +1,5 @@
%import shared_amiga_asyncio
asyncio {
%option no_symbol_prefixing, ignore_unused
}
@@ -0,0 +1,5 @@
%import shared_amiga_lowlevel
lowlevel {
%option no_symbol_prefixing, ignore_unused
}
@@ -29,6 +29,7 @@ p8_sys_startup {
; }
sub init_system() {
%option noframe
%asm {{
; save CLI arguments
move.l d0,-(sp)
@@ -92,6 +93,7 @@ proc_CLI = 172
}
sub init_system_phase2() {
%option noframe
%asm {{
moveq #0,d0
moveq #0,d1
@@ -113,6 +115,7 @@ proc_CLI = 172
}
sub cleanup_at_exit() {
%option noframe
%asm {{
movem.l d0,-(sp) ; keep return code
@@ -127,6 +130,8 @@ proc_CLI = 172
if sys.RexxSysBase != 0 exec.CloseLibrary(sys.RexxSysBase)
if sys.IFFParseBase != 0 exec.CloseLibrary(sys.IFFParseBase)
if sys.AsyncIOBase != 0 exec.CloseLibrary(sys.AsyncIOBase)
if sys.LowLevelBase != 0 exec.CloseLibrary(sys.LowLevelBase)
if sys.UtilityBase != 0 exec.CloseLibrary(sys.UtilityBase)
if sys.IconBase != 0 exec.CloseLibrary(sys.IconBase)
if sys.IntuitionBase != 0 exec.CloseLibrary(sys.IntuitionBase)
+93 -5
View File
@@ -39,11 +39,14 @@ sys {
pointer @shared UtilityBase ; kickstart 2.0+
pointer @shared RexxSysBase ; kickstart 2.0+, loaded from disk on demand
pointer @shared IFFParseBase ; kickstart 2.0+, loaded from disk on demand
pointer @shared AsyncIOBase ; loaded from disk on demand
pointer @shared LowLevelBase ; kickstart 2.0+, loaded from disk on demand
pointer @shared TimerBase ; opened on demand
^^ubyte @shared arguments ; CLI argument string (null-terminated), or NULL if Workbench launch
sub reset_system() {
%option noframe
%asm {{
move.l 4.w,a6
move.w 20(a6),d0 ; ExecBase version
@@ -60,8 +63,8 @@ sys {
}}
}
asmsub wait(long jiffies @D1) {
; --- wait approximately the given number of jiffies (1/60th seconds)
asmsub wait(long ticks @D1) {
; --- wait approximately the given number of ticks (1/50th seconds, i.e. 20ms each)
%asm {{
move.l sys.DOSBase,a6
jmp -198(a6) ; Delay
@@ -78,17 +81,17 @@ sys {
}}
}
sub exit(ubyte returnvalue) {
asmsub exit(word returnvalue @D0) {
; -- exit the program with a return code in D0. When invoked from Prog8 via sys.exit(), all active defers in the call chain are unwound program-wide (LIFO) before system cleanup. sys.reset_system() does not run defers.
%asm {{
moveq.l #0,d0
move.b sys.exit.returnvalue,d0
and.l #$ffff,d0
move.l p8_sys_startup.orig_stackpointer,sp
jmp p8_sys_startup.cleanup_at_exit
}}
}
sub set_carry() {
%option noframe
%asm {{
; set both C (comparison carry) and X (rotate carry) bits
moveq #$11,d0
@@ -97,6 +100,7 @@ sys {
}
sub clear_carry() {
%option noframe
%asm {{
; clear C and X bits
moveq #0,d0
@@ -135,12 +139,96 @@ sys {
}}
}
asmsub stack_size() -> long @D0 {
; -- Return the amount of stack space still available, in bytes.
%asm {{
move.l 4.w,a6 ; ExecBase
move.l $114(a6),a0 ; ThisTask
move.l sp,d0
sub.l $3a(a0),d0 ; minus tc_SPLower
rts
}}
}
asmsub cpuAtLeast68020() clobbers (A6, D0) -> bool @Pz {
%asm {{
move.l 4.w,a6
move.w 296(a6),d0
andi.w #$008e,d0
seq d0
tst.b d0
rts
}}
}
sub die(uword code, str message) {
%asm {{
moveq #0,d0
move.w 14(a5),d0
lea .alert(pc),a1
lea .alert_limit(pc),a3
move.w #20,(a1)+
move.b #16,(a1)+
lea .prefix(pc),a2
.copy_prefix:
move.b (a2)+,d1
beq.s .copy_code
move.b d1,(a1)+
bra.s .copy_prefix
.copy_code:
move.w 14(a5),d2
moveq #3,d4
.copy_code_digit:
move.w d2,d3
lsr.w #8,d3
lsr.w #4,d3
andi.w #$000f,d3
cmpi.w #10,d3
bcs.s .copy_code_number
addi.w #'A'-10,d3
bra.s .copy_code_store
.copy_code_number:
addi.w #'0',d3
.copy_code_store:
move.b d3,(a1)+
lsl.w #4,d2
dbra d4,.copy_code_digit
clr.b (a1)+
move.b #1,(a1)+
move.w #20,(a1)+
move.b #32,(a1)+
.copy_message:
move.l 8(a5),a0
.copy_message_loop:
cmpa.l a3,a1
bhi.s .message_full
move.b (a0)+,d1
move.b d1,(a1)+
bne.s .copy_message_loop
clr.b (a1)
bra.s .display
.message_full:
subq.l #1,a1
clr.b (a1)
clr.b 1(a1)
.display:
moveq #48,d1
movea.l sys.IntuitionBase,a6
lea .alert(pc),a0
jsr -90(a6)
moveq #0,d0
move.w 14(a5),d0
jmp sys.exit
even
.alert:
ds.b 158
.alert_limit:
ds.b 2
.alert_end:
.prefix:
dc.b "PROGRAM DIED code $",0
; !notreached!
}}
}
}
+44 -10
View File
@@ -81,50 +81,84 @@ txt {
private long @shared fmt_value_l
private uword @shared fmt_value_w
private asmsub vprintf(str format @D1, pointer argarray @D2) {
%asm {{
move.l 4.w,a6
cmp.w #36,20(a6) ; KS 2.0 = V36
bcs.s .no_ks20
move.l sys.DOSBase,a6
jmp dos.VPrintf(a6)
.no_ks20:
jmp p8b_txt.p8s_no_ks20_alert
}}
}
private asmsub no_ks20_alert() {
; VPrintf number formatting is unavailable before kickstart 2.0
%asm {{
move.l #$00070001,d0
lea _msg(pc),a0
moveq #40,d1
move.l sys.IntuitionBase,a6
jsr -90(a6) ; DisplayAlert
moveq #1,d0
jmp sys.exit
even
_msg:
dc.w 20
dc.b 16
dc.b "textio/conv number format routines require Kickstart 2.0+",0
dc.b 0
; !notreached!
}}
}
sub print_ub(ubyte value) {
fmt_value_w = value as uword
void dos.VPrintf("%d", &fmt_value_w)
vprintf("%d", &fmt_value_w)
}
sub print_ub0(ubyte value) {
fmt_value_w = value as uword
void dos.VPrintf("%03d", &fmt_value_w)
vprintf("%03d", &fmt_value_w)
}
sub print_b(byte value) {
fmt_value_w = value as uword
void dos.VPrintf("%d", &fmt_value_w)
vprintf("%d", &fmt_value_w)
}
sub print_uw(uword value) {
fmt_value_l = value as long
void dos.VPrintf("%lu", &fmt_value_l)
vprintf("%lu", &fmt_value_l)
}
sub print_uw0(uword value) {
fmt_value_l = value as long
void dos.VPrintf("%05lu", &fmt_value_l)
vprintf("%05lu", &fmt_value_l)
}
sub print_w(word value) {
void dos.VPrintf("%d", &value)
vprintf("%d", &value)
}
sub print_l(long value) {
void dos.VPrintf("%ld", &value)
vprintf("%ld", &value)
}
sub print_ulhex(long value, bool prefix) {
void dos.VPrintf(if prefix then "$%08lx" else "%08lx", &value)
vprintf(if prefix then "$%08lx" else "%08lx", &value)
}
sub print_uwhex(uword value, bool prefix) {
void dos.VPrintf(if prefix then "$%04x" else "%04x", &value)
vprintf(if prefix then "$%04x" else "%04x", &value)
}
sub print_ubhex(ubyte value, bool prefix) {
fmt_value_w = value as uword
void dos.VPrintf(if prefix then "$%02x" else "%02x", &fmt_value_w)
vprintf(if prefix then "$%02x" else "%02x", &fmt_value_w)
}
sub print_ubbin(ubyte value, bool prefix) {
@@ -0,0 +1,46 @@
%import exec
%import shared_amiga_trackdisk
trackdisk {
%option no_symbol_prefixing, ignore_unused
sub opendevice(ubyte unit) -> bool {
; open a trackdisk unit (0-3 = DF0:-DF3:) in a kickstart 1.3 compatible fashion
^^exec.MsgPort tdPort = exec.AllocMem(sizeof(exec.MsgPort), exec.MEMF_PUBLIC | exec.MEMF_CLEAR)
if tdPort == 0 return false
TrackIO = exec.AllocMem(sizeof(exec.IOStdReq), exec.MEMF_PUBLIC | exec.MEMF_CLEAR)
if TrackIO == 0 {
exec.FreeMem(tdPort, sizeof(exec.MsgPort))
return false
}
tdPort.Type = exec.NT_MSGPORT
tdPort.Flags = exec.PA_SIGNAL
tdPort.SigBit = exec.AllocSignal(-1) as ubyte
tdPort.SigTask = exec.FindTask(0)
^^exec.List listPtr = &&tdPort.Head as ^^exec.List
exec.NewList(listPtr)
TrackIO.ReplyPort = tdPort
if exec.OpenDevice("trackdisk.device", unit, TrackIO, 0)==0 {
return true
}
exec.FreeSignal(tdPort.SigBit as byte)
exec.FreeMem(TrackIO, sizeof(exec.IOStdReq))
exec.FreeMem(tdPort, sizeof(exec.MsgPort))
TrackIO = 0
return false
}
sub closedevice() {
if TrackIO != 0 {
^^exec.MsgPort tdPort = TrackIO.ReplyPort
byte sigbit = tdPort.SigBit as byte
exec.CloseDevice(TrackIO)
exec.FreeSignal(sigbit)
exec.FreeMem(TrackIO, sizeof(exec.IOStdReq))
exec.FreeMem(tdPort, sizeof(exec.MsgPort))
TrackIO = 0
}
}
}
@@ -0,0 +1,5 @@
%import shared_m68k_wavfile
wavfile {
%option ignore_unused
}
+791
View File
@@ -0,0 +1,791 @@
%option ignore_unused
vera {
; VERA register definitions and helper routines for the 'VERA 64' board for the C64
%option ignore_unused, no_symbol_prefixing
const uword VERA_BASE = $DE00 ; TODO to be decided?
&ubyte VERA_ADDR_L = VERA_BASE + $0000
&ubyte VERA_ADDR_M = VERA_BASE + $0001
&uword VERA_ADDR = VERA_BASE + $0000 ; still need to do the _H separately
&ubyte VERA_ADDR_H = VERA_BASE + $0002
&ubyte VERA_DATA0 = VERA_BASE + $0003
&ubyte VERA_DATA1 = VERA_BASE + $0004
&ubyte VERA_CTRL = VERA_BASE + $0005
&ubyte VERA_IEN = VERA_BASE + $0006
&ubyte VERA_ISR = VERA_BASE + $0007
&ubyte VERA_IRQLINE_L = VERA_BASE + $0008 ; write only
&ubyte VERA_SCANLINE_L = VERA_BASE + $0008 ; read only
&ubyte VERA_DC_VIDEO = VERA_BASE + $0009 ; DCSEL= 0
&ubyte VERA_DC_HSCALE = VERA_BASE + $000A ; DCSEL= 0
&ubyte VERA_DC_VSCALE = VERA_BASE + $000B ; DCSEL= 0
&ubyte VERA_DC_BORDER = VERA_BASE + $000C ; DCSEL= 0
&ubyte VERA_DC_HSTART = VERA_BASE + $0009 ; DCSEL= 1
&ubyte VERA_DC_HSTOP = VERA_BASE + $000A ; DCSEL= 1
&ubyte VERA_DC_VSTART = VERA_BASE + $000B ; DCSEL= 1
&ubyte VERA_DC_VSTOP = VERA_BASE + $000C ; DCSEL= 1
&ubyte VERA_DC_VER0 = VERA_BASE + $0009 ; DCSEL=63
&ubyte VERA_DC_VER1 = VERA_BASE + $000A ; DCSEL=63
&ubyte VERA_DC_VER2 = VERA_BASE + $000B ; DCSEL=63
&ubyte VERA_DC_VER3 = VERA_BASE + $000C ; DCSEL=63
&ubyte VERA_L0_CONFIG = VERA_BASE + $000D
&ubyte VERA_L0_MAPBASE = VERA_BASE + $000E
&ubyte VERA_L0_TILEBASE = VERA_BASE + $000F
&ubyte VERA_L0_HSCROLL_L = VERA_BASE + $0010
&ubyte VERA_L0_HSCROLL_H = VERA_BASE + $0011
&uword VERA_L0_HSCROLL = VERA_BASE + $0010
&ubyte VERA_L0_VSCROLL_L = VERA_BASE + $0012
&ubyte VERA_L0_VSCROLL_H = VERA_BASE + $0013
&uword VERA_L0_VSCROLL = VERA_BASE + $0012
&ubyte VERA_L1_CONFIG = VERA_BASE + $0014
&ubyte VERA_L1_MAPBASE = VERA_BASE + $0015
&ubyte VERA_L1_TILEBASE = VERA_BASE + $0016
&ubyte VERA_L1_HSCROLL_L = VERA_BASE + $0017
&ubyte VERA_L1_HSCROLL_H = VERA_BASE + $0018
&uword VERA_L1_HSCROLL = VERA_BASE + $0017
&ubyte VERA_L1_VSCROLL_L = VERA_BASE + $0019
&ubyte VERA_L1_VSCROLL_H = VERA_BASE + $001A
&uword VERA_L1_VSCROLL = VERA_BASE + $0019
&ubyte VERA_AUDIO_CTRL = VERA_BASE + $001B
&ubyte VERA_AUDIO_RATE = VERA_BASE + $001C
&ubyte VERA_AUDIO_DATA = VERA_BASE + $001D
&ubyte VERA_SPI_DATA = VERA_BASE + $001E
&ubyte VERA_SPI_CTRL = VERA_BASE + $001F
; Vera FX registers: (accessing depends on particular DCSEL value set in VERA_CTRL!)
&ubyte VERA_FX_CTRL = VERA_BASE + $0009
&ubyte VERA_FX_TILEBASE = VERA_BASE + $000a
&ubyte VERA_FX_MAPBASE = VERA_BASE + $000b
&ubyte VERA_FX_MULT = VERA_BASE + $000c
&ubyte VERA_FX_X_INCR_L = VERA_BASE + $0009
&ubyte VERA_FX_X_INCR_H = VERA_BASE + $000a
&uword VERA_FX_X_INCR = VERA_BASE + $0009
&ubyte VERA_FX_Y_INCR_L = VERA_BASE + $000b
&ubyte VERA_FX_Y_INCR_H = VERA_BASE + $000c
&uword VERA_FX_Y_INCR = VERA_BASE + $000b
&ubyte VERA_FX_X_POS_L = VERA_BASE + $0009
&ubyte VERA_FX_X_POS_H = VERA_BASE + $000a
&uword VERA_FX_X_POS = VERA_BASE + $0009
&ubyte VERA_FX_Y_POS_L = VERA_BASE + $000b
&ubyte VERA_FX_Y_POS_H = VERA_BASE + $000c
&uword VERA_FX_Y_POS = VERA_BASE + $000b
&ubyte VERA_FX_X_POS_S = VERA_BASE + $0009
&ubyte VERA_FX_Y_POS_S = VERA_BASE + $000a
&ubyte VERA_FX_POLY_FILL_L = VERA_BASE + $000b
&ubyte VERA_FX_POLY_FILL_H = VERA_BASE + $000c
&uword VERA_FX_POLY_FILL = VERA_BASE + $000b
&ubyte VERA_FX_CACHE_L = VERA_BASE + $0009
&ubyte VERA_FX_CACHE_M = VERA_BASE + $000a
&ubyte VERA_FX_CACHE_H = VERA_BASE + $000b
&ubyte VERA_FX_CACHE_U = VERA_BASE + $000c
&ubyte VERA_FX_ACCUM = VERA_BASE + $000a
&ubyte VERA_FX_ACCUM_RESET = VERA_BASE + $0009
sub reset() {
VERA_CTRL = %10000000 ; bit 7 = Reset Adapter
; the fpga takes milliseconds to reconfigure, so wait until vera
; responds again by polling a register write/readback until it sticks
repeat {
VERA_ADDR_L = $42
if VERA_ADDR_L == $42 {
break
}
}
VERA_CTRL = 0
}
asmsub vpeek(ubyte bank @A, uword address @XY) -> ubyte @A {
; -- get a byte from VERA's video memory
; note: inefficient when reading multiple sequential bytes!
%asm {{
pha
lda #0
sta VERA_CTRL
pla
sta VERA_ADDR_H
sty VERA_ADDR_M
stx VERA_ADDR_L
lda VERA_DATA0
rts
}}
}
asmsub vaddr(ubyte bank @A, uword address @R0, ubyte addrsel @R1, byte autoIncrOrDecrByOne @Y) clobbers(A) {
; -- setup the VERA's data address register 0 or 1 with optional auto increment or decrement of 1.
; This is a convenience routine, and not very efficient if you call it often;
; it's usually better to write a tailor made version of it that accounts for the repeated values.
; Note that the vaddr_autoincr() and vaddr_autodecr() routines allow to set all possible strides, not just 1.
; Note also that Vera's addrset is reset to 0 on exit, even if you set port #1's address.
%asm {{
pha
lda cx16.r1
and #1
sta VERA_CTRL
lda cx16.r0
sta VERA_ADDR_L
lda cx16.r0+1
sta VERA_ADDR_M
pla
cpy #0
bmi ++
beq +
ora #%00010000
+ sta VERA_ADDR_H
lda #0
sta VERA_CTRL
rts
+ ora #%00011000
sta VERA_ADDR_H
lda #0
sta VERA_CTRL
rts
}}
}
asmsub vaddr_clone(ubyte port @A) clobbers (A,X,Y) {
; -- clones Vera addresses from the given source port to the other one.
; This is a convenience routine, and not very efficient if you call it often;
; it's usually better to write a tailor made version of it that accounts for the repeated values.
%asm {{
sta VERA_CTRL
ldx VERA_ADDR_L
ldy VERA_ADDR_H
sty P8ZP_SCRATCH_B1
ldy VERA_ADDR_M
eor #1
sta VERA_CTRL
stx VERA_ADDR_L
sty VERA_ADDR_M
ldy P8ZP_SCRATCH_B1
sty VERA_ADDR_H
lda #0
sta VERA_CTRL
rts
}}
}
asmsub vaddr_autoincr(ubyte bank @A, uword address @R0, ubyte addrsel @R1, uword autoIncrAmount @R2) clobbers(A,Y) {
; -- setup the VERA's data address register 0 or 1, including setting up optional auto increment amount.
; Specifiying an unsupported amount results in amount of zero. See the Vera docs about what amounts are possible.
; This is a convenience routine, and not very efficient if you call it often;
; it's usually better to write a tailor made version of it that accounts for the repeated values.
%asm {{
jsr _setup
lda cx16.r2H
ora cx16.r2L
beq +
jsr _determine_incr_bits
+ ora P8ZP_SCRATCH_REG
sta VERA_ADDR_H
lda #0
sta VERA_CTRL
rts
_setup sta P8ZP_SCRATCH_REG
lda cx16.r1
and #1
sta VERA_CTRL
lda cx16.r0
sta VERA_ADDR_L
lda cx16.r0+1
sta VERA_ADDR_M
rts
_determine_incr_bits
lda cx16.r2H
bne _large
lda cx16.r2L
ldy #13
- cmp _strides_lsb,y
beq +
dey
bpl -
+ tya
asl a
asl a
asl a
asl a
rts
_large ora cx16.r2L
cmp #1 ; 256
bne +
lda #9<<4
rts
+ cmp #2 ; 512
bne +
lda #10<<4
rts
+ cmp #65 ; 320
bne +
lda #14<<4
rts
+ cmp #130 ; 640
bne +
lda #15<<4
rts
+ lda #0
rts
_strides_lsb .byte 0,1,2,4,8,16,32,64,128,255,255,40,80,160,255,255
; !notreached!
}}
}
asmsub vaddr_autodecr(ubyte bank @A, uword address @R0, ubyte addrsel @R1, uword autoDecrAmount @R2) clobbers(A,Y) {
; -- setup the VERA's data address register 0 or 1 including setting up optional auto decrement amount.
; Specifiying an unsupported amount results in amount of zero. See the Vera docs about what amounts are possible.
; This is a convenience routine, and not very efficient if you call it often;
; it's usually better to write a tailor made version of it that accounts for the repeated values.
%asm {{
jsr vaddr_autoincr._setup
lda cx16.r2H
ora cx16.r2L
beq +
jsr vaddr_autoincr._determine_incr_bits
ora #%00001000 ; autodecrement
+ ora P8ZP_SCRATCH_REG
sta VERA_ADDR_H
lda #0
sta VERA_CTRL
rts
}}
}
asmsub vpoke(ubyte bank @A, uword address @R0, ubyte value @Y) clobbers(A) {
; -- write a single byte to VERA's video memory
; note: inefficient when writing multiple sequential bytes!
%asm {{
pha
lda #0
sta VERA_CTRL
pla
sta VERA_ADDR_H
lda cx16.r0
sta VERA_ADDR_L
lda cx16.r0+1
sta VERA_ADDR_M
sty VERA_DATA0
rts
}}
}
asmsub vpoke_or(ubyte bank @A, uword address @R0, ubyte value @Y) clobbers (A) {
; -- or a single byte to the value already in the VERA's video memory at that location
; note: inefficient when writing multiple sequential bytes!
%asm {{
pha
lda #0
sta VERA_CTRL
pla
sta VERA_ADDR_H
lda cx16.r0
sta VERA_ADDR_L
lda cx16.r0+1
sta VERA_ADDR_M
tya
ora VERA_DATA0
sta VERA_DATA0
rts
}}
}
asmsub vpoke_and(ubyte bank @A, uword address @R0, ubyte value @Y) clobbers(A) {
; -- and a single byte to the value already in the VERA's video memory at that location
; note: inefficient when writing multiple sequential bytes!
%asm {{
pha
lda #0
sta VERA_CTRL
pla
sta VERA_ADDR_H
lda cx16.r0
sta VERA_ADDR_L
lda cx16.r0+1
sta VERA_ADDR_M
tya
and VERA_DATA0
sta VERA_DATA0
rts
}}
}
asmsub vpoke_xor(ubyte bank @A, uword address @R0, ubyte value @Y) clobbers (A) {
; -- xor a single byte to the value already in the VERA's video memory at that location
; note: inefficient when writing multiple sequential bytes!
%asm {{
pha
lda #0
sta VERA_CTRL
pla
sta VERA_ADDR_H
lda cx16.r0
sta VERA_ADDR_L
lda cx16.r0+1
sta VERA_ADDR_M
tya
eor VERA_DATA0
sta VERA_DATA0
rts
}}
}
asmsub vpoke_mask(ubyte bank @A, uword address @R0, ubyte mask @X, ubyte value @Y) clobbers (A) {
; -- bitwise or a single byte to the value already in the VERA's video memory at that location
; after applying the and-mask. Note: inefficient when writing multiple sequential bytes!
%asm {{
sty P8ZP_SCRATCH_B1
pha
lda #0
sta VERA_CTRL
pla
sta VERA_ADDR_H
lda cx16.r0
sta VERA_ADDR_L
lda cx16.r0+1
sta VERA_ADDR_M
txa
and VERA_DATA0
ora P8ZP_SCRATCH_B1
sta VERA_DATA0
rts
}}
}
asmsub save_virtual_registers() clobbers(A,Y) {
%asm {{
ldy #31
- lda cx16.r0,y
sta _cx16_vreg_storage,y
dey
bpl -
rts
.section BSS
_cx16_vreg_storage
.word ?,?,?,?,?,?,?,?
.word ?,?,?,?,?,?,?,?
.send BSS
; !notreached!
}}
}
asmsub restore_virtual_registers() clobbers(A,Y) {
%asm {{
ldy #31
- lda save_virtual_registers._cx16_vreg_storage,y
sta cx16.r0,y
dey
bpl -
rts
}}
}
asmsub save_vera_context() clobbers(A) {
; -- use this at the start of your IRQ handler if it uses Vera registers, to save the state
%asm {{
; note cannot store this on cpu hardware stack because this gets called as a subroutine
lda VERA_ADDR_L
sta _vera_storage
lda VERA_ADDR_M
sta _vera_storage+1
lda VERA_ADDR_H
sta _vera_storage+2
lda VERA_CTRL
sta _vera_storage+3
eor #1
sta _vera_storage+7
sta VERA_CTRL
lda VERA_ADDR_L
sta _vera_storage+4
lda VERA_ADDR_M
sta _vera_storage+5
lda VERA_ADDR_H
sta _vera_storage+6
rts
.section BSS
_vera_storage: .byte ?,?,?,?,?,?,?,?
.send BSS
; !notreached!
}}
}
asmsub restore_vera_context() clobbers(A) {
; -- use this at the end of your IRQ handler if it uses Vera registers, to restore the state
%asm {{
lda vera.save_vera_context._vera_storage+7
sta VERA_CTRL
lda vera.save_vera_context._vera_storage+6
sta VERA_ADDR_H
lda vera.save_vera_context._vera_storage+5
sta VERA_ADDR_M
lda vera.save_vera_context._vera_storage+4
sta VERA_ADDR_L
lda vera.save_vera_context._vera_storage+3
sta VERA_CTRL
lda vera.save_vera_context._vera_storage+2
sta VERA_ADDR_H
lda vera.save_vera_context._vera_storage+1
sta VERA_ADDR_M
lda vera.save_vera_context._vera_storage+0
sta VERA_ADDR_L
rts
}}
}
}
verafx {
; Partial Vera FX support:
; - fast 32 bit cached writes (clear, copy)
; - transparent write setting
; - hardware 16 bits multiplications
; - hardware accelerated line drawing (8 bpp screen mode only!)
;
; Docs:
; https://github.com/X16Community/x16-docs/blob/fb63156cca2d6de98be0577aacbe4ddef458f896/X16%20Reference%20-%2010%20-%20VERA%20FX%20Reference.md
; https://docs.google.com/document/d/1q34uWOiM3Be2pnaHRVgSdHySI-qsiQWPTo_gfE54PTg
%option no_symbol_prefixing, ignore_unused
sub available() -> bool {
; returns true if Vera FX is available (Vera V0.3.1 or later), false if not.
cx16.r0L = vera.VERA_CTRL
cx16.r0H = 0
vera.VERA_CTRL = $7e
if vera.VERA_DC_VER0 == $56 {
; Vera version number is valid. Vera fx is available on Vera version 0.3.1 and later.
if vera.VERA_DC_VER1>0
cx16.r0H = 1
else
cx16.r0H = mkword(vera.VERA_DC_VER2, vera.VERA_DC_VER3) >= $0301 as ubyte
}
vera.VERA_CTRL = cx16.r0L
return cx16.r0H as bool
}
sub clear(ubyte vbank, uword vaddr, ubyte data, uword num_longwords) {
; use cached 4-byte write to quickly clear a portion of the video memory to a given byte value
; this routine is around 3 times faster as gfx_hires/gfx_lores.clear_screen()
vera.VERA_CTRL = 0
vera.VERA_ADDR_H = vbank | %00110000 ; 4-byte increment
vera.VERA_ADDR_M = msb(vaddr)
vera.VERA_ADDR_L = lsb(vaddr)
vera.VERA_CTRL = 6<<1 ; dcsel = 6, fill the 32 bits cache
vera.VERA_FX_CACHE_L = data
vera.VERA_FX_CACHE_M = data
vera.VERA_FX_CACHE_H = data
vera.VERA_FX_CACHE_U = data
vera.VERA_CTRL = 2<<1 ; dcsel = 2
vera.VERA_FX_MULT = 0
vera.VERA_FX_CTRL = %01000000 ; cache write enable
cx16.r0 = num_longwords>>3
if cx16.r0H==0 {
repeat cx16.r0L {
unroll 8 vera.VERA_DATA0=0 ; write 8*4 bytes at a time, unrolled
}
} else {
repeat cx16.r0 {
unroll 8 vera.VERA_DATA0=0 ; write 8*4 bytes at a time, unrolled
}
}
repeat lsb(num_longwords) & 7 {
vera.VERA_DATA0=0 ; write 4 bytes at a time (remaining longs)
}
vera.VERA_FX_CTRL = 0 ; cache write disable
vera.VERA_CTRL = 0
}
sub copy(ubyte srcbank, uword srcaddr, ubyte tgtbank, uword tgtaddr, uword num_longwords) {
; use cached 4-byte writes to quickly copy a portion of the video memory to somewhere else
; this routine is about 50% faster as a plain byte-by-byte copy
vera.VERA_CTRL = 1
vera.VERA_ADDR_H = srcbank | %00010000 ; source: 1-byte increment
vera.VERA_ADDR_M = msb(srcaddr)
vera.VERA_ADDR_L = lsb(srcaddr)
vera.VERA_CTRL = 0
vera.VERA_ADDR_H = tgtbank | %00110000 ; target: 4-byte increment
vera.VERA_ADDR_M = msb(tgtaddr)
vera.VERA_ADDR_L = lsb(tgtaddr)
vera.VERA_CTRL = 2<<1 ; dcsel = 2
vera.VERA_FX_MULT = 0
vera.VERA_FX_CTRL = %01100000 ; cache write enable + cache fill enable
cx16.r0 = num_longwords>>1
if cx16.r0H==0 {
repeat cx16.r0L {
unroll 2 %asm {{
lda vera.VERA_DATA1
lda vera.VERA_DATA1
lda vera.VERA_DATA1
lda vera.VERA_DATA1
lda #0
sta vera.VERA_DATA0
}}
}
} else {
repeat cx16.r0 {
unroll 2 %asm {{
lda vera.VERA_DATA1
lda vera.VERA_DATA1
lda vera.VERA_DATA1
lda vera.VERA_DATA1
lda #0
sta vera.VERA_DATA0
}}
}
}
if lsb(num_longwords) & 1 == 1 {
%asm {{
lda vera.VERA_DATA1
lda vera.VERA_DATA1
lda vera.VERA_DATA1
lda vera.VERA_DATA1
lda #0
sta vera.VERA_DATA0
}}
}
vera.VERA_FX_CTRL = 0 ; cache write disable
vera.VERA_CTRL = 0
}
asmsub mult16(uword value1 @R0, uword value2 @R1) clobbers(X) -> uword @AY {
; Returns the lower 16 bits unsigned result of R0*R1 in AY
; Note: only the lower 16 bits! (the upper 16 bits are not valid for unsigned word multiplications, only for signed)
; Verafx doesn't support unsigned values like this for full 32 bit result.
; Note: clobbers VRAM $1f9bc - $1f9bf (inclusive)
%asm {{
jmp muls16
}}
}
asmsub muls16(word value1 @R0, word value2 @R1) clobbers(X) -> word @AY {
; Returns just the lower 16 bits signed result of the multiplication in cx16.AY.
; Note: clobbers R0, R1, and VRAM $1f9bc - $1f9bf (inclusive)
%asm {{
jsr muls
lda cx16.r0L
ldy cx16.r0H
rts
}}
}
asmsub muls(word value1 @R0, word value2 @R1) clobbers(X) -> long @R0R1 {
; Returns the 32 bits signed result in R0:R1 (lower word, upper word).
; Vera Fx multiplication support only works on signed values!
; Note: clobbers VRAM $1f9bc - $1f9bf (inclusive)
%asm {{
lda #(2 << 1)
sta vera.VERA_CTRL ; $9F25
lda #0
sta vera.VERA_FX_CTRL ; $9F29 (mainly to reset Addr1 Mode to 0)
lda #%00010000
sta vera.VERA_FX_MULT ; $9F2C
lda #(6 << 1)
sta vera.VERA_CTRL ; $9F25
lda cx16.r0
sta vera.VERA_FX_CACHE_L ; $9F29
lda cx16.r0+1
sta vera.VERA_FX_CACHE_M ; $9F2A
lda cx16.r1
sta vera.VERA_FX_CACHE_H ; $9F2B
lda cx16.r1+1
sta vera.VERA_FX_CACHE_U ; $9F2C
lda vera.VERA_FX_ACCUM_RESET ; $9F29 (DCSEL=6)
; Set the ADDR0 pointer to $1f9bc and write our multiplication result there
; (these are the 4 bytes just before the PSG registers start)
lda #(2 << 1)
sta vera.VERA_CTRL
lda #%01000000 ; Cache Write Enable
sta vera.VERA_FX_CTRL
lda #$bc
sta vera.VERA_ADDR_L
lda #$f9
sta vera.VERA_ADDR_M
lda #$01
sta vera.VERA_ADDR_H ; no increment
lda #0
sta vera.VERA_DATA0 ; multiply and write out result
lda #%00010001 ; $01 with Increment 1
sta vera.VERA_ADDR_H ; so we can read out the result
lda vera.VERA_DATA0 ; store the lower 16 bits of the result in R0
ldy vera.VERA_DATA0
sta cx16.r0L
sty cx16.r0H
lda vera.VERA_DATA0 ; store the upper 16 bits of the result in R1
ldy vera.VERA_DATA0 ; store the upper 16 bits of the result in R1
sta cx16.r1L
sty cx16.r1H
lda #0
sta vera.VERA_FX_CTRL ; Cache write disable
sta vera.VERA_FX_MULT ; $9F2C reset multiply bit
sta vera.VERA_CTRL ; reset DCSEL
rts
}}
}
sub line(uword x1, ubyte y1, uword x2, ubyte y2, ubyte color) {
; Use the Vera FX line draw helper to draw a line very fast in a 320x240 256 color (8 bpp) bitmap screen
; (the default cx16 screen mode 128, as used by the gfx_lores module, with the bitmap at vram address 0).
; WARNING: ONLY WORKS IN 8 BPP SCREEN MODE! The helper has a hardware bug in 4 bpp mode.
; No bounds checking or clipping is done, all coordinates must lie within the screen (0..319, 0..239).
; Also resets the address increments of DATA0 and DATA1 to 0 afterwards.
; The line is always drawn from top to bottom (y1<=y2 after sorting), this avoids the negative
; (decrement) y-increments that the helper handles poorly. x can go either left or right.
ubyte @zp octant
uword @zp dx
uword @zp dy
if y1>y2 {
cx16.r0 = x1
x1 = x2
x2 = cx16.r0
octant = y1
y1 = y2
y2 = octant
}
dy = y2
dy -= y1
uword @zp length
if x2>=x1 {
dx = x2-x1
length = dx
octant = 0 ; x goes right
} else {
dx = x1-x2
length = dx
octant = 1 ; x goes left
}
if dy>length {
octant |= 2
length = dy
dy = dx
}
; slope in 0.9 fixed point format for the FX increment register (1.0 = $200), rounded to nearest.
; Computed as (dy/length in 0.8 fixed point) << 1, which stays within 16 bits:
; dy<=239 so dy<<8 <= 61240, and dy<=length so the quotient is <=256, doubled to <=512.
uword slope = 0
if length!=0 {
slope = (((dy << 8) + (length>>1)) / length) << 1
}
length++
ubyte remainder_pixels = lsb(length) & 7
ubyte full_octets = lsb(length>>3)
; 4 "octants" (y1<=y2 always after the sorting above, so the line always goes down):
; octant 0 = right/down, shallow slope (dx>=dy): always step +1 in x, sometimes step +320 in y
; octant 1 = left/down, shallow slope: always step -1 in x, sometimes step +320 in y
; octant 2 = right/down, steep slope (dy>dx): always step +320 in y, sometimes step +1 in x
; octant 3 = left/down, steep slope: always step +320 in y, sometimes step -1 in x
; address increment values: +1 = $10, -1 = $18 (decrement), +320 = $e0
ubyte[4] @shared always_incr_table = [ $10, $18, $e0, $e0 ]
ubyte[4] @shared sometimes_incr_table = [ $e0, $e0, $10, $18 ]
%asm {{
; set up the FX line draw helper and the start address in ADDR1
lda #(2<<1)
sta vera.VERA_CTRL ; dcsel = 2
lda #%00000001
sta vera.VERA_FX_CTRL ; addr1 mode = line draw helper (8 bpp)
lda #(3<<1)
sta vera.VERA_CTRL ; dcsel = 3
lda slope
sta vera.VERA_FX_X_INCR ; (writing X_INCR also centers the subpixel position and resets overflow)
lda slope+1
sta vera.VERA_FX_X_INCR+1
; ADDR0 provides the 'sometimes' increment for the helper
lda #0
sta vera.VERA_CTRL ; addrsel = 0
ldx octant
lda sometimes_incr_table,x
sta vera.VERA_ADDR_H
; ADDR1 = start pixel, gets the 'always' increment
lda #1
sta vera.VERA_CTRL ; addrsel = 1 (bit 0)
lda x1
sta vera.VERA_ADDR_L
lda x1+1
sta vera.VERA_ADDR_M
lda always_incr_table,x
sta vera.VERA_ADDR_H
; add the y-offset to the start address in ADDR1
ldy y1
lda vera.VERA_ADDR_L
clc
adc times320_lo,y
sta vera.VERA_ADDR_L
lda vera.VERA_ADDR_M
adc times320_mid,y
sta vera.VERA_ADDR_M
lda vera.VERA_ADDR_H
and #$01
adc times320_hi,y
sta P8ZP_SCRATCH_B1
lda vera.VERA_ADDR_H
and #$f8
ora P8ZP_SCRATCH_B1
sta vera.VERA_ADDR_H
; draw the line: first the remainder pixels one at a time, then unrolled 8 pixels at a time
ldy remainder_pixels
beq +
lda color
- sta vera.VERA_DATA1
dey
bne -
+ ldy full_octets
beq _done
lda color
- sta vera.VERA_DATA1
sta vera.VERA_DATA1
sta vera.VERA_DATA1
sta vera.VERA_DATA1
sta vera.VERA_DATA1
sta vera.VERA_DATA1
sta vera.VERA_DATA1
sta vera.VERA_DATA1
dey
bne -
_done
; reset the FX registers back to normal
lda #(2<<1)
sta vera.VERA_CTRL ; dcsel = 2
ldx #0
stx vera.VERA_FX_CTRL ; addr1 mode = normal again
lda #1
sta vera.VERA_CTRL ; addrsel = 1 (bit 0)
stx vera.VERA_ADDR_H ; reset ADDR1 (DATA1) address increment
stx vera.VERA_CTRL ; addrsel = 0
stx vera.VERA_ADDR_H ; reset ADDR0 (DATA0) address increment
rts
; multiplication by 320 lookup table (used to add the y-offset to the start address above)
times320 := 320*range(240)
times320_lo .byte <times320
times320_mid .byte >times320
times320_hi .byte `times320
}}
}
sub transparency(bool enable) {
; Set transparent write mode for VeraFX cached writes and also for normal writes to DATA0/DATA.
; If enabled, pixels with value 0 do not modify VRAM when written (so they are "transparent")
vera.VERA_CTRL = 2<<1 ; dcsel = 2
if enable
vera.VERA_FX_CTRL |= %10000000
else
vera.VERA_FX_CTRL &= %01111111
vera.VERA_CTRL = 0
}
}
+878
View File
@@ -0,0 +1,878 @@
%import syslib
%import buffers
%import vera
%option ignore_unused
vera_gfx {
; optimized graphics routines for just the single screen mode: lores 320*240, 256c (8bpp)
; bitmap image needs to start at VRAM address $00000.
%option ignore_unused
const uword WIDTH = 320
const ubyte HEIGHT = 240
sub init() {
; 320x240 8bpp bitmap on layer 1, bitmap at vram $00000, set via direct vera register pokes
vera.VERA_CTRL = 0
vera.VERA_DC_VIDEO = (vera.VERA_DC_VIDEO & %11001100) | %00100001 ; layer 1 only, force VGA output (a vera reset leaves output mode 0=disabled)
vera.VERA_DC_HSCALE = 64
vera.VERA_DC_VSCALE = 64
vera.VERA_L1_CONFIG = %00000111 ; bitmap mode, 8 bpp
vera.VERA_L1_MAPBASE = 0
vera.VERA_L1_TILEBASE = 0 ; bitmap base $00000
vera.VERA_L1_HSCROLL = 0
vera.VERA_L1_VSCROLL = 0
clear_screen(0)
copy_petscii_charset()
drawmode_eor(false)
}
sub copy_petscii_charset() {
; copy the c64 petscii charset (all 256 characters) from the CHARGEN rom to Vera VRAM at $1f000 - $1f800
; this makes text() work; the charset is stored at vram bank 1, address $f000 (= $1f000)
; (a temporary ram copy must be made first, because the vera registers are inaccessible while the chargen rom is mapped in)
uword chargen_copy = memory("vera_chargen_copy", 256*8, 0)
sys.set_irqd()
c64.banks(%011) ; enable CHAREN, so the character rom is visible at $d000
sys.memcopy($d000, chargen_copy, 256*8)
c64.banks(%111) ; enable I/O (and thus the vera) again
sys.clear_irqd()
vera.vaddr(1, $f000, 0, 1) ; set up the vera data0 address with auto increment of 1 (so we can just write data0)
repeat 256*8 {
vera.VERA_DATA0 = @(chargen_copy)
chargen_copy++
}
}
sub drawmode_eor(bool enabled) {
; with EOR drawing mode you can have non destructive drawing (2*EOR=restore original)
eor_mode = enabled
}
private bool eor_mode
sub clear_screen(ubyte color) {
if verafx.available() {
; use verafx cache writes to quickly clear the screen
const ubyte vbank = 0
const uword vaddr = 0
vera.VERA_CTRL = 0
vera.VERA_ADDR_H = vbank | %00110000 ; 4-byte increment
vera.VERA_ADDR_M = msb(vaddr)
vera.VERA_ADDR_L = lsb(vaddr)
vera.VERA_CTRL = 6<<1 ; dcsel = 6, fill the 32 bits cache
vera.VERA_FX_CACHE_L = color
vera.VERA_FX_CACHE_M = color
vera.VERA_FX_CACHE_H = color
vera.VERA_FX_CACHE_U = color
vera.VERA_CTRL = 2<<1 ; dcsel = 2
vera.VERA_FX_MULT = 0
vera.VERA_FX_CTRL = %01000000 ; cache write enable
repeat 320/4/4 {
%asm {{
ldy #240
lda #0
- sta vera.VERA_DATA0
sta vera.VERA_DATA0
sta vera.VERA_DATA0
sta vera.VERA_DATA0
dey
bne -
}}
}
vera.VERA_FX_CTRL = 0 ; cache write disable
vera.VERA_CTRL = 0
return
}
; fallback to cpu clear
vera.VERA_CTRL=0
vera.VERA_ADDR=0
vera.VERA_ADDR_H = 1<<4 ; 1 pixel auto increment
repeat HEIGHT {
%asm {{
lda p8v_color
ldy #p8c_WIDTH/8
- .rept 8
sta vera.VERA_DATA0
.endrept
dey
bne -
}}
}
vera.VERA_ADDR=0
vera.VERA_ADDR_H = 0
}
sub rect(uword xx, ubyte yy, uword rwidth, ubyte rheight, ubyte color) {
if rwidth==0 or rheight==0
return
horizontal_line(xx, yy, rwidth, color)
if rheight==1
return
horizontal_line(xx, yy+rheight-1, rwidth, color)
vertical_line(xx, yy+1, rheight-2, color)
if rwidth==1
return
vertical_line(xx+rwidth-1, yy+1, rheight-2, color)
}
sub safe_rect(uword xx, ubyte yy, uword rwidth, ubyte rheight, ubyte color) {
; does bounds checking and clipping
safe_horizontal_line(xx, yy, rwidth, color)
if rheight==1
return
uword bottomyy = yy as uword + rheight -1
if bottomyy<HEIGHT
safe_horizontal_line(xx, lsb(bottomyy), rwidth, color)
safe_vertical_line(xx, yy+1, rheight-2, color)
if rwidth==1
return
safe_vertical_line(xx+rwidth-1, yy+1, rheight-2, color)
}
sub fillrect(uword xx, ubyte yy, uword rwidth, ubyte rheight, ubyte color) {
; Draw a filled rectangle of the given size and color.
; To fill the whole screen, use clear_screen(color) instead - it is much faster.
if rwidth==0
return
repeat rheight {
horizontal_line(xx, yy, rwidth, color)
yy++
}
}
sub safe_fillrect(uword xx, ubyte yy, uword rwidth, ubyte rheight, ubyte color) {
; Draw a filled rectangle of the given size and color.
; To fill the whole screen, use clear_screen(color) instead - it is much faster.
; This safe version does bounds checking and clipping.
if xx>=WIDTH or yy>=HEIGHT
return
if msb(xx)&$80!=0 {
rwidth += xx
xx = 0
}
if xx>=WIDTH
return
if xx+rwidth>WIDTH
rwidth = WIDTH-xx
if rwidth>WIDTH
return
if yy as uword + rheight > HEIGHT
rheight = HEIGHT-yy
if rheight>HEIGHT
return
repeat rheight {
horizontal_line(xx, yy, rwidth, color)
yy++
}
}
sub horizontal_line(uword xx, ubyte yy, uword length, ubyte color) {
if length==0
return
position(xx, yy)
; set vera auto-increment to 1 pixel
vera.VERA_ADDR_H = vera.VERA_ADDR_H & %00000111 | (1<<4)
if eor_mode {
vera.vaddr_clone(0) ; also setup port 1, for reading
%asm {{
ldx p8v_length+1
beq +
ldy #0
- lda p8v_color
eor vera.VERA_DATA1
sta vera.VERA_DATA0
iny
bne -
dex
bne -
+ ldy p8v_length ; remaining
beq +
- lda p8v_color
eor vera.VERA_DATA1
sta vera.VERA_DATA0
dey
bne -
+
}}
} else {
%asm {{
lda p8v_color
ldx p8v_length+1
beq +
ldy #0
- sta vera.VERA_DATA0
iny
bne -
dex
bne -
+ ldy p8v_length ; remaining
beq +
- sta vera.VERA_DATA0
dey
bne -
+
}}
}
}
sub safe_horizontal_line(uword xx, ubyte yy, uword length, ubyte color) {
; does bounds checking and clipping
if yy>=HEIGHT
return
if msb(xx)&$80!=0 {
length += xx
xx = 0
}
if xx>=WIDTH
return
if xx+length>WIDTH
length = WIDTH-xx
if length>WIDTH
return
horizontal_line(xx, yy, length, color)
}
sub vertical_line(uword xx, ubyte yy, ubyte lheight, ubyte color) {
if lheight==0
return
position(xx, yy)
; set vera auto-increment to 320 pixel increment (=next line)
vera.VERA_ADDR_H = vera.VERA_ADDR_H & %00000111 | (14<<4)
if eor_mode {
vera.vaddr_clone(0) ; also setup port 1, for reading
%asm {{
ldy p8v_lheight
beq +
- lda p8v_color
eor vera.VERA_DATA1
sta vera.VERA_DATA0
dey
bne -
+
}}
} else {
%asm {{
ldy p8v_lheight
lda p8v_color
- sta vera.VERA_DATA0
dey
bne -
}}
}
}
sub safe_vertical_line(uword xx, ubyte yy, ubyte lheight, ubyte color) {
; does bounds checking and clipping
if yy>=HEIGHT
return
if msb(xx)&$80!=0 or xx>=WIDTH
return
if yy as uword + lheight > HEIGHT
lheight = HEIGHT-yy
if lheight>HEIGHT
return
vertical_line(xx, yy, lheight, color)
}
sub line(uword x1, ubyte y1, uword x2, ubyte y2, ubyte color) {
; Bresenham algorithm.
; This code special-cases various quadrant loops to allow simple ++ and -- operations.
; NOTE: this is about twice as fast as the kernal routine GRAPH_draw_line
; it trades memory for speed (uses inline plot routine and multiplication lookup tables)
;
; NOTE: is currently still a regular 6502 routine, could likely be made much faster with the VeraFX line helper.
cx16.r4L = color ; cache color in r4L for internal_line_plot
cx16.r3L = y2 ; ensure zeropage
cx16.r1L = y1 ; ensure zeropage
if cx16.r1L > cx16.r3L {
; make sure dy is always positive to have only 4 instead of 8 special cases
cx16.r0 = x1
x1 = x2
x2 = cx16.r0
cx16.r0L = cx16.r1L
cx16.r1L = cx16.r3L
cx16.r3L = cx16.r0L
}
word @zp dx = x2 as word
word @zp dy = cx16.r3L
dx -= x1
dy -= cx16.r1L
if dx==0 {
vertical_line(x1, cx16.r1L, lsb(dy)+1, color)
return
}
if dy==0 {
if x1>x2
x1=x2
horizontal_line(x1, cx16.r1L, abs(dx) as uword +1, color)
return
}
bool positive_ix = true
if dx < 0 {
dx = -dx
positive_ix = false
}
word @zp dx2 = dx*2
word @zp dy2 = dy*2
word @zp d ; error term (initialized below based on shallow/steep)
cx16.r0 = x1 ; ensure zeropage
cx16.r2 = x2 ; ensure zeropage
vera.VERA_CTRL = 0
if dx >= dy {
d = dx >> 1 ; Initialize error to DX/2 for shallow lines
if positive_ix {
repeat {
internal_line_plot()
if cx16.r0==cx16.r2
return
cx16.r0++
d += dy2
if d > dx {
cx16.r1L++
d -= dx2
}
}
} else {
repeat {
internal_line_plot()
if cx16.r0==cx16.r2
return
cx16.r0--
d += dy2
if d > dx {
cx16.r1L++
d -= dx2
}
}
}
}
else {
d = dy >> 1 ; Initialize error to DY/2 for steep lines
if positive_ix {
repeat {
internal_line_plot()
if cx16.r1L == cx16.r3L
return
cx16.r1L++
d += dx2
if d > dy {
cx16.r0++
d -= dy2
}
}
} else {
repeat {
internal_line_plot()
if cx16.r1L == cx16.r3L
return
cx16.r1L++
d += dx2
if d > dy {
cx16.r0--
d -= dy2
}
}
}
}
}
private asmsub internal_line_plot() {
; Internal plot routine for line algorithm.
; Uses: x in cx16.r0, y in cx16.r1L, color in cx16.r4L
; Checks eor_mode flag for XOR vs normal drawing.
%asm {{
ldy cx16.r1L
clc
lda times320_lo,y
adc cx16.r0L
sta vera.VERA_ADDR_L
lda times320_mid,y
adc cx16.r0H
sta vera.VERA_ADDR_M
lda #0
adc times320_hi,y
sta vera.VERA_ADDR_H
lda p8v_eor_mode
bne +
lda cx16.r4L
sta vera.VERA_DATA0
rts
+ lda cx16.r4L
eor vera.VERA_DATA0
sta vera.VERA_DATA0
rts
}}
}
sub circle(uword @zp xcenter, ubyte @zp ycenter, ubyte radius, ubyte color) {
; Warning: NO BOUNDS CHECKS. Make sure circle fits in the screen.
; Midpoint algorithm.
if radius==0
return
ubyte @zp xx = radius
ubyte @zp yy = 0
word @zp decisionOver2 = (1 as word)-xx
; R14 = internal plot X
; R15 = internal plot Y
while xx>=yy {
cx16.r14 = xcenter + xx
cx16.r15 = ycenter + yy
plotq()
cx16.r14 = xcenter - xx
plotq()
cx16.r14 = xcenter + xx
cx16.r15 = ycenter - yy
plotq()
cx16.r14 = xcenter - xx
plotq()
cx16.r14 = xcenter + yy
cx16.r15 = ycenter + xx
plotq()
cx16.r14 = xcenter - yy
plotq()
cx16.r14 = xcenter + yy
cx16.r15 = ycenter - xx
plotq()
cx16.r14 = xcenter - yy
plotq()
yy++
if decisionOver2>=0 {
xx--
decisionOver2 -= xx*$0002
}
decisionOver2 += yy*$0002
decisionOver2++
}
sub plotq() {
; cx16.r14 = x, cx16.r15 = y, color=color.
plot(cx16.r14, cx16.r15L, color)
}
}
sub safe_circle(uword @zp xcenter, uword @zp ycenter, ubyte radius, ubyte color) {
; This version does bounds checks and clipping, but is a lot slower.
; Midpoint algorithm.
if radius==0
return
ubyte @zp xx = radius
ubyte @zp yy = 0
word @zp decisionOver2 = (1 as word)-xx
; R14 = internal plot X
; R15 = internal plot Y
while xx>=yy {
cx16.r14 = xcenter + xx
cx16.r15 = ycenter + yy
plotq()
cx16.r14 = xcenter - xx
plotq()
cx16.r14 = xcenter + xx
cx16.r15 = ycenter - yy
plotq()
cx16.r14 = xcenter - xx
plotq()
cx16.r14 = xcenter + yy
cx16.r15 = ycenter + xx
plotq()
cx16.r14 = xcenter - yy
plotq()
cx16.r14 = xcenter + yy
cx16.r15 = ycenter - xx
plotq()
cx16.r14 = xcenter - yy
plotq()
yy++
if decisionOver2>=0 {
xx--
decisionOver2 -= xx*$0002
}
decisionOver2 += yy*$0002
decisionOver2++
}
sub plotq() {
; cx16.r14 = x, cx16.r15 = y, color=color.
if cx16.r15 < HEIGHT
safe_plot(cx16.r14, cx16.r15L, color)
}
}
sub disc(uword @zp xcenter, ubyte @zp ycenter, ubyte @zp radius, ubyte color) {
; Warning: NO BOUNDS CHECKS. Make sure circle fits in the screen.
; Midpoint algorithm, filled
if radius==0
return
ubyte @zp yy = 0
word @zp decisionOver2 = (1 as word)-radius
ubyte pendingRadius
ubyte pendingWidth
bool hasPending = false
while radius>=yy {
horizontal_line(xcenter-radius, ycenter+yy, radius*$0002+1, color)
horizontal_line(xcenter-radius, ycenter-yy, radius*$0002+1, color)
if hasPending and pendingRadius != radius {
if pendingRadius != pendingWidth {
horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*$0002+1, color)
horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*$0002+1, color)
}
hasPending = false
}
if not hasPending {
pendingRadius = radius
hasPending = true
}
pendingWidth = yy
yy++
if decisionOver2>=0 {
radius--
decisionOver2 -= radius*$0002
}
decisionOver2 += yy*$0002
decisionOver2++
}
if hasPending and pendingRadius != pendingWidth {
horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*$0002+1, color)
horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*$0002+1, color)
}
}
sub safe_disc(uword @zp xcenter, uword @zp ycenter, ubyte @zp radius, ubyte color) {
; This version does bounds checks and clipping, but is a lot slower.
; Midpoint algorithm, filled
if radius==0
return
ubyte @zp yy = 0
word @zp decisionOver2 = (1 as word)-radius
ubyte pendingRadius
ubyte pendingWidth
bool hasPending = false
while radius>=yy {
uword liney = ycenter+yy
if msb(liney)==0
safe_horizontal_line(xcenter-radius, lsb(liney), radius*$0002+1, color)
liney = ycenter-yy
if msb(liney)==0
safe_horizontal_line(xcenter-radius, lsb(liney), radius*$0002+1, color)
if hasPending and pendingRadius != radius {
if pendingRadius != pendingWidth {
liney = ycenter+pendingRadius
if msb(liney)==0
safe_horizontal_line(xcenter-pendingWidth, lsb(liney), pendingWidth*$0002+1, color)
liney = ycenter-pendingRadius
if msb(liney)==0
safe_horizontal_line(xcenter-pendingWidth, lsb(liney), pendingWidth*$0002+1, color)
}
hasPending = false
}
if not hasPending {
pendingRadius = radius
hasPending = true
}
pendingWidth = yy
yy++
if decisionOver2>=0 {
radius--
decisionOver2 -= radius*$0002
}
decisionOver2 += yy*$0002
decisionOver2++
}
if hasPending and pendingRadius != pendingWidth {
uword flushLiney = ycenter+pendingRadius
if msb(flushLiney)==0
safe_horizontal_line(xcenter-pendingWidth, lsb(flushLiney), pendingWidth*$0002+1, color)
flushLiney = ycenter-pendingRadius
if msb(flushLiney)==0
safe_horizontal_line(xcenter-pendingWidth, lsb(flushLiney), pendingWidth*$0002+1, color)
}
}
asmsub plot(uword x @AX, ubyte y @Y, ubyte color @R0) {
; x in r0, y in r1, color.
%asm {{
clc
adc times320_lo,y
sta vera.VERA_ADDR_L
txa
adc times320_mid,y
sta vera.VERA_ADDR_M
lda #0
adc times320_hi,y
sta vera.VERA_ADDR_H
lda p8v_eor_mode
bne +
lda cx16.r0L
sta vera.VERA_DATA0
rts
+ lda cx16.r0L
eor vera.VERA_DATA0
sta vera.VERA_DATA0
rts
}}
}
sub safe_plot(uword xx, ubyte yy, ubyte color) {
; A plot that does bounds checks to see if the pixel is inside the screen.
if msb(xx)&$80!=0
return
if xx >= WIDTH or yy >= HEIGHT
return
plot(xx, yy, color)
}
asmsub pget(uword x @AX, ubyte y @Y) -> ubyte @A {
; returns the color of the pixel
%asm {{
jsr p8s_position
lda vera.VERA_DATA0
rts
}}
}
sub fill(uword x, ubyte y, ubyte new_color, ubyte stack_rambank) {
; reuse a few virtual registers in ZP for variables
&ubyte fillm = &cx16.r7L
&ubyte seedm = &cx16.r8L
&ubyte cmask = &cx16.r8H
&ubyte vub = &cx16.r13L
&ubyte nvub = &cx16.r13H
ubyte[4] amask = [$c0,$30,$0c,$03] ; array of cmask bytes
; Non-recursive scanline flood fill.
; based loosely on code found here https://www.codeproject.com/Articles/6017/QuickFill-An-efficient-flood-fill-algorithm
; with the fixes applied to the seedfill_4 routine as mentioned in the comments.
; Also see https://lodev.org/cgtutor/floodfill.html
word @zp xx = x as word
word @zp yy = y as word
word x1
word x2
byte dy
cx16.r10L = new_color
stack.init()
sub push_stack(word sxl, word sxr, word sy, byte sdy) {
cx16.r0s = sy+sdy
if cx16.r0s>=0 and cx16.r0s<=HEIGHT-1 {
stack.push_w(sxl as uword)
stack.push_w(sxr as uword)
stack.push_w(sy as uword)
stack.push_b(sdy as ubyte)
}
}
sub pop_stack() {
dy = stack.pop_b() as byte
yy = stack.pop_w() as word
x2 = stack.pop_w() as word
x1 = stack.pop_w() as word
yy+=dy
}
cx16.r11L = pget(xx as uword, lsb(yy)) ; old_color
if cx16.r11L == cx16.r10L
return
if xx<0 or xx>WIDTH-1 or yy<0 or yy>HEIGHT-1
return
push_stack(xx, xx, yy, 1)
push_stack(xx, xx, yy + 1, -1)
word left = 0
while not stack.isempty() {
pop_stack()
xx = x1
if fill_scanline_left_8bpp() goto skip
left = xx + 1
if left < x1
push_stack(left, x1 - 1, yy, -dy)
xx = x1 + 1
do {
fill_scanline_right_8bpp()
push_stack(left, xx - 1, yy, dy)
if xx > x2 + 1
push_stack(x2 + 1, xx - 1, yy, -dy)
skip:
xx++
while xx <= x2 {
if pget(xx as uword, lsb(yy)) == cx16.r11L
break
xx++
}
left = xx
} until xx>x2
}
sub set_vera_address(bool decr) {
; set both data0 and data1 addresses
position(xx as uword, lsb(yy))
cx16.r0 = vera.VERA_ADDR
cx16.r1L = vera.VERA_ADDR_H & 1 | if decr %00011000 else %00010000
vera.VERA_ADDR_H = cx16.r1L
vera.VERA_CTRL = 1
vera.VERA_ADDR = cx16.r0
vera.VERA_ADDR_H = cx16.r1L
vera.VERA_CTRL = 0
}
sub fill_scanline_left_8bpp() -> bool {
set_vera_address(true)
cx16.r9s = xx
while xx >= 0 {
if vera.VERA_DATA0 != cx16.r11L
break
vera.VERA_DATA1 = cx16.r10L
xx--
}
return xx==cx16.r9s
}
sub fill_scanline_right_8bpp() {
set_vera_address(false)
while xx <= WIDTH-1 {
if vera.VERA_DATA0 != cx16.r11L
break
vera.VERA_DATA1 = cx16.r10L
xx++
}
}
}
private const ubyte charset_bank = $1
private const uword charset_addr = $f000 ; in bank 1, so $1f000
sub text(uword @zp xx, ubyte yy, ubyte color, str textptr) {
; -- Write some text at the given pixel position. The text string must be in an encoding approprite for the charset.
; You must also have called text_charset() first to select and prepare the character set to use.
uword chardataptr
ubyte[8] @shared char_bitmap_bytes_left
ubyte[8] @shared char_bitmap_bytes_right
while @(textptr)!=0 {
chardataptr = charset_addr + (@(textptr) as uword)*8
vera.vaddr(charset_bank, chardataptr, 1, 1)
repeat 8 {
position(xx,lsb(yy))
yy++
%asm {{
ldx p8v_color
lda vera.VERA_DATA1
sta P8ZP_SCRATCH_B1
ldy #8
- asl P8ZP_SCRATCH_B1
bcc +
stx vera.VERA_DATA0 ; write a pixel
bcs ++
+ lda vera.VERA_DATA0 ; don't write a pixel, but do advance to the next address
+ dey
bne -
}}
}
xx+=8
yy-=8
textptr++
}
}
asmsub position(uword x @AX, ubyte y @Y) {
%asm {{
clc
adc times320_lo,y
sta vera.VERA_ADDR_L
txa
adc times320_mid,y
sta vera.VERA_ADDR_M
lda #%00010000 ; auto increment on
adc times320_hi,y
sta vera.VERA_ADDR_H
rts
}}
}
inline asmsub next_pixel(ubyte color @A) {
; -- sets the next pixel byte to the graphics chip.
; for 8 bpp screens this will plot 1 pixel.
; for 2 bpp screens it will plot 4 pixels at once (color = bit pattern).
%asm {{
sta vera.VERA_DATA0
}}
}
asmsub next_pixels(uword pixels @AY, uword amount @R0) clobbers(A, X, Y) {
; -- sets the next bunch of pixels from a prepared array of bytes.
; for 8 bpp screens this will plot 1 pixel per byte.
; for 2 bpp screens it will plot 4 pixels at once (colors are the bit patterns per byte).
%asm {{
sta P8ZP_SCRATCH_W1
sty P8ZP_SCRATCH_W1+1
ldx cx16.r0+1
beq +
ldy #0
- lda (P8ZP_SCRATCH_W1),y
sta vera.VERA_DATA0
iny
bne -
inc P8ZP_SCRATCH_W1+1 ; next page of 256 pixels
dex
bne -
+ ldx cx16.r0 ; remaining pixels
beq +
ldy #0
- lda (P8ZP_SCRATCH_W1),y
sta vera.VERA_DATA0
iny
dex
bne -
+ rts
}}
}
asmsub set_8_pixels_from_bits(ubyte bits @R0, ubyte oncolor @A, ubyte offcolor @Y) clobbers(X) {
; this is only useful in 256 color mode where one pixel equals one byte value.
%asm {{
ldx #8
- asl cx16.r0
bcc +
sta vera.VERA_DATA0
bcs ++
+ sty vera.VERA_DATA0
+ dex
bne -
rts
}}
}
%asm {{
; multiplication by 320 lookup table
times320 := 320*range(240)
times320_lo .byte <times320
times320_mid .byte >times320
times320_hi .byte `times320
}}
}
+291
View File
@@ -0,0 +1,291 @@
%import vera
%option ignore_unused
vera_palette {
; Manipulate the Vera's display color vera_palette.
; Should you want to restore the full default palette, you can call cbm.CINT()
; The first 16 colors can be restored to their default with set_default16()
; NOTE: assume R0, R1 and R2 are clobbered when using routines in this library!
%option ignore_unused
sub set_color(ubyte index, uword color) {
internal_set_vera_palette_addr(index)
vera.VERA_DATA0 = lsb(color)
vera.VERA_DATA0 = msb(color)
vera.VERA_ADDR_H &= 1
}
sub get_color(ubyte index) -> uword {
internal_set_vera_palette_addr(index)
cx16.r0L = vera.VERA_DATA0
cx16.r0H = vera.VERA_DATA0
vera.VERA_ADDR_H &= 1
return cx16.r0
}
sub set_rgb_be(uword palette_ptr, uword num_colors, ubyte startindex) {
; 1 word per color entry, $0rgb in big endian format (split arrays)
internal_set_vera_palette_addr(startindex)
repeat num_colors {
vera.VERA_DATA0 = @(palette_ptr+num_colors)
vera.VERA_DATA0 = @(palette_ptr)
palette_ptr++
}
vera.VERA_ADDR_H &= 1
}
sub set_rgb_be_nosplit(uword palette_ptr, uword num_colors, ubyte startindex) {
; 1 word per color entry, $0rgb in big endian format (linear arrays)
internal_set_vera_palette_addr(startindex)
repeat num_colors {
vera.VERA_DATA0 = @(palette_ptr+1)
vera.VERA_DATA0 = @(palette_ptr)
palette_ptr += 2
}
vera.VERA_ADDR_H &= 1
}
sub set_rgb(uword palette_words_ptr, uword num_colors, ubyte startindex) {
; 1 word per color entry (in little endian format as layed out in video memory, so $gb;$0r) (split arrays)
internal_set_vera_palette_addr(startindex)
repeat num_colors {
vera.VERA_DATA0 = @(palette_words_ptr)
vera.VERA_DATA0 = @(palette_words_ptr+num_colors)
palette_words_ptr++
}
vera.VERA_ADDR_H &= 1
}
sub set_rgb_nosplit(uword palette_words_ptr, uword num_colors, ubyte startindex) {
; 1 word per color entry (in little endian format as layed out in video memory, so $gb;$0r) (linear arrays)
internal_set_vera_palette_addr(startindex)
repeat num_colors {
vera.VERA_DATA0 = @(palette_words_ptr)
palette_words_ptr++
vera.VERA_DATA0 = @(palette_words_ptr)
palette_words_ptr++
}
vera.VERA_ADDR_H &= 1
}
sub set_rgb8(uword palette_bytes_ptr, uword num_colors, ubyte startindex) {
; 3 bytes per color entry, adjust color depth from 8 to 4 bits per channel.
internal_set_vera_palette_addr(startindex)
ubyte red
ubyte greenblue
repeat num_colors {
cx16.r1 = color8to4(palette_bytes_ptr)
palette_bytes_ptr+=3
vera.VERA_DATA0 = cx16.r1H ; $GB
vera.VERA_DATA0 = cx16.r1L ; $0R
}
vera.VERA_ADDR_H &= 1
}
sub set_all_black() {
internal_set_vera_palette_addr(0)
repeat 256 {
vera.VERA_DATA0 = 0
vera.VERA_DATA0 = 0
}
vera.VERA_ADDR_H &= 1
}
sub set_all_white() {
internal_set_vera_palette_addr(0)
repeat 256 {
vera.VERA_DATA0 = $ff
vera.VERA_DATA0 = $0f
}
vera.VERA_ADDR_H &= 1
}
sub set_grayscale(ubyte startindex) {
; set 16 consecutive colors to a grayscale gradient from black to white
internal_set_vera_palette_addr(startindex)
cx16.r0L=0
repeat 16 {
vera.VERA_DATA0 = cx16.r0L
vera.VERA_DATA0 = cx16.r0L
cx16.r0L += $11
}
vera.VERA_ADDR_H &= 1
}
sub color8to4(uword colorpointer) -> uword {
; accurately convert 24 bits (3 bytes) RGB color, in that order in memory, to 16 bits $GB;$0R colorvalue
cx16.r1 = colorpointer
cx16.r0 = channel8to4(@(cx16.r1)) ; (red) -> $00:0R
cx16.r0H = channel8to4(@(cx16.r1+1))<<4 ; (green) -> $G0:0R
cx16.r0H |= channel8to4(@(cx16.r1+2)) ; (blue) -> $GB:0R
return cx16.r0
}
sub channel8to4(ubyte channelvalue) -> ubyte {
; accurately convert a single 8 bit color channel value to 4 bits, see https://threadlocalmutex.com/?p=48
return msb(channelvalue * $000f + 135)
}
sub fade_step_multi(ubyte startindex, ubyte endindex, uword target_rgb) -> bool {
; Perform one color fade step for multiple consecutive palette entries.
; startindex = palette index of first color to fade
; endindex = palette index of last color to fade
; target_rgb = $RGB color value to fade towards
; Returns true if one or more colors were changed, false if no fade steps were done anymore.
; So you usually keep calling this until it returns false.
bool changed = false
while startindex <= endindex {
if fade_step(startindex, target_rgb)
changed=true
startindex++
if_z
break
}
return changed
}
sub fade_step_colors(ubyte startindex, ubyte endindex, uword target_colors) -> bool {
; Perform one color fade step for multiple consecutive palette entries, to different target colors.
; startindex = palette index of first color to fade
; endindex = palette index of last color to fade, inclusive
; target_colors = address of uword $RGB array of colors to fade towards,
; in *linear* storage format (@nosplit) which is usually how palette blobs are loaded from disk.
; Returns true if one or more colors were changed, false if no fade steps were done anymore.
; So you usually keep calling this until it returns false.
bool changed = false
while startindex <= endindex {
if fade_step(startindex, peekw(target_colors))
changed=true
target_colors += 2
startindex++
if_z
break
}
return changed
}
sub fade_step(ubyte index, uword target_rgb) -> bool {
; Perform one color fade step for a single palette entry.
; index = palette index of the color to fade
; target_rgb = $RGB color value to fade towards
; Returns true if the color was changed, false if no fade step was done anymore.
; So you usually keep calling this until it returns false.
uword color = vera_palette.get_color(index)
cx16.r0L = msb(color) ; r
cx16.r1L = lsb(color) >> 4 ; g
cx16.r2L = lsb(color) & 15 ; b
cx16.r0H = msb(target_rgb) & 15 ; r2
cx16.r1H = lsb(target_rgb) >> 4 ; g2
cx16.r2H = lsb(target_rgb) & 15 ; b2
ubyte changed
; use cmp() + status bits branches, to avoid multiple compares that could be done just once
cmp(cx16.r0L, cx16.r0H)
if_ne {
if_cc
cx16.r0L++
else
cx16.r0L--
changed++
}
cmp(cx16.r1L, cx16.r1H)
if_ne {
if_cc
cx16.r1L++
else
cx16.r1L--
changed++
}
cmp(cx16.r2L, cx16.r2H)
if_ne {
if_cc
cx16.r2L++
else
cx16.r2L--
changed++
}
vera_palette.set_color(index, mkword(cx16.r0L, cx16.r1L<<4 | cx16.r2L))
return changed!=0
}
sub set_c64pepto() {
; set first 16 colors to the "Pepto" PAL commodore-64 palette http://www.pepto.de/projects/colorvic/
uword[] @nosplit colors = [
$000, ; 0 = black
$FFF, ; 1 = white
$833, ; 2 = red
$7cc, ; 3 = cyan
$839, ; 4 = purple
$5a4, ; 5 = green
$229, ; 6 = blue
$ef7, ; 7 = yellow
$852, ; 8 = orange
$530, ; 9 = brown
$c67, ; 10 = light red
$444, ; 11 = dark grey
$777, ; 12 = medium grey
$af9, ; 13 = light green
$76e, ; 14 = light blue
$bbb ; 15 = light grey
]
set_rgb_nosplit(colors, len(colors), 0)
}
sub set_c64ntsc() {
; set first 16 colors to a NTSC commodore-64 palette
uword[] @nosplit colors = [
$000, ; 0 = black
$FFF, ; 1 = white
$934, ; 2 = red
$9ff, ; 3 = cyan
$73f, ; 4 = purple
$4b1, ; 5 = green
$20c, ; 6 = blue
$ee6, ; 7 = yellow
$b53, ; 8 = orange
$830, ; 9 = brown
$f8a, ; 10 = light red
$444, ; 11 = dark grey
$999, ; 12 = medium grey
$9f9, ; 13 = light green
$36f, ; 14 = light blue
$ccc ; 15 = light grey
]
set_rgb_nosplit(colors, len(colors), 0)
}
sub set_default16() {
; set first 16 colors to the defaults on the X16
; (doesn't use the rom table so this works on roms older than 49 as well)
uword[] @nosplit colors = [
$000, ; 0 = black
$fff, ; 1 = white
$800, ; 2 = red
$afe, ; 3 = cyan
$c4c, ; 4 = purple
$0c5, ; 5 = green
$00a, ; 6 = blue
$ee7, ; 7 = yellow
$d85, ; 8 = orange
$640, ; 9 = brown
$f77, ; 10 = light red
$333, ; 11 = dark grey
$777, ; 12 = medium grey
$af6, ; 13 = light green
$08f, ; 14 = light blue
$bbb ; 15 = light grey
]
set_rgb_nosplit(colors, len(colors), 0)
}
private sub internal_set_vera_palette_addr(ubyte index) {
vera.VERA_CTRL = 0
vera.VERA_ADDR_H = %00010001
vera.VERA_ADDR = $fa00+(index *$0002)
}
}
+213
View File
@@ -0,0 +1,213 @@
%import vera
%option ignore_unused
vera_sprites {
; Simple routines to control sprites.
; They're not written for high performance, but for simplicity.
; That's why they control 1 sprite at a time.
; The exceptions are pos_batch() and pos_batch_split(); these are quite efficient
; to update sprite positions of multiple sprites in one call.
; HIGH PERFORMANCE sprite handling would probably have a copy of the sprite registers for each sprite instead,
; and a unrolled loop that copies those into the VERA registers when needed.
; note: sprites z-order will be in front of all layers.
; note: collision mask is not supported here yet.
; note: "palette offset" is counted as 0-15 (vera multiplies the offset by 16 to get at the actual color index)
const ubyte SIZE_8 = 0
const ubyte SIZE_16 = 1
const ubyte SIZE_32 = 2
const ubyte SIZE_64 = 3
const ubyte COLORS_16 = 0
const ubyte COLORS_256 = 128
const uword VERA_SPRITEREGS = $fc00 ; $1fc00
private uword @zp sprite_reg
sub init(ubyte spritenum,
ubyte databank, uword dataaddr,
ubyte width_flag, ubyte height_flag,
ubyte colors_flag, ubyte palette_offset) {
pos(spritenum, -64, -64) ; move sprite off-screen initially
vera.VERA_DC_VIDEO |= %01000000 ; enable sprites globally
dataaddr >>= 5
dataaddr |= (databank as uword)<<11
sprite_reg = VERA_SPRITEREGS + spritenum*$0008
vera.vpoke(1, sprite_reg, lsb(dataaddr)) ; address 12:5
vera.vpoke(1, sprite_reg+1, colors_flag | msb(dataaddr)) ; 4 bpp + address 16:13
vera.vpoke(1, sprite_reg+6, %00001100) ; z depth %11 = in front of both layers, no flips
vera.vpoke(1, sprite_reg+7, height_flag<<6 | width_flag<<4 | palette_offset&15) ; 64x64 pixels, palette offset
}
sub reset(ubyte spritenum_start, ubyte count) {
; resets all sprite attributes for the given sprite range
; this removes these sprites from the screen completely
; (without disabling sprites globally so the mouse cursor remains visible)
if spritenum_start > 127
return
if count + spritenum_start > 128
return
vera.VERA_CTRL = $00
vera.VERA_ADDR_H = $11
vera.VERA_ADDR = VERA_SPRITEREGS + spritenum_start * $0008
repeat count {
unroll 8 vera.VERA_DATA0 = $00
}
}
sub data(ubyte spritenum, ubyte bank, uword addr) {
addr >>= 5
addr |= (bank as uword)<<11
sprite_reg = VERA_SPRITEREGS + spritenum*$0008
vera.vpoke(1, sprite_reg, lsb(addr)) ; address 12:5
vera.vpoke_mask(1, sprite_reg+1, %11110000, msb(addr)) ; address 16:13
}
inline asmsub get_data_ptr(ubyte spritenum @A) -> ubyte @R1, uword @R0 {
; -- returns the VRAM address where the sprite's bitmap data is stored
; R1 (byte) = the vera bank (0 or 1), R0 (word) = the address.
%asm {{
jsr p8b_sprites.p8s_get_data_ptr_internal
}}
}
private sub get_data_ptr_internal(ubyte spritenum) {
sprite_reg = VERA_SPRITEREGS + spritenum*$0008
cx16.r0L = vera.vpeek(1, sprite_reg)
cx16.r0H = vera.vpeek(1, sprite_reg+1)
cx16.r1L = cx16.r0H & %00001000 !=0 as ubyte ; bank
cx16.r0 <<= 5 ; address
}
sub pos(ubyte spritenum, word xpos, word ypos) {
sprite_reg = VERA_SPRITEREGS + 2 + spritenum*$0008
vera.vpoke(1, sprite_reg, lsb(xpos))
vera.vpoke(1, sprite_reg+1, msb(xpos))
vera.vpoke(1, sprite_reg+2, lsb(ypos))
vera.vpoke(1, sprite_reg+3, msb(ypos))
}
sub pos_batch(ubyte first_spritenum, ubyte num_sprites, uword xpositions_ptr, uword ypositions_ptr) {
; -- note: the x and y positions word arrays must both be split word arrays in this version of the routine! (the default)
sprite_reg = VERA_SPRITEREGS + 2 + first_spritenum*$0008
vera.vaddr_autoincr(1, sprite_reg, 0, 8)
vera.vaddr_autoincr(1, sprite_reg+1, 1, 8)
tovera(xpositions_ptr)
sprite_reg += 2
vera.vaddr_autoincr(1, sprite_reg, 0, 8)
vera.vaddr_autoincr(1, sprite_reg+1, 1, 8)
tovera(ypositions_ptr)
sub tovera(uword positions @R0) {
repeat num_sprites {
vera.VERA_DATA0 = @(cx16.r0)
vera.VERA_DATA1 = @(cx16.r0+num_sprites)
cx16.r0 ++
}
}
}
sub pos_batch_nosplit(ubyte first_spritenum, ubyte num_sprites, uword xpositions_ptr, uword ypositions_ptr) {
; -- note: the x and y positions word arrays must both be regular linear arrays in this version of the routine!
sprite_reg = VERA_SPRITEREGS + 2 + first_spritenum*$0008
vera.vaddr_autoincr(1, sprite_reg, 0, 8)
vera.vaddr_autoincr(1, sprite_reg+1, 1, 8)
tovera(xpositions_ptr)
sprite_reg += 2
vera.vaddr_autoincr(1, sprite_reg, 0, 8)
vera.vaddr_autoincr(1, sprite_reg+1, 1, 8)
tovera(ypositions_ptr)
sub tovera(uword positions @R0) {
repeat num_sprites {
vera.VERA_DATA0 = @(cx16.r0)
cx16.r0 ++
vera.VERA_DATA1 = @(cx16.r0)
cx16.r0 ++
}
}
}
sub setx(ubyte spritenum, word xpos) {
sprite_reg = VERA_SPRITEREGS + 2 + spritenum*$0008
vera.vpoke(1, sprite_reg, lsb(xpos))
vera.vpoke(1, sprite_reg+1, msb(xpos))
}
sub sety(ubyte spritenum, word ypos) {
sprite_reg = VERA_SPRITEREGS + 4 + spritenum*$0008
vera.vpoke(1, sprite_reg, lsb(ypos))
vera.vpoke(1, sprite_reg+1, msb(ypos))
}
sub move(ubyte spritenum, word dx, word dy) {
; move a sprite based on its current position
sprite_reg = VERA_SPRITEREGS + 2 + spritenum*$0008
cx16.r1s = mkword(vera.vpeek(1, sprite_reg+1), vera.vpeek(1, sprite_reg)) as word + dx
cx16.r2s = mkword(vera.vpeek(1, sprite_reg+3), vera.vpeek(1, sprite_reg+2)) as word + dy
vera.vpoke(1, sprite_reg, cx16.r1L)
vera.vpoke(1, sprite_reg+1, cx16.r1H)
vera.vpoke(1, sprite_reg+2, cx16.r2L)
vera.vpoke(1, sprite_reg+3, cx16.r2H)
}
sub movex(ubyte spritenum, word dx) {
; move a sprite horizontally based on its current position
sprite_reg = VERA_SPRITEREGS + 2 + spritenum*$0008
cx16.r1s = mkword(vera.vpeek(1, sprite_reg+1), vera.vpeek(1, sprite_reg)) as word + dx
vera.vpoke(1, sprite_reg, cx16.r1L)
vera.vpoke(1, sprite_reg+1, cx16.r1H)
}
sub movey(ubyte spritenum, word dy) {
; move a sprite vertically based on its current position
sprite_reg = VERA_SPRITEREGS + 4 + spritenum*$0008
cx16.r1s = mkword(vera.vpeek(1, sprite_reg+1), vera.vpeek(1, sprite_reg)) as word + dy
vera.vpoke(1, sprite_reg, cx16.r1L)
vera.vpoke(1, sprite_reg+1, cx16.r1H)
}
sub getx(ubyte spritenum) -> word {
sprite_reg = VERA_SPRITEREGS + 2 + spritenum*$0008
return mkword(vera.vpeek(1, sprite_reg+1), vera.vpeek(1, sprite_reg)) as word
}
sub gety(ubyte spritenum) -> word {
sprite_reg = VERA_SPRITEREGS + 4 + spritenum*$0008
return mkword(vera.vpeek(1, sprite_reg+1), vera.vpeek(1, sprite_reg)) as word
}
sub getxy(ubyte spritenum) -> word, word {
sprite_reg = VERA_SPRITEREGS + 2 + spritenum*$0008
cx16.r0s = mkword(vera.vpeek(1, sprite_reg+1), vera.vpeek(1, sprite_reg)) as word
sprite_reg += 2
cx16.r1s = mkword(vera.vpeek(1, sprite_reg+1), vera.vpeek(1, sprite_reg)) as word
return cx16.r0s, cx16.r1s
}
sub hide(ubyte spritenum) {
vera.vpoke_and(1, VERA_SPRITEREGS + 6 + spritenum*$0008, %11110011)
}
sub show(ubyte spritenum) {
vera.vpoke_or(1, VERA_SPRITEREGS + 6 + spritenum*$0008, %00001100)
}
sub zdepth(ubyte spritenum, ubyte depth) {
vera.vpoke_mask(1, VERA_SPRITEREGS + 6 + spritenum*$0008, %11110011, depth<<2)
}
sub flipx(ubyte spritenum, bool flipped) {
vera.vpoke_mask(1, VERA_SPRITEREGS + 6 + spritenum*$0008, %11111110, flipped as ubyte)
}
sub flipy(ubyte spritenum, bool flipped) {
vera.vpoke_mask(1, VERA_SPRITEREGS + 6 + spritenum*$0008, %11111101, (flipped as ubyte)<<1)
}
sub set_palette_offset(ubyte spritenum, ubyte offset) {
vera.vpoke_mask(1, VERA_SPRITEREGS + 7 + spritenum*$0008, %11110000, offset&15)
}
}
+239 -89
View File
@@ -12,6 +12,11 @@ adpcm {
; $ sox --guard source.mp3 -r 8000 -c 1 -e ima-adpcm out.wav trim 01:27.50 00:09
; $ ffmpeg -i source.mp3 -ss 00:01:27.50 -to 00:01:36.50 -ar 8000 -ac 1 -c:a adpcm_ima_wav -block_size 256 -map_metadata -1 -bitexact out.wav
; And/or use a tool such as https://github.com/dbry/adpcm-xq (make sure to set the correct block size, -b8)
; Possible pre-filter step to improve encoded sound quality:
; ffmpeg -i input.wav -af "highpass=f=50, lowpass=f=10000, acompressor=threshold=-12dB:ratio=3:attack=8:release=60, loudnorm=I=-16:TP=-1.5, aresample=22050" intermediate.wav
; (note that the filter parameters depend on the sample rate! Ask google what the correct parameters should be for different sample rates!)
;
; NOTE: sox may generate IMA-ADPCM files with a block size different than 256 bytes, which is not supported by this decoder. Use ffmpeg instead.
;
@@ -31,27 +36,10 @@ adpcm {
%option ignore_unused, private_symbols
byte[] t_index = [ -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8]
uword[] t_step = [
7, 8, 9, 10, 11, 12, 13, 14,
16, 17, 19, 21, 23, 25, 28, 31,
34, 37, 41, 45, 50, 55, 60, 66,
73, 80, 88, 97, 107, 118, 130, 143,
157, 173, 190, 209, 230, 253, 279, 307,
337, 371, 408, 449, 494, 544, 598, 658,
724, 796, 876, 963, 1060, 1166, 1282, 1411,
1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024,
3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484,
7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899,
15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794,
32767]
uword @requirezp predict ; decoded 16 bit pcm sample for first channel.
uword @requirezp predict_2 ; decoded 16 bit pcm sample for second channel.
ubyte @requirezp index
ubyte @requirezp index_2
uword @requirezp pstep
uword @requirezp pstep_2
uword @requirezp rowptr ; pointer to current adpcm_deltas row for first channel.
uword @requirezp rowptr_2 ; pointer to current adpcm_deltas row for second channel.
public sub decode_block_mono(uword @requirezp nibblesptr) {
; Decodes one 256 byte block of adpcm data, into the Vera's PCM FIFO buffer.
@@ -160,86 +148,248 @@ adpcm {
sub init(uword startPredict, ubyte startIndex) {
; initialize first decoding channel.
predict = startPredict
index = startIndex
pstep = t_step[index]
rowptr = &deltas_table + ((startIndex as uword) << 5)
}
sub init_second(uword startPredict_2, ubyte startIndex_2) {
; initialize second decoding channel.
predict_2 = startPredict_2
index_2 = startIndex_2
pstep_2 = t_step[index_2]
rowptr_2 = &deltas_table + ((startIndex_2 as uword) << 5)
}
sub decode_nibble(ubyte @zp nibble) {
/*
Prog8 equivalents of the optimized asm routines below:
sub decode_nibble(ubyte nibble) {
; Decoder for a single nibble for the first channel. (value of 'nibble' needs to be strictly 0-15 !)
; This is the hotspot of the decoder algorithm!
; Note that the generated assembly from this is pretty efficient,
; rewriting it by hand in asm seems to improve it only ~10%.
cx16.r0s = 0 ; difference
if nibble & %0100 !=0
cx16.r0s += pstep
pstep >>= 1
if nibble & %0010 !=0
cx16.r0s += pstep
pstep >>= 1
if nibble & %0001 !=0
cx16.r0s += pstep
pstep >>= 1
cx16.r0s += pstep
if nibble & %1000 !=0
predict -= cx16.r0
else
predict += cx16.r0
; NOTE: the original C/Python code uses a 32 bits prediction value and clips it to a 16 bit word
; but for speed reasons we only work with 16 bit words here all the time (with possible clipping error)
; if predicted > 32767:
; predicted = 32767
; elif predicted < -32767:
; predicted = - 32767
index += t_index[nibble] as ubyte
if_neg
index = 0
else if index >= len(t_step)-1
index = len(t_step)-1
pstep = t_step[index]
; This is the hotspot of the decoder algorithm.
predict += peekw(rowptr + (nibble as uword * 2))
rowptr = clamp(rowptr + t_rowdelta[nibble] as uword, &deltas_table, &deltas_table + 2816)
}
sub decode_nibble_second(ubyte @zp nibble) {
sub decode_nibble_second(ubyte nibble) {
; Decoder for a single nibble for the second channel. (value of 'nibble' needs to be strictly 0-15 !)
; This is the hotspot of the decoder algorithm!
; Note that the generated assembly from this is pretty efficient,
; rewriting it by hand in asm seems to improve it only ~10%.
cx16.r0s = 0 ; difference
if nibble & %0100 !=0
cx16.r0s += pstep_2
pstep_2 >>= 1
if nibble & %0010 !=0
cx16.r0s += pstep_2
pstep_2 >>= 1
if nibble & %0001 !=0
cx16.r0s += pstep_2
pstep_2 >>= 1
cx16.r0s += pstep_2
if nibble & %1000 !=0
predict_2 -= cx16.r0
else
predict_2 += cx16.r0
; NOTE: the original C/Python code uses a 32 bits prediction value and clips it to a 16 bit word
; but for speed reasons we only work with 16 bit words here all the time (with possible clipping error)
; if predicted > 32767:
; predicted = 32767
; elif predicted < -32767:
; predicted = - 32767
index_2 += t_index[nibble] as ubyte
if_neg
index_2 = 0
else if index_2 >= len(t_step)-1
index_2 = len(t_step)-1
pstep_2 = t_step[index_2]
; This is the hotspot of the decoder algorithm.
predict_2 += peekw(rowptr_2 + (nibble as uword * 2))
rowptr_2 = clamp(rowptr_2 + t_rowdelta[nibble] as uword, &deltas_table, &deltas_table + 2816)
}
private word[] t_rowdelta = [-32,-32,-32,-32,64,128,192,256,-32,-32,-32,-32,64,128,192,256]
*/
asmsub decode_nibble(ubyte nibble @A) clobbers(A, Y) {
; Decoder for a single nibble for the first channel. (value of 'nibble' needs to be strictly 0-15 !)
; This is the hotspot of the decoder algorithm.
%asm {{
sta P8ZP_SCRATCH_B1 ; save nibble
asl a ; nibble*2; C=0 guaranteed since nibble<=15
tay
lda (p8v_rowptr),y ; delta LSB
adc p8v_predict ; C=0 after asl
sta p8v_predict
iny
lda (p8v_rowptr),y ; delta MSB
adc p8v_predict+1
sta p8v_predict+1
ldy P8ZP_SCRATCH_B1 ; original nibble
clc
lda p8v_rowptr
adc t_rowdelta_lsb,y ; rowptr += signed word row delta
sta p8v_rowptr
lda p8v_rowptr+1
adc t_rowdelta_msb,y
sta p8v_rowptr+1
; clamp rowptr to [adpcm_deltas, adpcm_deltas+88*32]
lda p8v_rowptr+1
cmp #>adpcm_deltas
bcc _set_min
bne _check_max
lda p8v_rowptr
cmp #<adpcm_deltas
bcc _set_min
_check_max:
lda p8v_rowptr+1
cmp #(>adpcm_deltas)+11
bcc _done
bne _set_max
lda p8v_rowptr
cmp #<adpcm_deltas
beq _done
bcc _done
_set_max:
lda #<(adpcm_deltas+2816)
sta p8v_rowptr
lda #(>adpcm_deltas)+11
sta p8v_rowptr+1
bne _done
_set_min:
lda #<adpcm_deltas
sta p8v_rowptr
lda #>adpcm_deltas
sta p8v_rowptr+1
_done:
rts
}}
}
asmsub decode_nibble_second(ubyte nibble @A) clobbers(A, Y) {
; Decoder for a single nibble for the second channel. (value of 'nibble' needs to be strictly 0-15 !)
; This is the hotspot of the decoder algorithm.
%asm {{
sta P8ZP_SCRATCH_B1 ; save nibble
asl a ; nibble*2; C=0 guaranteed since nibble<=15
tay
lda (p8v_rowptr_2),y ; delta LSB
adc p8v_predict_2 ; C=0 after asl
sta p8v_predict_2
iny
lda (p8v_rowptr_2),y ; delta MSB
adc p8v_predict_2+1
sta p8v_predict_2+1
ldy P8ZP_SCRATCH_B1 ; original nibble
clc
lda p8v_rowptr_2
adc t_rowdelta_lsb,y ; rowptr_2 += signed word row delta
sta p8v_rowptr_2
lda p8v_rowptr_2+1
adc t_rowdelta_msb,y
sta p8v_rowptr_2+1
; clamp rowptr_2 to [adpcm_deltas, adpcm_deltas+88*32]
lda p8v_rowptr_2+1
cmp #>adpcm_deltas
bcc _set_min
bne _check_max
lda p8v_rowptr_2
cmp #<adpcm_deltas
bcc _set_min
_check_max:
lda p8v_rowptr_2+1
cmp #(>adpcm_deltas)+11
bcc _done
bne _set_max
lda p8v_rowptr_2
cmp #<adpcm_deltas
beq _done
bcc _done
_set_max:
lda #<(adpcm_deltas+2816)
sta p8v_rowptr_2
lda #(>adpcm_deltas)+11
sta p8v_rowptr_2+1
bne _done
_set_min:
lda #<adpcm_deltas
sta p8v_rowptr_2
lda #>adpcm_deltas
sta p8v_rowptr_2+1
_done:
rts
}}
}
deltas_table:
%asm {{
; Precomputed IMA ADPCM delta table: 89 step sizes * 16 nibbles = 1424 signed 16-bit values.
; Delta = step/8 + (bit2? step) + (bit1? step/2) + (bit0? step/4), negated if nibble bit 3 set.
; Stored as interleaved signed 16-bit (.sint) words; index into it is (index<<4)|nibble.
; Note: deltas for large steps exceed +32767; they are stored two's-complement (e.g. +36862 as -28674).
; The 16-bit wrapping add in the decoder treats these the same as their unsigned representation.
adpcm_deltas:
.sint 0, 1, 3, 4, 7, 8, 10, 11, 0, -1, -3, -4, -7, -8, -10, -11
.sint 1, 3, 5, 7, 9, 11, 13, 15, -1, -3, -5, -7, -9, -11, -13, -15
.sint 1, 3, 5, 7, 10, 12, 14, 16, -1, -3, -5, -7, -10, -12, -14, -16
.sint 1, 3, 6, 8, 11, 13, 16, 18, -1, -3, -6, -8, -11, -13, -16, -18
.sint 1, 3, 6, 8, 12, 14, 17, 19, -1, -3, -6, -8, -12, -14, -17, -19
.sint 1, 4, 7, 10, 13, 16, 19, 22, -1, -4, -7, -10, -13, -16, -19, -22
.sint 1, 4, 7, 10, 14, 17, 20, 23, -1, -4, -7, -10, -14, -17, -20, -23
.sint 1, 4, 8, 11, 15, 18, 22, 25, -1, -4, -8, -11, -15, -18, -22, -25
.sint 2, 6, 10, 14, 18, 22, 26, 30, -2, -6, -10, -14, -18, -22, -26, -30
.sint 2, 6, 10, 14, 19, 23, 27, 31, -2, -6, -10, -14, -19, -23, -27, -31
.sint 2, 6, 11, 15, 21, 25, 30, 34, -2, -6, -11, -15, -21, -25, -30, -34
.sint 2, 7, 12, 17, 23, 28, 33, 38, -2, -7, -12, -17, -23, -28, -33, -38
.sint 2, 7, 13, 18, 25, 30, 36, 41, -2, -7, -13, -18, -25, -30, -36, -41
.sint 3, 9, 15, 21, 28, 34, 40, 46, -3, -9, -15, -21, -28, -34, -40, -46
.sint 3, 10, 17, 24, 31, 38, 45, 52, -3, -10, -17, -24, -31, -38, -45, -52
.sint 3, 10, 18, 25, 34, 41, 49, 56, -3, -10, -18, -25, -34, -41, -49, -56
.sint 4, 12, 21, 29, 38, 46, 55, 63, -4, -12, -21, -29, -38, -46, -55, -63
.sint 4, 13, 22, 31, 41, 50, 59, 68, -4, -13, -22, -31, -41, -50, -59, -68
.sint 5, 15, 25, 35, 46, 56, 66, 76, -5, -15, -25, -35, -46, -56, -66, -76
.sint 5, 16, 27, 38, 50, 61, 72, 83, -5, -16, -27, -38, -50, -61, -72, -83
.sint 6, 18, 31, 43, 56, 68, 81, 93, -6, -18, -31, -43, -56, -68, -81, -93
.sint 6, 19, 33, 46, 61, 74, 88, 101, -6, -19, -33, -46, -61, -74, -88, -101
.sint 7, 22, 37, 52, 67, 82, 97, 112, -7, -22, -37, -52, -67, -82, -97, -112
.sint 8, 24, 41, 57, 74, 90, 107, 123, -8, -24, -41, -57, -74, -90, -107, -123
.sint 9, 27, 45, 63, 82, 100, 118, 136, -9, -27, -45, -63, -82, -100, -118, -136
.sint 10, 30, 50, 70, 90, 110, 130, 150, -10, -30, -50, -70, -90, -110, -130, -150
.sint 11, 33, 55, 77, 99, 121, 143, 165, -11, -33, -55, -77, -99, -121, -143, -165
.sint 12, 36, 60, 84, 109, 133, 157, 181, -12, -36, -60, -84, -109, -133, -157, -181
.sint 13, 39, 66, 92, 120, 146, 173, 199, -13, -39, -66, -92, -120, -146, -173, -199
.sint 14, 43, 73, 102, 132, 161, 191, 220, -14, -43, -73, -102, -132, -161, -191, -220
.sint 16, 48, 81, 113, 146, 178, 211, 243, -16, -48, -81, -113, -146, -178, -211, -243
.sint 17, 52, 88, 123, 160, 195, 231, 266, -17, -52, -88, -123, -160, -195, -231, -266
.sint 19, 58, 97, 136, 176, 215, 254, 293, -19, -58, -97, -136, -176, -215, -254, -293
.sint 21, 64, 107, 150, 194, 237, 280, 323, -21, -64, -107, -150, -194, -237, -280, -323
.sint 23, 70, 118, 165, 213, 260, 308, 355, -23, -70, -118, -165, -213, -260, -308, -355
.sint 26, 78, 130, 182, 235, 287, 339, 391, -26, -78, -130, -182, -235, -287, -339, -391
.sint 28, 85, 143, 200, 258, 315, 373, 430, -28, -85, -143, -200, -258, -315, -373, -430
.sint 31, 94, 157, 220, 284, 347, 410, 473, -31, -94, -157, -220, -284, -347, -410, -473
.sint 34, 103, 173, 242, 313, 382, 452, 521, -34, -103, -173, -242, -313, -382, -452, -521
.sint 38, 114, 191, 267, 345, 421, 498, 574, -38, -114, -191, -267, -345, -421, -498, -574
.sint 42, 126, 210, 294, 379, 463, 547, 631, -42, -126, -210, -294, -379, -463, -547, -631
.sint 46, 138, 231, 323, 417, 509, 602, 694, -46, -138, -231, -323, -417, -509, -602, -694
.sint 51, 153, 255, 357, 459, 561, 663, 765, -51, -153, -255, -357, -459, -561, -663, -765
.sint 56, 168, 280, 392, 505, 617, 729, 841, -56, -168, -280, -392, -505, -617, -729, -841
.sint 61, 184, 308, 431, 555, 678, 802, 925, -61, -184, -308, -431, -555, -678, -802, -925
.sint 68, 204, 340, 476, 612, 748, 884, 1020, -68, -204, -340, -476, -612, -748, -884, -1020
.sint 74, 223, 373, 522, 672, 821, 971, 1120, -74, -223, -373, -522, -672, -821, -971, -1120
.sint 82, 246, 411, 575, 740, 904, 1069, 1233, -82, -246, -411, -575, -740, -904, -1069, -1233
.sint 90, 271, 452, 633, 814, 995, 1176, 1357, -90, -271, -452, -633, -814, -995, -1176, -1357
.sint 99, 298, 497, 696, 895, 1094, 1293, 1492, -99, -298, -497, -696, -895, -1094, -1293, -1492
.sint 109, 328, 547, 766, 985, 1204, 1423, 1642, -109, -328, -547, -766, -985, -1204, -1423, -1642
.sint 120, 360, 601, 841, 1083, 1323, 1564, 1804, -120, -360, -601, -841, -1083, -1323, -1564, -1804
.sint 132, 397, 662, 927, 1192, 1457, 1722, 1987, -132, -397, -662, -927, -1192, -1457, -1722, -1987
.sint 145, 436, 728, 1019, 1311, 1602, 1894, 2185, -145, -436, -728, -1019, -1311, -1602, -1894, -2185
.sint 160, 480, 801, 1121, 1442, 1762, 2083, 2403, -160, -480, -801, -1121, -1442, -1762, -2083, -2403
.sint 176, 528, 881, 1233, 1587, 1939, 2292, 2644, -176, -528, -881, -1233, -1587, -1939, -2292, -2644
.sint 194, 582, 970, 1358, 1746, 2134, 2522, 2910, -194, -582, -970, -1358, -1746, -2134, -2522, -2910
.sint 213, 639, 1066, 1492, 1920, 2346, 2773, 3199, -213, -639, -1066, -1492, -1920, -2346, -2773, -3199
.sint 234, 703, 1173, 1642, 2112, 2581, 3051, 3520, -234, -703, -1173, -1642, -2112, -2581, -3051, -3520
.sint 258, 774, 1291, 1807, 2324, 2840, 3357, 3873, -258, -774, -1291, -1807, -2324, -2840, -3357, -3873
.sint 284, 852, 1420, 1988, 2556, 3124, 3692, 4260, -284, -852, -1420, -1988, -2556, -3124, -3692, -4260
.sint 312, 936, 1561, 2185, 2811, 3435, 4060, 4684, -312, -936, -1561, -2185, -2811, -3435, -4060, -4684
.sint 343, 1030, 1717, 2404, 3092, 3779, 4466, 5153, -343, -1030, -1717, -2404, -3092, -3779, -4466, -5153
.sint 378, 1134, 1890, 2646, 3402, 4158, 4914, 5670, -378, -1134, -1890, -2646, -3402, -4158, -4914, -5670
.sint 415, 1246, 2078, 2909, 3742, 4573, 5405, 6236, -415, -1246, -2078, -2909, -3742, -4573, -5405, -6236
.sint 457, 1372, 2287, 3202, 4117, 5032, 5947, 6862, -457, -1372, -2287, -3202, -4117, -5032, -5947, -6862
.sint 503, 1509, 2516, 3522, 4529, 5535, 6542, 7548, -503, -1509, -2516, -3522, -4529, -5535, -6542, -7548
.sint 553, 1660, 2767, 3874, 4981, 6088, 7195, 8302, -553, -1660, -2767, -3874, -4981, -6088, -7195, -8302
.sint 608, 1825, 3043, 4260, 5479, 6696, 7914, 9131, -608, -1825, -3043, -4260, -5479, -6696, -7914, -9131
.sint 669, 2008, 3348, 4687, 6027, 7366, 8706, 10045, -669, -2008, -3348, -4687, -6027, -7366, -8706, -10045
.sint 736, 2209, 3683, 5156, 6630, 8103, 9577, 11050, -736, -2209, -3683, -5156, -6630, -8103, -9577, -11050
.sint 810, 2431, 4052, 5673, 7294, 8915, 10536, 12157, -810, -2431, -4052, -5673, -7294, -8915, -10536, -12157
.sint 891, 2674, 4457, 6240, 8023, 9806, 11589, 13372, -891, -2674, -4457, -6240, -8023, -9806, -11589, -13372
.sint 980, 2941, 4902, 6863, 8825, 10786, 12747, 14708, -980, -2941, -4902, -6863, -8825, -10786, -12747, -14708
.sint 1078, 3235, 5393, 7550, 9708, 11865, 14023, 16180, -1078, -3235, -5393, -7550, -9708, -11865, -14023, -16180
.sint 1186, 3559, 5932, 8305, 10679, 13052, 15425, 17798, -1186, -3559, -5932, -8305, -10679, -13052, -15425, -17798
.sint 1305, 3915, 6526, 9136, 11747, 14357, 16968, 19578, -1305, -3915, -6526, -9136, -11747, -14357, -16968, -19578
.sint 1435, 4306, 7178, 10049, 12922, 15793, 18665, 21536, -1435, -4306, -7178, -10049, -12922, -15793, -18665, -21536
.sint 1579, 4737, 7896, 11054, 14214, 17372, 20531, 23689, -1579, -4737, -7896, -11054, -14214, -17372, -20531, -23689
.sint 1737, 5211, 8686, 12160, 15636, 19110, 22585, 26059, -1737, -5211, -8686, -12160, -15636, -19110, -22585, -26059
.sint 1911, 5733, 9555, 13377, 17200, 21022, 24844, 28666, -1911, -5733, -9555, -13377, -17200, -21022, -24844, -28666
.sint 2102, 6306, 10511, 14715, 18920, 23124, 27329, 31533, -2102, -6306, -10511, -14715, -18920, -23124, -27329, -31533
.sint 2312, 6937, 11562, 16187, 20812, 25437, 30062, -30849, -2312, -6937, -11562, -16187, -20812, -25437, -30062, 30849
.sint 2543, 7630, 12718, 17805, 22893, 27980, -32468, -27381, -2543, -7630, -12718, -17805, -22893, -27980, 32468, 27381
.sint 2798, 8394, 13990, 19586, 25183, 30779, -29161, -23565, -2798, -8394, -13990, -19586, -25183, -30779, 29161, 23565
.sint 3077, 9232, 15388, 21543, 27700, -31681, -25525, -19370, -3077, -9232, -15388, -21543, -27700, 31681, 25525, 19370
.sint 3385, 10156, 16928, 23699, 30471, -28294, -21522, -14751, -3385, -10156, -16928, -23699, -30471, 28294, 21522, 14751
.sint 3724, 11172, 18621, 26069, -32018, -24570, -17121, -9673, -3724, -11172, -18621, -26069, 32018, 24570, 17121, 9673
.sint 4095, 12286, 20478, 28669, -28674, -20483, -12291, -4100, -4095, -12286, -20478, -28669, 28674, 20483, 12291, 4100
; Row pointer deltas (t_index * 32) as signed 16-bit values, split into LSB/MSB tables.
t_rowdelta_lsb:
.byte 224, 224, 224, 224, 64, 128, 192, 0, 224, 224, 224, 224, 64, 128, 192, 0
t_rowdelta_msb:
.byte 255, 255, 255, 255, 0, 0, 0, 1, 255, 255, 255, 255, 0, 0, 0, 1
}}
}
+48 -47
View File
@@ -832,68 +832,69 @@ set_byte:
cx16.screen_set_charset(charset, 0)
}
private inline asmsub vera_addr_inc(ubyte amount @A) clobbers(A) {
; -- add amount to the current VERA address register (port 0)
%asm {{
stz cx16.VERA_CTRL
clc
adc cx16.VERA_ADDR_L
sta cx16.VERA_ADDR_L
lda cx16.VERA_ADDR_M
adc #0
sta cx16.VERA_ADDR_M
lda cx16.VERA_ADDR_H
adc #0
sta cx16.VERA_ADDR_H
}}
}
private inline asmsub vera_addr_next() clobbers(A) {
; -- increment the current VERA address register (port 0) by one.
; This is smaller and faster than vera_addr_inc(1) because it skips
; the high bytes when there is no carry.
%asm {{
stz cx16.VERA_CTRL
inc cx16.VERA_ADDR_L
bne +
inc cx16.VERA_ADDR_M
bne +
inc cx16.VERA_ADDR_H
+ }}
}
sub text(uword @zp xx, uword yy, ubyte color, str sctextptr) {
; -- Write some text at the given pixel position. The text string must be in screencode encoding (not petscii!).
; You must also have called text_charset() first to select and prepare the character set to use.
uword chardataptr
ubyte[8] @shared char_bitmap_bytes_left
ubyte[8] @shared char_bitmap_bytes_right
ubyte[4] @shared mask4c = [%00111111, %11001111, %11110011, %11111100]
ubyte[4] @shared colorshift
; prepare the color-shifted values for each pixel group once per call.
cx16.r11L = color & 3
colorshift[0] = cx16.r11L << 6
colorshift[1] = cx16.r11L << 4
colorshift[2] = cx16.r11L << 2
colorshift[3] = cx16.r11L
; we're going to use a few cx16 registers to make sure every variable is in zeropage in the inner loop.
cx16.r11L = color
while @(sctextptr)!=0 {
chardataptr = charset_addr + (@(sctextptr) as uword)*8
cx16.vaddr(charset_bank, chardataptr, 1, 1) ; for reading the chardata from Vera data channel 1
position(xx, yy) ; only calculated once, we update vera address in the loop instead
cx16.vaddr(charset_bank, chardataptr, 1, 1) ; read chardata from vera data channel 1
position(xx, yy)
cx16.VERA_ADDR_H &= $0f ; no auto increment
cx16.r7 = xx
repeat 8 {
cx16.r10L = cx16.VERA_DATA1 ; get the next 8 horizontal character bits
cx16.r7 = xx
cx16.r10L = cx16.VERA_DATA1 ; get the next 8 horizontal character bits
repeat 8 {
cx16.r10L <<= 1
if_cs {
cx16.r2L = cx16.r7L & 3 ; xbits
when cx16.r11L & 3 {
1 -> cx16.r12L = gfx_hires.plot.shiftedleft_4c_1[cx16.r2L]
2 -> cx16.r12L = gfx_hires.plot.shiftedleft_4c_2[cx16.r2L]
3 -> cx16.r12L = gfx_hires.plot.shiftedleft_4c_3[cx16.r2L]
else -> cx16.r12L = 0
}
cx16.VERA_DATA0 = cx16.VERA_DATA0 & gfx_hires.plot.mask4c[cx16.r2L] | cx16.r12L
cx16.r2L = cx16.r7L & 3
cx16.VERA_DATA0 = (cx16.VERA_DATA0 & mask4c[cx16.r2L]) | colorshift[cx16.r2L]
}
cx16.r7++
if (cx16.r7 & 3) == 0 {
; increment the pixel address by one
%asm {{
stz cx16.VERA_CTRL
clc
lda cx16.VERA_ADDR_L
adc #1
sta cx16.VERA_ADDR_L
lda cx16.VERA_ADDR_M
adc #0
sta cx16.VERA_ADDR_M
lda cx16.VERA_ADDR_H
adc #0
sta cx16.VERA_ADDR_H
}}
}
if (cx16.r7 & 3) == 0
vera_addr_next()
}
; increment pixel address to the next line
%asm {{
stz cx16.VERA_CTRL
clc
lda cx16.VERA_ADDR_L
adc #(640-8)/4
sta cx16.VERA_ADDR_L
lda cx16.VERA_ADDR_M
adc #0
sta cx16.VERA_ADDR_M
lda cx16.VERA_ADDR_H
adc #0
sta cx16.VERA_ADDR_H
}}
vera_addr_inc(158) ; advance to next row (160 bytes minus the 2 bytes already stepped)
}
xx+=8
sctextptr++
+16 -4
View File
@@ -745,7 +745,7 @@ skip:
private const ubyte charset_bank = $1
private const uword charset_addr = $f000 ; in bank 1, so $1f000
sub text(uword @zp xx, uword yy, ubyte color, str textptr) {
sub text(uword @zp xx, ubyte yy, ubyte color, str textptr) {
; -- Write some text at the given pixel position. The text string must be in an encoding approprite for the charset.
; You must also have called text_charset() first to select and prepare the character set to use.
uword chardataptr
@@ -755,9 +755,8 @@ skip:
while @(textptr)!=0 {
chardataptr = charset_addr + (@(textptr) as uword)*8
cx16.vaddr(charset_bank, chardataptr, 1, 1)
position(xx, yy)
repeat 8 {
position(xx,lsb(yy))
yy++
%asm {{
ldx p8v_color
lda cx16.VERA_DATA1
@@ -771,9 +770,22 @@ skip:
+ dey
bne -
}}
; advance to next row (320 bytes minus the 8 pixels already stepped)
%asm {{
stz cx16.VERA_CTRL
clc
lda cx16.VERA_ADDR_L
adc #<(320-8)
sta cx16.VERA_ADDR_L
lda cx16.VERA_ADDR_M
adc #>(320-8)
sta cx16.VERA_ADDR_M
lda cx16.VERA_ADDR_H
adc #0
sta cx16.VERA_ADDR_H
}}
}
xx+=8
yy-=8
textptr++
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ sprites {
const ubyte COLORS_16 = 0
const ubyte COLORS_256 = 128
const uword VERA_SPRITEREGS = $fc00 ; $1fc00
uword @zp sprite_reg
private uword @zp sprite_reg
sub init(ubyte spritenum,
ubyte databank, uword dataaddr,
+115 -71
View File
@@ -186,6 +186,51 @@ result .byte ?,?,?,? ; routine could be faster if this were in Zeropage.
.pend
multiply_ubyte_word .proc
; -- multiply a 16-bit word by an UNSIGNED 8-bit value, giving the low 16 bits
; input: A = the 8-bit value (the high byte of A/Y is ignored, so this may be
; a zero-extended ubyte), multiply_ubyte_word.multiplier = the 16-bit word
; output: low 16 bits of the product in A/Y
;
; Same calling convention as multiply_words, but only the 8 bits in A are significant, so the
; shift-add runs 8 times instead of 16. The 16-bit accumulator discards the carries out of the
; top, which is exactly the mod-2^16 truncation Prog8's word semantics require (the true
; product of a byte and a word can need 24 bits).
;
; The byte operand must be UNSIGNED: a signed byte is sign-extended into A/Y by the caller and
; this routine deliberately ignores the high byte, so it would compute the unsigned product.
; The compiler only emits a call here when the operand is isUnsignedByte.
multiplier = P8ZP_SCRATCH_W1 ; 2 bytes, the word, shifted left in place
_result = P8ZP_SCRATCH_W2 ; 2 bytes
_multiplicand = P8ZP_SCRATCH_B1 ; 1 byte, the 8-bit value
sta _multiplicand
lda #0
sta _result
sta _result+1
ldx #8
-
lsr _multiplicand ; next bit of the 8-bit value, LSB first
bcc +
clc
lda _result
adc multiplier
sta _result
lda _result+1
adc multiplier+1
sta _result+1
+
asl multiplier ; word <<= 1
rol multiplier+1
dex
bne -
lda _result
ldy _result+1
rts
.pend
multiply_longs .proc
; 32-bit SIGNED multiplication.
; long1 in cx16.r12/r13, long2 in cx16.r14/r15, result in cx16.r14/r15
@@ -259,9 +304,17 @@ _shift
ror P8ZP_SCRATCH_W1+1
ror P8ZP_SCRATCH_W1
; Early exit once the multiplier is exhausted: the remaining iterations
; would only add zero, so skip them. Branch to the sign fixup and not to
; _done, or a negative product would never be negated.
lda P8ZP_SCRATCH_W1
ora P8ZP_SCRATCH_W2
beq _signfix
dex
bne _loop
_signfix
; Apply sign if needed
lda P8ZP_SCRATCH_B1
bpl _done
@@ -914,14 +967,6 @@ mul_word_25 .proc
rts
.pend
mul_byte_40 .proc
and #7
tay
lda _forties,y
rts
_forties .byte 0*40, 1*40, 2*40, 3*40, 4*40, 5*40, 6*40, 7*40 & 255
.pend
mul_word_40 .proc
; AY = (AY*4 + AY)*8
sta P8ZP_SCRATCH_W1
@@ -948,14 +993,6 @@ mul_word_40 .proc
rts
.pend
mul_byte_50 .proc
and #7
tay
lda _fifties, y
rts
_fifties .byte 0*50, 1*50, 2*50, 3*50, 4*50, 5*50, 6*50 & 255, 7*50 & 255
.pend
mul_word_50 .proc
; AY = AY * 25 * 2
jsr mul_word_25
@@ -966,14 +1003,6 @@ mul_word_50 .proc
rts
.pend
mul_byte_80 .proc
and #3
tay
lda _eighties, y
rts
_eighties .byte 0*80, 1*80, 2*80, 3*80
.pend
mul_word_80 .proc
; AY = AY * 40 * 2
jsr mul_word_40
@@ -984,14 +1013,6 @@ mul_word_80 .proc
rts
.pend
mul_byte_100 .proc
and #3
tay
lda _hundreds, y
rts
_hundreds .byte 0*100, 1*100, 2*100, 3*100 & 255
.pend
mul_word_100 .proc
; AY = AY * 25 * 4
jsr mul_word_25
@@ -1004,38 +1025,6 @@ mul_word_100 .proc
rts
.pend
mul_word_320 .proc
; AY = A * 256 + A * 64 (msb in Y doesn't matter)
sta P8ZP_SCRATCH_B1
ldy #0
sty P8ZP_SCRATCH_REG
asl a
rol P8ZP_SCRATCH_REG
asl a
rol P8ZP_SCRATCH_REG
asl a
rol P8ZP_SCRATCH_REG
asl a
rol P8ZP_SCRATCH_REG
asl a
rol P8ZP_SCRATCH_REG
asl a
rol P8ZP_SCRATCH_REG
pha
clc
lda P8ZP_SCRATCH_B1
adc P8ZP_SCRATCH_REG
tay
pla
rts
.pend
mul_word_640 .proc
; AY = (A * 2 * 320) (msb in Y doesn't matter)
asl a
jmp mul_word_320
.pend
; ----------- end optimized multiplications -----------
@@ -1124,7 +1113,36 @@ square_long .proc
; Input: cx16.R14:R15 = 32-bit value to square (R14=low word, R15=high word)
; Output: cx16.R14:R15 = 32-bit square result (R14=low word, R15=high word)
; Clobbers: A, X, Y
;
; The square below only squares the low word, so it is only valid when the long actually
; fits in a signed 16-bit word (the high word is the sign extension of the low word's
; sign bit). The range test at the top of the routine handles that: values outside it
; tail-call the general multiply_longs instead. Note a high word of $0000 alone is not
; sufficient: 40000 as a signed word is -25536, and squaring that low word would be
; wrong. Doing the test here rather than inline at every sqr() call site costs the code
; size only once.
; Range check: is the long representable as a signed 16-bit word?
lda cx16.r14+1
and #128
beq _nonneg
; negative low word: the high word must be the sign extension ($FF,$FF)
lda cx16.r15
cmp #$FF
bne _out_of_range
lda cx16.r15+1
cmp #$FF
bne _out_of_range
jmp _square
_nonneg
; non-negative low word: the high word must be zero
lda cx16.r15
ora cx16.r15+1
bne _out_of_range
; fall through into the fast path
_square
lda cx16.r14
ldy cx16.r14+1
ldx cx16.r15+1
@@ -1146,20 +1164,31 @@ square_long .proc
sta cx16.r15+1
rts
_out_of_range
; r12:r13 is a 32-bit pair, so copy all four bytes, not just the low word
lda cx16.r14
sta cx16.r12
lda cx16.r14+1
sta cx16.r12+1
lda cx16.r15
sta cx16.r13
lda cx16.r15+1
sta cx16.r13+1
jmp multiply_longs
.pend
square .proc
; -- calculate square of signed word (actually -255..255) in AY, result in AY
; -- calculate square of signed word in AY, result in AY
; routine by Lee Davison, source: http://6502.org/source/integers/square.htm
; using this routine is a lot faster as doing a regular multiplication (for words)
;
; Calculates the 16 bit unsigned integer square of the signed 16 bit integer in
; Numberl/Numberh. The result is always in the range 0 to 65025 and is held in
; Squarel/Squareh
;
; The maximum input range is only +/-255 and no checking is done to ensure that
; this is so.
; The fast path below (by Lee Davison) only computes (abs(x) and 255) squared, so it is
; only valid for the input range -255..255. The range test at the top of the routine
; handles that: values outside it tail-call the general multiply_words instead, which
; yields the correct mod-2^16 result that Prog8's word semantics require. Doing the
; test here rather than inline at every sqr() call site costs the code size only once.
;
; This routine is useful if you are trying to draw circles as for any circle
; x^2+y^2=r^2 where x and y are the co-ordinates of any point on the circle and
@@ -1171,6 +1200,21 @@ squarel = P8ZP_SCRATCH_W2 ; square low byte
squareh = P8ZP_SCRATCH_W2+1 ; square high byte
tempsq = P8ZP_SCRATCH_B1 ; temp byte for intermediate result
; Range check: the fast path is only valid for -255..255. The high byte alone
; settles it: $00 is 0..255 (always in range), $FF is -256..-1 (in range except
; -256 itself, whose square is $10000 and so also needs the general path).
cpy #0
beq _in_range
cpy #$FF
bne _out_of_range
cmp #0
bne _in_range
_out_of_range
sta multiply_words.multiplier
sty multiply_words.multiplier+1
jmp multiply_words
_in_range
sta numberl
sty numberh
+5
View File
@@ -0,0 +1,5 @@
%import shared_m68k_adpcm
adpcm {
%option ignore_unused, private_symbols
}
@@ -27,6 +27,7 @@ p8_sys_startup {
}
sub init_system_phase2() {
%option noframe
%asm {{
moveq #0,d0
moveq #0,d1
@@ -48,6 +49,7 @@ p8_sys_startup {
}
sub cleanup_at_exit() {
%option noframe
%asm {{
movea.l #qemu.CTRL_REG_CMD,a1
move.l #qemu.CTRL_CMD_HALT,(a1)
+32 -6
View File
@@ -310,6 +310,7 @@ sys {
sub reset_system() {
%option noframe
%asm {{
movea.l #qemu.CTRL_REG_CMD,a1
move.l #qemu.CTRL_CMD_RESET,(a1)
@@ -318,6 +319,7 @@ sys {
}
sub poweroff_system() {
%option noframe
%asm {{
movea.l #qemu.CTRL_REG_CMD,a1
move.l #qemu.CTRL_CMD_HALT,(a1)
@@ -325,7 +327,8 @@ sys {
}}
}
sub die(ubyte code, str message) {
sub die(uword code, str message) {
%option noframe
; -- kill the program: print death message to goldfish TTY, then exit.
str @shared warning = iso:"\n\nPROGRAM DIED: "
%asm {{
@@ -346,7 +349,7 @@ sys {
moveq #10,d0
move.l d0,qemu.TTY_PUT_CHAR
}}
exit(code)
exit(code as word)
}
sub wait(uword jiffies) {
@@ -355,16 +358,19 @@ sys {
}
sub exit(ubyte returnvalue) {
asmsub exit(word returnvalue @D0) {
; -- exit the program with a return code in D0. When invoked from Prog8 via sys.exit(), all active defers in the call chain are unwound program-wide (LIFO) before system cleanup. sys.reset_system()/poweroff_system() do not run defers.
%asm {{
moveq.l #0,d0
move.b sys.exit.returnvalue,d0
and.l #$ffff,d0
movea.l #qemu.CTRL_REG_CMD,a1
move.l #qemu.CTRL_CMD_HALT,(a1)
stop #0
; !notreached!
}}
poweroff_system()
}
sub set_carry() {
%option noframe
%asm {{
; set both C (comparison carry) and X (rotate carry) bits
moveq #$11,d0
@@ -373,6 +379,7 @@ sys {
}
sub clear_carry() {
%option noframe
%asm {{
moveq #0,d0
move.w d0,ccr
@@ -387,12 +394,14 @@ sys {
}
sub clear_irqd() {
%option noframe
%asm {{
andi.w #$f8ff,sr ; privileged !
}}
}
sub set_irqd() {
%option noframe
%asm {{
ori.w #$0700,sr ; privileged !
}}
@@ -465,4 +474,21 @@ sys {
lea prog8_program_end,a0
}}
}
asmsub stack_size() -> long @D0 {
; -- Return the amount of stack space still available, in bytes.
%asm {{
move.l sp,d0
lea prog8_program_end,a0
sub.l a0,d0
rts
}}
}
asmsub cpuAtLeast68020() -> bool @Pz {
%asm {{
moveq #0,d0 ; qemu68k always targets a 68020 or newer
rts
}}
}
}
+5
View File
@@ -0,0 +1,5 @@
%import shared_m68k_wavfile
wavfile {
%option ignore_unused
}
Loaded 100 of 259 files, more files were not shown because too many files have changed in this diff. Show more