Compare commits

...
59 Commits
Author SHA1 Message Date
Irmen de Jong 86f9b6aa69 fixed cmp() sign/flags errors 2026-06-20 19:49:45 +02:00
Irmen de Jong 33f30d925e Update documentation with proper example links 2026-06-20 15:45:01 +02:00
Irmen de Jong db227679a8 fix empty/full boundary checks on various datastructions in the buffers module 2026-06-20 15:05:37 +02:00
Irmen de Jong 0f3475128a fix divmod_uw_asm to not clobber R15 anymore 2026-06-19 23:23:53 +02:00
Irmen de Jong d451953e40 improve prog8 skill 2026-06-19 02:18:41 +02:00
Irmen de Jong d505c42d15 improve undefined symbol error handling
(cherry picked from commit 8c92663824)
2026-06-19 02:16:11 +02:00
Irmen de Jong 5a593b9ef3 Preserve cx16.r15 register in % for words. Update related code and documentation accordingly. 2026-06-17 19:23:19 +02:00
Irmen de Jong b411914624 Document virtual register clobbering details and update related comments. 2026-06-17 03:00:33 +02:00
Irmen de Jong 0f80b695e7 Fix invalid optimizer edge cases 2026-06-17 01:18:39 +02:00
Irmen de Jong 276627abcb remove broken expression optimization that combined a=x, a*=y into a=x*y 2026-06-16 23:19:42 +02:00
Irmen de Jong 34225d10aa add missing division and modulus code generation support for LONG variables 2026-06-16 23:18:07 +02:00
Irmen de Jong 282c3aa00b fix a return split word array codegen issue where it didn't use lsb suffix 2026-06-16 00:39:51 +02:00
Irmen de Jong 19f3b03ac3 remove problematic inlining for subroutines having 1 or more parameters, to avoid an endless loop 2026-06-16 00:02:05 +02:00
Irmen de Jong 6cb6f37871 exclude private symbols from completion suggestions in the LSP 2026-06-13 12:15:11 +02:00
Irmen de Jong d0edc0b484 prevent TCO with defer blocks 2026-06-12 03:05:26 +02:00
Irmen de Jong e9a3ed457e fix optimizer problem that removed var inits for pointer vars in certain cases. Fixes #225 2026-06-11 22:37:51 +02:00
Irmen de Jong 6001da30d8 add comments to float library routines about the Z flag wrapper routines
(cherry picked from commit c55f5e4b50)
2026-06-11 21:50:25 +02:00
Irmen de Jong e983c0c2b9 fixed the c64 float heisenbug
(cherry picked from commit 532bf13057)
2026-06-11 21:40:42 +02:00
Irmen de Jong 131134d24b fix daemon output path 2026-06-09 02:06:07 +02:00
Irmen de Jong f2d1dffbe0 preparing for 12.2 release 2026-06-08 20:15:19 +02:00
Irmen de Jong 4008280a62 fix 64tass output order in daemon 2026-06-07 02:45:52 +02:00
Irmen de Jong 2e7235ec02 fix daemon mode error reporting paths 2026-06-07 02:31:06 +02:00
Irmen de Jong a4330f5048 fix -emu program name problem with -daemon 2026-06-07 00:34:35 +02:00
Irmen de Jong f65aeff231 -emu works again together with -daemon 2026-06-07 00:08:07 +02:00
Irmen de Jong 505243aff7 fix a optimizer crash 2026-06-06 23:29:16 +02:00
Irmen de Jong d0f2fc13ba add dummy rambank() and rombank() support for virtual target 2026-06-06 23:26:47 +02:00
Irmen de Jong 53d51dcd38 improve -libsearch by adding fuzzy search fallback 2026-06-06 19:28:53 +02:00
Irmen de Jong c1aa0e49fe fix a possible vera registers clobbering issue when using psg2 from irq handler 2026-06-06 17:30:08 +02:00
Irmen de Jong e1e72d4317 fix crash when using const ubyte pointer as string argument 2026-06-06 17:12:15 +02:00
Irmen de Jong feec343cbc made -daemon more robust 2026-06-06 12:41:15 +02:00
Irmen de Jong 5cdc4233f3 ideas & update prog8-coder skill 2026-06-04 01:16:41 +02:00
Irmen de Jong 1ae89d18cc prevent string deduplication for struct initializers 2026-06-03 01:43:50 +02:00
Irmen de Jong eb19d00a1e prevent faulty inlining of subs with register-aliased parameters 2026-06-02 23:14:55 +02:00
Irmen de Jong e01ee10ef0 fix output binary path displayed to the user 2026-06-02 21:05:03 +02:00
Irmen de Jong f4b3cb12e1 history 2026-06-01 23:19:31 +02:00
Irmen de Jong 77ced60ad6 fix psg2 release timing to make it linear just as attack.
clarify envelope timing details in docs.
2026-06-01 22:58:42 +02:00
Irmen de Jong 6fad90af6e long for loop warning 2026-05-31 23:42:58 +02:00
Irmen de Jong 3076318d7e implement missing for loops on long rages for 6502 2026-05-31 23:38:35 +02:00
Irmen de Jong 9c99110738 doc 2026-05-31 23:07:16 +02:00
Irmen de Jong 1457d36997 beta3 2026-05-31 17:47:37 +02:00
Irmen de Jong 43fb9296dd clarify serial module docs 2026-05-31 17:46:33 +02:00
Irmen de Jong 8bf744cc4a fix some bitwise expression optimization bugs 2026-05-31 07:35:32 +02:00
Irmen de Jong f1f5f070cf fix invalid asm for some in-place long operations on zp vars 2026-05-31 05:14:33 +02:00
Irmen de Jong 191a240d0d adjust extsub jump table offsets to more closely match reality 2026-05-30 23:42:52 +02:00
Irmen de Jong cafb2e355b implement long loop counter support 2026-05-30 23:36:08 +02:00
Irmen de Jong c99c5751f8 tweak serial 2026-05-30 21:11:19 +02:00
Irmen de Jong 435b7e56fa IR VM refactor to reduce class size 2026-05-29 23:14:44 +02:00
Irmen de Jong 14f13fac53 IR tweaks 2026-05-29 22:17:56 +02:00
Irmen de Jong f8b5037924 spelling corrections 2026-05-29 20:58:06 +02:00
Irmen de Jong d2870cb02e implement hex mode file transfer in serial module
as an attempt to have a working file transfer because the binary mode is still failing right now
2026-05-29 20:04:40 +02:00
Irmen de Jong 8e43fc426c IR: handle extsub with banked addresses properly 2026-05-28 03:25:04 +02:00
Irmen de Jong ba1b6e127a IR: properly consume registers for void targets 2026-05-28 00:57:00 +02:00
Irmen de Jong 4f36a6958c fix a few if-expression typecast issues 2026-05-28 00:55:15 +02:00
Irmen de Jong df8f08b4ab more serial tweaks to attempt to fix binary downloads 2026-05-27 23:42:55 +02:00
Irmen de Jong fd5746d3fc tweak the serial module a bit for more robustness 2026-05-27 02:22:35 +02:00
Irmen de Jong 6ab74fd51d don't erroneously allow taking the address of const values 2026-05-27 00:58:01 +02:00
Irmen de Jong 245082ca1d plain console output in github action builds 2026-05-24 12:40:43 +02:00
Irmen de Jong 845e58ea10 64tass install step 2026-05-24 12:33:47 +02:00
Irmen de Jong 2a5dbc1ef7 update project urls that have been moved from github to codeberg 2026-05-24 12:29:09 +02:00
146 changed files with 4914 additions and 2186 deletions

No files matched your search

+117 -7
View File
@@ -9,6 +9,9 @@ compatibility: opencode
You are writing **6502/65C02 assembly** using **64tass syntax**, in separate `*.asm` files or embedded in a Prog8 program (inside `%asm {{ }}` blocks or `asmsub` routines). Follow all rules below.
## Git Operations
- When moving, renaming, or deleting git-tracked files, **always use `git mv` or `git rm`** instead of plain `mv`/`rm`. This preserves history and properly stages the change. Plain `mv`/`rm` causes git to see them as delete+add (losing history).
## Debugging Generated Assembly
- The prog8 compiler outputs `*.asm` (assembly source) and (with -asmlist option) also `*.list` (full listing with address/symbols) when compiling. Inspect these to debug generated code, verify optimizations, and trace instruction sequences.
@@ -16,6 +19,7 @@ You are writing **6502/65C02 assembly** using **64tass syntax**, in separate `*.
- **NOT ca65/cc65** or other assemblers. Key differences:
- `.proc` / `.pend` for procedures (scoping)
- `_label` for local labels (prefixed with underscore, scoped to `.proc`)
- **Symbol aliases**: inside a `.proc`, use `_name = P8ZP_SCRATCH_B1` to give a scratch variable a descriptive name.
- **Anonymous labels**: defined as `+` (forward) or `-` (backward) at the start of a line. Reference them in branches using `+`, `++`, `+++` etc. (first/second/third *upcoming* anonymous forward label) and `-`, `--`, `---` etc. (first/second/third *preceding* anonymous backward label).
- Crucial: `+` refers to the NEXT upcoming `+` label, `++` refers to the ONE AFTER that, etc.
- `-` refers to the MOST RECENT `-` label, `--` to the one before that, etc.
@@ -33,23 +37,35 @@ You are writing **6502/65C02 assembly** using **64tass syntax**, in separate `*.
rts
++ inc $d020 ; third forward label
```
- Data directives: `.byte`, `.word`, `.dword`
- Data directives: `.byte`, `.word`, `.dword`, `.fill` (allocate space)
- Equates: `label = value` (not `label .equ value` or `#define`)
- **Number Literals**: Hex `$1234`, Binary `%10101010`, Decimal `123`.
- Zero-page variables defined with `=`
- `.text` for inline string data
- **Conditional assembly**: `.if`, `.elsif`, `.else`, `.endif`
- **Memory Sections**: `.section <name>`, `.send <name>` (common sections: `CODE`, `DATA`, `BSS`, `BSS_NOCLEAR`)
## Instructions
- **Instructions like `rol`, `ror`, `asl`, `lsr`, `php`, `pla` require an explicit operand for accumulator**: write `rol a`, `ror a`, not just `rol`/`ror`
- **Instructions like `rol`, `ror`, `asl`, `lsr` (and `inc`, `dec` on 65C02) require an explicit operand for accumulator**: write `rol a`, `inc a`, etc., not just `rol` or `inc`.
- Standard 6502 addressing modes: implied, immediate (`#`), zero-page (`zp`), zero-page,X (`zp,x`), absolute (`abs`), absolute,X (`abs,x`), absolute,Y (`abs,y`), indirect (`(abs)`), indirect,X (`(zp,x)`), indirect,Y (`(zp),y`), relative (branches), accumulator
- Branches: `bne`, `beq`, `bmi`, `bpl`, `bcs`, `bcc`, `bvs`, `bvc` (relative, max +127/-128 bytes)
- Jumps: `jmp` (absolute or indirect), `jsr`/`rts` (subroutine call/return)
- No `push`/`pop` mnemonics — use `pha`/`pla` (byte) and `txa`/`phx`/`plx`/`tay`/`phy`/`ply` for registers
### Instruction Side Effects (Flags)
- **`Z` (Zero)**: Set if the result of an operation is 0.
- **`N` (Negative)**: Set if bit 7 of the result is 1.
- **`C` (Carry)**: Used for unsigned overflow and shifts. `cmp` sets `C` if `Register >= Operand`.
- **`V` (Overflow)**: Set if a signed arithmetic operation overflowed.
- **Commonly affected by**: `lda`, `ldx`, `ldy`, `inx`, `dex`, `tax`, `tay`, `txa`, `tya`, `and`, `ora`, `eor`, `asl`, `lsr`, `rol`, `ror`, `adc`, `sbc`, `cmp`, `cpx`, `cpy`, `bit`.
- **Note**: `lda`, `ldx`, `ldy` do NOT affect the Carry flag. Only `adc`, `sbc`, `cmp`, and shift/rotate instructions affect Carry.
## 6502 vs 65C02
- **CX16 target only**: can use 65C02 instructions — `stz`, `phx`, `plx`, `phy`, `ply`, `bra`, `trb`, `tsb`, `stp`, `wai`, `clr`, `ina`, `dea`, `cmp (zp)`, `dec`/`inc abs,x` etc.
- **C64, C128, PET32 targets**: original 6502 only — no `stz`, no `phx`/`plx`/`phy`/`ply`, no `bra`
- **CX16 target only**: can use WDC 65C02 instructions — `stz`, `phx`, `plx`, `phy`, `ply`, `bra`, `trb`, `tsb`, `stp`, `wai`, `inc a`, `dec a`, `bit #imm`, `bit zp,x`, `bit abs,x`, `jmp (abs,x)`, and `(zp)` indirect addressing mode (e.g., `lda (zp)`).
- **C64, C128, PET32 targets**: original 6502 only — no `stz`, no `phx`/`plx`/`phy`/`ply`, no `bra` etc.
- **Note**: The Rockwell/bit-manipulation instructions (`rmb`, `smb`, `bbr`, `bbs`) are **NOT** available.
- Check the target before using 65C02-specific instructions
- **6502 / 65C02 instruction reference**: https://www.pagetable.com/c64ref/6502/?cpu=65c02&tab=2
- **6502 / 65C02 instruction reference table**: https://www.pagetable.com/c64ref/6502/?cpu=65c02&tab=4 (provides exact instruction details for all opcodes: operation, addressing modes, byte length, and cycle count).
## Calling Convention / Register Conventions
- **Accumulator (A)**: 8-bit, used for most arithmetic, data movement, return values
@@ -71,8 +87,9 @@ You are writing **6502/65C02 assembly** using **64tass syntax**, in separate `*.
- Parameters passed via registers: `@A`, `@X`, `@Y`, `@AX` (A low, X high), `@AY` (A low, Y high), `@R0`-`@R15`, `@FAC1`/`@FAC2` (float), `@Pc` (carry), `@Pz` (zero)
- Return value: `-> type @register` — also via `@Pz`/`@Pc` for flags
- Clobbers: `clobbers (A, X, Y)` — MUST list all modified registers
- **Parameter names are documentation only** — use the actual registers in assembly, NOT parameter names
- **CRITICAL**: Parameter names in `asmsub` are **documentation only**. You MUST use the actual registers in your assembly code, NOT the parameter names (unless you create aliases yourself).
- Create symbolic aliases at assembly top for clarity: `x1 = cx16.r0`, `y1 = cx16.r0L`
- Accessing Prog8 parameters if they were NOT mapped to registers: use `p8v_paramname`. (Mapping to registers is preferred for speed).
Example:
```prog8
@@ -130,12 +147,31 @@ All Prog8 symbols are prefixed when accessed from assembly:
## Common 6502 Patterns
### Common Branch Logic (Comparisons)
| Logic | Unsigned | Signed |
|-------|----------|--------|
| `A == imm` | `cmp #imm`, `beq label` | (Same) |
| `A != imm` | `cmp #imm`, `bne label` | (Same) |
| `A < imm` | `cmp #imm`, `bcc label` | `sec`, `sbc #imm`, `bvc *+4`, `eor #$80`, `bmi label` |
| `A >= imm` | `cmp #imm`, `bcs label` | `sec`, `sbc #imm`, `bvc *+4`, `eor #$80`, `bpl label` |
| `A <= imm` | `beq label`, `bcc label` | (Use complex signed logic or reorder) |
| `A > imm` | `beq +`, `bcs label`, `+` | (Use complex signed logic or reorder) |
### Looping (downto with BNE)
```asm
ldx #count
loop ; do work here
dex
bne loop ; loop while X != 0
bne loop ; loop while X != 0 (runs 'count' times)
```
### Self-Modifying Code (SMC) Detection
Look for `sta`, `stx`, or `sty` pointing into code labels:
```asm
lda #$42
sta _target+1 ; Modifies the immediate operand of the LDA at _target
...
_target lda #$00 ; This #$00 will be replaced by #$42 at runtime
```
### Indirect indexed read (table of data)
@@ -165,3 +201,77 @@ loop ; do work here
lda p8v_myword ; loads LSB
ldy p8v_myword+1 ; loads MSB (word variables are stored LSB-first)
```
## CPU Quirks and Pitfalls
### JMP ($xxFF) Page Wrap Bug (NMOS 6502)
- **Problem**: On original 6502 CPUs, `jmp ($caff)` will fetch the LSB from `$caff` but the MSB from `$ca00` (instead of `$cb00`).
- **Target**: Affects C64, C128, PET32.
- **Solution**: Avoid placing indirect jump vectors on a page boundary, or use the CX16 (65C02) which fixed this bug.
### BRK Instruction and the "Signature Byte"
- **Behavior**: After a `BRK` instruction, the return address on the stack is incremented by **2**. This means the CPU skips the byte immediately following the `BRK` opcode.
- **Usage**: This skipped byte is often used as a "signature" or parameter byte for the BRK handler.
### BIT Instruction Flags
- **Absolute/Zero-page**: `bit $1234` copies bit 7 of the memory value to the **N** flag and bit 6 to the **V** flag. The **Z** flag is set based on `A AND memory`.
- **Immediate (65C02 only)**: `bit #$01` only affects the **Z** flag; it does **NOT** modify N or V.
- **NMOS 6502**: Does **not** support `bit #imm`.
### The "B" (Break) Flag
- **Quirk**: The B flag (bit 4 of the status register) doesn't actually exist in the hardware status register. It only exists on the stack after a `PHP` or `BRK` instruction (set to 1) or a hardware IRQ/NMI (set to 0).
- **Detection**: To tell if an interrupt was caused by `BRK` or a hardware IRQ, your handler must `pla`, `and #$10`, and check the result.
### Decimal Mode Flag (D) Persistence
- **Pitfall**: On NMOS 6502, the `D` flag is **not** cleared on interrupt. Always use `cld` in IRQ handlers. On 65C02, it is cleared automatically, but `cld` is still good practice.
- **ADC/SBC**: Be extremely careful with arithmetic if you haven't explicitly set or cleared the `D` flag, as its state might be unknown.
## Optimization Tips
- **`stz` (65C02 only)**: Saves cycles and bytes compared to `lda #0`, `sta ...`.
- **`bra` (65C02 only)**: Shorter and usually faster than `jmp`.
- **Avoid `clc` before `bcc`**: `cmp` already sets the carry flag correctly for `bcc`/`bcs`.
- **`inx` / `dex` vs `clc`+`adc #1`**: Incrementing/decrementing is faster and doesn't affect the carry flag.
- **Zero-page usage**: Accessing variables in zeropage is 1 cycle faster and 1 byte shorter than absolute addressing.
- **Cycle Counting**: Most instructions take 2-4 cycles. `jsr` takes 6, `rts` takes 6. Branches take 2 (no branch), 3 (branch taken), or 4 (branch taken across page boundary).
## 64tass Macros
Define macros for common tasks:
```asm
pushax .macro
pha
txa
pha
.endm
popax .macro
pla
tax
pla
.endm
```
Call them with `#pushax` and `#popax`.
## IRQ Handler Best Practices
- **Clear the Decimal Flag (`cld`)**: On the original 6502, the decimal flag (`D`) is **not** automatically cleared when an interrupt occurs. If the interrupted code was in decimal mode, your handler will also run in decimal mode, causing arithmetic errors. Always call `cld` at the beginning of your handler. The 65C02 clears it automatically, but `cld` is still recommended for portability.
- Keep handlers extremely short and fast — they run with interrupts disabled and steal cycles from the main program.
- Do NOT do lengthy processing, I/O, or complex subroutine calls inside the handler.
- Instead, set a boolean flag or semaphore that the main loop checks periodically, and do the actual work there.
## Invoking the Assembler (64tass)
If you need to manually invoke `64tass` to assemble a generated `.asm` file, you should be aware of the default arguments that `prog8c` supplies to ensure compatibility with the generated code:
- `--ascii`: **CRITICAL.** Prog8 generates character and string data in ASCII. Without this flag, `64tass` defaults to PETSCII, which will garble your strings.
- `--case-sensitive`: Prog8 is case-sensitive and expects the assembler to be as well.
- `--long-branch`: Enables automatic conversion of relative branches (`beq`, `bne`, etc.) to absolute jumps if the target is out of range. Prog8 relies on this.
- `-Wno-implied-reg`: Suppresses warnings when the accumulator `a` is omitted from instructions like `rol`, `lsr`, etc. (though the skill recommends always using `rol a` for clarity).
- `-Wall`: Enables all warnings.
- `--cbm-prg` (or `--atari-xex` / `--nostart`): Sets the output format and adds the appropriate load address header.
### Optional but Recommended for Debugging:
- `--vice-labels --labels=labels.txt`: Generates a label file that can be loaded into the VICE monitor (`load_labels "labels.txt"`) to see your Prog8 symbol names while debugging.
- `--list=listing.txt`: Generates a full assembly listing file with addresses and opcodes.
### Example manual invocation:
```bash
64tass --ascii --case-sensitive --long-branch -Wall --cbm-prg -o myprogram.prg myprogram.asm
```
+42 -16
View File
@@ -12,6 +12,7 @@ You are working with **Prog8** source code (`.p8` files) or its Intermediate Rep
Follow ALL the rules below carefully.
## General & Setup
- **Git operations**: When moving, renaming, or deleting git-tracked files, **always use `git mv` or `git rm`** instead of plain `mv`/`rm`. This preserves history and properly stages the change. Plain `mv`/`rm` causes git to see them as delete+add (losing history).
- A program = a `main` block containing a `start` subroutine entry point, plus optional other subroutines/blocks
- Add `%zeropage basicsafe` at the top of your program to allow clean return on exit (instead of resetting the machine/emulator)
- Module imports: `%import modulename` — no `as` aliasing. Use the module's defined prefix (e.g., `%import textio` → `txt.xxx`)
@@ -20,9 +21,17 @@ Follow ALL the rules below carefully.
- **Output directory**: `prog8c -out outdir input.p8` (default: same dir as source)
- **Compilation outputs**: `*.prg` (program binary), `*.asm` (assembly listing), `*.list` (full listing), `*.p8ir` (IR for VM), `*.vice-mon-list` (Vice debug symbols)
- `.p8ir` files contain the **Intermediate Representation** — a sequence of named chunks, each containing typed instructions and virtual registers. This is a target-independent representation of the program, executable by the built-in Virtual Machine via `prog8c -vm file.p8ir`. Useful for debugging the compiler's code generation path without involving 6502 assembly.
- **Debugging switches**: `-noopt` (disable optimizations), `-printast1` (parsed AST), `-printast2` (optimized simple AST), `-compareir` (compare IR outputs)
- **TODO list**: Check `docs/source/todo.rst` to see what features are NOT yet implemented
- **Debugging switches**: `-noopt` (disable optimizations), `-printast1` (parsed AST), `-printast2` (optimized simple AST), `-compareir` (compare IR outputs), `-dumpsymbols` (print all symbols), `-dumpvars` (print all variables)
- **Library search (preferred)**: `prog8c -libsearch <regex>` — search for a regex pattern in the embedded library files. Extremely useful to quickly find library routines, variables, strings, or signatures (e.g., `prog8c -libsearch "txt\."` lists all textio routines; `prog8c -libsearch "sin"` finds math functions)
- **Library dump**: `prog8c -libdump <dir>` — extract all embedded library source files into a directory for direct inspection (less common, use `-libsearch` first)
- **Other useful flags**: `-quiet` (suppress messages), `-warnimplicitcasts` (warn on implicit type widening), `-daemon` (keep a background compiler process alive to speed up multiple successive compilations — must be passed on every `prog8c` invocation)
- **Test programs**: add `%zeropage basicsafe` and `%option no_sysinit` at top
- **Compiler unit test snippets**: These are **prog8 code snippets embedded in the Prog8 compiler's own Kotlin unit tests** (e.g., in `TestAstChecks.kt`, `TestOptimization.kt`, etc.). They are NOT standalone Prog8 programs:
- Keep them self-contained — avoid `%import` directives so they don't depend on library files (the test setup may not have the library search path configured)
- Use the `cx16` target (not `virtual`, unless specifically testing the VM)
- Consider setting `writeAssembly=false` if the test only needs to check the generated AST (much faster)
- Use `optimize=false` by default for these snippets
- Do NOT use the `%encoding iso` / `txt.iso()` / `sys.poweroff_system()` pattern in these snippets — that's only for real CX16 emulator runs
- **`sys` module**: always available, no import needed
- **CX16 debugging**: Add `%encoding iso`, call `txt.iso()` in `start()`, end with `sys.poweroff_system()`. For emulator: `x16emu -echo iso -run -prg input.prg 2>&1 | grep ...`
@@ -32,17 +41,31 @@ Follow ALL the rules below carefully.
- **float requires `%import floats`** at top of file, else compiler errors
- Arrays: max 256 bytes (512 for split word arrays). For larger data, use `memory()` + pointers
- `memory(name, size)` returns a `uword` address to a statically reserved block of memory
- Struct initialization: `^^StructType ptr = ^^StructType:[val1,val2,...]` (the `^^StructType:` can be omitted if inferable)
- To point a typed pointer at a `memory()` block, assign the uword directly: `^^MyStruct ptr = memory("name", size)`. The `^^Type:expression` syntax (below) only works with array literals, not general uword expressions.
- Struct initialization: `^^StructType ptr = ^^StructType:[val1,val2,...]` (the `^^StructType:` can be omitted if inferable). This `^^Type:[...]` syntax does NOT work with variables or `memory()` — only literal arrays.
- Struct definitions must be inside a block, not at file level.
- Struct fields: only simple types + `str` allowed. NO arrays as fields. `str` in struct = `^^ubyte`
- Word/pointer arrays split into LSB/MSB by default. Override with `@nosplit`
- **No call stack**: all variables statically allocated. No recursion without manual stack management
- Variables zero-initialized (globals at start, locals on subroutine entry)
- `@shared` marks variables as "used by external code" (assembly), prevents optimization
- `@shared`, `@zp`, `@requirezp`, `@dirty`, `@nosplit` are **tags** that go on variable declarations. **Place them after the datatype (and array specifiers), before the variable name(s):**
```
private ubyte[8] @shared vera_storage ; single var with tag
ubyte @zp @shared varname ; multiple tags allowed
ubyte @requirezp var1 ; require zeropage
ubyte[] @shared names ; array with tag
```
The grammar is: `[private] datatype [arraydims] [tags...] identifierlist`
- `@shared` marks variables as "used by external code" (assembly), prevents the optimizer from removing them
- `@zp`/`@requirezp`: use sparingly — only for pointers (limited zeropage space)
- Pointer-like typed pointers (`^^type`) support C-style scaled arithmetic; `uword` pointers always treat element as 1 byte
- `&` = untyped address (uword); `&&` = typed pointer
- Available zeropage scratch: `P8ZP_SCRATCH_B1`, `P8ZP_SCRATCH_REG`, `P8ZP_SCRATCH_W1`, `P8ZP_SCRATCH_W2`, `P8ZP_SCRATCH_PTR` — and cx16 virtual registers R0-R15 on all targets
- Virtual registers (`cx16.r0`–`cx16.r15`): global 16-bit, NOT preserved across calls. R12-R15 may be clobbered by long ops. Save/restore in IRQs with `cx16.save_virtual_registers()`/`cx16.restore_virtual_registers()`
- Virtual registers (`cx16.r0`–`cx16.r15`): global 16-bit, NOT preserved across calls.
- **WARNING: Virtual registers in ISR/IRQ handlers**: The virtual registers R0-R15 are *not preserved* across the IRQ handler call. If your handler uses them, it will corrupt the interrupted program's state. Either avoid using them in the handler, or save/restore with `cx16.save_virtual_registers()` / `cx16.restore_virtual_registers()`. This applies to all targets, not just CX16.
- **WARNING: Long operations clobber R12-R15**: Some operations on `long` values use R12-R15 as temporary storage and will silently overwrite them. Do not rely on R12-R15 values when working with longs, and avoid using R12-R15 explicitly if your code uses long arithmetic.
- **WARNING: VERA registers in ISR handlers (CX16)**: If your IRQ handler reads or writes VERA control registers (e.g., `cx16.VERA_DATA0`, `cx16.VERA_ADDR_L`, etc.), you must save and restore the VERA context around the handler's work using `cx16.save_vera_context()` / `cx16.restore_vera_context()`. Without this, the handler will corrupt any VERA operations (tilemap updates, sprite positioning, etc.) happening in the interrupted main program.
- **IRQ handler best practices**: Keep handlers extremely short and fast — they run with interrupts disabled and steal cycles from the main program. Do NOT do lengthy processing, I/O, or complex subroutine calls inside the handler. Instead, set a boolean flag or semaphore that the main loop checks periodically, and do the actual work there.
- Math performance: integer trig (`math.sin8`, `math.cos8`) uses fast LUTs; float trig (`floats.sin`/`cos`) is much slower
## Recursion & Stack Management
@@ -62,6 +85,7 @@ No call stack for variable storage — recursion overwrites locals. To handle it
- String concat is expensive on 6502. Prefer separate prints over concatenation
- **Efficient buffer iteration**: prefer `ptr++` + `@(ptr)` over `@(buffer + offset)`. Exception: if offset is a `ubyte` (≤ 255), `buffer[offset]` works fine
- `@(ptr)` = peek/poke byte. For words: `peekw`/`pokew`, longs: `peekl`/`pokel`, floats: `peekf`/`pokef`, bools: `peekbool`/`pokebool`
- Assigning a uword address to a `str` field of a struct is done by direct assignment. Compute the address in a uword variable first, then assign: `cx16.r0 = &namebufs + offset; entry.name = cx16.r0`. The `^^ubyte:(expr)` cast syntax does not parse — use a temp uword instead.
- Use `len(array)` instead of hardcoded sizes
- Array indexing is 0-based: `arr[0]` is first element
- Static memory only — real dynamic allocation impossible, but can emulate with a simple arena allocator over a `memory()` slab
@@ -74,17 +98,24 @@ No call stack for variable storage — recursion overwrites locals. To handle it
- Bitwise operators: `&`, `|`, `^`, `~`, `<<`, `>>`
- Bit rotation: `rol()`/`ror()` (through carry), `rol2()`/`ror2()` (no carry)
- CPU status flag branches: `if_cs`, `if_cc`, `if_z`, `if_nz` (compile to single 6502 branch instructions)
- Use `when` with choice blocks instead of multiple `if`
- If-expressions for simple value assignments based on a choice
- **Optional braces in if/else**: when the `if` or `else` body is a single statement, the `{ }` can be omitted. Place the statement on the next line, indented. Example:
```
if x < 5
txt.print("small")
```
- `defer` defers statement execution until scope exit
- `defer` defers statement execution until scope exit. Multiple defers fire in **reverse registration order** (LIFO / stack order — last deferred runs first). A defer is only registered if execution reaches that statement — conditional paths that skip the `defer` line will not register it.
- `goto`, labels, jump lists allowed
- **Common mistake**: `and`/`or` for bitmasking — use `&`/`|` instead!
### The when Statement
The `when` statement is a control flow construct that enables you to execute a specific action based on the value of an expression. It is generally more readable and often more efficient than a sequence of `if-else if` statements, as the compiler can optimize it into more efficient branching structures, such as a jump table.
- **Expression**: Evaluates an expression and compares it against case values.
- **Cases**: Defined by a value followed by the `->` operator.
- **Blocks**: Use `{ }` to enclose multiple statements for a case.
- **Else Clause**: Serves as a default handler; mandatory unless the expression type is fully covered.
- **Efficiency**: Recommended for handling sets of fixed choices as it typically results in better assembly code.
## Loop Constructs
Prog8 supports these loop types. All support `break` and `continue` (except `unroll`).
@@ -162,9 +193,9 @@ unroll 80 {
```
## Subroutines & Return Values
- Symbols public by default; use `private` to restrict to block
- **Don't use `private` on subroutines and variables** (including nested ones) unless the user asks for it. Everything is public by default in Prog8 — follow that convention.
- `inline` keyword for subroutines to suggest inlining
- No function overloading (except builtins). Cannot use builtin names (msw, lsw, msb, lsb, mkword, mklong, peek, peekw, peekl, etc.) as variable/sub names
- No **function overloading** (except builtins) and no **polymorphism** in general. This means you must call specific routines for different types (e.g., `txt.print_ub(val)` vs `txt.print_w(val)` instead of a generic `print(val)`). Cannot use builtin names (msw, lsw, msb, lsb, mkword, mklong, peek, peekw, peekl, etc.) as variable/sub names
- Can return 0, 1, or multiple values: `a, b, c = routine()`. Use `void` to skip: `void routine()`, `a, void, c = routine()`
- Nested subroutines access parent scope variables directly
@@ -183,7 +214,6 @@ For low-level assembly that gets arguments via registers and returns values in r
- **Clobbers**: `clobbers (A, X, Y)` — list all hardware registers modified by the routine.
- **Parameter names** are for documentation and type checking only. Use the registers in your assembly code.
- **Inlining**: `inline asmsub` will paste the assembly code directly at the call site, avoiding `jsr`/`rts` overhead.
- **Symbol prefixes** in assembly: `p8v_` (vars), `p8s_` (subs), `p8b_` (blocks), etc.
## External Subroutines (extsub)
Used to call routines at fixed memory addresses (like ROM KERNAL routines or third-party drivers).
@@ -225,7 +255,7 @@ Used to call routines at fixed memory addresses (like ROM KERNAL routines or thi
- Find routines, functions, variables, modules and signatures in the symbol dump file for the given compilation target.
- Online location: https://prog8.readthedocs.io/en/latest/libraries.html#low-fi-variable-and-subroutine-definitions-in-all-available-library-modules they are linked there 1 for each compilation target
- Structure: builtin functions, then module sections with variables/constants (`type name`) and subroutines (`name (params) -> returntype`)
- Text output: `textio` module (`txt.print`, `txt.chrout`, `txt.print_b`/`_ub`/`_w`/`_uw`/`_l`/`_bool`, `txt.print_f` for floats, `txt.spc()`, `txt.nl()`)
- Text output: `textio` module (`txt.print`, `txt.chrout`, `txt.print_b`/`_ub`/`_w`/`_uw`/`_l`/`_bool`, `txt.print_f` for floats, `txt.spc()`, `txt.nl()`). **Note: Prog8 has no function overloading**, so you cannot use `txt.print(number)` — you must call the specific routine for the type (e.g., `txt.print_ub(val)` for an unsigned byte).
- Math: `math` module — integer trig (`sin8`, `cos8`) via fast LUTs; `math.rnd()` for random numbers
- String conversion: `conv` module (`str_uword`, `str2word`, etc.) — for printing numbers use txt routines instead
- Char operations: `strings` module (`isdigit`, `isxdigit`, `isupper`, `islower`, `isletter`, `isspace`, `isprint`) — use these instead of manual ASCII/PETSCII arithmetic
@@ -239,15 +269,11 @@ Used to call routines at fixed memory addresses (like ROM KERNAL routines or thi
- **No `elif`**: use nested `else { if ... }`
- Type casting: `expression as type` (e.g., `bytevar as word`). `as` has very low precedence (lower than arithmetic)
- **No automatic type widening**: `byte*byte=byte` (overflow possible!). Cast explicitly
- **No block scope**: `for`/`if` don't introduce scope. Only subroutines do. Variables in blocks are hoisted to subroutine level
- **No bare `{ }` blocks** like C/Java
- Indentation: 4 spaces for .p8 and .asm files (no tabs)
- Character encoding: 6502 targets use PETSCII by default (call `txt.lowercase()` at start for lowercase). Virtual target uses ISO (`%encoding iso` + `txt.iso()`)
- Array size inferred from initializer: `str[] types = ["a", "b", "c"]`
- Enums: `Enum::Value` syntax (double colon), declared inside a block. Use enums for related values, `const` for standalone
- Enums: `Enum::Value` syntax (double colon), declared inside a block. They are syntactic sugar for a list of `const` declarations, not a type. Use enums for related values, `const` for standalone
- Avoid `globals.XXXX` — move constants closer to where they're used
- Member access through pointers: use `.` for both direct and pointer access. The compiler infers the type. For complex assignment targets, `^^` may be needed: `ptr^^.field = value`
- Qualified names: must use full path from top level (e.g., `cx16.r0`, not relative)
- **No block scope** — variables in `if`/`for`/`repeat` blocks are hoisted to subroutine level
- **No reassigning strings** after declaration — use `strings.copy()`/`strings.ncopy()`
- **Pre-allocate string buffers** — `""` allocates nothing
+8 -7
View File
@@ -12,13 +12,11 @@ jobs:
- name: Checkout code
uses: actions/checkout@v5
- name: build and install recent 64tass
- name: install 64tass
run: |
sudo apt-get install -y make build-essential
git clone --depth=1 https://github.com/irmen/64tass
cd 64tass
make -j4
sudo make install
sudo apt update
sudo apt install 64tass
64tass --version
- name: Set up JDK 17
uses: actions/setup-java@v5
@@ -26,9 +24,12 @@ jobs:
java-version: 17
distribution: temurin
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
- name: Build and test with Gradle
run: |
./gradlew build shadowJar --no-daemon
./gradlew build shadowJar --no-daemon --console=plain
sha256sum -b compiler/build/libs/*-all.jar > compiler/build/libs/hash.txt
- name: Create compiler shadowJar artifact
+1 -1
View File
@@ -5,6 +5,7 @@
.idea/shelf/
.junie/
.junie/memory/
.serena
build/
dist/
output/
@@ -38,7 +39,6 @@ parsetab.py
.pytest_cache/
.attach_pid*
compiler/lib/
.gradle
sd*.img
*.d64
Generated
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>
+2 -2
View File
@@ -12,9 +12,9 @@ endif
all:
$(GRADLE) installdist installshadowdist
@echo "compiler launch script can be found here: compiler/build/install/compiler-shadow/bin/prog8c"
@echo "compiler launch script can be found here: compiler/build/install/prog8c-shadow/bin/prog8c"
test:
$(GRADLE) build
@echo "compiler launch script can be found here: compiler/build/install/compiler-shadow/bin/prog8c"
@echo "compiler launch script can be found here: compiler/build/install/prog8c-shadow/bin/prog8c"
+4 -1
View File
@@ -15,10 +15,12 @@ which aims to provide many conveniences over raw assembly code (even when using
**Want to buy me a coffee or a pizza perhaps?**
This project was created over the last couple of years by dedicating thousands of hours of my free time to it, to make it the best I possibly can.
This project was created over the last couple of years by dedicating a huge amoung of my free time to it, to make it the best I possibly can.
If you like Prog8, and think it's worth a nice cup of hot coffee or a delicious pizza,
you can help me out a little bit over at [ko-fi.com/irmen](https://ko-fi.com/irmen) or [PayPal](https://paypal.me/irmendejong)
[![JetBrains logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSource)
I am using JetBrains products to develop the Prog8 compiler.
Documentation
-------------
@@ -63,6 +65,7 @@ What does Prog8 provide?
- programs run very fast because it's compiled to native machine code
- compiled code is very small; much smaller than equivalent C code compiled with CC65, and usually runs faster as well
- modularity, symbol scoping, subroutines. No need for forward declarations.
- Can make vars and routines private, as a simple way to restrict access and hide implementation details
- various data types other than just bytes (16-bit words, long integers, floats, strings)
- Structs and typed pointers
- 2D arrays (`matrix[row][col]` syntax)
@@ -73,6 +73,9 @@ val BaseDataType.isWordOrByteOrBool get() = this in setOf(BaseDataType.UBYTE, Ba
val BaseDataType.isNumeric get() = this == BaseDataType.FLOAT || this.isInteger
val BaseDataType.isNumericOrBool get() = this == BaseDataType.BOOL || this.isNumeric
val BaseDataType.isSigned get() = this in setOf(BaseDataType.BYTE, BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
val BaseDataType.isUnsigned get() = !this.isSigned
val BaseDataType.isSignedInteger get() = this.isSigned && this.isInteger
val BaseDataType.isUnsignedInteger get() = this.isUnsigned && this.isInteger
val BaseDataType.isArray get() = this == BaseDataType.ARRAY || this == BaseDataType.ARRAY_SPLITW || this == BaseDataType.ARRAY_POINTER
val BaseDataType.isPointer get() = this == BaseDataType.POINTER
val BaseDataType.isStructInstance get() = this == BaseDataType.STRUCT_INSTANCE
@@ -13,9 +13,10 @@ import kotlin.io.path.Path
object ImportFileSystem {
val userHome: Path get() = Path.of(System.getProperty("user.home"))
fun expandTilde(path: String): String = if (path.startsWith("~")) {
val userHome = System.getProperty("user.home")
userHome + path.drop(1)
userHome.toString() + path.drop(1)
} else {
path
}
@@ -8,7 +8,7 @@ import java.io.CharConversionException
object PetsciiEncoding {
// decoding: from Petscii/Screencodes (0-255) to Unicode
// character tables used from https://github.com/irmen/cbmcodecs2
// character tables used from https://codeberg.org/irmen/cbmcodecs2.git
private val decodingPetsciiLowercase = charArrayOf(
'\u0000', // 0x00 -> \u0000
@@ -1024,7 +1024,7 @@ class AsmGen6502Internal (
assignExpressionToRegister(value, target.register!!)
} else {
assignExpressionToRegister(value, RegisterOrPair.AX)
pointerGen.assignWordReg(PtrTarget(target), RegisterOrPair.AX)
assignmentAsmGen.assignRegisterpairWord(target, RegisterOrPair.AX)
}
}
target.datatype.isWord || target.datatype.isPassByRef -> {
@@ -1175,6 +1175,7 @@ class AsmGen6502Internal (
when {
iterations == 0 -> {}
iterations == 1 -> translate(stmt.statements)
iterations > 65536 -> repeatLongCount(iterations.toLong(), stmt)
iterations !in 0..65536 -> throw AssemblyError("invalid number of iterations")
iterations <= 256 -> repeatByteCount(iterations, stmt)
else -> repeatWordCount(iterations, stmt)
@@ -1186,26 +1187,36 @@ class AsmGen6502Internal (
val name = asmVariableName(stmt.count as PtIdentifier)
when {
vardecl.type.isByte -> {
assignVariableToRegister(name, RegisterOrPair.Y, stmt.definingISub(), stmt.count.position)
assignVariableToRegister(name, RegisterOrPair.Y, stmt.definingISub(), stmt.count.position, vardecl.type.isSigned)
repeatCountInY(stmt, endLabel)
}
vardecl.type.isWord -> {
assignVariableToRegister(name, RegisterOrPair.AY, stmt.definingISub(), stmt.count.position)
assignVariableToRegister(name, RegisterOrPair.AY, stmt.definingISub(), stmt.count.position, vardecl.type.isSigned)
repeatWordCountInAY(endLabel, stmt)
}
vardecl.type.isLong -> {
val reg = CombinedLongRegisters.last()
assignVariableToRegister(name, reg, stmt.definingISub(), (stmt.count as PtIdentifier).position, vardecl.type.isSigned)
repeatLongCountInReg(reg, endLabel, stmt)
}
else -> throw AssemblyError("invalid loop variable datatype ${vardecl.type}")
}
}
else -> {
when {
stmt.count.type.isByte -> {
assignExpressionToRegister(stmt.count, RegisterOrPair.Y)
assignExpressionToRegister(stmt.count, RegisterOrPair.Y, stmt.count.type.isSigned)
repeatCountInY(stmt, endLabel)
}
stmt.count.type.isWord -> {
assignExpressionToRegister(stmt.count, RegisterOrPair.AY)
assignExpressionToRegister(stmt.count, RegisterOrPair.AY, stmt.count.type.isSigned)
repeatWordCountInAY(endLabel, stmt)
}
stmt.count.type.isLong -> {
val reg = CombinedLongRegisters.last()
assignExpressionToRegister(stmt.count, reg, stmt.count.type.isSigned)
repeatLongCountInReg(reg, endLabel, stmt)
}
else -> throw AssemblyError("invalid loop expression datatype ${stmt.count.type}")
}
}
@@ -1213,6 +1224,75 @@ class AsmGen6502Internal (
loopEndLabels.removeLast()
}
private fun repeatLongCount(iterations: Long, stmt: PtRepeatLoop) {
require(iterations > 65536) { "invalid repeat count ${stmt.position}" }
val repeatLabel = makeLabel("repeat")
val counterVar = createTempVarReused(BaseDataType.LONG, true, stmt)
val bytes = mutableListOf<Int>()
var currentN = iterations
while (currentN > 0) {
val b = (currentN % 256).toInt()
bytes.add(b)
currentN = if (b == 0) currentN / 256 else currentN / 256 + 1
if (currentN == 1L) break
}
for (i in bytes.indices) {
val v = bytes[i]
val suffix = if (i > 0) "+$i" else ""
if (v == 0 && isTargetCpu(CpuType.CPU65C02)) {
out(" stz $counterVar$suffix")
} else {
out(" lda #$v")
out(" sta $counterVar$suffix")
}
}
out(repeatLabel)
translate(stmt.statements)
for (i in bytes.indices) {
val suffix = if (i > 0) "+$i" else ""
out(" dec $counterVar$suffix")
out(" bne $repeatLabel")
}
}
private fun repeatLongCountInReg(reg: RegisterOrPair, endLabel: String, stmt: PtRepeatLoop) {
require(reg in CombinedLongRegisters)
val repeatLabel = makeLabel("repeat")
val regName = "cx16." + reg.startregname()
val d1 = makeLabel("skip_dec1")
val d2 = makeLabel("skip_dec2")
val d3 = makeLabel("skip_dec3")
out("""
lda $regName
ora $regName+1
ora $regName+2
ora $regName+3
beq $endLabel
$repeatLabel""")
translate(stmt.statements)
out("""
lda $regName
bne $d3
lda $regName+1
bne $d2
lda $regName+2
bne $d1
dec $regName+3
$d1 dec $regName+2
$d2 dec $regName+1
$d3 dec $regName
lda $regName
ora $regName+1
ora $regName+2
ora $regName+3
bne $repeatLabel""")
out(endLabel)
}
private fun repeatWordCount(iterations: Int, stmt: PtRepeatLoop) {
require(iterations in 257..65536) { "invalid repeat count ${stmt.position}" }
@@ -136,9 +136,27 @@ internal class AssemblyProgram(
}
val proc = ProcessBuilder(assemblerCommand)
if(!options.quiet)
proc.inheritIO()
val process = proc.start()
.redirectErrorStream(true)
if (options.quiet) {
proc.redirectOutput(ProcessBuilder.Redirect.DISCARD)
}
val process = try {
proc.start()
} catch (e: Exception) {
System.err.println("daemon: assembler failed to start: ${e.message}")
return false
}
if (!options.quiet) {
process.inputStream.bufferedReader().use { reader ->
reader.forEachLine {
println(it)
}
}
}
val result = process.waitFor()
if (result == 0) {
removeGeneratedLabelsFromMonlist()
@@ -275,8 +275,14 @@ import prog8.codegen.cpu6502.assignment.*
asmgen.assignWordOperandsToAYAndVar(fcall.args[1], fcall.args[0], "P8ZP_SCRATCH_W1")
// math.divmod_uw_asm: -- divide two unsigned words (16 bit each) into 16 bit results
// input: P8ZP_SCRATCH_W1 in ZP: 16-bit number, A/Y: 16 bit divisor
// output: cx16.r15: 16-bit remainder, A/Y: 16 bit division result
// output: P8ZP_SCRATCH_W2: 16-bit remainder, A/Y: 16 bit division result
asmgen.out(" jsr prog8_math.divmod_uw_asm")
// copy remainder from P8ZP_SCRATCH_W2 to R15 (needed as second return value)
asmgen.out("""
ldx P8ZP_SCRATCH_W2
stx cx16.r15L
ldx P8ZP_SCRATCH_W2+1
stx cx16.r15H""".trimIndent())
return arrayOf(RegisterOrPair.AY, RegisterOrPair.R15)
}
@@ -494,83 +500,115 @@ import prog8.codegen.cpu6502.assignment.*
val arg2 = fcall.args[1]
if(arg1.type.isByte) {
if(arg2.type.isByte) {
val op = if (arg1.type.isSigned || arg2.type.isSigned) "sec | sbc" else "cmp"
when (arg2) {
is PtIdentifier -> {
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.A)
asmgen.out(" cmp ${asmgen.asmVariableName(arg2)}")
asmgen.out(" $op ${asmgen.asmVariableName(arg2)}")
}
is PtNumber -> {
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.A)
asmgen.out(" cmp #${arg2.number.toInt()}")
asmgen.out(" $op #${arg2.number.toInt()}")
}
is PtBool -> {
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.A)
asmgen.out(" cmp #${arg2.asInt()}")
asmgen.out(" $op #${arg2.asInt()}")
}
is PtMemoryByte -> {
if(arg2.address is PtNumber) {
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.A)
asmgen.out(" cmp ${arg2.address.asConstInteger()!!.toHex()}")
asmgen.out(" $op ${arg2.address.asConstInteger()!!.toHex()}")
} else {
asmgen.assignByteOperandsToAAndVar(arg1, arg2, "P8ZP_SCRATCH_B1")
asmgen.out(" cmp P8ZP_SCRATCH_B1")
asmgen.out(" $op P8ZP_SCRATCH_B1")
}
}
else -> {
asmgen.assignByteOperandsToAAndVar(arg1, arg2, "P8ZP_SCRATCH_B1")
asmgen.out(" cmp P8ZP_SCRATCH_B1")
asmgen.out(" $op P8ZP_SCRATCH_B1")
}
}
} else
throw AssemblyError("args for cmp() should have same dt")
} else if(arg1.type.isWord) {
if(arg2.type.isWord) {
if(arg1.type.isSigned) {
if(arg1.type.isSigned || arg2.type.isSigned) {
when (arg2) {
is PtIdentifier -> {
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.AY)
val var2 = asmgen.asmVariableName(arg2)
asmgen.out("""
sec
sbc ${asmgen.asmVariableName(arg2)}
sbc $var2
sta P8ZP_SCRATCH_B1
tya
sbc ${asmgen.asmVariableName(arg2)}+1""")
sbc $var2+1
php
bne +
lda P8ZP_SCRATCH_B1
beq +
pla
and #%11111101
pha
+ plp""")
}
is PtBool -> TODO("word compare against bool ${arg2.position}")
is PtNumber -> {
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.AY)
val num = arg2.number.toInt()
asmgen.out("""
sec
sbc #<${arg2.number.toInt()}
sbc #<($num)
sta P8ZP_SCRATCH_B1
tya
sbc #>${arg2.number.toInt()}""")
sbc #>($num)
php
bne +
lda P8ZP_SCRATCH_B1
beq +
pla
and #%11111101
pha
+ plp""")
}
else -> {
asmgen.assignWordOperandsToAYAndVar(arg1, arg2, "P8ZP_SCRATCH_W1")
asmgen.out("""
sec
sbc P8ZP_SCRATCH_W1
sta P8ZP_SCRATCH_B1
tya
sbc P8ZP_SCRATCH_W1+1""")
sbc P8ZP_SCRATCH_W1+1
php
bne +
lda P8ZP_SCRATCH_B1
beq +
pla
and #%11111101
pha
+ plp""")
}
}
} else {
when (arg2) {
is PtIdentifier -> {
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.AY)
val var2 = asmgen.asmVariableName(arg2)
asmgen.out("""
cpy ${asmgen.asmVariableName(arg2)}+1
cpy $var2+1
bne +
cmp ${asmgen.asmVariableName(arg2)}
+""")
cmp $var2
+""")
}
is PtBool -> TODO("word compare against bool ${arg2.position}")
is PtNumber -> {
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.AY)
val num = arg2.number.toInt()
asmgen.out("""
cpy #>${arg2.number.toInt()}
cpy #>($num)
bne +
cmp #<${arg2.number.toInt()}
+""")
cmp #<($num)
+""")
}
else -> {
asmgen.assignWordOperandsToAYAndVar(arg1, arg2, "P8ZP_SCRATCH_W1")
@@ -578,7 +616,7 @@ import prog8.codegen.cpu6502.assignment.*
cpy P8ZP_SCRATCH_W1+1
bne +
cmp P8ZP_SCRATCH_W1
+""")
+""")
}
}
}
@@ -593,12 +631,25 @@ import prog8.codegen.cpu6502.assignment.*
sec
lda $var1
sbc #$${hex.substring(6, 8)}
sta P8ZP_SCRATCH_B1
lda $var1+1
sbc #$${hex.substring(4, 6)}
ora P8ZP_SCRATCH_B1
sta P8ZP_SCRATCH_B1
lda $var1+2
sbc #$${hex.substring(2, 4)}
ora P8ZP_SCRATCH_B1
sta P8ZP_SCRATCH_B1
lda $var1+3
sbc #$${hex.take(2)}""")
sbc #$${hex.take(2)}
php
bne +
lda P8ZP_SCRATCH_B1
beq +
pla
and #%11111101
pha
+ plp""")
} else if(arg1 is PtIdentifier && arg2 is PtIdentifier) {
val var1 = asmgen.asmVariableName(arg1)
val var2 = asmgen.asmVariableName(arg2)
@@ -606,12 +657,25 @@ import prog8.codegen.cpu6502.assignment.*
sec
lda $var1
sbc $var2
sta P8ZP_SCRATCH_B1
lda $var1+1
sbc $var2+1
ora P8ZP_SCRATCH_B1
sta P8ZP_SCRATCH_B1
lda $var1+2
sbc $var2+2
ora P8ZP_SCRATCH_B1
sta P8ZP_SCRATCH_B1
lda $var1+3
sbc $var2+3""")
sbc $var2+3
php
bne +
lda P8ZP_SCRATCH_B1
beq +
pla
and #%11111101
pha
+ plp""")
} else {
assignAsmGen.assignExpressionToRegister(arg2, RegisterOrPair.R14R15, true)
if(!assignAsmGen.isRightTrivial(arg1))
@@ -623,12 +687,25 @@ import prog8.codegen.cpu6502.assignment.*
sec
lda cx16.r12
sbc cx16.r14
sta P8ZP_SCRATCH_B1
lda cx16.r12+1
sbc cx16.r14+1
ora P8ZP_SCRATCH_B1
sta P8ZP_SCRATCH_B1
lda cx16.r12+2
sbc cx16.r14+2
ora P8ZP_SCRATCH_B1
sta P8ZP_SCRATCH_B1
lda cx16.r12+3
sbc cx16.r14+3""")
sbc cx16.r14+3
php
bne +
lda P8ZP_SCRATCH_B1
beq +
pla
and #%11111101
pha
+ plp""")
}
} else
throw AssemblyError("args for cmp() should have same dt")
@@ -1121,19 +1198,16 @@ import prog8.codegen.cpu6502.assignment.*
if(arg is PtIdentifier) {
val varname = asmgen.asmVariableName(arg)
when(val dt = arg.type.base) {
BaseDataType.UBYTE -> asmgen.out(" lda $varname | jsr prog8_lib.func_sign_ub_into_A")
BaseDataType.BYTE -> asmgen.out(" lda $varname | jsr prog8_lib.func_sign_b_into_A")
BaseDataType.UWORD -> asmgen.out(" lda $varname+1 | jsr prog8_lib.func_sign_ub_into_A")
BaseDataType.WORD -> asmgen.out(" lda $varname+1 | jsr prog8_lib.func_sign_b_into_A")
BaseDataType.LONG -> asmgen.out(" lda $varname+3 | jsr prog8_lib.func_sign_b_into_A")
BaseDataType.FLOAT -> {
translateArguments(fcall, null, scope)
asmgen.out(" jsr floats.func_sign_f_into_A")
BaseDataType.UBYTE -> {
asmgen.out(" lda $varname | jsr prog8_lib.func_sign_ub_into_A")
return arrayOf(RegisterOrPair.A)
}
else -> throw AssemblyError("weird type $dt")
BaseDataType.BYTE -> {
asmgen.out(" lda $varname | jsr prog8_lib.func_sign_b_into_A")
return arrayOf(RegisterOrPair.A)
}
else -> {}
}
// result in A
return arrayOf(RegisterOrPair.A)
}
when (val dt = arg.type.base) {
@@ -42,12 +42,129 @@ internal class ForLoopsAsmGen(
when {
iterableDt.isByteArray -> forOverNonconstByteRange(stmt, iterableDt, range)
iterableDt.isWordArray && !iterableDt.isSplitWordArray -> forOverNonconstWordRange(stmt, iterableDt, range)
else -> throw AssemblyError("range expression can only be byte or word")
iterableDt.isLongArray -> forOverNonconstLongRange(stmt, iterableDt, range)
else -> throw AssemblyError("range expression can only be byte, word or long")
}
asmgen.loopEndLabels.removeLast()
}
private fun forOverNonconstLongRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
val stepsize = range.step.asConstInteger()!!
val loopLabel = asmgen.makeLabel("for_loop")
val endLabel = asmgen.makeLabel("for_end")
asmgen.loopEndLabels.add(endLabel)
val varname = asmgen.asmVariableName(stmt.variable)
asmgen.assignExpressionToVariable(range.from, varname, iterableDt.elementType())
if (stepsize == 1 || stepsize == -1)
forOverLongsRangeStepOne(range, varname, iterableDt, loopLabel, endLabel, stmt)
else
forOverLongsRangeStepGreaterOne(range, varname, iterableDt, loopLabel, endLabel, stmt)
}
private fun forOverLongsRangeStepOne(range: PtRange, varname: String, iterableDt: DataType, loopLabel: String, endLabel: String, forloop: PtForLoop) {
val stepsize = range.step.asConstInteger()!!
// Use temporary variable to store the limit
val toValueVar = asmgen.createTempVarReused(iterableDt.elementType().base, false, range)
asmgen.assignExpressionToVariable(range.to, toValueVar, iterableDt.elementType())
if (stepsize == 1) {
// Increment toValueVar to make the loop inclusive (0 to N runs N+1 times)
asmgen.out("""
inc $toValueVar
bne +
inc $toValueVar+1
bne +
inc $toValueVar+2
bne +
inc $toValueVar+3
+
""")
} else {
// Decrement toValueVar to make the loop inclusive (N downto 0 runs N+1 times)
val label1 = asmgen.makeLabel("dec_1")
val label2 = asmgen.makeLabel("dec_2")
val label3 = asmgen.makeLabel("dec_3")
asmgen.out("""
lda $toValueVar
bne $label1
lda $toValueVar+1
bne $label2
lda $toValueVar+2
bne $label3
dec $toValueVar+3
$label3: dec $toValueVar+2
$label2: dec $toValueVar+1
$label1: dec $toValueVar
""")
}
asmgen.out(loopLabel)
asmgen.translate(forloop.statements)
if (stepsize == 1) {
asmgen.out("""
inc $varname
bne +
inc $varname+1
bne +
inc $varname+2
bne +
inc $varname+3
+ lda $varname
cmp $toValueVar
bne $loopLabel
lda $varname+1
cmp $toValueVar+1
bne $loopLabel
lda $varname+2
cmp $toValueVar+2
bne $loopLabel
lda $varname+3
cmp $toValueVar+3
bne $loopLabel
""")
} else {
asmgen.out("""
lda $varname
bne +++
lda $varname+1
bne ++
lda $varname+2
bne +
dec $varname+3
+ dec $varname+2
+ dec $varname+1
+ dec $varname
lda $varname
cmp $toValueVar
bne $loopLabel
lda $varname+1
cmp $toValueVar+1
bne $loopLabel
lda $varname+2
cmp $toValueVar+2
bne $loopLabel
lda $varname+3
cmp $toValueVar+3
bne $loopLabel
""")
}
asmgen.out(endLabel)
}
private fun forOverLongsRangeStepGreaterOne(range: PtRange, varname: String, iterableDt: DataType, loopLabel: String, endLabel: String, forloop: PtForLoop) {
// TODO: implement 32-bit loop
asmgen.romableError("for loops over long ranges with step != 1 not yet fully implemented", forloop.position)
asmgen.out(loopLabel)
asmgen.translate(forloop.statements)
asmgen.jmp(loopLabel)
asmgen.out(endLabel)
}
private fun forOverNonconstByteRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
val stepsize = range.step.asConstInteger()!!
val loopLabel = asmgen.makeLabel("for_loop")
@@ -83,7 +83,7 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
asmgen.out(endLabel)
assignmentAsmGen.assignRegisterByte(target, CpuRegister.A, false, false)
}
expr.type.isWord || expr.type.isString -> {
expr.type.isWord || expr.type.isString || expr.type.isPointer -> {
asmgen.assignExpressionToRegister(expr.falsevalue, RegisterOrPair.AY)
asmgen.jmp(endLabel)
asmgen.out(trueLabel)
@@ -125,10 +125,13 @@ internal class AnyExprAsmGen(
}
private fun assignFloatBinExpr(expr: PtBinaryExpression, assign: AsmAssignment): Boolean {
// C64/PET32 ROM T-variant entries skip CONUPK and need arisgn+Z flag set up beforehand.
// CX16 ROM entries are already fixed and handle this internally.
val tSuffix = if(asmgen.options.compTarget.name in listOf(C64Target.NAME, PETTarget.NAME)) "_NZ" else ""
when(expr.operator) {
"+" -> {
assignFloatOperandsToFACandARG(expr.left, expr.right)
asmgen.out(" jsr floats.FADDT")
asmgen.out(" jsr floats.FADDT$tSuffix")
asmgen.assignRegister(RegisterOrPair.FAC1, assign.target)
return true
}
@@ -140,13 +143,13 @@ internal class AnyExprAsmGen(
}
"*" -> {
assignFloatOperandsToFACandARG(expr.left, expr.right)
asmgen.out(" jsr floats.FMULTT")
asmgen.out(" jsr floats.FMULTT$tSuffix")
asmgen.assignRegister(RegisterOrPair.FAC1, assign.target)
return true
}
"/" -> {
assignFloatOperandsToFACandARG(expr.right, expr.left)
asmgen.out(" jsr floats.FDIVT")
asmgen.out(" jsr floats.FDIVT$tSuffix")
asmgen.assignRegister(RegisterOrPair.FAC1, assign.target)
return true
}
@@ -227,7 +227,7 @@ internal class AssignmentAsmGen(
}
targetDt.isByte -> assignVariableByte(assign.target, variable)
targetDt.isSignedWord -> assignVariableWord(assign.target, variable, assign.source.datatype)
targetDt.isUnsignedWord -> {
targetDt.isUnsignedWord || targetDt.isPointer -> {
if(assign.source.datatype.isPassByRef)
assignAddressOf(assign.target, variable, false, assign.source.datatype, assign.source.array ?: PtNumber(BaseDataType.UBYTE, 0.0, assign.position))
else
@@ -236,7 +236,6 @@ internal class AssignmentAsmGen(
targetDt.isLong -> assignVariableLong(assign.target, variable, assign.source.datatype)
targetDt.isFloat -> assignVariableFloat(assign.target, variable)
targetDt.isString -> assignVariableString(assign.target, variable)
targetDt.isPointer -> assignVariableWord(assign.target, variable, assign.source.datatype)
else -> throw AssemblyError("unsupported assignment target type ${assign.target.datatype} ${assign.position}")
}
}
@@ -493,13 +492,7 @@ internal class AssignmentAsmGen(
val identifier = value.identifier!!
val source = asmgen.symbolTable.lookup(identifier.name)
require(source !is StConstant) { "addressOf of a constant should have been rewritten to a simple addition expression" }
val sourceName =
if (value.isMsbForSplitArray)
asmgen.asmSymbolName(identifier) + "_msb"
else if (identifier.type.isSplitWordArray)
asmgen.asmSymbolName(identifier) + "_lsb" // the _lsb split array comes first in memory
else
asmgen.asmSymbolName(identifier)
val sourceName = asmgen.asmSymbolName(identifier)
assignAddressOf(assign.target, sourceName, value.isMsbForSplitArray, identifier.type, value.arrayIndexExpr)
} else {
val ptrderef = value.dereference!!
@@ -3661,17 +3654,26 @@ $endLabel""")
}
private fun assignAddressOf(target: AsmAssignTarget, sourceName: String, msb: Boolean, arrayDt: DataType?, arrayIndexExpr: PtExpression?) {
var actualSourceName = sourceName
var actualMsb = msb
if (arrayDt?.isSplitWordArray == true) {
if (!sourceName.endsWith("_lsb") && !sourceName.endsWith("_msb")) {
actualSourceName = if (msb) sourceName + "_msb" else sourceName + "_lsb"
}
actualMsb = false
}
if(arrayIndexExpr!=null) {
if(arrayDt?.isPointer==true) {
require(!msb)
return pointergen.assignAddressOfIndexedPointer(target, sourceName, arrayDt, arrayIndexExpr)
require(!actualMsb)
return pointergen.assignAddressOfIndexedPointer(target, actualSourceName, arrayDt, arrayIndexExpr)
}
val constIndex = arrayIndexExpr.asConstInteger()
if(constIndex!=null) {
if (arrayDt!!.isUnsignedWord) {
// using a UWORD pointer with array indexing, always bytes
require(!msb)
assignVariableToRegister(sourceName, RegisterOrPair.AY, false, arrayIndexExpr.definingISub(), arrayIndexExpr.position)
require(!actualMsb)
assignVariableToRegister(actualSourceName, RegisterOrPair.AY, false, arrayIndexExpr.definingISub(), arrayIndexExpr.position)
if(constIndex in 1..255)
asmgen.out("""
clc
@@ -3693,9 +3695,9 @@ $endLabel""")
else {
if(constIndex>0) {
val offset = if(arrayDt.isSplitWordArray) constIndex else program.memsizer.memorySize(arrayDt, constIndex) // add arrayIndexExpr * elementsize to the address of the array variable.
asmgen.out(" lda #<($sourceName + $offset) | ldy #>($sourceName + $offset)")
asmgen.out(" lda #<($actualSourceName + $offset) | ldy #>($actualSourceName + $offset)")
} else {
asmgen.out(" lda #<$sourceName | ldy #>$sourceName")
asmgen.out(" lda #<$actualSourceName | ldy #>$actualSourceName")
}
}
assignRegisterpairWord(target, RegisterOrPair.AY)
@@ -3703,8 +3705,8 @@ $endLabel""")
} else {
if (arrayDt!!.isUnsignedWord) {
// using a UWORD pointer with array indexing, always bytes
require(!msb)
assignVariableToRegister(sourceName, RegisterOrPair.AY, false, arrayIndexExpr.definingISub(), arrayIndexExpr.position)
require(!actualMsb)
assignVariableToRegister(actualSourceName, RegisterOrPair.AY, false, arrayIndexExpr.definingISub(), arrayIndexExpr.position)
asmgen.saveRegisterStack(CpuRegister.A, false)
asmgen.saveRegisterStack(CpuRegister.Y, false)
if(arrayIndexExpr.type.isWord) {
@@ -3757,9 +3759,9 @@ $endLabel""")
)
} else throw AssemblyError("weird type $subtype")
asmgen.out("""
ldy #>$sourceName
ldy #>$actualSourceName
clc
adc #<$sourceName
adc #<$actualSourceName
bcc +
iny
+""")
@@ -3770,12 +3772,12 @@ $endLabel""")
}
// address of a normal variable
require(!msb)
require(!actualMsb)
when(target.kind) {
TargetStorageKind.VARIABLE -> {
asmgen.out("""
lda #<$sourceName
ldy #>$sourceName
lda #<$actualSourceName
ldy #>$actualSourceName
sta ${target.asmVarname}
sty ${target.asmVarname}+1""")
}
@@ -3783,25 +3785,25 @@ $endLabel""")
throw AssemblyError("can't store word into memory byte")
}
TargetStorageKind.ARRAY -> {
asmgen.out(" lda #<$sourceName | ldy #>$sourceName")
asmgen.out(" lda #<$actualSourceName | ldy #>$actualSourceName")
assignRegisterpairWord(target, RegisterOrPair.AY)
}
TargetStorageKind.REGISTER -> {
when(target.register!!) {
RegisterOrPair.AX -> asmgen.out(" ldx #>$sourceName | lda #<$sourceName")
RegisterOrPair.AY -> asmgen.out(" ldy #>$sourceName | lda #<$sourceName")
RegisterOrPair.XY -> asmgen.out(" ldy #>$sourceName | ldx #<$sourceName")
RegisterOrPair.AX -> asmgen.out(" ldx #>$actualSourceName | lda #<$actualSourceName")
RegisterOrPair.AY -> asmgen.out(" ldy #>$actualSourceName | lda #<$actualSourceName")
RegisterOrPair.XY -> asmgen.out(" ldy #>$actualSourceName | ldx #<$actualSourceName")
in Cx16VirtualRegisters -> {
asmgen.out("""
lda #<$sourceName
ldy #>$sourceName
lda #<$actualSourceName
ldy #>$actualSourceName
sta cx16.${target.register.toString().lowercase()}
sty cx16.${target.register.toString().lowercase()}+1""")
}
else -> throw AssemblyError("can only load address into 16 bit register")
}
}
TargetStorageKind.POINTER -> pointergen.assignAddressOf(PtrTarget(target), sourceName)
TargetStorageKind.POINTER -> pointergen.assignAddressOf(PtrTarget(target), actualSourceName)
TargetStorageKind.VOID -> { /* do nothing */ }
}
}
@@ -318,14 +318,13 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
SourceStorageKind.REGISTER -> inplacemodificationLongWithVariable(targetVarName, operator, regName(value))
SourceStorageKind.MEMORY -> TODO("inplace long modification with memread value ${target.position}")
SourceStorageKind.ARRAY -> TODO("inplace long modification with array value ${target.position}")
SourceStorageKind.EXPRESSION -> {
SourceStorageKind.EXPRESSION ->
if(value.expression is PtTypeCast) {
if (tryInplaceModifyWithRemovedRedundantCast(value.expression, target, operator)) return
TODO("inplace long modification ${target.position}")
} else {
TODO("inplace long modification ${target.position}")
}
}
}
}
@@ -577,24 +576,40 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
internal fun inplacemodificationLongWithVariable(targetVar: String, operator: String, sourceVar: String) {
when(operator) {
"+" -> {
// need to unroll to correctly keep Carry flag for multi-byte arithmetic
asmgen.out("""
clc
ldx #252
- lda $targetVar+4,x
adc $sourceVar+4,x
sta $targetVar+4,x
inx
bne -""")
lda $targetVar+0
adc $sourceVar+0
sta $targetVar+0
lda $targetVar+1
adc $sourceVar+1
sta $targetVar+1
lda $targetVar+2
adc $sourceVar+2
sta $targetVar+2
lda $targetVar+3
adc $sourceVar+3
sta $targetVar+3
""")
}
"-" -> {
// need to unroll to correctly keep Carry flag for multi-byte arithmetic
asmgen.out("""
sec
ldx #252
- lda $targetVar+4,x
sbc $sourceVar+4,x
sta $targetVar+4,x
inx
bne -""")
lda $targetVar+0
sbc $sourceVar+0
sta $targetVar+0
lda $targetVar+1
sbc $sourceVar+1
sta $targetVar+1
lda $targetVar+2
sbc $sourceVar+2
sta $targetVar+2
lda $targetVar+3
sbc $sourceVar+3
sta $targetVar+3
""")
}
"<<" -> {
asmgen.out("""
@@ -667,6 +682,53 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
dey
bpl -""")
}
"/" -> {
asmgen.out("""
; Copy target to R12R13 (dividend)
ldy #3
- lda $targetVar,y
sta cx16.r12,y
dey
bpl -
; Copy source to R14R15 (divisor)
ldy #3
- lda $sourceVar,y
sta cx16.r14,y
dey
bpl -
jsr prog8_math.div_longs
; Copy result from R14R15 back to target
ldy #3
- lda cx16.r14,y
sta $targetVar,y
dey
bpl -""")
}
"%" -> {
asmgen.out("""
; Copy target to R12R13 (dividend)
ldy #3
- lda $targetVar,y
sta cx16.r12,y
dey
bpl -
; Copy source to R14R15 (divisor)
ldy #3
- lda $sourceVar,y
sta cx16.r14,y
dey
bpl -
jsr prog8_math.div_longs
; Copy remainder from P8ZP_SCRATCH_W1/W2 back to target
lda P8ZP_SCRATCH_W1
sta $targetVar
lda P8ZP_SCRATCH_W1+1
sta $targetVar+1
lda P8ZP_SCRATCH_W2
sta $targetVar+2
lda P8ZP_SCRATCH_W2+1
sta $targetVar+3""")
}
else -> {
TODO("in-place modify LONG with variable, operator=$operator")
}
@@ -1021,6 +1083,65 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
dey
bpl -""")
}
"/" -> {
// long /= constant - use division routine
val lo = value and 0xFFFF
val hi = (value ushr 16) and 0xFFFF
asmgen.out("""
; Copy variable to R12R13 (dividend)
ldy #3
- lda $variable,y
sta cx16.r12,y
dey
bpl -
; Load constant into R14R15 (divisor)
lda #<${lo}
sta cx16.r14
lda #>${lo}
sta cx16.r14+1
lda #<${hi}
sta cx16.r15
lda #>${hi}
sta cx16.r15+1
jsr prog8_math.div_longs
; Copy result from R14R15 back to variable
ldy #3
- lda cx16.r14,y
sta $variable,y
dey
bpl -""")
}
"%" -> {
// long %= constant - use division routine, remainder is in P8ZP_SCRATCH_W1/W2
val lo = value and 0xFFFF
val hi = (value ushr 16) and 0xFFFF
asmgen.out("""
; Copy variable to R12R13 (dividend)
ldy #3
- lda $variable,y
sta cx16.r12,y
dey
bpl -
; Load constant into R14R15 (divisor)
lda #<${lo}
sta cx16.r14
lda #>${lo}
sta cx16.r14+1
lda #<${hi}
sta cx16.r15
lda #>${hi}
sta cx16.r15+1
jsr prog8_math.div_longs
; Copy remainder from P8ZP_SCRATCH_W1/W2 back to variable
lda P8ZP_SCRATCH_W1
sta $variable
lda P8ZP_SCRATCH_W1+1
sta $variable+1
lda P8ZP_SCRATCH_W2
sta $variable+2
lda P8ZP_SCRATCH_W2+1
sta $variable+3""")
}
"<<" -> if (value > 0) inplaceLongShiftLeft()
">>" -> if (value > 0) inplaceLongShiftRight()
"|" -> {
@@ -3149,6 +3270,7 @@ $shortcutLabel:""")
lda $name+1
sta P8ZP_SCRATCH_W1+1
txa
oweroff
jsr prog8_math.divmod_uw_asm
lda P8ZP_SCRATCH_W2
ldy P8ZP_SCRATCH_W2+1
@@ -33,10 +33,10 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
extsub.returns.zip(assignmentTargets).forEach { pair ->
val returns = pair.first
val target = pair.second as PtAssignTarget
// consume the register even for void targets to keep indices aligned
val isFpRegister = returns.register.registerOrPair in listOf(RegisterOrPair.FAC1, RegisterOrPair.FAC2)
val regNumber = if(isFpRegister) fpRegs.removeAt(0) else cpuRegs.removeAt(0)
if (!target.void) {
// Determine if this return uses an FP register or CPU register
val isFpRegister = returns.register.registerOrPair in listOf(RegisterOrPair.FAC1, RegisterOrPair.FAC2)
val regNumber = if(isFpRegister) fpRegs.removeAt(0) else cpuRegs.removeAt(0)
result += assignCpuRegister(returns, regNumber, target)
}
}
@@ -100,7 +100,6 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
is PtContainmentCheck -> translate(expr)
is PtPointerDeref -> translate(expr)
is PtConstant -> {
val name = expr.name
val slab = expr.memorySlab
// Check if the constant has a numeric value or is a memory slab reference
if(slab != null) {
@@ -1176,8 +1175,22 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
address = address.address.toAddress(),
fcallArgs = FunctionCallArgs(argRegisters, returnRegisters)
)
} else if(address.constbank!=null) {
IRInstruction(
Opcode.CALLFAR,
address = address.address.toAddress(),
immediate = address.constbank!!.toInt()
)
} else {
val tr = translateExpression(address.varbank!!)
require(tr.dt==IRDataType.BYTE)
result += tr.chunks
IRInstruction(
Opcode.CALLFARVB,
address = address.address.toAddress(),
reg1 = tr.resultReg
)
}
else TODO("extsub with banked address got called ${callTarget.name} ${fcall.position}")
}
addInstr(result, call, null)
val resultRegs = returnRegisters.filter{it.dt!=IRDataType.FLOAT}.map{it.registerNum.value}
@@ -347,6 +347,12 @@ internal class ConstantIdentifierReplacer(
override fun before(addressOf: AddressOf, parent: Node): Iterable<AstModification> {
val constValue = addressOf.constValue(program)
if(constValue!=null) {
// don't replace &constVar (without array index); leave it for AstChecker to reject as invalid address-of.
// But still allow &constArray[idx] (those are valid address-of operations).
val target = addressOf.identifier?.targetVarDecl()
if(target?.type==VarDeclType.CONST && addressOf.arrayIndex==null) {
return noModifications
}
return listOf(AstReplaceNode(addressOf, constValue, parent))
}
return noModifications
@@ -355,7 +361,7 @@ internal class ConstantIdentifierReplacer(
override fun after(identifier: IdentifierReference, parent: Node): Iterable<AstModification> {
// replace identifiers that refer to const value, with the value itself
// if it's a simple type and if it's not a left hand side variable
if (identifier.parent is AssignTarget || identifier.parent is Alias)
if (identifier.parent is AssignTarget || identifier.parent is Alias || identifier.parent is AddressOf)
return noModifications
var forloop = identifier.parent as? ForLoop
if (forloop == null)
@@ -12,7 +12,7 @@ import kotlin.math.floor
import kotlin.math.log2
class ExpressionSimplifier(private val program: Program, private val errors: IErrorReporter) : AstWalker() {
class ExpressionSimplifier(private val program: Program, private val errors: IErrorReporter, private val options: CompilationOptions) : AstWalker() {
override fun after(typecast: TypecastExpression, parent: Node): Iterable<AstModification> {
val mods = mutableListOf<AstModification>()
@@ -177,7 +177,8 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
}
// X <= Y-1 ---> X<Y , X >= Y+1 ---> X>Y
if(leftDt.isInteger && rightDt.isInteger) {
// Only for signed integers to avoid unsigned wraparound issues.
if(leftDt.isSignedInteger && rightDt.isSignedInteger) {
val rightExpr = expr.right as? BinaryExpression
if(rightExpr!=null && rightExpr.right.constValue(program)?.number==1.0) {
if (expr.operator == "<=" && rightExpr.operator == "-") {
@@ -463,12 +464,12 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
when {
leftVal?.number==0.0 -> expr.right
rightVal?.number==0.0 -> expr.left
rightIDt.isBytes && rightVal?.number==255.0 -> NumericLiteral(BaseDataType.UBYTE, 255.0, rightVal.position)
rightIDt.isWords && rightVal?.number==65535.0 -> NumericLiteral(BaseDataType.UWORD, 65535.0, rightVal.position)
rightIDt.isLong && rightVal?.number==-1.0 -> NumericLiteral(BaseDataType.LONG, -1.0, rightVal.position)
leftIDt.isBytes && leftVal?.number==255.0 -> NumericLiteral(BaseDataType.UBYTE, 255.0, leftVal.position)
leftIDt.isWords && leftVal?.number==65535.0 -> NumericLiteral(BaseDataType.UWORD, 65535.0, leftVal.position)
leftIDt.isLong && leftVal?.number==-1.0 -> NumericLiteral(BaseDataType.LONG, -1.0, leftVal.position)
leftIDt.isBytes && rightIDt.isBytes && rightVal?.number==255.0 -> NumericLiteral(BaseDataType.UBYTE, 255.0, rightVal.position)
leftIDt.isWords && rightIDt.isWords && rightVal?.number==65535.0 -> NumericLiteral(BaseDataType.UWORD, 65535.0, rightVal.position)
leftIDt.isLong && rightIDt.isLong && rightVal?.number==-1.0 -> NumericLiteral(BaseDataType.LONG, -1.0, rightVal.position)
leftIDt.isBytes && rightIDt.isBytes && leftVal?.number==255.0 -> NumericLiteral(BaseDataType.UBYTE, 255.0, leftVal.position)
leftIDt.isWords && rightIDt.isWords && leftVal?.number==65535.0 -> NumericLiteral(BaseDataType.UWORD, 65535.0, leftVal.position)
leftIDt.isLong && rightIDt.isLong && leftVal?.number==-1.0 -> NumericLiteral(BaseDataType.LONG, -1.0, leftVal.position)
else -> null
}
}
@@ -476,12 +477,12 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
when {
leftVal?.number==0.0 -> expr.right
rightVal?.number==0.0 -> expr.left
rightIDt.isBytes && rightVal?.number==255.0 -> PrefixExpression("~", expr.left, expr.left.position)
rightIDt.isWords && rightVal?.number==65535.0 -> PrefixExpression("~", expr.left, expr.left.position)
rightIDt.isLong && rightVal?.number==-1.0 -> PrefixExpression("~", expr.left, expr.left.position)
leftIDt.isBytes && leftVal?.number==255.0 -> PrefixExpression("~", expr.right, expr.right.position)
leftIDt.isWords && leftVal?.number==65535.0 -> PrefixExpression("~", expr.right, expr.right.position)
leftIDt.isLong && leftVal?.number==-1.0 -> PrefixExpression("~", expr.right, expr.right.position)
leftIDt.isBytes && rightIDt.isBytes && rightVal?.number==255.0 -> PrefixExpression("~", expr.left, expr.left.position)
leftIDt.isWords && rightIDt.isWords && rightVal?.number==65535.0 -> PrefixExpression("~", expr.left, expr.left.position)
leftIDt.isLong && rightIDt.isLong && rightVal?.number==-1.0 -> PrefixExpression("~", expr.left, expr.left.position)
leftIDt.isBytes && rightIDt.isBytes && leftVal?.number==255.0 -> PrefixExpression("~", expr.right, expr.right.position)
leftIDt.isWords && rightIDt.isWords && leftVal?.number==65535.0 -> PrefixExpression("~", expr.right, expr.right.position)
leftIDt.isLong && rightIDt.isLong && leftVal?.number==-1.0 -> PrefixExpression("~", expr.right, expr.right.position)
else -> null
}
}
@@ -489,12 +490,12 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
when {
leftVal?.number==0.0 -> constFalse
rightVal?.number==0.0 -> constFalse
rightIDt.isBytes && rightVal?.number==255.0 -> expr.left
rightIDt.isWords && rightVal?.number==65535.0 -> expr.left
rightIDt.isLong && rightVal?.number==-1.0 -> expr.left
leftIDt.isBytes && leftVal?.number==255.0 -> expr.right
leftIDt.isWords && leftVal?.number==65535.0 -> expr.right
leftIDt.isLong && leftVal?.number==-1.0 -> expr.right
leftIDt.isBytes && rightIDt.isBytes && rightVal?.number==255.0 -> expr.left
leftIDt.isWords && rightIDt.isWords && rightVal?.number==65535.0 -> expr.left
leftIDt.isLong && rightIDt.isLong && rightVal?.number==-1.0 -> expr.left
leftIDt.isBytes && rightIDt.isBytes && leftVal?.number==255.0 -> expr.right
leftIDt.isWords && rightIDt.isWords && leftVal?.number==65535.0 -> expr.right
leftIDt.isLong && rightIDt.isLong && leftVal?.number==-1.0 -> expr.right
else -> null
}
}
@@ -679,23 +680,23 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
return null
// absorption laws: a or (a and b) --> a, a and (a or b) --> a
if(expr.operator=="or" && rightB.operator=="and") {
if(expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) {
if((expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) && !expr.right.hasSideEffects(options.compTarget)) {
return expr.left
}
}
else if(expr.operator=="and" && rightB.operator=="or") {
if(expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) {
if((expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) && !expr.right.hasSideEffects(options.compTarget)) {
return expr.left
}
}
else if(expr.operator=="or" && rightB.operator=="or") {
if(expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) {
if((expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) && !expr.left.hasSideEffects(options.compTarget)) {
// a or (a or b) -> a or b
return expr.right
}
}
else if(expr.operator=="and" && rightB.operator=="and") {
if(expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) {
if((expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) && !expr.left.hasSideEffects(options.compTarget)) {
// a and (a and b) -> a and b
return expr.right
}
@@ -707,23 +708,23 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
return null
// absorption laws: (a and b) or a --> a, (a or b) and a --> a
if(expr.operator=="or" && leftB.operator=="and") {
if(expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) {
if((expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) && !expr.left.hasSideEffects(options.compTarget)) {
return expr.right
}
}
else if(expr.operator=="and" && leftB.operator=="or") {
if(expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) {
if((expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) && !expr.left.hasSideEffects(options.compTarget)) {
return expr.right
}
}
else if(expr.operator=="or" && leftB.operator=="or") {
if(expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) {
if((expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) && !expr.right.hasSideEffects(options.compTarget)) {
// (a or b) or a -> a or b
return expr.left
}
}
else if(expr.operator=="and" && leftB.operator=="and") {
if(expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) {
if((expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) && !expr.right.hasSideEffects(options.compTarget)) {
// (a and b) or a -> a and b
return expr.left
}
@@ -844,7 +845,7 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
}
private fun optimizeAdd(expr: BinaryExpression, leftVal: NumericLiteral?, rightVal: NumericLiteral?): Expression? {
if(expr.left.isSameAs(expr.right)) {
if(expr.left.isSameAs(expr.right) && !expr.left.hasSideEffects(options.compTarget)) {
// optimize X+X into X *2
expr.operator = "*"
expr.right = NumericLiteral.optimalInteger(2, expr.right.position)
@@ -879,7 +880,7 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
}
private fun optimizeSub(expr: BinaryExpression, leftVal: NumericLiteral?, rightVal: NumericLiteral?): Expression? {
if(expr.left.isSameAs(expr.right)) {
if(expr.left.isSameAs(expr.right) && !expr.left.hasSideEffects(options.compTarget)) {
// optimize X-X into 0
return NumericLiteral.optimalInteger(0, expr.position)
}
@@ -924,12 +925,12 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
val cv = rightVal?.number?.toInt()?.toDouble()
when (expr.operator) {
"%" -> {
if (cv == 1.0) {
if (cv == 1.0 && !expr.left.hasSideEffects(options.compTarget)) {
val idt = expr.inferType(program)
if(!idt.isKnown)
throw FatalAstException("unknown dt")
return NumericLiteral(idt.getOrUndef().base, 0.0, expr.position)
} else if (cv in powersOfTwoFloat) {
} else if (cv in powersOfTwoFloat && leftVal?.type?.isUnsignedInteger == true) {
expr.operator = "&"
expr.right = NumericLiteral.optimalInteger(cv!!.toInt()-1, expr.position)
return null
@@ -969,7 +970,7 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
}
256.0 -> {
when(leftDt.base) {
BaseDataType.UBYTE -> return NumericLiteral(BaseDataType.UBYTE, 0.0, expr.position)
BaseDataType.UBYTE -> if (!expr.left.hasSideEffects(options.compTarget)) return NumericLiteral(BaseDataType.UBYTE, 0.0, expr.position) else return null
BaseDataType.BYTE -> return null // is either 0 or -1 we cannot tell here
BaseDataType.UWORD, BaseDataType.WORD -> {
// just use: msb(value) as type
@@ -992,11 +993,11 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
}
if (leftDt.isUnsignedByte) {
if (abs(rightConst.number) >= 256.0) {
if (abs(rightConst.number) >= 256.0 && !expr.left.hasSideEffects(options.compTarget)) {
return NumericLiteral(BaseDataType.UBYTE, 0.0, expr.position)
}
} else if (leftDt.isUnsignedWord) {
if (abs(rightConst.number) >= 65536.0) {
if (abs(rightConst.number) >= 65536.0 && !expr.left.hasSideEffects(options.compTarget)) {
return NumericLiteral(BaseDataType.UBYTE, 0.0, expr.position)
}
}
@@ -1006,8 +1007,9 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
// left value is a constant, see if we can optimize
when (leftVal.number) {
0.0 -> {
// 0
return NumericLiteral(leftVal.type, 0.0, expr.position)
// 0 (only if not float and no side effects)
if (!expr.inferType(program).isFloat && !expr.right.hasSideEffects(options.compTarget))
return NumericLiteral(leftVal.type, 0.0, expr.position)
}
}
}
@@ -1030,8 +1032,11 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
return PrefixExpression("-", leftValue, expr.position)
}
0.0 -> {
// 0
return NumericLiteral(rightConst.type, 0.0, expr.position)
// 0 (only if not float and no side effects)
if (!expr.inferType(program).isFloat && !leftValue.hasSideEffects(options.compTarget))
return NumericLiteral(rightConst.type, 0.0, expr.position)
else
return null
}
1.0 -> {
// left
@@ -64,8 +64,8 @@ fun Program.inlineSubroutines(options: CompilationOptions): Int {
return inliner.applyModifications()
}
fun Program.simplifyExpressions(errors: IErrorReporter) : Int {
val opti = ExpressionSimplifier(this, errors)
fun Program.simplifyExpressions(errors: IErrorReporter, options: CompilationOptions) : Int {
val opti = ExpressionSimplifier(this, errors, options)
opti.visit(this)
return opti.applyModifications()
}
+43 -32
View File
@@ -14,10 +14,26 @@ import prog8.code.target.VMTarget
private fun isEmptyReturn(stmt: Statement): Boolean = stmt is Return && stmt.values.isEmpty()
/**
* The Inliner performs subroutine inlining on the AST.
*
* It supports both manual inlining (subroutines marked with the 'inline' keyword)
* and automatic inlining for very simple subroutines.
*
* Current restrictions and considerations:
* - Automatic inlining is currently restricted to subroutines with ZERO parameters.
* - Inlining subroutines with parameters is complicated because it requires careful
* parameter substitution to avoid side effects and name collisions.
* - Handling parameterized inlining can easily lead to infinite loops or recursion
* in the optimizer if not done VERY CAREFULLY (e.g. self-referential subroutines).
* - Subroutines must have a single functional statement in their body to be auto-inlined.
* - Multi-return value subroutines are not yet supported for auto-inlining.
*/
class Inliner(private val program: Program, private val options: CompilationOptions): AstWalker() {
inner class DetermineInlineSubs(val program: Program): IAstVisitor {
private val modifications = mutableListOf<AstModification>()
private val visitedSubroutines = java.util.Collections.newSetFromMap(java.util.IdentityHashMap<Subroutine, Boolean>())
init {
visit(program)
@@ -25,7 +41,24 @@ class Inliner(private val program: Program, private val options: CompilationOpti
modifications.clear()
}
private fun isRecursive(sub: Subroutine): Boolean {
var recursive = false
val visitor = object : IAstVisitor {
override fun visit(functionCallExpr: FunctionCallExpression) {
if (!recursive && functionCallExpr.target.targetSubroutine() == sub) recursive = true
if (!recursive) super.visit(functionCallExpr)
}
override fun visit(functionCallStatement: FunctionCallStatement) {
if (!recursive && functionCallStatement.target.targetSubroutine() == sub) recursive = true
if (!recursive) super.visit(functionCallStatement)
}
}
sub.statements.forEach { if (!recursive) it.accept(visitor) }
return recursive
}
override fun visit(subroutine: Subroutine) {
if (!visitedSubroutines.add(subroutine)) return
fun isBodyInlineable(stmt: Return): Boolean {
if (stmt.values.isEmpty())
@@ -109,28 +142,15 @@ class Inliner(private val program: Program, private val options: CompilationOpti
stmt.target is NumericLiteral
}
if (!subroutine.isAsmSubroutine) {
// NOTE: We allow subroutines with ANY number of parameters to be considered for inlining.
// For void calls where the body doesn't use the parameters, inlining is trivial -
// we just remove the call entirely (no parameter substitution needed).
// For calls that return values or use parameters in the body, we need simple arguments
// (NumericLiteral or IdentifierReference) for parameter substitution to work.
// See canInlineAtCallSite() for the argument simplicity check.
val containsSubsOrVariables = subroutine.statements.any {
it is Subroutine || (it is VarDecl && it.origin != VarDeclOrigin.SUBROUTINEPARAM)
}
if (!subroutine.isAsmSubroutine && subroutine.parameters.isEmpty()) {
val containsSubsOrVariables = subroutine.statements.any { it is Subroutine || it is VarDecl }
if (!containsSubsOrVariables) {
// For subroutines with parameters, there will be a VarDecl(SUBROUTINEPARAM) per parameter
// plus the body statement(s)
val expectedMaxStmts = subroutine.parameters.size + 1 // param VarDecls + 1 body statement
val hasOnlyBodyStatements = subroutine.statements.size == expectedMaxStmts ||
(subroutine.statements.size == expectedMaxStmts + 1 && subroutine.statements.lastOrNull()?.let(::isEmptyReturn) == true)
val hasOnlyBodyStatements = subroutine.statements.size == 1 ||
(subroutine.statements.size == 2 && subroutine.statements.lastOrNull()?.let(::isEmptyReturn) == true)
if (hasOnlyBodyStatements) {
if (subroutine !== program.entrypoint) {
// Find the first non-parameter statement (the actual body)
val bodyStmt = subroutine.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
val bodyStmt = subroutine.statements.firstOrNull()
val isAutoInlineable =
when (val stmt = bodyStmt) {
is Return -> isBodyInlineable(stmt)
@@ -139,15 +159,14 @@ class Inliner(private val program: Program, private val options: CompilationOpti
is Jump -> isBodyInlineable(stmt)
else -> false
}
if (isAutoInlineable)
if (isAutoInlineable && !isRecursive(subroutine))
subroutine.inline = true
}
}
if (subroutine.inline && subroutine.statements.size > 1) {
// Remove trailing return if it's empty and there's another body statement
val bodyStmts = subroutine.statements.filter { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
if (bodyStmts.size > 1 && subroutine.statements.lastOrNull()?.let(::isEmptyReturn) == true) {
if (subroutine.statements.lastOrNull()?.let(::isEmptyReturn) == true) {
subroutine.statements.removeLast()
}
}
@@ -227,16 +246,8 @@ class Inliner(private val program: Program, private val options: CompilationOpti
if (!sub.inline)
return false to "subroutine not marked as inlineable"
// Check parameter argument complexity
if (sub.parameters.isNotEmpty()) {
if (sub.isAsmSubroutine) {
return false to "parameterized asmsub cannot be inlined"
}
// Only inline if all arguments are simple (literals or identifiers)
if (!fcall.args.all { it is NumericLiteral || it is IdentifierReference }) {
val complexArgs = fcall.args.filterNot { it is NumericLiteral || it is IdentifierReference }
return false to "complex arguments (${complexArgs.size}x): ${complexArgs.map { it::class.simpleName }.joinToString(", ")}"
}
return false to "subroutines with parameters cannot be inlined in this compiler version"
}
if (options.compTarget.name != VMTarget.NAME) {
@@ -336,7 +347,7 @@ class Inliner(private val program: Program, private val options: CompilationOpti
fun possiblyInlineFunctionBody(toInline: Statement): Iterable<AstModification> {
val inlinedStatement = substituteParameters(sub, origNode as IFunctionCall, toInline) as Statement
// println(">>> INLINER: INLINED '${sub.name}' at ${functionCallStatement.position} (body inserted, ${sub.parameters.size} param(s))")
// println(">>> INLINER: INLINED '${sub.name}' at ${functionCallStatement.position} (body inserted)")
return if (origNode !== toInline) {
sub.hasBeenInlined = true
listOf(AstReplaceNode(origNode, inlinedStatement, parent))
@@ -344,7 +355,7 @@ class Inliner(private val program: Program, private val options: CompilationOpti
noModifications
}
if(sub.inline && sub.parameters.size >= 0) {
if(sub.inline) {
// Get the first non-parameter statement (skip VarDecl with SUBROUTINEPARAM origin)
val bodyStmt = sub.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
val hasOnlyBodyAndReturn = bodyStmt != null && (
@@ -366,7 +366,7 @@ class StatementOptimizer(private val program: Program,
}
override fun after(assignment: Assignment, parent: Node): Iterable<AstModification> {
if(assignment.target isSameAs assignment.value) {
if(assignment.target isSameAs assignment.value && !assignment.target.hasSideEffects(options.compTarget) && !assignment.value.hasSideEffects(options.compTarget)) {
// remove assignment to self
return listOf(AstRemove(assignment, parent as IStatementContainer))
}
@@ -380,7 +380,7 @@ class StatementOptimizer(private val program: Program,
if(bexpr!=null) {
val rightCv = bexpr.right.constValue(program)?.number
if (rightCv != null && assignment.target isSameAs bexpr.left) {
if (rightCv != null && assignment.target isSameAs bexpr.left && !assignment.target.hasSideEffects(options.compTarget) && !bexpr.left.hasSideEffects(options.compTarget)) {
// assignments of the form: X = X <operator> <expr>
// remove assignments that have no effect (such as X=X+0)
// optimize/rewrite some other expressions
@@ -255,7 +255,7 @@ class UnusedCodeRemover(private val program: Program,
}
override fun after(assignment: Assignment, parent: Node): Iterable<AstModification> {
if(assignment.target isSameAs assignment.value)
if(assignment.target isSameAs assignment.value && !assignment.target.hasSideEffects(options.compTarget) && !assignment.value.hasSideEffects(options.compTarget))
return listOf(AstRemove(assignment, parent as IStatementContainer))
return noModifications
}
+14 -10
View File
@@ -25,15 +25,16 @@ smallringbuffer {
}
sub free() -> ubyte {
return 255-fill
return 255 - fill
}
sub isfull() -> bool {
return fill>=254
; returns true if less than 2 bytes of space remaining (to allow putw)
return fill >= 254
}
sub isempty() -> bool {
return fill<=1
return fill == 0
}
sub put(ubyte value) {
@@ -92,11 +93,12 @@ smallstack {
}
sub isfull() -> bool {
return sp==0
; returns true if less than 2 bytes of space remaining (to allow push_w)
return sp <= 1
}
sub isempty() -> bool {
return sp==255
return sp == 255
}
sub push_b(ubyte value) {
@@ -150,11 +152,12 @@ stack {
}
sub isfull() -> bool {
return sp==0
; returns true if less than 2 bytes of space remaining (to allow push_w)
return sp == 0 or sp > 8191
}
sub isempty() -> bool {
return sp==8191
return sp == 8191
}
sub push_b(ubyte value) {
@@ -207,15 +210,16 @@ ringbuffer {
}
sub free() -> uword {
return 8191-fill
return 8192-fill
}
sub isempty() -> bool {
return fill==0
return fill == 0
}
sub isfull() -> bool {
return fill>=8191
; returns true if less than 2 bytes of space remaining (to allow putw)
return fill >= 8191
}
sub put(ubyte value) {
+1 -1
View File
@@ -14,7 +14,7 @@ txt {
const ubyte DEFAULT_WIDTH = 40
const ubyte DEFAULT_HEIGHT = 25
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly ofcourse. Note: takes a PETSCII encoded character.
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly of course. Note: takes a PETSCII encoded character.
sub bell() {
+40
View File
@@ -539,3 +539,43 @@ swap_floats .proc
bpl -
rts
.pend
; Wrappers for the ROM's bare T-variant math routines (FADDT, FMULTT, FDIVT, FPWRT).
; These ROM entries skip CONUPK which normally sets the Z flag and arisgn byte.
; Callers that have not just come through CONUPK must set these up themselves
; or the result can be wrong (heisenbug with some values).
; The corresponding entries on the X16 (via its KERNAL jump table) do this correctly,
; so these wrappers are only needed on C64 and PET32.
FADDT_NZ .proc
lda FAC_ADDR+5 ; facsgn
eor FAC_ADDR+$0D ; argsgn
sta FAC_ADDR+$0E ; arisgn
lda FAC_ADDR+0 ; facexp to set Z flag
jmp FADDT
.pend
FMULTT_NZ .proc
lda FAC_ADDR+5 ; facsgn
eor FAC_ADDR+$0D ; argsgn
sta FAC_ADDR+$0E ; arisgn
lda FAC_ADDR+0 ; facexp to set Z flag
jmp FMULTT
.pend
FDIVT_NZ .proc
lda FAC_ADDR+5 ; facsgn
eor FAC_ADDR+$0D ; argsgn
sta FAC_ADDR+$0E ; arisgn
lda FAC_ADDR+0 ; facexp to set Z flag
jmp FDIVT
.pend
FPWRT_NZ .proc
lda FAC_ADDR+5 ; facsgn
eor FAC_ADDR+$0D ; argsgn
sta FAC_ADDR+$0E ; arisgn
lda FAC_ADDR+0 ; facexp to set Z flag
jmp FPWRT
.pend
+5 -5
View File
@@ -51,15 +51,15 @@ extsub $bae2 = MUL10() clobbers(A,X,Y) ; fac1 *= 10
extsub $bafe = DIV10() clobbers(A,X,Y) ; fac1 /= 10 , CAUTION: result is always positive! You have to fix sign manually!
extsub $bc5b = FCOMP(uword mflpt @ AY) clobbers(X,Y) -> ubyte @ A ; A = compare fac1 to mflpt in A/Y, 0=equal 1=fac1 is greater, 255=fac1 is less than
extsub $b86a = FADDT() clobbers(A,X,Y) ; fac1 += fac2
extsub $b86a = FADDT() clobbers(A,X,Y) ; fac1 += fac2 ; WARNING: bare entry, needs Z flag=facexp + arisgn; use FADDT_NZ wrapper
extsub $b867 = FADD(uword mflpt @ AY) clobbers(A,X,Y) ; fac1 += mflpt value from A/Y
extsub $b853 = FSUBT() clobbers(A,X,Y) ; fac1 = fac2-fac1 mind the order of the operands
extsub $b853 = FSUBT() clobbers(A,X,Y) ; fac1 = fac2-fac1 mind the order of the operands ; (FSUBT does NOT need the Z flag hack, it already does the setup internally)
extsub $b850 = FSUB(uword mflpt @ AY) clobbers(A,X,Y) ; fac1 = mflpt from A/Y - fac1
extsub $ba2b = FMULTT() clobbers(A,X,Y) ; fac1 *= fac2
extsub $ba2b = FMULTT() clobbers(A,X,Y) ; fac1 *= fac2 ; WARNING: bare entry, needs Z flag=facexp + arisgn; use FMULTT_NZ wrapper
extsub $ba28 = FMULT(uword mflpt @ AY) clobbers(A,X,Y) ; fac1 *= mflpt value from A/Y
extsub $bb12 = FDIVT() clobbers(A,X,Y) ; fac1 = fac2/fac1 mind the order of the operands
extsub $bb12 = FDIVT() clobbers(A,X,Y) ; fac1 = fac2/fac1 mind the order of the operands ; WARNING: bare entry, needs Z flag=facexp + arisgn; use FDIVT_NZ wrapper
extsub $bb0f = FDIV(uword mflpt @ AY) clobbers(A,X,Y) ; fac1 = mflpt in A/Y / fac1
extsub $bf7b = FPWRT() clobbers(A,X,Y) ; fac1 = fac2 ** fac1
extsub $bf7b = FPWRT() clobbers(A,X,Y) ; fac1 = fac2 ** fac1 ; WARNING: bare entry, needs Z flag=facexp + arisgn; use FPWRT_NZ wrapper
extsub $bf78 = FPWR(uword mflpt @ AY) clobbers(A,X,Y) ; fac1 = fac2 ** mflpt from A/Y
extsub $bd7e = FINLOG(byte value @A) clobbers (A, X, Y) ; fac1 += signed byte in A
+1 -1
View File
@@ -13,7 +13,7 @@ txt {
const ubyte DEFAULT_WIDTH = 40
const ubyte DEFAULT_HEIGHT = 25
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly ofcourse. Note: takes a PETSCII encoded character.
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly of course. Note: takes a PETSCII encoded character.
sub bell() {
+13 -9
View File
@@ -27,11 +27,12 @@ smallringbuffer {
}
sub isfull() -> bool {
return fill>=254
; returns true if less than 2 bytes of space remaining (to allow putw)
return fill >= 254
}
sub isempty() -> bool {
return fill<=1
return fill == 0
}
sub put(ubyte value) {
@@ -90,11 +91,12 @@ smallstack {
}
sub isfull() -> bool {
return sp==0
; returns true if less than 2 bytes of space remaining (to allow push_w)
return sp <= 1
}
sub isempty() -> bool {
return sp==255
return sp == 255
}
sub push_b(ubyte value) {
@@ -150,11 +152,12 @@ stack {
}
sub isfull() -> bool {
return sp<=$a001
; returns true if less than 2 bytes of space remaining (to allow push_w)
return sp <= $a000
}
sub isempty() -> bool {
return sp==$bfff
return sp == $bfff
}
sub push_b(ubyte value) {
@@ -227,15 +230,16 @@ ringbuffer {
}
sub free() -> uword {
return $1fff-fill
return 8192-fill
}
sub isempty() -> bool {
return fill<=1
return fill == 0
}
sub isfull() -> bool {
return fill>=8191
; returns true if less than 2 bytes of space remaining (to allow putw)
return fill >= 8191 or head == $bfff
}
sub put(ubyte value) {
+1 -1
View File
@@ -872,7 +872,7 @@ exit:
}
sub get_loadaddress(str filename) -> uword {
; get the load adress from a PRG file (usually $0801 but it can be different)
; get the load address from a PRG file (usually $0801 but it can be different)
cbm.SETNAM(strings.length(filename), filename)
cbm.SETLFS(READ_IO_CHANNEL, drivenumber, READ_IO_CHANNEL)
+1 -1
View File
@@ -185,7 +185,7 @@ gfx_hires {
sub vertical_line(uword xx, uword yy, uword lheight, ubyte color) {
; highres 4c
; use TWO vera adress pointers simultaneously one for reading, one for writing, so auto-increment is possible
; use TWO vera address pointers simultaneously one for reading, one for writing, so auto-increment is possible
if lheight==0
return
position2(xx,yy,true)
+2 -2
View File
@@ -1,5 +1,5 @@
; optimized graphics routines for just the single screen mode: lores 320*240, 256c (8bpp)
; bitmap image needs to start at VRAM addres $00000.
; bitmap image needs to start at VRAM address $00000.
; This is compatible with the CX16's screen mode 128.
@@ -33,7 +33,7 @@ gfx_lores {
sub clear_screen(ubyte color) {
if verafx.available() {
; use verafx cache writes to quicly clear the screen
; use verafx cache writes to quickly clear the screen
const ubyte vbank = 0
const uword vaddr = 0
cx16.VERA_CTRL = 0
+94 -17
View File
@@ -4,6 +4,23 @@
; and then calling the update() routine periodically to apply the changes.
; You can read and write all parameters in the Voice structure as desired.
; For efficiency reasons, the volume envelope state is not kept in there though.
;
; Envelope timing (ASR - Attack, Sustain, Release):
; The envelope advances by one tick each time update() is called.
; The actual real-time duration depends on how often you call update().
; If you call it every vsync (60Hz), 1 tick = ~16.7ms.
; Attack and Release produce linear volume ramps.
; Higher speed = faster ramp (shorter duration).
; Timing formula: duration_ticks = ceil(256 / speed)
; speed=1 -> 256 ticks (at 60Hz: ~4.3s)
; speed=4 -> 64 ticks (at 60Hz: ~1.07s)
; speed=64 -> 4 ticks (at 60Hz: ~67ms)
; speed=255 -> 1 tick (instant envelope change on next update() call)
; Even speed=255 applies on the *next* call to ``update()`` — there is always at least ~1 frame of delay if you depend on the normal update cycle.
; For truly immediate changes (0ms), set the voice struct fields and call ``update()`` directly.
; Sustain holds for speed ticks (0-255) then transitions to release.
; If speed=0 the envelope stalls at that phase (no progression - useful for infinite sustain).
; The envelope volume is tracked internally as 8.8 fixed point.
; Vera PSG registers:
; $1F9C0 - $1F9FF 16 blocks of 4 PSG registers (16 voices)
@@ -43,21 +60,25 @@ psg2 {
ubyte volume ; 0-63, use setvolume() to adjust this if you are also using ADSR envelope
ubyte waveform ; PULSE/SQUARE,SAWTOOTH,TRIANGLE,NOISE
ubyte pulsewidth ; 0-63
uword frequency
uword frequency ; direct VERA PSG register value (no scaling): freqword = HERZ(Hz) / 0.3725290298461914
}
; envelope parameters are kept in arrays for maximum efficiency
ubyte[16] envelope_states
ubyte[16] envelope_attacks
ubyte[16] envelope_sustains
ubyte[16] envelope_releases
ubyte[16] envelope_maxvolumes
uword[16] envelope_volumes ; 8.8 fixed point (scaled by 256)
private ubyte[16] envelope_states
private ubyte[16] envelope_attacks
private ubyte[16] envelope_sustains
private ubyte[16] envelope_releases
private ubyte[16] envelope_maxvolumes
private uword[16] envelope_volumes ; 8.8 fixed point (scaled by 256)
; private VERA context save buffer, so we don't clobber the global _vera_storage in syslib
; (which is shared with the cx16 module's save_vera_context and would cause a race
; condition if update() is interrupted by a VSYNC IRQ handler that also saves VERA context)
private ubyte[8] @shared vera_storage
^^Voice voices = memory("psg2voices", 16*sizeof(Voice), 0) ; can't use array of structs so use mem block
^^Voice vptr
private ^^Voice vptr
sub init() {
; -- required before using this module; initializes all registers to default (off) values
@@ -133,7 +154,7 @@ psg2 {
}
sub update() -> bool {
; -- update adsr envelopes, then write all 16 voices to Vera PSG
; -- update adsr envelopes, then write all 16 voices to Vera PSG. This call is IRQ-safe!
; This has to be called every time you want to apply changed psg voice parameters.
; If you want to use real-time volume envelopes (Attack-Sustain-Release),
; you have to call this routine every 1/60th second, for example from your vsync irq handler.
@@ -155,11 +176,16 @@ psg2 {
when envelope_states[voice] {
EnvelopeState::ATTACK -> {
; while current volume is less than max volume, increase volume by maxvolume * (attackspeed /256.0)
; special case: speed=255 jumps directly to max volume (instant)
maxvolume = mkword(envelope_maxvolumes[voice], 0)
currentvolume = envelope_volumes[voice]
if currentvolume < maxvolume {
newvolume = currentvolume + (maxvolume >> 8)*envelope_attacks[voice]
newvolume = min(newvolume, maxvolume)
if envelope_attacks[voice] == 255
newvolume = maxvolume
else {
newvolume = currentvolume + (maxvolume >> 8)*envelope_attacks[voice]
newvolume = min(newvolume, maxvolume)
}
envelope_volumes[voice] = newvolume
vptr.volume = msb(newvolume)
} else
@@ -173,13 +199,18 @@ psg2 {
}
EnvelopeState::RELEASE -> {
; while current volume is not zero, decrease volume by maxvolume * (releasespeed /256.0)
; special case: speed=255 jumps directly to zero (instant)
currentvolume = envelope_volumes[voice]
if currentvolume > 0 {
uword subtraction = (currentvolume>>8)*envelope_releases[voice]
if subtraction > currentvolume
if envelope_releases[voice] == 255
newvolume = 0
else
newvolume = currentvolume - subtraction
else {
uword subtraction = (envelope_maxvolumes[voice] as uword) * envelope_releases[voice]
if subtraction > currentvolume
newvolume = 0
else
newvolume = currentvolume - subtraction
}
envelope_volumes[voice] = newvolume
vptr.volume = msb(newvolume)
} else
@@ -190,7 +221,7 @@ psg2 {
vptr++
}
cx16.save_vera_context()
psg2_save_vera_context()
cx16.VERA_CTRL = 0
cx16.VERA_ADDR = lsw(cx16.VERA_PSG_BASE)
cx16.VERA_ADDR_H = %00010000 | msw(cx16.VERA_PSG_BASE)
@@ -207,7 +238,7 @@ psg2 {
}
vptr++
}
cx16.restore_vera_context()
psg2_restore_vera_context()
cx16.r3 = popw()
cx16.r2 = popw()
cx16.r1 = popw()
@@ -215,4 +246,50 @@ psg2 {
return true
}
private asmsub psg2_save_vera_context() clobbers(A) {
%asm {{
; save VERA registers to our own private storage (not clobbering syslib's global _vera_storage)
lda cx16.VERA_ADDR_L
sta p8b_psg2.p8v_vera_storage
lda cx16.VERA_ADDR_M
sta p8b_psg2.p8v_vera_storage+1
lda cx16.VERA_ADDR_H
sta p8b_psg2.p8v_vera_storage+2
lda cx16.VERA_CTRL
sta p8b_psg2.p8v_vera_storage+3
eor #1
sta p8b_psg2.p8v_vera_storage+7
sta cx16.VERA_CTRL
lda cx16.VERA_ADDR_L
sta p8b_psg2.p8v_vera_storage+4
lda cx16.VERA_ADDR_M
sta p8b_psg2.p8v_vera_storage+5
lda cx16.VERA_ADDR_H
sta p8b_psg2.p8v_vera_storage+6
rts
}}
}
private asmsub psg2_restore_vera_context() clobbers(A) {
%asm {{
lda p8b_psg2.p8v_vera_storage+7
sta cx16.VERA_CTRL
lda p8b_psg2.p8v_vera_storage+6
sta cx16.VERA_ADDR_H
lda p8b_psg2.p8v_vera_storage+5
sta cx16.VERA_ADDR_M
lda p8b_psg2.p8v_vera_storage+4
sta cx16.VERA_ADDR_L
lda p8b_psg2.p8v_vera_storage+3
sta cx16.VERA_CTRL
lda p8b_psg2.p8v_vera_storage+2
sta cx16.VERA_ADDR_H
lda p8b_psg2.p8v_vera_storage+1
sta cx16.VERA_ADDR_M
lda p8b_psg2.p8v_vera_storage+0
sta cx16.VERA_ADDR_L
rts
}}
}
}
+192 -102
View File
@@ -19,6 +19,26 @@ serial {
const ubyte REG_DIVISOR_LATCH_LOW = 0 ;same as TXRX, but when bit-7 of line control high
const ubyte REG_DIVISOR_LATCH_HI = 1 ;same as IRQE, but when bit-7 of line control high
; Baud rate divisor values for TL16C2550 with 14.7456 MHz clock
; Formula: divisor = 14745600 / (16 * baudrate)
enum BAUD {
B921600 = $0001,
B460800 = $0002,
B230400 = $0004,
B115200 = $0008,
B57600 = $0010,
B38400 = $0018,
B28800 = $0020,
B19200 = $0030,
B14400 = $0040,
B9600 = $0060,
B4800 = $00C0,
B2400 = $0180,
B1200 = $0300,
B600 = $0600,
B300 = $0C00,
}
; Bitfield masks for TL16C2550's Line Status Register
enum LSR {
DR = %00000001, ; Data Ready
@@ -54,13 +74,12 @@ serial {
}
}
}
return uart1, uart2
private sub probe(uword address) -> bool {
sys.set_irqd()
address[REG_INTERRUPT_ENABLE] = $ff
bool found = address[REG_INTERRUPT_ENABLE] == $0f ; The high nybble of IER is always clear
address[REG_INTERRUPT_ENABLE] = $f0
bool found = address[REG_INTERRUPT_ENABLE] & $f0 == 0 ; The high nybble of IER is always clear
address[REG_INTERRUPT_ENABLE] = 0
if found {
address[REG_MODEM_CONTROL] = $ff
@@ -73,15 +92,14 @@ serial {
}
sub initialize_uart(uword uart_addr) {
; TODO baud rate is currently fixed at 921600, make this configurable
; 921600 baud, 8,N,1, AutoFlow Control, FIFOS, no interrupts
uart_addr[REG_INTERRUPT_ENABLE] = $00 ; No Interrupts
; 921600 baud, 8,N,1, AutoFlow Control, FIFOS, no interrupts
uart_addr[REG_LINE_CONTROL] = $80 ; Set DLAB
uart_addr[REG_DIVISOR_LATCH_HI] = $00
uart_addr[REG_DIVISOR_LATCH_LOW] = $01 ; $0001 = 921600
uart_addr[REG_LINE_CONTROL] = $03 ; 8,N,1
uart_addr[REG_FIFO_CONTROL] = $C7 ; FIFO enable & reset
uart_addr[REG_MODEM_CONTROL] = $23 ; DTR/RTS & AutoFlow Control
uart_addr[REG_DIVISOR_LATCH_HI] = msb(BAUD::B921600)
uart_addr[REG_DIVISOR_LATCH_LOW] = lsb(BAUD::B921600)
uart_addr[REG_LINE_CONTROL] = $03 ; 8,N,1, DLAB off
uart_addr[REG_FIFO_CONTROL] = $C3 ; FIFO enable & reset, trigger 14
uart_addr[REG_MODEM_CONTROL] = $23 ; DTR/RTS + AutoFlow Control
uart_addr[REG_INTERRUPT_ENABLE] = $00 ; No interrupts
}
sub write(uword @zp uart_addr, str data) {
@@ -98,7 +116,8 @@ serial {
}
sub read_until(uword @zp uart_addr, str match, ^^ubyte buffer, uword max_size) -> uword {
; Reads up to and including the match string from the uart, into buffer.
; Reads bytes from the uart into buffer until the match string is found.
; The match string itself is also included in the buffer.
; Stops when the match string is found or max_size bytes have been read.
; Returns the number of bytes read.
@@ -119,6 +138,7 @@ serial {
}
@(buffer_ptr) = data = @(uart_addr)
buffer_ptr++
read_size++
if data == @(match_ptr) {
match_ptr++
if @(match_ptr)==0
@@ -131,96 +151,9 @@ serial {
return read_size
}
; zimodem routines:
sub zi_initialize(uword uart_addr) {
; initializes the ZiModem on the given uart address
zi_uart = uart_addr
if zi_uart == 0
return ; no uart present
initialize_uart(zi_uart)
; ZiModem sends a version banner of the `ati` command when the ESP32 boots up.
; Read it off if present. It is not always ready immediately so wait a tiny bit
sys.wait(10)
zi_write_cmd(iso:"\x03ate0") ; first send CTRL-C to abort any previous file streams
sys.wait(5)
if zi_uart[REG_LINE_STATUS] & LSR::DR != 0
discard_until(zi_uart, iso:"OK\x0d\x0a")
zi_write_cmd("atq0v1x1f0r1s45=0s40=4096&p0b921600")
discard_until(zi_uart, iso:"OK\x0d\x0a")
}
sub zi_reset() {
; reset all zimodem connections
serial.zi_write_cmd(iso:"atz")
discard_until(zi_uart, iso:"OK\x0d\x0a")
}
sub zi_write_cmd(str command) {
; writes the zimodem command (followed by a proper line ending)
write(zi_uart, command)
write_eol(zi_uart)
}
sub zi_start_get_file(str filename) -> long {
; starts a file download from the given url, returns the size of the file to download.
; you can download chunks of the file by repeatedly calling zi_get_file_chunk() until that returns 0
write(zi_uart, iso:"at&g\"")
write(zi_uart, filename)
write(zi_uart, "\"")
write_eol(zi_uart)
if read_one(zi_uart)=='[' {
; parse header [ 0 <filesize> <sum> ]
ubyte[25] header_buf
void read_until(zi_uart, iso:"]\x0d\x0a", &header_buf, sizeof(header_buf))
return conv.str2long(&header_buf + 3)
} else {
discard_until(zi_uart, iso:"RROR\x0d\x0a")
return 0
}
}
sub zi_get_file_chunk(^^ubyte @zp buffer, uword buffer_size, long remaining_file_size) -> uword {
; read the next chunk of data from the file, into buffer, up to buffer_size bytes.
; returns the number of bytes read. 0 if no more data was available.
uword bytes_to_read = min(remaining_file_size, buffer_size) as uword
if bytes_to_read == 0
return 0
repeat bytes_to_read {
while zi_uart[REG_LINE_STATUS] & LSR::DR == 0 {
; wait until data is present
}
@(buffer) = @(zi_uart)
buffer++
}
return bytes_to_read
}
sub zi_get_ip_address() -> str {
; returns the ip address of the zimodem wifi connection
ubyte[30] ip_buffer
zi_write_cmd(iso:"ati2")
void read_until(zi_uart, iso:"OK\x0d\x0a", &ip_buffer, len(ip_buffer))
cx16.r0 = &ip_buffer
while not strings.isspace(@(cx16.r0)) {
cx16.r0++
}
@(cx16.r0) = 0
return &ip_buffer
}
; private stuff
private uword @zp zi_uart ; the uart address to use for ZiModem
private sub discard_until(uword @zp uart_addr, str match) {
; Reads up to and including the match string from the uart, discards all data.
sub discard_until(uword @zp uart_addr, str match) {
; Reads bytes from the uart until the match string is found, discards all data.
; The match string itself is also discarded.
alias match_ptr = cx16.r0
match_ptr = match
@@ -239,6 +172,163 @@ serial {
}
}
; zimodem routines:
sub zi_initialize(uword uart_addr) {
; initializes the ZiModem on the given uart address
zi_uart = uart_addr
if zi_uart == 0
return ; no uart present
initialize_uart(zi_uart)
; ZiModem sends a version banner of the `ati` command when the ESP32 boots up.
; Read it off if present. It is not always ready immediately so wait a tiny bit
sys.wait(5)
write(zi_uart, "\x03") ; first send CTRL-C (+ no echo) to abort any previous file streams
sys.wait(10)
zi_write_cmd(iso:"ate0q0v1x1f0r1s45=3&p0&k3")
discard_until(zi_uart, iso:"OK\x0d\x0a")
}
sub zi_reset() {
; reset all zimodem connections
write(zi_uart, "\x03") ; first send CTRL-C (+ no echo) to abort any previous file streams
sys.wait(10)
serial.zi_write_cmd(iso:"atz")
discard_until(zi_uart, iso:"OK\x0d\x0a")
}
sub zi_write_cmd(str command) {
; writes the zimodem command (followed by a proper line ending)
write(zi_uart, command)
write_eol(zi_uart)
}
sub zi_start_get_file_hexmode(str filename) -> bool {
; starts a file download from the given url. Returns true if ok, false if file could not be found
; you can download chunks of the file by repeatedly calling zi_get_file_chunk_hexmode() until that returns 0.
; Note: this uses zimodem hex mode transfer for now because binary mode transfer is broken at the moment.
zi_write_cmd(iso:"ats45=1") ; enable hex mode transfer (unfortunately this also forces caching mode on the esp32)
discard_until(zi_uart, iso:"OK\x0d\x0a")
write(zi_uart, iso:"at&g\"")
write(zi_uart, filename)
write(zi_uart, "\"\x0d\x0a")
if read_one(zi_uart)=='[' {
; note: zimodem file size in hex mode is bogus (only can go up to $FFFF), just skip the whole header line.
discard_until(zi_uart, "\x0d\x0a")
return true
} else {
discard_until(zi_uart, iso:"RROR\x0d\x0a")
return false
}
}
sub zi_start_get_file(str filename) -> long {
; TODO: binary file transfer seems unreliable at the moment (text files are still ok, but often freezes on binary files. Could be a problem with my particular X16 serial card or zimodem firmware though), use hex mode transfer for reliable results.
; starts a file download from the given url, returns the size of the file to download.
; you can download chunks of the file by repeatedly calling zi_get_file_chunk() until that returns 0
write(zi_uart, iso:"at&g\"")
write(zi_uart, filename)
write(zi_uart, "\"\x0d\x0a")
if read_one(zi_uart)=='[' {
; parse header [ 0 <filesize> <sum> ]
ubyte[25] header_buf
void read_until(zi_uart, iso:"]\x0d\x0a", &header_buf, sizeof(header_buf))
return conv.str2long(&header_buf + 3)
} else {
discard_until(zi_uart, iso:"RROR\x0d\x0a")
return 0
}
}
sub zi_get_file_chunk_hexmode(^^ubyte @zp buffer, uword buffer_size) -> uword {
; read the next chunk of data from the file (opened in hex mode), into buffer, up to buffer_size bytes (minimum 40 bytes!!)
; returns the number of bytes read. 0 if no more data was available.
ubyte[90] linebuffer
cx16.r0 = read_until(zi_uart, "\x0d\x0a", linebuffer, sizeof(linebuffer))
if cx16.r0 == 4 and linebuffer[0] == iso:'O' ; OK\r\n was received -- end of transmission
return 0
ubyte digits = lsb(cx16.r0)-2
decode(digits)
return digits/2
private sub decode(ubyte numdigits) {
cx16.r1L = 0
for cx16.r0L in 0 to numdigits-1 step 2 {
buffer[cx16.r1L] = decodehex(cx16.r0L)
cx16.r1L++
}
asmsub decodehex(ubyte digitindex @Y) -> ubyte @A {
%asm {{
lda p8v_linebuffer,y
sec
sbc #$30
cmp #$0a
bmi +
sbc #$07
+ asl a
asl a
asl a
asl a
sta P8ZP_SCRATCH_REG
lda p8v_linebuffer+1,y
sec
sbc #$30
cmp #$0a
bmi +
sbc #$07
+ ora P8ZP_SCRATCH_REG
rts
}}
}
}
}
sub zi_get_file_chunk(^^ubyte @zp buffer, uword buffer_size, long remaining_file_size) -> uword {
; TODO: binary file transfer seems unreliable at the moment (text files are still ok, but often freezes on binary files. Could be a problem with my particular X16 serial card or zimodem firmware though), use hex mode transfer for reliable results.
; read the next chunk of data from the file, into buffer, up to buffer_size bytes.
; returns the number of bytes read. 0 if no more data was available.
uword bytes_to_read = if msw(remaining_file_size)!=0 then buffer_size else min(lsw(remaining_file_size), buffer_size)
repeat bytes_to_read {
while zi_uart[REG_LINE_STATUS] & LSR::DR == 0 {
; wait until data is present
}
@(buffer) = @(zi_uart)
buffer++
}
return bytes_to_read
}
sub zi_end_get_file() {
; makes sure the 'OK' response after the actual file data is also read away
discard_until(zi_uart, iso:"OK\x0d\x0a")
}
sub zi_get_ip_address() -> str {
; returns the ip address of the zimodem wifi connection. Assumes IPV4 only.
ubyte[25] ip_buffer
zi_write_cmd(iso:"ati2")
void read_until(zi_uart, iso:"OK\x0d\x0a", &ip_buffer, len(ip_buffer))
cx16.r0 = &ip_buffer
while not strings.isspace(@(cx16.r0)) {
cx16.r0++
}
@(cx16.r0) = 0
return &ip_buffer
}
; private stuff
private uword @zp zi_uart ; the uart address to use for ZiModem
private sub read_one(uword @zp uart_addr) -> ubyte {
while uart_addr[REG_LINE_STATUS] & LSR::DR == 0 {
; wait until data is present
+1 -1
View File
@@ -14,7 +14,7 @@ const ubyte DEFAULT_WIDTH = 80
const ubyte DEFAULT_HEIGHT = 60
const long VERA_TEXTMATRIX = $1b000
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly ofcourse. Note: takes a PETSCII encoded character.
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly of course. Note: takes a PETSCII encoded character.
sub bell() {
+5 -8
View File
@@ -434,20 +434,21 @@ divmod_w_asm .proc
divmod_uw_asm .proc
; -- divide two unsigned words (16 bit each) into 16 bit results
; input: P8ZP_SCRATCH_W1 in ZP: 16 bit number, A/Y: 16 bit divisor
; output: cx16.r15: 16 bit remainder, A/Y: 16 bit division result
; output: P8ZP_SCRATCH_W2: 16 bit remainder, A/Y: 16 bit division result
; division by zero will result in quotient = 65535 and remainder = 0
; NOTE: does not clobber any cx16.rX registers.
dividend = P8ZP_SCRATCH_W1
remainder = P8ZP_SCRATCH_W2
result = dividend ;save memory by reusing divident to store the result
result = dividend ;save memory by reusing dividend to store the result
cpy #0
bne _nonzero
ora #0
bne _nonzero
lda #0 ; remainder = 0
sta cx16.r15L
sta cx16.r15H
sta remainder
sta remainder+1
lda #$ff ; quotient = 65535 (A/Y)
tay
rts
@@ -478,10 +479,6 @@ _nonzero
+ dex
bne -
lda P8ZP_SCRATCH_W2
ldy P8ZP_SCRATCH_W2+1
sta cx16.r15L
sty cx16.r15H
lda result
ldy result+1
rts
+8 -2
View File
@@ -105,6 +105,7 @@ _sinecosR8 .char trunc(127.0 * sin(range(180+45) * rad(360.0/180.0)))
; -- return random number uniformly distributed from 0 to n-1
; NOTE: does not work for code in ROM, use randrange_rom instead for that
; why this works: https://www.youtube.com/watch?v=3DvlLUWTNMY&t=347s
; Uses cx16.r0 as temporary
cx16.r0 = math.rnd() * (n as uword)
return cx16.r0H
}
@@ -113,6 +114,7 @@ _sinecosR8 .char trunc(127.0 * sin(range(180+45) * rad(360.0/180.0)))
; -- return random number uniformly distributed from 0 to n-1
; NOTE: works for code in ROM, make sure you have initialized the seed using rndseed_rom
; why this works: https://www.youtube.com/watch?v=3DvlLUWTNMY&t=347s
; Uses cx16.r0 as temporary
cx16.r0 = math.rnd_rom() * (n as uword)
return cx16.r0H
}
@@ -177,6 +179,7 @@ _sinecosR8 .char trunc(127.0 * sin(range(180+45) * rad(360.0/180.0)))
}
asmsub log2w(uword value @AY) -> ubyte @Y {
; Clobbers cx16.r0
%asm {{
sta P8ZP_SCRATCH_W1
sty P8ZP_SCRATCH_W1+1
@@ -212,7 +215,7 @@ _sinecosR8 .char trunc(127.0 * sin(range(180+45) * rad(360.0/180.0)))
; some simpler multiplications will be optimized away into faster routines. These will not set the upper 16 bits at all!
; - THE RESULT IS ONLY VALID IF THE MULTIPLICATION WAS DONE WITH UWORD ARGUMENTS (or two positive WORD arguments)
; as soon as a negative word value (or 2) was used in the multiplication, these upper 16 bits are not valid!!
; Suggestion (if you are on the Commander X16): use verafx.muls() to get a hardware accelerated 32 bit signed multplication.
; Suggestion (if you are on the Commander X16): use verafx.muls() to get a hardware accelerated 32 bit signed multiplication.
%asm {{
lda prog8_math.multiply_words.result+2
ldy prog8_math.multiply_words.result+3
@@ -267,6 +270,7 @@ asmsub direction_qd(ubyte quadrant @A, ubyte xdelta @X, ubyte ydelta @Y) -> ubyt
; .reg:x @in x_delta Delta for x direction.
; .reg:y @in y_delta Delta for y direction.
; Returns A as the direction (0-23).
; Clobbers cx16.r0 through cx16.r5 (used as temporary variables).
%asm {{
x_delta = cx16.r0L
@@ -385,9 +389,10 @@ _quadrant_region_to_direction:
asmsub atan2(ubyte x1 @R0, ubyte y1 @R1, ubyte x2 @R2, ubyte y2 @R3) -> ubyte @A {
;; Calculate the angle, in a 256-degree circle, between two points into A.
;; The points (x1, y1) and (x2, y2) have to use *unsigned coordinates only* from the positive quadrant in the carthesian plane!
;; The points (x1, y1) and (x2, y2) have to use *unsigned coordinates only* from the positive quadrant in the cartesian plane!
;; http://codebase64.net/doku.php?id=base:8bit_atan2_8-bit_angle
;; This uses 2 large lookup tables so uses a lot of memory but is super fast.
;; Clobbers cx16.r0 through cx16.r4 (r0-r3 are input params, r4 is temporary).
%asm {{
@@ -531,6 +536,7 @@ log2_tab
asmsub diffw(uword w1 @R0, uword w2 @AY) -> uword @AY {
; -- returns the (absolute) difference, or distance, between the two words
; clobbers cx16.r0 (used as input and temporary)
%asm {{
sec
sbc cx16.r0L
+1 -1
View File
@@ -416,7 +416,7 @@ exit:
}
sub get_loadaddress(str filename) -> uword {
; get the load adress from a PRG file (usually $0801 but it can be different)
; get the load address from a PRG file (usually $0801 but it can be different)
cbm.SETNAM(strings.length(filename), filename)
cbm.SETLFS(READ_IO_CHANNEL, drivenumber, 0)
+5 -5
View File
@@ -54,19 +54,19 @@ extsub $cd42 = MOVAF() clobbers(A,X) ; copy fac1 to fac2
extsub $cd45 = MOVEF() clobbers(A,X) ; copy fac1 to fac2
extsub $cb20 = LOG() clobbers(A,X,Y) ; fac1 = LN(fac1) (natural log)
extsub $cb61 = FMULTT() clobbers(A,X,Y) ; fac1 *= fac2
extsub $cb61 = FMULTT() clobbers(A,X,Y) ; fac1 *= fac2 ; WARNING: bare entry, needs Z flag=facexp + arisgn; use FMULTT_NZ wrapper
extsub $cb5e = FMULT(uword mflpt @ AY) clobbers(A,X,Y) ; fac1 *= mflpt value from A/Y
extsub $cc48 = FDIVT() clobbers(A,X,Y) ; fac1 = fac2/fac1 mind the order of the operands
extsub $cc48 = FDIVT() clobbers(A,X,Y) ; fac1 = fac2/fac1 mind the order of the operands ; WARNING: bare entry, needs Z flag=facexp + arisgn; use FDIVT_NZ wrapper
extsub $cc45 = FDIV(uword mflpt @ AY) clobbers(A,X,Y) ; fac1 = mflpt in A/Y / fac1
extsub $cc18 = MUL10() clobbers(A,X,Y) ; fac1 *= 10
extsub $cc34 = DIV10() clobbers(A,X,Y) ; fac1 /= 10 , CAUTION: result is always positive! You have to fix sign manually!
extsub $c9a0 = FADDT() clobbers(A,X,Y) ; fac1 += fac2
extsub $c9a0 = FADDT() clobbers(A,X,Y) ; fac1 += fac2 ; WARNING: bare entry, needs Z flag=facexp + arisgn; use FADDT_NZ wrapper
extsub $c99d = FADD(uword mflpt @ AY) clobbers(A,X,Y) ; fac1 += mflpt value from A/Y
extsub $c97f = FADDH() clobbers(A,X,Y) ; fac1 += 0.5, for integer rounding- call this before INT
extsub $c989 = FSUBT() clobbers(A,X,Y) ; fac1 = fac2-fac1 mind the order of the operands
extsub $c989 = FSUBT() clobbers(A,X,Y) ; fac1 = fac2-fac1 mind the order of the operands ; (FSUBT does NOT need the Z flag hack, it already does the setup internally)
extsub $c986 = FSUB(uword mflpt @ AY) clobbers(A,X,Y) ; fac1 = mflpt from A/Y - fac1
extsub $cd91 = FCOMP(uword mflpt @ AY) clobbers(X,Y) -> ubyte @ A ; A = compare fac1 to mflpt in A/Y, 0=equal 1=fac1 is greater, 255=fac1 is less than
extsub $d112 = FPWRT() clobbers(A,X,Y) ; fac1 = fac2 ** fac1
extsub $d112 = FPWRT() clobbers(A,X,Y) ; fac1 = fac2 ** fac1 ; WARNING: bare entry, needs Z flag=facexp + arisgn; use FPWRT_NZ wrapper
extsub $ca0d = NORMAL() clobbers(A) ; normalize FAC1
+1 -1
View File
@@ -12,7 +12,7 @@ txt {
const ubyte DEFAULT_WIDTH = 40
const ubyte DEFAULT_HEIGHT = 25
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly ofcourse. Note: takes a PETSCII encoded character.
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly of course. Note: takes a PETSCII encoded character.
sub bell() {
+1 -1
View File
@@ -482,7 +482,7 @@ from_scratchW1
.pend
func_peekl .proc
; -- read the ;pmg value on the address in AY, into R14:R15
; -- read the long value on the address in AY, into R14:R15
sta P8ZP_SCRATCH_W1
sty P8ZP_SCRATCH_W1+1
from_scratchW1
+1 -1
View File
@@ -716,7 +716,7 @@ exit:
}
sub get_loadaddress(str filename) -> uword {
; get the load adress from a PRG file (usually $0801 but it can be different)
; get the load address from a PRG file (usually $0801 but it can be different)
cbm.SETNAM(strings.length(filename), filename)
cbm.SETLFS(READ_IO_CHANNEL, drivenumber, READ_IO_CHANNEL)
@@ -293,7 +293,7 @@ sub interpolate(float v, float inputMin, float inputMax, float outputMin, float
}
asmsub internal_long_AY_to_FAC() {
; Used by the compiler to cast a long to a a float in FAC
; Used by the compiler to cast a long to a float in FAC
; convert the long pointed to by AY into a float and store it in FAC
%asm {{
sta cx16.r1L
@@ -308,7 +308,7 @@ sub interpolate(float v, float inputMin, float inputMax, float outputMin, float
}
private sub internal_long_R1_to_float_AY() {
; Used by the compiler to cast a long to a a float variable:
; Used by the compiler to cast a long to a float variable:
; make the float pointed to by AY equal to the long pointed to by R1 (as float)
%asm {{
sta cx16.r0L
@@ -124,14 +124,13 @@ strings {
sub startswith(str st, str prefix) -> bool {
ubyte prefix_len = length(prefix)
ubyte str_len = length(st)
if prefix_len > str_len
if prefix_len > length(st)
return false
cx16.r9L = st[prefix_len]
push(st[prefix_len])
st[prefix_len] = 0
cx16.r9H = compare(st, prefix) as ubyte
st[prefix_len] = cx16.r9L
return cx16.r9H==0
bool result = compare(st, prefix) == 0
st[prefix_len] = pop()
return result
}
sub endswith(str st, str suffix) -> bool {
+4 -4
View File
@@ -44,7 +44,7 @@ _done
}
sub gnomesort_by_ub(^^ubyte @requirezp ub_keys, ^^uword wordvalues, ubyte num_elements) {
; sorts the 'wordvalues' array (no-split array of words) according to the 'ub_keys' array (which also gets sorted ofcourse).
; sorts the 'wordvalues' array (no-split array of words) according to the 'ub_keys' array (which also gets sorted of course).
ubyte @zp pos=1
while pos != num_elements {
if ub_keys[pos]>=ub_keys[pos-1]
@@ -88,7 +88,7 @@ _done
}
sub gnomesort_by_uw(uword @requirezp uw_keys, uword wordvalues, ubyte num_elements) {
; Sorts the 'wordvalues' array according to the 'uw_keys' array (which also gets sorted ofcourse).
; Sorts the 'wordvalues' array according to the 'uw_keys' array (which also gets sorted of course).
; both arrays should be no-split array of words. uw_keys are unsigned.
; Max number of elements is 128 to keep indexing code size small and fast. Clobbers R0 and R1.
ubyte @zp pos=2
@@ -161,7 +161,7 @@ _done
}
sub shellsort_by_ub(^^ubyte @requirezp ub_keys, ^^uword @requirezp wordvalues, ubyte num_elements) {
; sorts the 'wordvalues' array (no-split array of words) according to the 'ub_keys' array (which also gets sorted ofcourse).
; sorts the 'wordvalues' array (no-split array of words) according to the 'ub_keys' array (which also gets sorted of course).
num_elements--
ubyte @zp gap
for gap in [132, 57, 23, 10, 4, 1] {
@@ -187,7 +187,7 @@ _done
}
sub shellsort_by_uw(^^uword @requirezp uw_keys, ^^uword @requirezp wordvalues, ubyte num_elements) {
; sorts the 'wordvalues' array according to the 'uw_keys' array (which also gets sorted ofcourse).
; sorts the 'wordvalues' array according to the 'uw_keys' array (which also gets sorted of course).
; both arrays should be no-split array of words. uw_keys are unsigned.
num_elements--
ubyte gap
+13 -13
View File
@@ -7,7 +7,7 @@ strings {
asmsub length(str string @AY) clobbers(A) -> ubyte @Y {
; Returns the number of bytes in the string.
; This value is determined during runtime and counts upto the first terminating 0 byte in the string,
; This value is determined during runtime and counts up to the first terminating 0 byte in the string,
; regardless of the size of the string during compilation time. Don’t confuse this with len and sizeof!
; uses P8ZP_SCRATCH_W1 to store the string address, do not change this, other routines here may depend on it (as optimization implementation detail)
@@ -29,7 +29,7 @@ strings {
; Also, you have to make sure yourself that length is smaller or equal to the length of the source string.
; Modifies in-place, doesn’t return a value (so can’t be used in an expression).
%asm {{
; need to copy the the cx16 virtual registers to zeropage to be compatible with C64...
; need to copy the cx16 virtual registers to zeropage to be compatible with C64...
sta P8ZP_SCRATCH_W1
stx P8ZP_SCRATCH_W1+1
lda cx16.r1
@@ -56,7 +56,7 @@ _loop dey
; Also, you have to make sure yourself that length is smaller or equal to the length of the source string.
; Modifies in-place, doesn’t return a value (so can’t be used in an expression).
%asm {{
; need to copy the the cx16 virtual registers to zeropage to be compatible with C64...
; need to copy the cx16 virtual registers to zeropage to be compatible with C64...
stx P8ZP_SCRATCH_B1
sta cx16.r0
sty cx16.r0+1
@@ -96,7 +96,7 @@ _loop dey
; Also, you have to make sure yourself that start and length are within bounds of the strings.
; Modifies in-place, doesn’t return a value (so can’t be used in an expression).
%asm {{
; need to copy the the cx16 virtual registers to zeropage to be compatible with C64...
; need to copy the cx16 virtual registers to zeropage to be compatible with C64...
; substr(source, target, start, length)
sta P8ZP_SCRATCH_B1
lda cx16.r0
@@ -131,7 +131,7 @@ _startloop dey
; Locates the first position of the given character in the string,
; returns Carry set if found + index in A, or Carry clear if not found (and A will be 255, an invalid index).
%asm {{
; need to copy the the cx16 virtual registers to zeropage to make this run on C64...
; need to copy the cx16 virtual registers to zeropage to make this run on C64...
sta P8ZP_SCRATCH_W1
sty P8ZP_SCRATCH_W1+1
stx P8ZP_SCRATCH_B1
@@ -157,7 +157,7 @@ _found tya
; This is a convenience function that looks for both a CR or LF (byte 13 or byte 10) as being a possible Line Ending.
; returns Carry set if found + index in A, or Carry clear if not found (and A will be 255, an invalid index).
%asm {{
; need to copy the the cx16 virtual registers to zeropage to make this run on C64...
; need to copy the cx16 virtual registers to zeropage to make this run on C64...
sta P8ZP_SCRATCH_W1
sty P8ZP_SCRATCH_W1+1
ldy #0
@@ -297,7 +297,7 @@ _max_too_small
asmsub compare(str string1 @R0, str string2 @AY) clobbers(Y) -> byte @A {
; Compares two strings for sorting, case-sensitively.
; Returns -1 (255), 0 or 1, meaning: string1 sorts before, equal or after string2.
; Note that you can also directly compare strings and string values with eachother using
; Note that you can also directly compare strings and string values with each other using
; comparison operators ==, < etcetera (this will use strcmp automatically).
%asm {{
sta P8ZP_SCRATCH_W2
@@ -311,7 +311,7 @@ _max_too_small
asmsub compare_nocase(str string1 @R0, str string2 @AY) clobbers(Y) -> byte @A {
; Compares two strings for sorting, case-insensitively for PETSCII strings.
; Returns -1 (255), 0 or 1, meaning: string1 sorts before, equal or after string2.
; Note that you can also directly compare strings and string values with eachother using
; Note that you can also directly compare strings and string values with each other using
; comparison operators ==, < etcetera (this will use strcmp automatically).
%asm {{
sta P8ZP_SCRATCH_W2
@@ -350,7 +350,7 @@ _c1_zero
asmsub compare_nocase_iso(str string1 @R0, str string2 @AY) clobbers(Y) -> byte @A {
; Compares two strings for sorting, case-insensitively for ISO strings.
; Returns -1 (255), 0 or 1, meaning: string1 sorts before, equal or after string2.
; Note that you can also directly compare strings and string values with eachother using
; Note that you can also directly compare strings and string values with each other using
; comparison operators ==, < etcetera (this will use strcmp automatically).
%asm {{
sta P8ZP_SCRATCH_W2
@@ -525,7 +525,7 @@ _done rts
; ? matches any one character. For example, F?? matches FOO but not FU, and ?? matches all two-character strings.
; * matches any string, including the empty string.
; For example, F* matches all strings starting with F. *O*O* matches all strings with at least two Os. Finally, ?* matches all non-empty strings.
; Both the pattern and the string must be NUL-terminated (that it, followed with a 00 byte) and at most 255 characters long (excluding the NUL).
; Both the pattern and the string must be NUL-terminated (that is, followed with a 00 byte) and at most 255 characters long (excluding the NUL).
; Code taken from http://6502.org/source/strings/patmatch.htm
;
; Input: cx16.r0: A NUL-terminated, <255-length pattern
@@ -562,7 +562,7 @@ next lda $ffff,x ; look at next pattern character MODIFIED
cmp #'*' ; is it a star?
beq star ; yes, do the complicated stuff
iny ; no, let's look at the string
cmp #'?' ; is the pattern caracter a ques?
cmp #'?' ; is the pattern character a ques?
bne reg ; no, it's a regular character
lda (strptr),y ; yes, so it will match anything
beq fail ; except the end of string
@@ -602,7 +602,7 @@ fail clc ; yes, no match found, return with c=0
; ? matches any one character. For example, F?? matches FOO but not FU, and ?? matches all two-character strings.
; * matches any string, including the empty string.
; For example, F* matches all strings starting with F. *O*O* matches all strings with at least two Os. Finally, ?* matches all non-empty strings.
; Both the pattern and the string must be NUL-terminated (that it, followed with a 00 byte) and at most 255 characters long (excluding the NUL).
; Both the pattern and the string must be NUL-terminated (that is, followed with a 00 byte) and at most 255 characters long (excluding the NUL).
; Code taken from http://6502.org/source/strings/patmatch.htm
;
; Input: cx16.r0: A NUL-terminated, <255-length pattern
@@ -651,7 +651,7 @@ next lda $ffff,x ; look at next pattern character MODIFIED
cmp #'*' ; is it a star?
beq star ; yes, do the complicated stuff
iny ; no, let's look at the string
cmp #'?' ; is the pattern caracter a ques?
cmp #'?' ; is the pattern character a ques?
bne reg ; no, it's a regular character
lda (strptr),y ; yes, so it will match anything
beq fail ; except the end of string
+1 -1
View File
@@ -300,7 +300,7 @@ diskio {
}
sub get_loadaddress(str filename) -> uword {
; get the load adress from a PRG file (usually $0801 but it can be different)
; get the load address from a PRG file (usually $0801 but it can be different)
if f_open(filename) {
uword address
void f_read(&address, 2)
+1 -1
View File
@@ -277,7 +277,7 @@ math {
sub atan2(ubyte x1, ubyte y1, ubyte x2, ubyte y2) -> ubyte {
;; Calculate the angle, in a 256-degree circle, between two points into A.
;; The points (x1, y1) and (x2, y2) have to use *unsigned coordinates only* from the positive quadrant in the carthesian plane!
;; The points (x1, y1) and (x2, y2) have to use *unsigned coordinates only* from the positive quadrant in the cartesian plane!
%ir {{
loadm.b r99100,math.atan2.x1
loadm.b r99101,math.atan2.y1
+2 -2
View File
@@ -50,7 +50,7 @@ sorting {
sub shellsort_by_ub(^^ubyte @requirezp ub_keys, ^^uword @requirezp wordvalues, ubyte num_elements) {
; sorts the 'wordvalues' array (no-split array of words) according to the 'ub_keys' array (which also gets sorted ofcourse).
; sorts the 'wordvalues' array (no-split array of words) according to the 'ub_keys' array (which also gets sorted of course).
num_elements--
ubyte @zp gap
for gap in [132, 57, 23, 10, 4, 1] {
@@ -76,7 +76,7 @@ sorting {
}
sub shellsort_by_uw(^^uword @requirezp uw_keys, ^^uword @requirezp wordvalues, ubyte num_elements) {
; sorts the 'wordvalues' array according to the 'uw_keys' array (which also gets sorted ofcourse).
; sorts the 'wordvalues' array according to the 'uw_keys' array (which also gets sorted of course).
; both arrays should be no-split array of words. uw_keys are unsigned.
num_elements--
ubyte gap
+2 -2
View File
@@ -147,7 +147,7 @@ strings {
sub compare(str st1, str st2) -> byte {
; Compares two strings for sorting (case sensitive).
; Returns -1 (255), 0 or 1, meaning: string1 sorts before, equal or after string2.
; Note that you can also directly compare strings and string values with eachother using
; Note that you can also directly compare strings and string values with each other using
; comparison operators ==, < etcetera (this will use strcmp automatically).
%ir {{
loadm.w r99000,strings.compare.st1
@@ -160,7 +160,7 @@ strings {
sub compare_nocase(str st1, str st2) -> byte {
; Compares two strings for sorting (case insensitive).
; Returns -1 (255), 0 or 1, meaning: string1 sorts before, equal or after string2.
; Note that you can also directly compare strings and string values with eachother using
; Note that you can also directly compare strings and string values with each other using
; comparison operators ==, < etcetera (this will use strcmp automatically).
%ir {{
loadm.w r99000,strings.compare_nocase.st1
+36
View File
@@ -440,4 +440,40 @@ cx16 {
r14 = cx16.save_virtual_registers.storage[14]
r15 = cx16.save_virtual_registers.storage[15]
}
private sub print_error (str message) {
%ir {{
loadm.w r99000,cx16.print_error.message
syscall 3 (r99000.w)
}}
}
sub rombank(ubyte bank) {
if bank==0
return
print_error("\nerror: rombank() only accepts 0 - aborting")
sys.exit(1)
}
sub rambank(ubyte bank) {
if bank==0
return
print_error("\nerror: rambank() only accepts 0 - aborting")
sys.exit(1)
}
inline sub getrombank() -> ubyte {
return 0
}
inline sub getrambank() -> ubyte {
return 0
}
sub numbanks() -> uword {
return 1
}
}
+113 -34
View File
@@ -2,6 +2,7 @@ package prog8
import kotlinx.cli.*
import prog8.ast.AstException
import prog8.code.core.Position
import prog8.code.source.ImportFileSystem
import prog8.code.source.ImportFileSystem.expandTilde
import prog8.code.target.CompilationTargets
@@ -280,9 +281,14 @@ private fun compileMain(args: Array<String>): Boolean {
}
} else if (daemonMode == true) {
if((startEmulator1==true || startEmulator2==true) && moduleFiles.size>1) {
System.err.println("can't start emulator when multiple module files are specified")
return false
}
for(filepathRaw in moduleFiles) {
val filepath = pathFrom(filepathRaw).normalize()
val filepath = pathFrom(filepathRaw).normalize().toAbsolutePath()
val txtcolors = if(plainText==true) ErrorReporter.PlainText else ErrorReporter.AnsiColors
val absoluteSrcDirs = srcdirs.map { Path.of(it).toAbsolutePath().toString() }
val compilerArgs = CompilerArguments(
filepath,
if(checkSource==true) false else dontOptimize != true,
@@ -309,12 +315,34 @@ private fun compileMain(args: Array<String>): Boolean {
profilingInstrumentation == true,
nostdlib == true,
processedSymbols,
srcdirs,
absoluteSrcDirs,
outputPath,
cwd = Path.of(System.getProperty("user.dir")),
errors = ErrorReporter(txtcolors)
)
if (!compileViaDaemon(compilerArgs))
val response = compileViaDaemon(compilerArgs, plainText == true)
if (response == null || !response.ok)
return false
if (startEmulator1 == true || startEmulator2 == true) {
if (response.outputFiles.isEmpty()) {
println("\nCan't start emulator because no program was assembled.")
} else {
val programPathRaw = response.outputFiles.first()
val programPath = Path.of(programPathRaw.removeSuffix(".prg"))
val target = getCompilationTargetByName(compilationTarget!!)
if (startEmulator1 == true) {
if (target is VMTarget) {
target.launchEmulatorWithTrace(programPath, quietAll==true, vmTrace==true)
} else {
target.launchEmulator(1, programPath, quietAll==true)
}
} else {
target.launchEmulator(2, programPath, quietAll==true)
}
}
}
}
return true
} else {
@@ -378,6 +406,7 @@ private fun compileMain(args: Array<String>): Boolean {
}
val programNameInPath = outputPath.resolve(compilationResult.compilerAst.name)
val programPath = Path.of(programNameInPath.toString().removeSuffix(".prg"))
if (compareIR != null) {
compareIrFiles(outputPath.resolve("${compilationResult.compilerAst.name}.p8ir"), Path(compareIR!!))
@@ -386,12 +415,12 @@ private fun compileMain(args: Array<String>): Boolean {
if (startEmulator1 == true) {
if (compilationResult.compilationOptions.compTarget is VMTarget) {
(compilationResult.compilationOptions.compTarget as VMTarget).launchEmulatorWithTrace(
programNameInPath, quietAll==true, vmTrace==true)
programPath, quietAll==true, vmTrace==true)
} else {
compilationResult.compilationOptions.compTarget.launchEmulator(1, programNameInPath, quietAll==true)
compilationResult.compilationOptions.compTarget.launchEmulator(1, programPath, quietAll==true)
}
} else if (startEmulator2 == true)
compilationResult.compilationOptions.compTarget.launchEmulator(2, programNameInPath, quietAll==true)
compilationResult.compilationOptions.compTarget.launchEmulator(2, programPath, quietAll==true)
}
}
@@ -591,7 +620,7 @@ private fun scanLibraryFiles(dump: String?, searchPattern: String?) {
if(pattern!=null) {
println("You can also have a look in the documentation for the libraries at https://prog8.readthedocs.io/en/latest/libraries.html")
println("The library source files are available in the Github repository at https://github.com/irmen/prog8/tree/master/compiler/res/prog8lib")
println("Searching for pattern '$pattern' in embedded library files.\n")
println("Searching for pattern '$searchPattern' in embedded library files.\n")
}
if(dumpPath!=null) {
println("Dumping embedded library files into $dumpPath\n")
@@ -603,20 +632,22 @@ private fun scanLibraryFiles(dump: String?, searchPattern: String?) {
println("Note: the exported library source files have the same software license as the compiler itself.\n")
}
fun search(path: Path) {
fun search(path: Path, currentPattern: Regex, maxHits: Int = Int.MAX_VALUE): Int {
val hits = mutableListOf<String>()
path.useLines { lines ->
lines.forEachIndexed { index, line ->
if (pattern!!.containsMatchIn(line)) {
if (currentPattern.containsMatchIn(line)) {
hits.add("${(index + 1).toString().padStart(6)}: ${line.trimStart()}")
if (hits.size >= maxHits) return@useLines
}
}
}
if(hits.isNotEmpty()) {
if (hits.isNotEmpty()) {
println("Found in Library file '${path.absolutePathString().drop(libraryPrefix.length+1)}':")
hits.forEach(::println)
println()
}
return hits.size
}
fun dump(path: Path) {
@@ -630,13 +661,27 @@ private fun scanLibraryFiles(dump: String?, searchPattern: String?) {
val jarUrl = object {}.javaClass.protectionDomain.codeSource.location
FileSystems.newFileSystem(URI.create("jar:$jarUrl"), emptyMap<String, String>()).use { fs ->
val dir = fs.getPath(libraryPrefix)
Files.walk(dir).use { paths ->
paths.filter { Files.isRegularFile(it) }.forEach { path ->
if(pattern!=null)
search(path)
else
dump(path)
val allFiles = Files.walk(dir).use { paths -> paths.filter { Files.isRegularFile(it) }.toList() }
if (searchPattern != null) {
val p = pattern!!
var totalHits = 0
allFiles.forEach { totalHits += search(it, p) }
if (totalHits == 0 && searchPattern.length >= 2 && searchPattern.none { it in "\\.[]{}()^$|?*+" }) {
val fuzzyPatternString = searchPattern.map { Regex.escape(it.toString()) }.joinToString(".{0,3}?")
val fuzzyPattern = fuzzyPatternString.toRegex(RegexOption.IGNORE_CASE)
println("No exact matches found. Trying fuzzy search...\n")
var totalFuzzyHits = 0
for (file in allFiles) {
val remaining = 100 - totalFuzzyHits
if (remaining <= 0) break
totalFuzzyHits += search(file, fuzzyPattern, remaining)
}
if (totalFuzzyHits >= 100) {
println("... (stopped after 100 hits)")
}
}
} else if (dumpPath != null) {
allFiles.forEach { dump(it) }
}
}
}
@@ -644,27 +689,46 @@ private fun scanLibraryFiles(dump: String?, searchPattern: String?) {
// ---- daemon client ----
private fun compileViaDaemon(compilerArgs: CompilerArguments): Boolean {
private fun compileViaDaemon(compilerArgs: CompilerArguments, plainText: Boolean): DaemonResponse? {
val socketPath = CompilerDaemon.getDefaultSocketPath()
var channel = connectToDaemon(socketPath)
var wasExisting = true
if (channel == null) {
wasExisting = false
println("Starting prog8c daemon...")
if (!startDaemonProcess()) {
System.err.println("Failed to start prog8c daemon")
return false
return null
}
channel = connectToDaemon(socketPath, 4_000)
if (channel == null) {
System.err.println("prog8c daemon did not start in time")
return false
return null
}
println("prog8c daemon started.")
} else {
println("Using existing prog8c daemon at $socketPath")
}
val ok = communicateWithDaemon(channel, compilerArgs)
try { channel.close() } catch (_: Exception) {}
return ok
val response = communicateWithDaemon(channel, compilerArgs, plainText)
if (response != null && (response.ok || !wasExisting || response.versionError == null)) return response
// Existing daemon rejected us (version mismatch) and has self-terminated.
// Start a fresh daemon.
println("Starting new prog8c daemon (previous was stale)...")
if (!startDaemonProcess()) {
System.err.println("Failed to start prog8c daemon")
return null
}
channel = connectToDaemon(socketPath, 4_000)
if (channel == null) {
System.err.println("prog8c daemon did not start in time")
return null
}
println("prog8c daemon (new) started.")
return communicateWithDaemon(channel, compilerArgs, plainText)
}
private fun startDaemonProcess(): Boolean {
@@ -687,6 +751,7 @@ private fun startDaemonProcess(): Boolean {
cmd.add("--daemon-server")
val pb = ProcessBuilder(cmd)
pb.directory(ImportFileSystem.userHome.toFile())
pb.inheritIO()
pb.start()
true
@@ -718,13 +783,13 @@ private fun connectToDaemon(socketPath: Path, timeoutMs: Long): SocketChannel? {
return null
}
private fun communicateWithDaemon(channel: SocketChannel, compilerArgs: CompilerArguments): Boolean {
private fun communicateWithDaemon(channel: SocketChannel, compilerArgs: CompilerArguments, plainText: Boolean): DaemonResponse? {
try {
val writer = BufferedWriter(OutputStreamWriter(Channels.newOutputStream(channel), Charsets.UTF_8))
val reader = BufferedReader(InputStreamReader(Channels.newInputStream(channel), Charsets.UTF_8))
val request = DaemonRequest(
version = prog8.buildversion.GIT_SHA,
version = prog8.buildversion.BUILD_UNIX_TIME.toString(),
filepath = compilerArgs.filepath.toString(),
optimize = compilerArgs.optimize,
writeAssembly = compilerArgs.writeAssembly,
@@ -751,7 +816,8 @@ private fun communicateWithDaemon(channel: SocketChannel, compilerArgs: Compiler
nostdlib = compilerArgs.nostdlib,
symbolDefs = compilerArgs.symbolDefs,
sourceDirs = compilerArgs.sourceDirs,
outputDir = compilerArgs.outputDir.toString()
outputDir = compilerArgs.outputDir.toString(),
cwd = compilerArgs.cwd.toString()
)
val requestJson = DaemonProtocol.encodeRequest(request)
@@ -759,30 +825,43 @@ private fun communicateWithDaemon(channel: SocketChannel, compilerArgs: Compiler
writer.newLine()
writer.flush()
val responseJson = reader.readLine()
val responseJson = try {
reader.readLine()
} catch (e: IOException) {
System.err.println("daemon connection lost: ${e.message}")
return null
}
if (responseJson == null) {
System.err.println("daemon connection lost")
return false
return null
}
val response = DaemonProtocol.decodeResponse(responseJson)
if (response.versionError != null) {
System.err.println(response.versionError)
return false
return response
}
System.out.print(response.output)
System.out.flush()
val txtcolors = if(plainText) ErrorReporter.PlainText else ErrorReporter.AnsiColors
val reporter = ErrorReporter(txtcolors)
for (error in response.errors) {
val stream = if (error.severity == "ERROR") System.err else System.out
stream.println("${error.file?.let { "$it:" } ?: ""}${error.line}:${error.col} ${error.severity} ${error.message}")
val pos = Position(error.file ?: "", error.line, error.startCol, error.endCol)
when (error.severity) {
"ERROR" -> reporter.err(error.message, pos)
"WARNING" -> reporter.warn(error.message, pos)
"INFO" -> reporter.info(error.message, pos)
}
}
reporter.report()
System.out.print(response.stdout)
System.out.flush()
System.err.print(response.stderr)
System.out.flush()
System.err.flush()
return response.ok
return response
} finally {
try { channel.close() } catch (_: Exception) {}
}
+7 -3
View File
@@ -26,6 +26,7 @@ import prog8.parser.MultipleParseErrors
import prog8.parser.ParseError
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.absolute
import kotlin.io.path.isRegularFile
import kotlin.io.path.nameWithoutExtension
import kotlin.system.exitProcess
@@ -73,6 +74,7 @@ class CompilerArguments(val filepath: Path,
val symbolDefs: Map<String, String>,
val sourceDirs: List<String> = emptyList(),
val outputDir: Path = Path(""),
val cwd: Path = Path("").absolute(),
val errors: IErrorReporter = ErrorReporter(ErrorReporter.AnsiColors))
@@ -111,6 +113,7 @@ fun compileProgram(args: CompilerArguments): CompilationResult? {
compTarget,
args.sourceDirs,
libraryDirs,
args.cwd,
args.quietAll,
args.nostdlib
)
@@ -408,13 +411,14 @@ fun parseMainModule(filepath: Path,
compTarget: ICompilationTarget,
sourceDirs: List<String>,
libraryDirs: List<String>,
cwd: Path,
quiet: Boolean,
nostdlib: Boolean): Triple<Program, CompilationOptions, List<Path>> {
val bf = BuiltinFunctionsFacade(BuiltinFunctions)
val program = Program(filepath.nameWithoutExtension, bf, compTarget, compTarget)
bf.program = program
val importer = ModuleImporter(program, compTarget.name, errors, sourceDirs, libraryDirs, quiet, nostdlib)
val importer = ModuleImporter(program, compTarget.name, errors, sourceDirs, libraryDirs, cwd, quiet, nostdlib)
val importedModuleResult = importer.importMainModule(filepath)
importedModuleResult.onErr { throw it }
errors.report()
@@ -577,7 +581,7 @@ private fun optimizeAst(program: Program, compilerOptions: CompilationOptions, e
for(numCycles in 0..10000) {
// keep optimizing expressions and statements until no more steps remain
val optsDone1 = program.simplifyExpressions(errors)
val optsDone1 = program.simplifyExpressions(errors, compilerOptions)
val optsDone2 = program.optimizeStatements(errors, functions, compilerOptions)
program.constantFold(errors, compilerOptions) // because simplified statements and expressions can result in more constants that can be folded away
val optsDone3 = program.inlineSubroutines(compilerOptions) // inlining can expose new calls to inline
@@ -597,7 +601,7 @@ private fun optimizeAst(program: Program, compilerOptions: CompilationOptions, e
removeUnusedCode(program, errors, compilerOptions)
if(errors.noErrors()) {
// last round of optimizations because inlining may have enabled more...
program.simplifyExpressions(errors)
program.simplifyExpressions(errors, compilerOptions)
program.optimizeStatements(errors, functions, compilerOptions)
program.constantFold(errors, compilerOptions) // because simplified statements and expressions can result in more constants that can be folded away
}
+60 -20
View File
@@ -1,7 +1,9 @@
package prog8.compiler
import prog8.buildversion.GIT_SHA
import prog8.buildversion.BUILD_UNIX_TIME
import prog8.code.core.Position
import prog8.code.source.ImportFileSystem
import prog8.code.source.SourceCode
import java.io.*
import java.net.StandardProtocolFamily
import java.net.UnixDomainSocketAddress
@@ -20,7 +22,7 @@ internal class CompilerDaemon(private val socketPath: Path) {
fun getDefaultSocketPath(): Path {
val baseDir = System.getenv("XDG_RUNTIME_DIR")
?.let { Path.of(it) }
?: Path.of(System.getProperty("user.home"), ".cache")
?: ImportFileSystem.userHome.resolve(".cache")
return baseDir.resolve("prog8c-daemon.sock")
}
@@ -31,6 +33,8 @@ internal class CompilerDaemon(private val socketPath: Path) {
}
}
private var shutdownRequested = false
fun run() {
socketPath.deleteIfExists()
socketPath.parent?.let { Files.createDirectories(it) }
@@ -71,6 +75,7 @@ internal class CompilerDaemon(private val socketPath: Path) {
handleClient(client)
client.close()
lastRequestTime = System.currentTimeMillis()
if (shutdownRequested) break
}
}
}
@@ -94,13 +99,14 @@ internal class CompilerDaemon(private val socketPath: Path) {
return
}
// Version check
if (request.version != GIT_SHA) {
// Version check (build timestamp must match)
if (request.version != BUILD_UNIX_TIME.toString()) {
val resp = DaemonResponse(
ok = false,
versionError = "daemon version mismatch: client=${request.version.take(8)}, daemon=${GIT_SHA.take(8)}",
versionError = "daemon version mismatch: build time differs",
errors = emptyList(),
output = "",
stdout = "",
stderr = "",
t_ms = 0,
outputFiles = emptyList(),
importedFiles = emptyList()
@@ -108,19 +114,22 @@ internal class CompilerDaemon(private val socketPath: Path) {
writer.write(DaemonProtocol.encodeResponse(resp))
writer.newLine()
writer.flush()
System.err.println("daemon: version mismatch, shutting down")
shutdownRequested = true
return
}
// Capture stdout/stderr
val outBytes = ByteArrayOutputStream()
val errBytes = ByteArrayOutputStream()
val combinedBytes = ByteArrayOutputStream()
val combinedStream = PrintStream(combinedBytes, true, Charsets.UTF_8.name())
val oldOut = System.out
val oldErr = System.err
System.setOut(PrintStream(outBytes, true, Charsets.UTF_8.name()))
System.setErr(PrintStream(errBytes, true, Charsets.UTF_8.name()))
System.setOut(combinedStream)
System.setErr(combinedStream)
val daemonErr = DaemonErrorReporter()
var result: CompilationResult? = null
ImportFileSystem.clearCaches()
val t_ms = try {
measureTime {
val compilerArgs = request.toCompilerArguments(daemonErr)
@@ -140,12 +149,12 @@ internal class CompilerDaemon(private val socketPath: Path) {
val outputFiles = if (result != null)
listOfNotNull(
request.outputDir.let { d ->
val name = request.filepath.substringBeforeLast('.').substringAfterLast('/').substringAfterLast('\\')
val name = Path.of(request.filepath).fileName.toString().substringBeforeLast('.')
val dir = if (d == ".") "" else "$d/"
listOf(
"${dir}${name}.prg".takeIf { request.writeAssembly },
"${dir}${name}.asm".takeIf { request.writeAssembly },
"${dir}${name}.p8ir".takeIf { !request.writeAssembly }
"${dir}${name}.prg".takeIf { request.writeAssembly && request.compilationTarget != "virtual" },
"${dir}${name}.asm".takeIf { request.writeAssembly && request.compilationTarget != "virtual" },
"${dir}${name}.p8ir".takeIf { !request.writeAssembly || request.compilationTarget == "virtual" }
).filterNotNull()
}
).flatten()
@@ -154,21 +163,43 @@ internal class CompilerDaemon(private val socketPath: Path) {
val importedFiles = result?.importedFiles?.map { it.toString() } ?: emptyList()
val combinedOutput = outBytes.toString(Charsets.UTF_8.name()) + errBytes.toString(Charsets.UTF_8.name())
val stdout = combinedBytes.toString(Charsets.UTF_8.name())
val stderr = ""
val resp = DaemonResponse(
ok = result != null,
versionError = null,
errors = daemonErr.getMessages().map { msg ->
// Strip "ERROR ", "WARNING ", "INFO " prefixes
var message = if (msg.message.startsWith("${msg.severity} ")) {
msg.message.substring(msg.severity.length + 1)
} else {
msg.message
}
// Strip path prefix if it exists (e.g. file:///...:line:col: )
// The path prefix usually ends with a colon followed by a space
val pathRegex = Regex("^file:///[^:]+:[0-9]+:[0-9]+: ")
message = message.replace(pathRegex, "")
DaemonError(
severity = msg.severity,
message = msg.message,
file = msg.position?.file,
message = message,
file = msg.position?.let {
val origin = it.file
if (SourceCode.isLibraryResource(origin)) {
origin
} else {
ImportFileSystem.userHome.resolve(origin).normalize().toString()
}
},
line = msg.position?.line ?: 1,
col = msg.position?.startCol ?: 1
startCol = msg.position?.startCol ?: 1,
endCol = msg.position?.endCol ?: 1
)
},
output = combinedOutput,
stdout = stdout,
stderr = stderr,
t_ms = t_ms,
outputFiles = outputFiles,
importedFiles = importedFiles
@@ -184,6 +215,14 @@ internal class CompilerDaemon(private val socketPath: Path) {
}
private fun DaemonRequest.toCompilerArguments(daemonErr: DaemonErrorReporter): CompilerArguments {
// Resolve the output directory path: if relative, make it absolute by resolving against
// the client's working directory (cwd) to avoid writing files in the daemon's CWD.
val clientCwd = Path.of(cwd)
val resolvedOutputDir = if (Path.of(outputDir).isAbsolute) {
Path.of(outputDir)
} else {
clientCwd.resolve(outputDir)
}
return CompilerArguments(
filepath = Path.of(filepath),
optimize = optimize,
@@ -211,7 +250,8 @@ internal class CompilerDaemon(private val socketPath: Path) {
nostdlib = nostdlib,
symbolDefs = symbolDefs,
sourceDirs = sourceDirs,
outputDir = Path.of(outputDir),
outputDir = resolvedOutputDir,
cwd = clientCwd,
errors = daemonErr
)
}
+17 -8
View File
@@ -29,14 +29,16 @@ internal data class DaemonRequest(
val nostdlib: Boolean,
val symbolDefs: Map<String, String>,
val sourceDirs: List<String>,
val outputDir: String
val outputDir: String,
val cwd: String
)
internal data class DaemonResponse(
val ok: Boolean,
val versionError: String?,
val errors: List<DaemonError>,
val output: String,
val stdout: String,
val stderr: String,
val t_ms: Long,
val outputFiles: List<String>,
val importedFiles: List<String>
@@ -47,7 +49,8 @@ internal data class DaemonError(
val message: String,
val file: String?,
val line: Int,
val col: Int
val startCol: Int,
val endCol: Int
)
@@ -84,6 +87,7 @@ internal object DaemonProtocol {
append(prop("symbolDefs", req.symbolDefs))
append(prop("sourceDirs", req.sourceDirs))
append(prop("outputDir", req.outputDir))
append(prop("cwd", req.cwd))
append("null") // placeholder, gets overwritten by trimEnd
setLength(length - 5)
append('}')
@@ -101,13 +105,15 @@ internal object DaemonProtocol {
append(prop("message", e.message))
append(propOpt("file", e.file))
append(prop("line", e.line))
append(prop("col", e.col))
append(prop("startCol", e.startCol))
append(prop("endCol", e.endCol))
append("null")
setLength(length - 4)
append('}')
}
append("],")
append(prop("output", resp.output))
append(prop("stdout", resp.stdout))
append(prop("stderr", resp.stderr))
append(prop("t_ms", resp.t_ms))
append(prop("outputFiles", resp.outputFiles))
append(prop("importedFiles", resp.importedFiles))
@@ -126,7 +132,8 @@ internal object DaemonProtocol {
message = em["message"] as String,
file = em["file"] as? String,
line = (em["line"] as Number).toInt(),
col = (em["col"] as Number).toInt()
startCol = (em["startCol"] as Number).toInt(),
endCol = (em["endCol"] as Number).toInt()
)
}
val outputFiles = (map["outputFiles"] as? List<*>)?.map { it as String } ?: emptyList()
@@ -135,7 +142,8 @@ internal object DaemonProtocol {
ok = map["ok"] as Boolean,
versionError = map["versionError"] as? String,
errors = errors,
output = map["output"] as String,
stdout = map["stdout"] as String,
stderr = map["stderr"] as String,
t_ms = (map["t_ms"] as Number).toLong(),
outputFiles = outputFiles,
importedFiles = importedFiles
@@ -172,7 +180,8 @@ internal object DaemonProtocol {
nostdlib = map["nostdlib"] as Boolean,
symbolDefs = (map["symbolDefs"] as? Map<*, *>)?.mapKeys { it.key as String }?.mapValues { it.value as String } ?: emptyMap(),
sourceDirs = (map["sourceDirs"] as? List<*>)?.map { it as String } ?: emptyList(),
outputDir = map["outputDir"] as String
outputDir = map["outputDir"] as String,
cwd = map["cwd"] as String
)
}
+16 -6
View File
@@ -15,7 +15,6 @@ import prog8.parser.Prog8Parser
import java.io.File
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.absolute
import kotlin.io.path.exists
@@ -24,6 +23,7 @@ class ModuleImporter(private val program: Program,
val errors: IErrorReporter,
sourceDirs: List<String>,
libraryDirs: List<String>,
val cwd: Path,
val quiet: Boolean,
val nostdlib: Boolean = false) {
@@ -31,15 +31,18 @@ class ModuleImporter(private val program: Program,
private val libraryPaths: List<Path> = libraryDirs.map { Path(it).sanitize() }.toSortedSet().toList()
fun importMainModule(filePath: Path): Result<Module, NoSuchFileException> {
val searchIn = (listOf(Path("").absolute()) + sourcePaths).toSortedSet()
val searchIn = sourcePaths.toSortedSet()
val normalizedFilePath = filePath.normalize()
if (normalizedFilePath.exists()) {
printCompileInfo(normalizedFilePath.toAbsolutePath())
return Ok(importModule(ImportFileSystem.getFile(normalizedFilePath)))
}
for(path in searchIn) {
val programPath = path.resolve(normalizedFilePath)
if(programPath.exists()) {
if(!quiet) {
println("Compiling program ${Path("").absolute().relativize(programPath)}")
println("Compiler target: $compilationTargetName")
}
printCompileInfo(programPath)
val source = ImportFileSystem.getFile(programPath)
return Ok(importModule(source))
}
@@ -49,6 +52,13 @@ class ModuleImporter(private val program: Program,
reason = "Searched in $searchIn"))
}
private fun printCompileInfo(programPath: Path) {
if(!quiet) {
println("Compiling program ${cwd.toAbsolutePath().relativize(programPath)}")
println("Compiler target: $compilationTargetName")
}
}
fun importImplicitLibraryModule(name: String): Module? {
val import = Directive("%import", listOf(
DirectiveArg(name, 42u, position = Position("~implicit-import~", 0, 0, 0))
@@ -274,8 +274,7 @@ internal class AstChecker(private val program: Program,
}
BaseDataType.LONG -> {
if(!iterableDt.elementType().isInteger)
errors.err("long loop variable can only loop over integers", forLoop.position)
errors.warn("for loop using a long counter could be very slow", forLoop.position)
}
BaseDataType.FLOAT -> {
@@ -698,7 +697,12 @@ internal class AstChecker(private val program: Program,
val iterations = repeatLoop.iterations?.constValue(program)
if (iterations != null) {
require(floor(iterations.number)==iterations.number)
if (iterations.number.toInt() > 65536) errors.err("repeat cannot exceed 65536 iterations", iterations.position)
if (iterations.number.toInt() > 65536)
errors.warn("repeat using a long counter could be very slow", iterations.position)
}
if(repeatLoop.iterations?.inferType(program)?.isLong==true) {
errors.warn("repeat using a long counter could be very slow", repeatLoop.iterations!!.position)
}
val ident = repeatLoop.iterations as? IdentifierReference
@@ -907,7 +911,7 @@ internal class AstChecker(private val program: Program,
val variable=addressOf.identifier?.targetVarDecl()
if (variable!=null) {
if (variable.type == VarDeclType.CONST && addressOf.arrayIndex == null)
errors.err("invalid pointer-of operand type", addressOf.position)
errors.err("invalid address-of operand type", addressOf.position)
}
if(addressOf.msb) {
@@ -975,7 +979,9 @@ internal class AstChecker(private val program: Program,
// CONST can only occur on simple types (byte, word, float) and pointers
if(decl.type==VarDeclType.CONST) {
if (!decl.datatype.isNumericOrBool && !decl.datatype.isPointer)
if(decl.datatype.isString)
err("strings cannot be made const they are always potentially mutable")
else if (!decl.datatype.isNumericOrBool && !decl.datatype.isPointer)
err("const can only be used on numbers, booleans and pointers")
}
@@ -47,8 +47,36 @@ internal class BeforeAsmTypecastCleaner(val program: Program,
}
} else if(sourceDt.isString && typecast.type.isPointer && typecast.type.sub==BaseDataType.UBYTE) {
// casting a string to a ^^ubyte is just taking the address of the string.
val addr = AddressOf(typecast.expression as IdentifierReference, null, null, false, true, typecast.position)
return listOf(AstReplaceNode(typecast, addr, parent))
when (typecast.expression) {
is IfExpression -> {
val ifExpr = typecast.expression as IfExpression
val trueCast = TypecastExpression(ifExpr.truevalue, typecast.type, typecast.implicit, typecast.position)
val falseCast = TypecastExpression(ifExpr.falsevalue, typecast.type, typecast.implicit, typecast.position)
trueCast.linkParents(ifExpr)
falseCast.linkParents(ifExpr)
ifExpr.truevalue = trueCast
ifExpr.falsevalue = falseCast
return listOf(AstReplaceNode(typecast, ifExpr, parent))
}
is BranchConditionExpression -> {
val branchExpr = typecast.expression as BranchConditionExpression
val trueCast = TypecastExpression(branchExpr.truevalue, typecast.type, typecast.implicit, typecast.position)
val falseCast = TypecastExpression(branchExpr.falsevalue, typecast.type, typecast.implicit, typecast.position)
trueCast.linkParents(branchExpr)
falseCast.linkParents(branchExpr)
branchExpr.truevalue = trueCast
branchExpr.falsevalue = falseCast
return listOf(AstReplaceNode(typecast, branchExpr, parent))
}
else -> {
val identifier = typecast.expression as? IdentifierReference
if (identifier != null) {
val addr = AddressOf(identifier, null, null, false, true, typecast.position)
return listOf(AstReplaceNode(typecast, addr, parent))
}
errors.err("cannot take address of this expression", typecast.position)
}
}
} else {
errors.err("cannot cast pass-by-reference value to type ${typecast.type} (only to UWORD)", typecast.position)
}
@@ -1232,33 +1232,6 @@ _after:
}
}
// consolidates simple assignment followed by augmented assignment into a single combined assignment.
// a = simplevalue | a += expression --> a = simplevalue + expression (for all augmented operators)
// NOTE: this is NOT SAFE to do when dealing with pointer arithmetic because the semantics of the combined expression is different!
// NOTE: this is also NOT SAFE when the augmented assignment's RHS references the target variable (e.g., a = b; a *= a)
if(!assignment.isAugmentable && assignment.value.isSimple) {
val next = assignment.nextSibling() as? Assignment
if(next?.isAugmentable==true && next.value is BinaryExpression && next.target.isSameAs(assignment.target, program)) {
val combined = next.value as BinaryExpression
if(combined.left isSameAs assignment.target) {
// Check if RHS also references the target variable - if so, can't optimize
if(!(combined.right isSameAs assignment.target)) {
if(!assignment.target.inferType(program).isPointer && (assignment.value.inferType(program).isPointer || combined.right.inferType(program).isPointer)) {
// avoid pointer arithmetic: a = (simplevalue as uword) + expression
combined.left = TypecastExpression(assignment.value, DataType.UWORD, true, combined.left.position)
} else {
// no pointer arithmetic done, just transform it into a = simplevalue + expression
combined.left = assignment.value
}
assignment.value = combined
combined.linkParents(assignment)
return listOf(AstRemove(next, parent as IStatementContainer))
}
}
}
}
if(assignment.target.pointerDereference!=null && assignment.value is PtrDereference) {
val targetPtr = assignment.target.pointerDereference!!
val sourcePtr = assignment.value as PtrDereference
@@ -142,9 +142,15 @@ internal object DeferProcessor {
}
// complex return value, need to store it before calling the defer block
val pushAndPopCalls = ret.children.map { makePushPopFunctionCalls(it as PtExpression) }
val pushAndPopCalls = ret.children.map {
val expr = it as PtExpression
if (expr.type == DataType.UNDEFINED)
Pair(expr, null)
else
makePushPopFunctionCalls(expr)
}
val pushCalls = pushAndPopCalls.map { it.first }.reversed() // push in reverse order
val popCalls = pushAndPopCalls.map { it.second }
val popCalls = pushAndPopCalls.mapNotNull { it.second }
val newRet = PtReturn(ret.position)
val group = PtNodeGroup()
pushCalls.forEach { group.add(it) }
@@ -25,7 +25,8 @@ internal class LiteralsToAutoVarsAndRecombineIdentifiers(private val program: Pr
return noModifications
}
}
val scopedName = program.internString(string)
val isInStruct = findParentNode<StaticStructInitializer>(string) != null
val scopedName = program.internString(string, deduplicate = !isInStruct)
val identifier = IdentifierReference(scopedName, string.position)
return listOf(AstReplaceNode(string, identifier, parent))
}
@@ -454,9 +454,13 @@ internal class StatementReorderer(
val targetSub = funcValue.target.targetSubroutine()
if(targetSub!=null && !targetSub.isAsmSubroutine) {
if(targetSub.parameters.isEmpty()) {
// replace return func() --> goto func
val goto = Jump(funcValue.target, returnStmt.position)
return listOf(AstReplaceNode(returnStmt, goto, parent))
val currentSub = returnStmt.definingSubroutine
if (currentSub == null || !currentSub.hasDeferStatement()) {
// replace return func() --> goto func
// but only if current sub has no defers that must be executed!
val goto = Jump(funcValue.target, returnStmt.position)
return listOf(AstReplaceNode(returnStmt, goto, parent))
}
}
}
}
@@ -46,7 +46,17 @@ internal class VariousCleanups(val program: Program, val errors: IErrorReporter,
VarDeclType.CONST -> {
// change the vardecl type itself as well, but only if new type is smaller
if(valueDt.largerSizeThan(decl.datatype)) {
val constValue = decl.value!!.constValue(program)!!
val constValue = decl.value!!.constValue(program)
if(constValue==null) {
// non-numeric const value, such as a string literal assigned to a pointer type
// convert it to a regular variable instead
if(decl.value is StringLiteral && decl.datatype.isPointer) {
val vardecl = decl.copy(DataType.STR)
vardecl.type = VarDeclType.VAR
return listOf(AstReplaceNode(decl, vardecl, parent))
}
return noModifications
}
errors.err("value '${constValue.number}' out of range for ${decl.datatype}", constValue.position)
} else if(decl.datatype.largerSizeThan(valueDt)) {
// don't make it signed if it was unsigned and vice versa, except when it is a long const declaration
@@ -54,7 +64,7 @@ internal class VariousCleanups(val program: Program, val errors: IErrorReporter,
(valueDt.isSigned && decl.datatype.isUnsigned ||
valueDt.isUnsigned && decl.datatype.isSigned))
{
val constValue = decl.value!!.constValue(program)!!
val constValue = decl.value!!.constValue(program) ?: return noModifications
errors.err("value '${constValue.number}' out of range for ${decl.datatype}", constValue.position)
} else {
val changed = decl.copy(valueDt)
+122 -134
View File
@@ -2,7 +2,6 @@ package prog8tests.compiler
import io.kotest.common.ExperimentalKotest
import io.kotest.core.spec.style.FunSpec
import io.kotest.datatest.withData
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
@@ -146,47 +145,45 @@ private fun verifyIrRegisterCount(result: CompilationResult, expectedCount: Int)
class TestCompilerOnExamplesC64: FunSpec({
@OptIn(ExperimentalKotest::class)
//testExecutionMode = TestExecutionMode.LimitedConcurrency(6)
this.concurrency = 4
val outputDir = tempdir().toPath()
this.threads = 4
val onlyC64 = cartesianProduct(
listOf(
ExampleSizes("balloonflight", c64SizeOptimized=1674, c64SizeUnoptimized=1745),
ExampleSizes("balloonflight", c64SizeOptimized=1674, c64SizeUnoptimized=1737),
ExampleSizes("banking", c64SizeOptimized=680, c64SizeUnoptimized=1451),
ExampleSizes("bdmusic-irq", c64SizeOptimized=1001, c64SizeUnoptimized=1019),
ExampleSizes("bdmusic", c64SizeOptimized=938, c64SizeUnoptimized=1011),
ExampleSizes("charset", c64SizeOptimized=835, c64SizeUnoptimized=860),
ExampleSizes("cube3d-sprites", c64SizeOptimized=2712, c64SizeUnoptimized=2764),
ExampleSizes("bdmusic", c64SizeOptimized=944, c64SizeUnoptimized=1011),
ExampleSizes("charset", c64SizeOptimized=841, c64SizeUnoptimized=860),
ExampleSizes("cube3d-sprites", c64SizeOptimized=2704, c64SizeUnoptimized=2756),
ExampleSizes("fileselector", c64SizeOptimized=3568, c64SizeUnoptimized=4758),
ExampleSizes("library/main", c64SizeOptimized=321, c64SizeUnoptimized=1237),
ExampleSizes("multiplexer", c64SizeOptimized=1356, c64SizeUnoptimized=1507),
ExampleSizes("petswirl", c64SizeOptimized=959, c64SizeUnoptimized=1029),
ExampleSizes("plasma", c64SizeOptimized=1690, c64SizeUnoptimized=1731),
ExampleSizes("petswirl", c64SizeOptimized=953, c64SizeUnoptimized=1021),
ExampleSizes("plasma", c64SizeOptimized=1688, c64SizeUnoptimized=1723),
ExampleSizes("rasterbars", c64SizeOptimized=845, c64SizeUnoptimized=892),
ExampleSizes("simplemultiplexer", c64SizeOptimized=843, c64SizeUnoptimized=914),
ExampleSizes("sprites", c64SizeOptimized=963, c64SizeUnoptimized=1052),
ExampleSizes("simplemultiplexer", c64SizeOptimized=843, c64SizeUnoptimized=902),
ExampleSizes("sprites", c64SizeOptimized=963, c64SizeUnoptimized=1040),
ExampleSizes("starfield", c64SizeOptimized=743, c64SizeUnoptimized=758),
ExampleSizes("turtle-gfx", c64SizeOptimized=2745, c64SizeUnoptimized=2861),
ExampleSizes("wizzine", c64SizeOptimized=1124, c64SizeUnoptimized=1236),
ExampleSizes("turtle-gfx", c64SizeOptimized=2758, c64SizeUnoptimized=2860),
ExampleSizes("wizzine", c64SizeOptimized=1124, c64SizeUnoptimized=1224),
// boingball, cube3d, tehtriz are in BothC64andCx16
),
listOf(false, true)
)
val target = C64Target()
withData(
nameFn = { "${it.first.first.name}: ${target.name}, optimize=${it.first.second}" },
onlyC64.map { it to prepareTestFiles(it.first.name, it.second, target) }
) { (params, prep) ->
val filepath = prep.second
val optimize = params.second
val expectedSize = if (optimize) params.first.c64SizeOptimized else params.first.c64SizeUnoptimized
val result = compileTheThing(filepath, optimize, target, outputDir)
result shouldNotBe null
if (expectedSize > 0) {
verifyOutputFileSize(result!!, expectedSize)
onlyC64.forEach { (params, optimize) ->
val prep = prepareTestFiles(params.name, optimize, target)
test(prep.first) {
val testOutputDir = tempdir().toPath()
val filepath = prep.second
val expectedSize = if (optimize) params.c64SizeOptimized else params.c64SizeUnoptimized
val result = compileTheThing(filepath, optimize, target, testOutputDir)
result shouldNotBe null
if (expectedSize > 0) {
verifyOutputFileSize(result!!, expectedSize)
}
}
}
})
@@ -194,21 +191,19 @@ class TestCompilerOnExamplesC64: FunSpec({
class TestCompilerOnExamplesCx16: FunSpec({
@OptIn(ExperimentalKotest::class)
//testExecutionMode = TestExecutionMode.LimitedConcurrency(6)
this.concurrency = 4
val outputDir = tempdir().toPath()
this.threads = 4
val onlyCx16 = cartesianProduct(
listOf(
ExampleSizes("vtui/testvtui", cx16SizeOptimized=3041, cx16SizeUnoptimized=3041),
ExampleSizes("pcmaudio/play-adpcm", cx16SizeOptimized=36147, cx16SizeUnoptimized=36645),
ExampleSizes("pcmaudio/stream-wav", cx16SizeOptimized=5627, cx16SizeUnoptimized=7863),
ExampleSizes("pcmaudio/stream-simple-aflow", cx16SizeOptimized=1850, cx16SizeUnoptimized=3753),
ExampleSizes("pcmaudio/stream-simple-poll", cx16SizeOptimized=1564, cx16SizeUnoptimized=3462),
ExampleSizes("pcmaudio/play-adpcm", cx16SizeOptimized=36147, cx16SizeUnoptimized=36637),
ExampleSizes("pcmaudio/stream-wav", cx16SizeOptimized=5636, cx16SizeUnoptimized=7878),
ExampleSizes("pcmaudio/stream-simple-aflow", cx16SizeOptimized=1842, cx16SizeUnoptimized=3745),
ExampleSizes("pcmaudio/stream-simple-poll", cx16SizeOptimized=1556, cx16SizeUnoptimized=3454),
ExampleSizes("pcmaudio/vumeter", cx16SizeOptimized=3888, cx16SizeUnoptimized=6381),
ExampleSizes("sprites/dragon", cx16SizeOptimized=1858, cx16SizeUnoptimized=4374),
ExampleSizes("sprites/dragons", cx16SizeOptimized=1511, cx16SizeUnoptimized=3686),
ExampleSizes("sprites/dragons", cx16SizeOptimized=1503, cx16SizeUnoptimized=3678),
ExampleSizes("zsmkit_v1/demo1", cx16SizeOptimized=7546, cx16SizeUnoptimized=7984),
ExampleSizes("zsmkit_v1/demo2", cx16SizeOptimized=708, cx16SizeUnoptimized=2579),
// zsmkit_v1/zsmkit_high and zsmkit_v1/zsmkit_low don't compile (empty source files)
@@ -223,54 +218,54 @@ class TestCompilerOnExamplesCx16: FunSpec({
ExampleSizes("audioroutines", cx16SizeOptimized=948, cx16SizeUnoptimized=1244),
ExampleSizes("automatons", cx16SizeOptimized=1037, cx16SizeUnoptimized=1342),
ExampleSizes("balloonflight", cx16SizeOptimized=3187, cx16SizeUnoptimized=4163),
ExampleSizes("bdmusic", cx16SizeOptimized=2386, cx16SizeUnoptimized=2761),
ExampleSizes("bobs", cx16SizeOptimized=2800, cx16SizeUnoptimized=3414),
ExampleSizes("bdmusic", cx16SizeOptimized=2427, cx16SizeUnoptimized=2792),
ExampleSizes("bobs", cx16SizeOptimized=2800, cx16SizeUnoptimized=3406),
ExampleSizes("bubbleuniverse", cx16SizeOptimized=1450, cx16SizeUnoptimized=1958),
ExampleSizes("charfade", cx16SizeOptimized=1054, cx16SizeUnoptimized=1362),
ExampleSizes("charsets", cx16SizeOptimized=1125, cx16SizeUnoptimized=1417),
ExampleSizes("circles", cx16SizeOptimized=2385, cx16SizeUnoptimized=4547),
ExampleSizes("cobramk3-gfx", cx16SizeOptimized=7705, cx16SizeUnoptimized=10612),
ExampleSizes("circles", cx16SizeOptimized=2384, cx16SizeUnoptimized=4539),
ExampleSizes("cobramk3-gfx", cx16SizeOptimized=7697, cx16SizeUnoptimized=10604),
ExampleSizes("colorbars", cx16SizeOptimized=1606, cx16SizeUnoptimized=1904),
ExampleSizes("cxlogo", cx16SizeOptimized=599, cx16SizeUnoptimized=895),
ExampleSizes("diskspeed", cx16SizeOptimized=6280, cx16SizeUnoptimized=8283),
ExampleSizes("fileseek", cx16SizeOptimized=2468, cx16SizeUnoptimized=4496),
ExampleSizes("floatparse", cx16SizeOptimized=8303, cx16SizeUnoptimized=9190),
ExampleSizes("interpolation", cx16SizeOptimized=3791, cx16SizeUnoptimized=4768),
ExampleSizes("kefrenbars", cx16SizeOptimized=2386, cx16SizeUnoptimized=4654),
ExampleSizes("interpolation", cx16SizeOptimized=3783, cx16SizeUnoptimized=4760),
ExampleSizes("kefrenbars", cx16SizeOptimized=2393, cx16SizeUnoptimized=4654),
ExampleSizes("keyboardhandler", cx16SizeOptimized=412, cx16SizeUnoptimized=717),
ExampleSizes("landscape", cx16SizeOptimized=1673, cx16SizeUnoptimized=1997),
ExampleSizes("landscape", cx16SizeOptimized=1665, cx16SizeUnoptimized=1989),
ExampleSizes("life", cx16SizeOptimized=1320, cx16SizeUnoptimized=1751),
ExampleSizes("mandelbrot", cx16SizeOptimized=1120, cx16SizeUnoptimized=1434),
ExampleSizes("multi-irq-new", cx16SizeOptimized=1073, cx16SizeUnoptimized=1519),
ExampleSizes("multi-irq-old", cx16SizeOptimized=962, cx16SizeUnoptimized=1410),
ExampleSizes("plasma", cx16SizeOptimized=1739, cx16SizeUnoptimized=2066),
ExampleSizes("plasma", cx16SizeOptimized=1731, cx16SizeUnoptimized=2058),
ExampleSizes("rasterbars", cx16SizeOptimized=1268, cx16SizeUnoptimized=1685),
ExampleSizes("serialdownload", cx16SizeOptimized=2471, cx16SizeUnoptimized=3649),
ExampleSizes("showbmx", cx16SizeOptimized=2745, cx16SizeUnoptimized=5269),
ExampleSizes("snow", cx16SizeOptimized=3120, cx16SizeUnoptimized=5295),
ExampleSizes("serialdownload", cx16SizeOptimized=2516, cx16SizeUnoptimized=3945),
ExampleSizes("showbmx", cx16SizeOptimized=2745, cx16SizeUnoptimized=5261),
ExampleSizes("snow", cx16SizeOptimized=3119, cx16SizeUnoptimized=5287),
ExampleSizes("sortingbench", cx16SizeOptimized=2928, cx16SizeUnoptimized=3847),
ExampleSizes("spotlight", cx16SizeOptimized=908, cx16SizeUnoptimized=1364),
ExampleSizes("starszoom", cx16SizeOptimized=2399, cx16SizeUnoptimized=4863),
ExampleSizes("starszoom", cx16SizeOptimized=2406, cx16SizeUnoptimized=4855),
ExampleSizes("test_gfx_hires", cx16SizeOptimized=4835, cx16SizeUnoptimized=7514),
ExampleSizes("test_gfx_lores", cx16SizeOptimized=5841, cx16SizeUnoptimized=7188),
ExampleSizes("testmonogfx", cx16SizeOptimized=10463, cx16SizeUnoptimized=11982),
ExampleSizes("test_gfx_lores", cx16SizeOptimized=5849, cx16SizeUnoptimized=7188),
ExampleSizes("testmonogfx", cx16SizeOptimized=10480, cx16SizeUnoptimized=11974),
ExampleSizes("textspotlight", cx16SizeOptimized=3431, cx16SizeUnoptimized=3788),
),
listOf(false, true)
)
val target = Cx16Target()
withData(
nameFn = { "${it.first.first.name}: ${target.name}, optimize=${it.first.second}" },
onlyCx16.map { it to prepareTestFiles(it.first.name, it.second, target) }
) { (params, prep) ->
val filepath = prep.second
val optimize = params.second
val expectedSize = if (optimize) params.first.cx16SizeOptimized else params.first.cx16SizeUnoptimized
val result = compileTheThing(filepath, optimize, target, outputDir)
result shouldNotBe null
if (expectedSize > 0) {
verifyOutputFileSize(result!!, expectedSize)
onlyCx16.forEach { (params, optimize) ->
val prep = prepareTestFiles(params.name, optimize, target)
test(prep.first) {
val testOutputDir = tempdir().toPath()
val filepath = prep.second
val expectedSize = if (optimize) params.cx16SizeOptimized else params.cx16SizeUnoptimized
val result = compileTheThing(filepath, optimize, target, testOutputDir)
result shouldNotBe null
if (expectedSize > 0) {
verifyOutputFileSize(result!!, expectedSize)
}
}
}
})
@@ -278,102 +273,97 @@ class TestCompilerOnExamplesCx16: FunSpec({
class TestCompilerOnExamplesBothC64andCx16: FunSpec({
@OptIn(ExperimentalKotest::class)
//testExecutionMode = TestExecutionMode.LimitedConcurrency(6)
this.concurrency = 4
val outputDir = tempdir().toPath()
this.threads = 4
val bothCx16AndC64 = cartesianProduct(
listOf(
ExampleSizes("balls", c64SizeOptimized=885, c64SizeUnoptimized=919, cx16SizeOptimized=874, cx16SizeUnoptimized=1188),
ExampleSizes("boingball", c64SizeOptimized=1478, c64SizeUnoptimized=1544, cx16SizeOptimized=1486, cx16SizeUnoptimized=1957),
ExampleSizes("cube3d", c64SizeOptimized=2444, c64SizeUnoptimized=2509, cx16SizeOptimized=2381, cx16SizeUnoptimized=2712),
ExampleSizes("cube3d-float", c64SizeOptimized=2619, c64SizeUnoptimized=2668, cx16SizeOptimized=2511, cx16SizeUnoptimized=2835),
ExampleSizes("cube3d-gfx", c64SizeOptimized=3536, c64SizeUnoptimized=3589, cx16SizeOptimized=2206, cx16SizeUnoptimized=2522),
ExampleSizes("boingball", c64SizeOptimized=1513, c64SizeUnoptimized=1579, cx16SizeOptimized=1486, cx16SizeUnoptimized=1957),
ExampleSizes("cube3d", c64SizeOptimized=2436, c64SizeUnoptimized=2501, cx16SizeOptimized=2373, cx16SizeUnoptimized=2704),
ExampleSizes("cube3d-float", c64SizeOptimized=2654, c64SizeUnoptimized=2703, cx16SizeOptimized=2511, cx16SizeUnoptimized=2835),
ExampleSizes("cube3d-gfx", c64SizeOptimized=3528, c64SizeUnoptimized=3581, cx16SizeOptimized=2198, cx16SizeUnoptimized=2514),
ExampleSizes("dirlist", c64SizeOptimized=1543, c64SizeUnoptimized=2405, cx16SizeOptimized=1525, cx16SizeUnoptimized=3372),
ExampleSizes("fibonacci", c64SizeOptimized=570, c64SizeUnoptimized=585, cx16SizeOptimized=626, cx16SizeUnoptimized=925),
ExampleSizes("fractal-tree", c64SizeOptimized=2808, c64SizeUnoptimized=2845, cx16SizeOptimized=1220, cx16SizeUnoptimized=1511),
ExampleSizes("fractal-tree", c64SizeOptimized=2832, c64SizeUnoptimized=2869, cx16SizeOptimized=1220, cx16SizeUnoptimized=1511),
ExampleSizes("maze", c64SizeOptimized=3088, c64SizeUnoptimized=3189, cx16SizeOptimized=2873, cx16SizeUnoptimized=3251),
ExampleSizes("mandelbrot", c64SizeOptimized=1138, c64SizeUnoptimized=1172, cx16SizeOptimized=1120, cx16SizeUnoptimized=1434),
ExampleSizes("mandelbrot-gfx", c64SizeOptimized=1457, c64SizeUnoptimized=1479, cx16SizeOptimized=957, cx16SizeUnoptimized=1250),
ExampleSizes("mandelbrot", c64SizeOptimized=1173, c64SizeUnoptimized=1207, cx16SizeOptimized=1120, cx16SizeUnoptimized=1434),
ExampleSizes("mandelbrot-gfx", c64SizeOptimized=1492, c64SizeUnoptimized=1514, cx16SizeOptimized=957, cx16SizeUnoptimized=1250),
ExampleSizes("multitasking", c64SizeOptimized=1956, c64SizeUnoptimized=2029, cx16SizeOptimized=1826, cx16SizeUnoptimized=2159),
ExampleSizes("numbergame", c64SizeOptimized=1034, c64SizeUnoptimized=1050, cx16SizeOptimized=1079, cx16SizeUnoptimized=1382),
ExampleSizes("primes", c64SizeOptimized=520, c64SizeUnoptimized=541, cx16SizeOptimized=565, cx16SizeUnoptimized=870),
ExampleSizes("queens", c64SizeOptimized=1061, c64SizeUnoptimized=1093, cx16SizeOptimized=1111, cx16SizeUnoptimized=1427),
ExampleSizes("queens", c64SizeOptimized=1053, c64SizeUnoptimized=1085, cx16SizeOptimized=1103, cx16SizeUnoptimized=1419),
ExampleSizes("screencodes", c64SizeOptimized=667, c64SizeUnoptimized=687, cx16SizeOptimized=655, cx16SizeUnoptimized=959),
ExampleSizes("swirl", c64SizeOptimized=828, c64SizeUnoptimized=831, cx16SizeOptimized=923, cx16SizeUnoptimized=1208),
ExampleSizes("swirl-float", c64SizeOptimized=708, c64SizeUnoptimized=735, cx16SizeOptimized=595, cx16SizeUnoptimized=906),
ExampleSizes("tehtriz", c64SizeOptimized=4333, c64SizeUnoptimized=4504, cx16SizeOptimized=6211, cx16SizeUnoptimized=6742),
ExampleSizes("textelite", c64SizeOptimized=11440, c64SizeUnoptimized=12423, cx16SizeOptimized=10778, cx16SizeUnoptimized=13046),
ExampleSizes("swirl-float", c64SizeOptimized=732, c64SizeUnoptimized=759, cx16SizeOptimized=595, cx16SizeUnoptimized=906),
ExampleSizes("tehtriz", c64SizeOptimized=4331, c64SizeUnoptimized=4496, cx16SizeOptimized=6244, cx16SizeUnoptimized=6765),
ExampleSizes("textelite", c64SizeOptimized=11428, c64SizeUnoptimized=12411, cx16SizeOptimized=10770, cx16SizeUnoptimized=13038),
ExampleSizes("pointers/animalgame", c64SizeOptimized=1570, c64SizeUnoptimized=2126, cx16SizeOptimized=1605, cx16SizeUnoptimized=2412),
ExampleSizes("pointers/binarytree", c64SizeOptimized=2562, c64SizeUnoptimized=2640, cx16SizeOptimized=2085, cx16SizeUnoptimized=2389),
ExampleSizes("pointers/hashtable", c64SizeOptimized=3294, c64SizeUnoptimized=4140, cx16SizeOptimized=2832, cx16SizeUnoptimized=3786),
ExampleSizes("pointers/sortedlist", c64SizeOptimized=1274, c64SizeUnoptimized=1319, cx16SizeOptimized=1223, cx16SizeUnoptimized=1532),
ExampleSizes("pointers/sortedlist", c64SizeOptimized=1262, c64SizeUnoptimized=1307, cx16SizeOptimized=1215, cx16SizeUnoptimized=1524),
ExampleSizes("pointers/sorting", c64SizeOptimized=1776, c64SizeUnoptimized=2676, cx16SizeOptimized=1788, cx16SizeUnoptimized=2659),
),
listOf(false, true),
listOf(C64Target(), Cx16Target())
)
withData(
nameFn = { "${it.first.name}: ${it.third.first.name}, optimize=${it.second}" },
bothCx16AndC64.map {
val prep = prepareTestFiles(it.first.name, it.second, it.third)
Triple(it.first, it.second, Triple(it.third, prep.first, prep.second))
}
) { params ->
val filepath = params.third.third
val optimize = params.second
val target = params.third.first
val exampleSizes = params.first
val expectedSize = when (target) {
is C64Target -> if (optimize) exampleSizes.c64SizeOptimized else exampleSizes.c64SizeUnoptimized
is Cx16Target -> if (optimize) exampleSizes.cx16SizeOptimized else exampleSizes.cx16SizeUnoptimized
else -> 0
}
val result = compileTheThing(filepath, optimize, target, outputDir)
result shouldNotBe null
if (expectedSize > 0) {
verifyOutputFileSize(result!!, expectedSize)
bothCx16AndC64.forEach { (exampleSizes, optimize, target) ->
val prep = prepareTestFiles(exampleSizes.name, optimize, target)
test(prep.first) {
val testOutputDir = tempdir().toPath()
val filepath = prep.second
val expectedSize = when (target) {
is C64Target -> if (optimize) exampleSizes.c64SizeOptimized else exampleSizes.c64SizeUnoptimized
is Cx16Target -> if (optimize) exampleSizes.cx16SizeOptimized else exampleSizes.cx16SizeUnoptimized
else -> 0
}
val result = compileTheThing(filepath, optimize, target, testOutputDir)
result shouldNotBe null
if (expectedSize > 0) {
verifyOutputFileSize(result!!, expectedSize)
}
}
}
})
class TestCompilerOnExamplesVirtual: FunSpec({
val outputDir = tempdir().toPath()
@OptIn(ExperimentalKotest::class)
this.concurrency = 4
this.threads = 4
val onlyVirtual = cartesianProduct(
listOf(
ExampleSizes("bouncegfx", virtualInstrCountOptimized=186, virtualInstrCountUnoptimized=824, virtualRegCountOptimized=92, virtualRegCountUnoptimized=380),
ExampleSizes("bsieve", virtualInstrCountOptimized=158, virtualInstrCountUnoptimized=1939, virtualRegCountOptimized=66, virtualRegCountUnoptimized=860),
ExampleSizes("fountain", virtualInstrCountOptimized=150, virtualInstrCountUnoptimized=791, virtualRegCountOptimized=75, virtualRegCountUnoptimized=363),
ExampleSizes("pixelshader", virtualInstrCountOptimized=43, virtualInstrCountUnoptimized=286, virtualRegCountOptimized=15, virtualRegCountUnoptimized=109),
ExampleSizes("sincos", virtualInstrCountOptimized=276, virtualInstrCountUnoptimized=898, virtualRegCountOptimized=133, virtualRegCountUnoptimized=403),
ExampleSizes("pointers/animalgame", virtualInstrCountOptimized=221, virtualInstrCountUnoptimized=2083, virtualRegCountOptimized=103, virtualRegCountUnoptimized=933),
ExampleSizes("pointers/binarytree", virtualInstrCountOptimized=510, virtualInstrCountUnoptimized=2237, virtualRegCountOptimized=251, virtualRegCountUnoptimized=1020),
ExampleSizes("pointers/hashtable", virtualInstrCountOptimized=578, virtualInstrCountUnoptimized=2255, virtualRegCountOptimized=293, virtualRegCountUnoptimized=1045),
ExampleSizes("pointers/sortedlist", virtualInstrCountOptimized=267, virtualInstrCountUnoptimized=2406, virtualRegCountOptimized=118, virtualRegCountUnoptimized=1086),
ExampleSizes("pointers/fountain-virtual", virtualInstrCountOptimized=145, virtualInstrCountUnoptimized=785, virtualRegCountOptimized=70, virtualRegCountUnoptimized=357),
ExampleSizes("pointers/sorting", virtualInstrCountOptimized=369, virtualInstrCountUnoptimized=2251, virtualRegCountOptimized=173, virtualRegCountUnoptimized=1010)
ExampleSizes("bouncegfx", virtualInstrCountOptimized=186, virtualInstrCountUnoptimized=846, virtualRegCountOptimized=92, virtualRegCountUnoptimized=386),
ExampleSizes("bsieve", virtualInstrCountOptimized=158, virtualInstrCountUnoptimized=1961, virtualRegCountOptimized=66, virtualRegCountUnoptimized=866),
ExampleSizes("fountain", virtualInstrCountOptimized=150, virtualInstrCountUnoptimized=813, virtualRegCountOptimized=75, virtualRegCountUnoptimized=369),
ExampleSizes("pixelshader", virtualInstrCountOptimized=43, virtualInstrCountUnoptimized=308, virtualRegCountOptimized=15, virtualRegCountUnoptimized=115),
ExampleSizes("sincos", virtualInstrCountOptimized=276, virtualInstrCountUnoptimized=920, virtualRegCountOptimized=133, virtualRegCountUnoptimized=409),
ExampleSizes("pointers/animalgame", virtualInstrCountOptimized=221, virtualInstrCountUnoptimized=2105, virtualRegCountOptimized=103, virtualRegCountUnoptimized=939),
ExampleSizes("pointers/binarytree", virtualInstrCountOptimized=510, virtualInstrCountUnoptimized=2259, virtualRegCountOptimized=251, virtualRegCountUnoptimized=1026),
ExampleSizes("pointers/hashtable", virtualInstrCountOptimized=578, virtualInstrCountUnoptimized=2277, virtualRegCountOptimized=293, virtualRegCountUnoptimized=1051),
ExampleSizes("pointers/sortedlist", virtualInstrCountOptimized=267, virtualInstrCountUnoptimized=2428, virtualRegCountOptimized=118, virtualRegCountUnoptimized=1092),
ExampleSizes("pointers/fountain-virtual", virtualInstrCountOptimized=145, virtualInstrCountUnoptimized=807, virtualRegCountOptimized=70, virtualRegCountUnoptimized=363),
ExampleSizes("pointers/sorting", virtualInstrCountOptimized=369, virtualInstrCountUnoptimized=2273, virtualRegCountOptimized=173, virtualRegCountUnoptimized=1016)
),
listOf(false, true)
)
val target = VMTarget()
withData(
nameFn = { "${it.first.first.name}: ${target.name}, optimize=${it.first.second}" },
onlyVirtual.map { it to prepareTestFiles(it.first.name, it.second, target) }
) { (params, prep) ->
val filepath = prep.second
val optimize = params.second
val expectedInstrCount = if (optimize) params.first.virtualInstrCountOptimized else params.first.virtualInstrCountUnoptimized
val expectedRegCount = if (optimize) params.first.virtualRegCountOptimized else params.first.virtualRegCountUnoptimized
val result = compileTheThing(filepath, optimize, target, outputDir)
result shouldNotBe null
if (expectedInstrCount > 0) {
verifyIrInstructionCount(result!!, expectedInstrCount)
verifyIrRegisterCount(result, expectedRegCount)
onlyVirtual.forEach { (params, optimize) ->
val prep = prepareTestFiles(params.name, optimize, target)
test(prep.first) {
val testOutputDir = tempdir().toPath()
val filepath = prep.second
val expectedInstrCount = if (optimize) params.virtualInstrCountOptimized else params.virtualInstrCountUnoptimized
val expectedRegCount = if (optimize) params.virtualRegCountOptimized else params.virtualRegCountUnoptimized
val result = compileTheThing(filepath, optimize, target, testOutputDir)
result shouldNotBe null
if (expectedInstrCount > 0) {
verifyIrInstructionCount(result!!, expectedInstrCount)
verifyIrRegisterCount(result, expectedRegCount)
}
}
}
})
@@ -381,31 +371,29 @@ class TestCompilerOnExamplesVirtual: FunSpec({
class TestCompilerOnExamplesPET32: FunSpec({
@OptIn(ExperimentalKotest::class)
//testExecutionMode = TestExecutionMode.LimitedConcurrency(6)
this.concurrency = 4
val outputDir = tempdir().toPath()
this.threads = 4
val onlyPET32 = cartesianProduct(
listOf(
ExampleSizes("boingball", pet32SizeOptimized=1497, pet32SizeUnoptimized=1586),
ExampleSizes("petswirl", pet32SizeOptimized=915, pet32SizeUnoptimized=985),
ExampleSizes("boingball", pet32SizeOptimized=1532, pet32SizeUnoptimized=1621),
ExampleSizes("petswirl", pet32SizeOptimized=909, pet32SizeUnoptimized=977),
),
listOf(false, true)
)
val target = PETTarget()
withData(
nameFn = { "${it.first.first.name}: ${target.name}, optimize=${it.first.second}" },
onlyPET32.map { it to prepareTestFiles(it.first.name, it.second, target) }
) { (params, prep) ->
val filepath = prep.second
val optimize = params.second
val expectedSize = if (optimize) params.first.pet32SizeOptimized else params.first.pet32SizeUnoptimized
val result = compileTheThing(filepath, optimize, target, outputDir)
result shouldNotBe null
if (expectedSize > 0) {
verifyOutputFileSize(result!!, expectedSize)
onlyPET32.forEach { (params, optimize) ->
val prep = prepareTestFiles(params.name, optimize, target)
test(prep.first) {
val testOutputDir = tempdir().toPath()
val filepath = prep.second
val expectedSize = if (optimize) params.pet32SizeOptimized else params.pet32SizeUnoptimized
val result = compileTheThing(filepath, optimize, target, testOutputDir)
result shouldNotBe null
if (expectedSize > 0) {
verifyOutputFileSize(result!!, expectedSize)
}
}
}
})
+126
View File
@@ -0,0 +1,126 @@
package prog8tests.compiler
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.types.instanceOf
import prog8.code.ast.*
import prog8.code.target.Cx16Target
import prog8.code.target.VMTarget
import prog8tests.helpers.compileText
class TestDefers : FunSpec({
val outputDir = tempdir().toPath()
test("defer syntactic sugaring") {
val src = """
main {
sub start() {
void test()
}
sub test() -> uword {
defer {
cx16.r0++
cx16.r1++
}
if cx16.r0==0 {
defer cx16.r1++
}
if cx16.r0==0
return cx16.r0+cx16.r1
defer cx16.r2++
return 999
}
}"""
val result = compileText(Cx16Target(), optimize = true, src, outputDir, writeAssembly = true)!!
val main = result.codegenAst!!.allBlocks().single { it.name == "p8b_main" }
val sub = main.children[1] as PtSub
sub.scopedName shouldBe "p8b_main.p8s_test"
// check the desugaring of the defer statements
sub.children[0] shouldBe instanceOf<PtSubSignature>()
(sub.children[1] as PtVariable).name shouldBe "p8v_prog8_defers_mask"
val firstDefer = sub.children[3] as PtAugmentedAssign
firstDefer.operator shouldBe "|="
firstDefer.target.identifier?.name shouldBe "p8b_main.p8s_test.p8v_prog8_defers_mask"
firstDefer.value.asConstInteger() shouldBe 4
val firstIf = sub.children[4] as PtIfElse
val deferInIf = firstIf.ifScope.children[0] as PtAugmentedAssign
deferInIf.operator shouldBe "|="
deferInIf.target.identifier?.name shouldBe "p8b_main.p8s_test.p8v_prog8_defers_mask"
deferInIf.value.asConstInteger() shouldBe 2
val lastDefer = sub.children[6] as PtAugmentedAssign
lastDefer.operator shouldBe "|="
lastDefer.target.identifier?.name shouldBe "p8b_main.p8s_test.p8v_prog8_defers_mask"
lastDefer.value.asConstInteger() shouldBe 1
val ifelse = sub.children[5] as PtIfElse
val ifscope = ifelse.ifScope.children[0] as PtNodeGroup
val ifscope_push = ifscope.children[0] as PtFunctionCall
val ifscope_defer = ifscope.children[1] as PtFunctionCall
val ifscope_return = ifscope.children[2] as PtReturn
ifscope_defer.name shouldBe "p8b_main.p8s_test.p8s_prog8_invoke_defers"
ifscope_push.name shouldBe "pushw"
(ifscope_return.children.single() as PtFunctionCall).name shouldBe "popw"
val ending = sub.children[7] as PtFunctionCall
ending.name shouldBe "p8b_main.p8s_test.p8s_prog8_invoke_defers"
sub.children[8] shouldBe instanceOf<PtReturn>()
val handler = sub.children[9] as PtSub
handler.name shouldBe "p8s_prog8_invoke_defers"
}
test("defer blocks prevent tail call optimization") {
val src = """
main {
sub start() {
void test()
}
sub test() {
defer {
cx16.r0 = 1
}
return other()
}
sub other() {
cx16.r1 = 2
}
}"""
// We compile for Cx16Target because tail call optimization (TCO) is a 6502 backend optimization
// (actually it's in StatementReorderer which runs before codegen, but it targets JMP vs JSR)
val result = compileText(Cx16Target(), optimize = true, src, outputDir, writeAssembly = true)!!
val main = result.codegenAst!!.allBlocks().single { it.name == "p8b_main" }
val testSub = main.children.filterIsInstance<PtSub>().find { it.scopedName == "p8b_main.p8s_test" }!!
// If TCO happened, we would see a PtJump to other instead of a PtFunctionCall to other.
// But with defer, it should remain a PtFunctionCall (or rather, it's inside a node group that calls invoke_defers)
// Let's inspect the children of testSub.
// It should have: signature, mask var, initialize mask, set mask bit, the call to other, call to invoke_defers, return.
fun collectCalls(node: PtNode): List<PtFunctionCall> {
val calls = mutableListOf<PtFunctionCall>()
if (node is PtFunctionCall) calls.add(node)
node.children.forEach { calls.addAll(collectCalls(it)) }
return calls
}
val callToOther = collectCalls(testSub).find { it.name == "p8b_main.p8s_other" }
callToOther shouldNotBe null
// Verify it is NOT a PtJump (which would be used for tail call optimization)
// Actually, TCO in StatementReorderer converts return other() into a Goto.
// In PtSub, a Goto becomes a PtJump.
testSub.children.any { it is PtJump && (it.target as? PtIdentifier)?.name == "p8b_main.p8s_other" } shouldBe false
}
})
@@ -7,6 +7,7 @@ import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldStartWith
import java.nio.file.Path
import prog8.code.INTERNED_STRINGS_MODULENAME
import prog8.code.core.ZeropageType
import prog8.code.target.C64Target
@@ -96,7 +97,7 @@ main {
val filepath = outputDir.resolve("$filenameBase.p8")
filepath.toFile().writeText(sourceText)
val (program, options, importedfiles) = parseMainModule(filepath, errors, C64Target(), emptyList(), emptyList(), false, false)
val (program, options, importedfiles) = parseMainModule(filepath, errors, C64Target(), emptyList(), emptyList(), Path.of("."), false, false)
program.toplevelModule.name shouldBe filenameBase
withClue("all imports other than the test source must have been internal resources library files") {
+1 -1
View File
@@ -29,7 +29,7 @@ class TestModuleImporter: FunSpec({
}
fun makeImporter(errors: IErrorReporter? = null, searchIn: Iterable<String>) =
ModuleImporter(program, "blah", errors ?: ErrorReporterForTests(false), searchIn.toList(), emptyList(), false)
ModuleImporter(program, "blah", errors ?: ErrorReporterForTests(false), searchIn.toList(), emptyList(), Path(".").absolute(), false)
fun makeImporter(errors: IErrorReporter?, vararg searchIn: String): ModuleImporter {
return makeImporter(errors, searchIn.asList())
+296 -20
View File
@@ -1,5 +1,6 @@
package prog8tests.compiler
import io.kotest.assertions.fail
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
@@ -14,6 +15,7 @@ import prog8.ast.ParentSentinel
import prog8.ast.Program
import prog8.ast.expressions.*
import prog8.ast.statements.*
import prog8.ast.walk.IAstVisitor
import prog8.code.ast.*
import prog8.code.core.BaseDataType
import prog8.code.core.DataType
@@ -275,9 +277,9 @@ other {
}
"""
val result = compileText(C64Target(), optimize=true, src, outputDir, writeAssembly=false)!!
result.compilerAst.entrypoint.statements.size shouldBe 6
result.compilerAst.entrypoint.statements.size shouldBe 9
val alldecls = result.compilerAst.entrypoint.allDefinedSymbols.toList()
alldecls.map { it.first } shouldBe listOf("unused_but_shared", "usedvar")
alldecls.map { it.first } shouldBe listOf("unused_but_shared", "usedvar_only_written", "usedvar")
}
test("unused variable removal from subscope") {
@@ -1231,18 +1233,26 @@ main {
val result = compileText(Cx16Target(), true, src, outputDir, writeAssembly = false)!!
val st = result.compilerAst.entrypoint.statements
st.size shouldBe 4
val ifCond1 = (st[0] as IfElse).condition as BinaryExpression
val ifCond2 = (st[1] as IfElse).condition as BinaryExpression
val ifCond3 = (st[2] as IfElse).condition as BinaryExpression
(ifCond1.left as FunctionCallExpression).target.nameInSource shouldBe listOf("f_open")
(ifCond2.left as FunctionCallExpression).target.nameInSource shouldBe listOf("f_open")
(ifCond3.left as FunctionCallExpression).target.nameInSource shouldBe listOf("f_open")
val right1 = ifCond1.right as BinaryExpression
val right2 = ifCond2.right as BinaryExpression
val right3 = ifCond3.right as BinaryExpression
(right1.left as FunctionCallExpression).target.nameInSource shouldBe listOf("f_read")
(right2.left as FunctionCallExpression).target.nameInSource shouldBe listOf("f_read")
(right3.left as FunctionCallExpression).target.nameInSource shouldBe listOf("f_read")
fun assertCondition(cond: Expression) {
fun findFunctionCalls(expr: Expression): List<FunctionCallExpression> {
return when (expr) {
is FunctionCallExpression -> listOf(expr)
is BinaryExpression -> findFunctionCalls(expr.left) + findFunctionCalls(expr.right)
is ContainmentCheck -> findFunctionCalls(expr.element) + findFunctionCalls(expr.iterable)
else -> emptyList()
}
}
val calls = findFunctionCalls(cond)
if (calls.isNotEmpty()) {
calls.any { it.target.nameInSource == listOf("f_open") } shouldBe true
calls.any { it.target.nameInSource == listOf("f_read") } shouldBe true
}
}
assertCondition((st[0] as IfElse).condition)
assertCondition((st[1] as IfElse).condition)
assertCondition((st[2] as IfElse).condition)
}
test("eliminate same target register assignments") {
@@ -1665,7 +1675,7 @@ main {
idAssigns.size shouldBe 3
}
test("inline void call with one parameter") {
xtest("inline void call with one parameter") {
// Tests that void calls with one parameter are inlined when args are simple
val src = """
main {
@@ -1689,7 +1699,7 @@ main {
hasVoidCall shouldBe false
}
test("inline void call with two parameters") {
xtest("inline void call with two parameters") {
// Tests that void calls with two parameters are inlined when args are simple
val src = """
main {
@@ -1713,7 +1723,7 @@ main {
hasVoidCall shouldBe false
}
test("inline call with one return value and one parameter") {
xtest("inline call with one return value and one parameter") {
// Tests that function calls returning one value with one parameter are inlined
// and that the parameter is correctly substituted with the argument
val src = """
@@ -1743,7 +1753,7 @@ main {
binExpr.right shouldBe instanceOf<NumericLiteral>()
}
test("inline call with two return values and two parameters") {
xtest("inline call with two return values and two parameters") {
// Tests that function calls returning two values with two parameters are inlined
// and that both parameters are correctly substituted
val src = """
@@ -1786,7 +1796,37 @@ main {
}
}
test("inline void call with six parameters") {
test("call with two parameters where one has a register alias is not inlined") {
// Tests that a function call with two parameters where one has a register alias is NOT inlined
val src = """
main {
ubyte @shared v1 = 10
ubyte @shared v2 = 20
sub start() {
ubyte a
a = get_two(1, 2)
cx16.r0 = a
}
sub get_two(ubyte x, ubyte y @R0) -> ubyte {
return v1
}
}"""
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
val startSub = result.compilerAst.entrypoint
val stmts = startSub.statements
// Should NOT be inlined, so the FunctionCallExpression should still exist in start()
val callNodes = stmts.filter { stmt ->
if (stmt is Assignment) stmt.value is FunctionCallExpression && (stmt.value as FunctionCallExpression).target.nameInSource.last() == "get_two"
else false
}
// There should be exactly one call remaining
callNodes.size shouldBe 1
}
xtest("inline void call with six parameters") {
// Tests that void calls with six parameters are inlined when args are simple
val src = """
main {
@@ -1803,13 +1843,55 @@ main {
// Void call should be removed entirely
val hasVoidCall = startSub.statements.any { stmt ->
stmt is FunctionCallStatement &&
stmt is FunctionCallStatement &&
stmt.target.nameInSource.last() == "take_six" &&
stmt.void
}
hasVoidCall shouldBe false
}
test("parameterized subroutine with 'inline' is NOT inlined") {
val src = """
main {
sub start() {
foo(1)
}
inline sub foo(ubyte x) {
cx16.r0 = x
}
}"""
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
val startSub = result.compilerAst.entrypoint
// Should still contain a function call to foo
val hasFooCall = startSub.statements.any { stmt ->
stmt is FunctionCallStatement &&
stmt.target.nameInSource.last() == "foo"
}
hasFooCall shouldBe true
}
test("parameterized subroutine WITHOUT 'inline' is NOT auto-inlined") {
val src = """
main {
sub start() {
foo(1)
}
sub foo(ubyte x) {
cx16.r0 = x
}
}"""
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
val startSub = result.compilerAst.entrypoint
// Should still contain a function call to foo
val hasFooCall = startSub.statements.any { stmt ->
stmt is FunctionCallStatement &&
stmt.target.nameInSource.last() == "foo"
}
hasFooCall shouldBe true
}
test("memory-mapped IO reads should not be eliminated (c64)") {
val src = $$"""
@@ -1992,4 +2074,198 @@ main {
(y3.left as PtPrefix).operator shouldBe "-"
(y3.right as PtNumber).number shouldBe 3.0
}
test("self-referential subroutine is not inlined (would cause infinite loop)") {
val src = """
main {
sub start() {
void self_ref()
cx16.r0++
}
sub self_ref() {
self_ref()
}
}
"""
val result = compileText(VMTarget(), true, src, outputDir)!!
val startSub = result.compilerAst.entrypoint
// The self-referential call should NOT be inlined - verify the function call still exists
val hasSelfRefCall = startSub.statements.any { stmt ->
stmt is FunctionCallStatement && stmt.target.nameInSource.lastOrNull() == "self_ref"
}
hasSelfRefCall shouldBe true
}
test("string literals in struct instances are not deduplicated") {
val src = """
main {
struct Node {
str name
}
^^Node n1 = ^^Node:["test"]
^^Node n2 = ^^Node:["test"]
sub start() {
cx16.r0 = n1^^.name
cx16.r1 = n2^^.name
}
}
"""
val errors = ErrorReporterForTests()
val result = compileText(VMTarget(), true, src, outputDir, errors = errors)
if (result == null) {
println(errors.errors)
fail("Compilation failed: " + errors.errors)
}
class InitializerCollector : IAstVisitor {
val initializers = mutableListOf<StaticStructInitializer>()
override fun visit(initializer: StaticStructInitializer) {
initializers.add(initializer)
// Default traversal
initializer.structname.accept(this)
initializer.args.forEach { it.accept(this) }
}
}
val collector = InitializerCollector()
result.compilerAst.modules.forEach { it.accept(collector) }
collector.initializers.size shouldBe 2
// The first argument is the string (the "name" field)
fun getIdentifier(expr: Expression): IdentifierReference {
return when (expr) {
is IdentifierReference -> expr
is AddressOf -> expr.identifier!!
else -> fail("Expected IdentifierReference or AddressOf, got " + expr::class.simpleName)
}
}
val name1 = getIdentifier(collector.initializers[0].args[0])
val name2 = getIdentifier(collector.initializers[1].args[0])
// They should point to different interned strings
name1.nameInSource shouldNotBe name2.nameInSource
}
test("signed modulo strength reduction") {
val src = """
main {
sub start() {
byte @shared x = -5
byte @shared res = x % 8
}
}
""".trimIndent()
val result = compileText(VMTarget(), true, src, outputDir)!!
val sub = result.codegenAst!!.entrypoint()!!
val assignments = sub.children.filterIsInstance<IPtAssignment>()
val resAssign = assignments.find { it.target.identifier?.name?.contains("res") == true }
?: fail("Could not find assignment to 'res'")
val resMod = resAssign.value
resMod shouldBe instanceOf<PtBinaryExpression>()
(resMod as PtBinaryExpression).operator shouldBe "%"
}
test("unsigned comparison wraparound") {
val src = """
main {
sub start() {
ubyte @shared x = 10
ubyte @shared y = 0
bool @shared b1 = x <= (y - 1)
ubyte @shared y2 = 255
bool @shared b2 = x >= (y2 + 1)
}
}
""".trimIndent()
val result = compileText(VMTarget(), true, src, outputDir)!!
val sub = result.codegenAst!!.entrypoint()!!
val assignments = sub.children.filterIsInstance<IPtAssignment>()
val b1Assign = assignments.find { it.target.identifier?.name?.contains("b1") == true }
?: fail("Could not find assignment to 'b1'")
val b1 = b1Assign.value as PtBinaryExpression
// Might be swapped to (y-1) >= x
if (b1.operator == "<=") {
(b1.left as PtIdentifier).name shouldContain "x"
} else {
b1.operator shouldBe ">="
(b1.right as PtIdentifier).name shouldContain "x"
}
val b2Assign = assignments.find { it.target.identifier?.name?.contains("b2") == true }
?: fail("Could not find assignment to 'b2'")
val b2 = b2Assign.value as PtBinaryExpression
// Might be swapped to (y2+1) <= x
if (b2.operator == ">=") {
(b2.left as PtIdentifier).name shouldContain "x"
} else {
b2.operator shouldBe "<="
(b2.right as PtIdentifier).name shouldContain "x"
}
}
test("identity folding with side effects (volatility)") {
val src = """
main {
sub start() {
bool @shared b = peek(${'$'}d012) == peek(${'$'}d012)
}
}
""".trimIndent()
val errors = ErrorReporterForTests()
val result = compileText(C64Target(), true, src, outputDir, errors = errors)
if (result == null) {
println(errors.errors)
fail("Compilation failed: " + errors.errors)
}
val sub = result.codegenAst!!.entrypoint()!!
val assignments = sub.children.filterIsInstance<IPtAssignment>()
val bAssign = assignments.find { it.target.identifier?.name?.contains("b") == true }
?: fail("Could not find assignment to 'b'")
val b = bAssign.value
// Should not be folded to 'true'
b shouldBe instanceOf<PtBinaryExpression>()
(b as PtBinaryExpression).operator shouldBe "=="
b.left shouldBe instanceOf<PtMemoryByte>()
b.right shouldBe instanceOf<PtMemoryByte>()
}
test("floating-point algebraic identities") {
val src = """
%option enable_floats
main {
sub start() {
float @shared f = 0.0
float @shared res1 = f * 0.0
float @shared res2 = 0.0 * f
float @shared res3 = 0.0 / f
}
}
""".trimIndent()
val errors = ErrorReporterForTests()
val result = compileText(VMTarget(), true, src, outputDir, errors = errors)
if (result == null) {
println(errors.errors)
fail("Compilation failed: " + errors.errors)
}
val sub = result.codegenAst!!.entrypoint()!!
val assignments = sub.children.filterIsInstance<IPtAssignment>()
val res1Assign = assignments.find { it.target.identifier?.name?.contains("res1") == true }!!
res1Assign.value shouldBe instanceOf<PtBinaryExpression>()
(res1Assign.value as PtBinaryExpression).operator shouldBe "*"
val res2Assign = assignments.find { it.target.identifier?.name?.contains("res2") == true }!!
res2Assign.value shouldBe instanceOf<PtBinaryExpression>()
(res2Assign.value as PtBinaryExpression).operator shouldBe "*"
val res3Assign = assignments.find { it.target.identifier?.name?.contains("res3") == true }!!
res3Assign.value shouldBe instanceOf<PtBinaryExpression>()
(res3Assign.value as PtBinaryExpression).operator shouldBe "/"
}
})
+4 -10
View File
@@ -2693,19 +2693,13 @@ main {
}"""
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = false)
val st = result!!.compilerAst.entrypoint.statements
st.size shouldBe 7
st.size shouldBe 9
val a1 = st[3] as Assignment
a1.target.identifier!!.nameInSource shouldBe listOf("temp")
val v1 = a1.value as BinaryExpression
v1.operator shouldBe "+"
(v1.left as TypecastExpression).type shouldBe DataType.UWORD
v1.right shouldBe instanceOf<NumericLiteral>()
val a2 = st[5] as Assignment
a1.value shouldBe instanceOf<IdentifierReference>()
val a2 = st[6] as Assignment
a2.target.identifier!!.nameInSource shouldBe listOf("temp")
val v2 = a2.value as BinaryExpression
v2.operator shouldBe "+"
v2.left shouldBe instanceOf<IdentifierReference>()
v2.right shouldBe instanceOf<NumericLiteral>()
a2.value shouldBe instanceOf<IdentifierReference>()
}
test("indexed pointer derefs") {
+85
View File
@@ -0,0 +1,85 @@
%encoding iso
%import textio
%zeropage basicsafe
main {
sub start() {
txt.iso()
; test divmod with uword values (hits funcDivmodW -> divmod_uw_asm, remainder via R15)
check_divmod(40000, 500, 80, 0)
check_divmod(43211, 2, 21605, 1)
check_divmod(65535, 1000, 65, 535)
check_divmod(12345, 777, 15, 690)
; test plain division with uword (hits optimizedDivideExpr -> divmod_uw_asm directly, quotient only)
check_div(40000, 500, 80)
check_div(43211, 2, 21605)
check_div(65535, 1000, 65)
check_div(12345, 777, 15)
; test remainder with uword (hits optimizedRemainderExpr -> divmod_uw_preserve_r15)
check_mod(40000, 500, 0)
check_mod(43211, 2, 1)
check_mod(65535, 1000, 535)
check_mod(12345, 777, 690)
txt.print("ALL TESTS DONE\n")
sys.poweroff_system()
}
sub check_divmod(uword a1, uword a2, uword expected_q, uword expected_r) {
uword q, r = divmod(a1, a2)
if q==expected_q and r==expected_r
txt.print(" ok: ")
else
txt.print("FAIL: ")
txt.print("divmod(")
txt.print_uw(a1)
txt.print(", ")
txt.print_uw(a2)
txt.print(") = (")
txt.print_uw(q)
txt.print(", ")
txt.print_uw(r)
txt.print(") expected (")
txt.print_uw(expected_q)
txt.print(", ")
txt.print_uw(expected_r)
txt.print(")\n")
}
sub check_div(uword a1, uword a2, uword expected) {
uword r = a1 / a2
if r==expected
txt.print(" ok: ")
else
txt.print("FAIL: ")
txt.print("uword ")
txt.print_uw(a1)
txt.print(" / ")
txt.print_uw(a2)
txt.print(" = ")
txt.print_uw(r)
txt.print(" expected ")
txt.print_uw(expected)
txt.print("\n")
}
sub check_mod(uword a1, uword a2, uword expected) {
uword r = a1 % a2
if r==expected
txt.print(" ok: ")
else
txt.print("FAIL: ")
txt.print("uword ")
txt.print_uw(a1)
txt.print(" % ")
txt.print_uw(a2)
txt.print(" = ")
txt.print_uw(r)
txt.print(" expected ")
txt.print_uw(expected)
txt.print("\n")
}
}
+15
View File
@@ -498,6 +498,21 @@ main {
compileText(Cx16Target(), true, src, outputDir, writeAssembly = false) shouldNotBe null
}
test("const string to pointer type compiles") {
val src = """
main {
sub start() {
const ^^ubyte name = "irmen"
thing(name)
}
sub thing(str x) {
cx16.r0L++
}
}"""
compileText(VMTarget(), true, src, outputDir, writeAssembly = false) shouldNotBe null
}
test("const long with large unsigned long values should be converted to signed longs") {
val src = $$"""
main {
@@ -1123,69 +1123,6 @@ main {
array2.type shouldBe DataType.arrayFor(BaseDataType.UWORD, true)
}
test("defer syntactic sugaring") {
val src="""
main {
sub start() {
void test()
}
sub test() -> uword {
defer {
cx16.r0++
cx16.r1++
}
if cx16.r0==0 {
defer cx16.r1++
}
if cx16.r0==0
return cx16.r0+cx16.r1
defer cx16.r2++
return 999
}
}"""
val result = compileText(Cx16Target(), optimize=true, src, outputDir, writeAssembly=true)!!
val main = result.codegenAst!!.allBlocks().single {it.name=="p8b_main"}
val sub = main.children[1] as PtSub
sub.scopedName shouldBe "p8b_main.p8s_test"
// check the desugaring of the defer statements
sub.children[0] shouldBe instanceOf<PtSubSignature>()
(sub.children[1] as PtVariable).name shouldBe "p8v_prog8_defers_mask"
val firstDefer = sub.children[3] as PtAugmentedAssign
firstDefer.operator shouldBe "|="
firstDefer.target.identifier?.name shouldBe "p8b_main.p8s_test.p8v_prog8_defers_mask"
firstDefer.value.asConstInteger() shouldBe 4
val firstIf = sub.children[4] as PtIfElse
val deferInIf = firstIf.ifScope.children[0] as PtAugmentedAssign
deferInIf.operator shouldBe "|="
deferInIf.target.identifier?.name shouldBe "p8b_main.p8s_test.p8v_prog8_defers_mask"
deferInIf.value.asConstInteger() shouldBe 2
val lastDefer = sub.children[6] as PtAugmentedAssign
lastDefer.operator shouldBe "|="
lastDefer.target.identifier?.name shouldBe "p8b_main.p8s_test.p8v_prog8_defers_mask"
lastDefer.value.asConstInteger() shouldBe 1
val ifelse = sub.children[5] as PtIfElse
val ifscope = ifelse.ifScope.children[0] as PtNodeGroup
val ifscope_push = ifscope.children[0] as PtFunctionCall
val ifscope_defer = ifscope.children[1] as PtFunctionCall
val ifscope_return = ifscope.children[2] as PtReturn
ifscope_defer.name shouldBe "p8b_main.p8s_test.p8s_prog8_invoke_defers"
ifscope_push.name shouldBe "pushw"
(ifscope_return.children.single() as PtFunctionCall).name shouldBe "popw"
val ending = sub.children[7] as PtFunctionCall
ending.name shouldBe "p8b_main.p8s_test.p8s_prog8_invoke_defers"
sub.children[8] shouldBe instanceOf<PtReturn>()
val handler = sub.children[9] as PtSub
handler.name shouldBe "p8s_prog8_invoke_defers"
}
test("unknown variable in for loop gives proper errors") {
val src="""
@@ -5,6 +5,7 @@ import io.kotest.engine.spec.tempdir
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldNotContain
import io.kotest.matchers.types.instanceOf
import prog8.code.ast.*
import prog8.code.target.C64Target
@@ -12,6 +13,7 @@ import prog8.code.target.Cx16Target
import prog8.code.target.VMTarget
import prog8tests.helpers.ErrorReporterForTests
import prog8tests.helpers.compileText
import prog8tests.helpers.simulate
import kotlin.io.path.readText
class TestArrayThings: FunSpec({
@@ -149,6 +151,72 @@ main {
assembly shouldContain "thearray2"
}
test("split word array return and address-of uses lsb symbol") {
val src = """
main {
uword[3] myarray = [1000, 2000, 3000]
sub start() {
uword addr1 = get_addr()
uword addr2 = &myarray
}
sub get_addr() -> uword {
return myarray
}
}
""".trimIndent()
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
val result = compileText(Cx16Target(), false, src, outputDir, writeAssembly = true, errors = errors)
errors.report()
result shouldNotBe null
val assemblyFile = result!!.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm")
val assembly = assemblyFile.readText()
// it should use _lsb suffix for both the return and the direct assignment
// because both are treating the array as a pointer/address
assembly shouldContain "_lsb"
// Ensure it DOES NOT contain p8v_myarray followed by space, tab or newline (which would mean no suffix)
// or used in lda #< / ldy #> without the suffix
assembly shouldNotContain Regex("""#<.*p8v_myarray(?![_a-zA-Z0-9])""")
assembly shouldNotContain Regex("""#>.*p8v_myarray(?![_a-zA-Z0-9])""")
}
test("functional test: split word array return address") {
val src = $$"""
%option no_sysinit
%launcher none
%address $1000
main {
&ubyte poweroff = $f203
&ubyte result = $02
uword[2] myarray = [1234, 5678]
sub start() {
uword addr = get_addr()
; addr points to the LSB array.
; result should get the LSB of the first element (1234 & 0xff = 210? No, 1234 = 4 * 256 + 210? 1234 / 256 = 4, 1234 % 256 = 210. 1234 = 0x04D2, so LSB is 0xD2 = 210)
^^ubyte ptr = addr
result = ptr[0]
poweroff = 1
}
sub get_addr() -> uword {
return myarray
}
}
""".trimIndent()
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
val compileResult = compileText(Cx16Target(), false, src, outputDir, errors = errors)
errors.report()
compileResult shouldNotBe null
val machine = compileResult!!.simulate()
machine.assertMemory(0x02, 1234 and 0xff)
}
test("indexing str or pointervar with expression") {
val text = """
main {
@@ -23,7 +23,11 @@ class TestComparisonIssues : FunSpec({
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asmFile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm")
val asm = asmFile.readText()
asm.shouldContainInOrder("ldx", "#252", "lda", "p8v_a+4,x", "adc", "p8v_b+4,x", "sta", "p8v_a+4,x", "inx", "bne")
asm.shouldContainInOrder("clc",
"lda", "p8v_a+0", "adc", "p8v_b+0", "sta", "p8v_a+0",
"lda", "p8v_a+1", "adc", "p8v_b+1", "sta", "p8v_a+1",
"lda", "p8v_a+2", "adc", "p8v_b+2", "sta", "p8v_a+2",
"lda", "p8v_a+3", "adc", "p8v_b+3", "sta", "p8v_a+3")
}
test("long augmented subtraction generates loop") {
@@ -39,7 +43,28 @@ class TestComparisonIssues : FunSpec({
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asmFile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm")
val asm = asmFile.readText()
asm.shouldContainInOrder("ldx", "#252", "lda", "p8v_a+4,x", "sbc", "p8v_b+4,x", "sta", "p8v_a+4,x", "inx", "bne")
asm.shouldContainInOrder("sec",
"lda", "p8v_a+0", "sbc", "p8v_b+0", "sta", "p8v_a+0",
"lda", "p8v_a+1", "sbc", "p8v_b+1", "sta", "p8v_a+1",
"lda", "p8v_a+2", "sbc", "p8v_b+2", "sta", "p8v_a+2",
"lda", "p8v_a+3", "sbc", "p8v_b+3", "sta", "p8v_a+3")
}
test("long augmented addition generates loop with nozp variables") {
val text = """
main {
sub start() {
long @nozp a = 1000
long @nozp b = 2000
a += b
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asmFile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm")
val asm = asmFile.readText()
// verify unrolled structure
asm.shouldContainInOrder("clc", "lda", "adc", "sta", "lda", "adc", "sta", "lda", "adc", "sta", "lda", "adc", "sta")
}
test("word <= 0 signed comparison (identifier)") {
@@ -119,6 +119,43 @@ class TestExecution6502 : FunSpec({
machine.cpu.totalCycles shouldBeLessThan 200
}
test("long repeat loops") {
val src = $$"""
%option no_sysinit
%launcher none
%address $1000
main {
&ubyte poweroff = $f203
&long result = $02 ; result is stored in memory at $02
sub start() {
long @shared qq = 0
repeat 70000 {
qq++
}
result = qq
long @shared iters = 70000
repeat iters {
qq++
}
result = qq
poweroff = 1
}
}
""".trimIndent()
val compileResult = compileText(Cx16Target(), false, src, outputDir)
// 140000 iterations * ~40 cycles = 5.6 million. Let's use 10 million to be safe.
val machine = compileResult!!.simulate(maxCycles = 10000000L)
machine.assertMemory(0x02, (140000L and 0xff).toInt())
machine.assertMemory(0x03, ((140000L shr 8) and 0xff).toInt())
machine.assertMemory(0x04, ((140000L shr 16) and 0xff).toInt())
machine.assertMemory(0x05, ((140000L shr 24) and 0xff).toInt())
}
test("top level RTS exits simulator") {
val src = $$"""
%option no_sysinit
@@ -0,0 +1,46 @@
package prog8tests.codegeneration
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.shouldNotBe
import prog8.code.target.Cx16Target
import prog8tests.helpers.compileText
class TestExperiCodeGen: FunSpec({
val outputDir = tempdir().toPath()
test("banked extsub with multiple return values (constbank)") {
val src = $$"""
main {
sub start() {
uword ptr
ubyte bank
void, ptr, bank = mymodule.myfunc(0)
}
}
mymodule {
const ubyte MYBANK = 1
extsub @bank MYBANK $A000 = myfunc(ubyte x @X) -> bool @Pc, uword @XY, ubyte @A
}"""
compileText(Cx16Target(), false, src, outputDir, experimentalCodegen = true) shouldNotBe null
}
test("banked extsub with multiple return values (varbank)") {
val src = $$"""
main {
sub start() {
uword ptr
ubyte bank
void, ptr, bank = mymodule.myfunc(0)
}
}
mymodule {
ubyte @shared bankvar = 1
extsub @bank bankvar $A000 = myfunc(ubyte x @X) -> bool @Pc, uword @XY, ubyte @A
}"""
compileText(Cx16Target(), false, src, outputDir, experimentalCodegen = true) shouldNotBe null
}
})
@@ -0,0 +1,49 @@
package prog8tests.codegeneration
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import prog8.code.target.Cx16Target
import prog8tests.helpers.compileText
class TestLongAugmentedAssignment : FunSpec({
val outputDir = tempdir().toPath()
test("long augmented assignment with literal") {
val text = """
main {
sub start() {
long @shared a = 1000
a += 500
}
}
""".trimIndent()
// Should compile successfully
compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)
}
test("long augmented assignment with expression") {
val text = """
main {
sub start() {
long @shared a = 1000
long @shared b = 500
a += b + 100
}
}
""".trimIndent()
compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)
}
test("long augmented assignment with ubyte") {
val text = """
main {
sub start() {
long @shared a = 1000
ubyte @shared b = 200
a += b
}
}
""".trimIndent()
compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)
}
})
@@ -18,6 +18,7 @@ import prog8.code.target.*
import prog8tests.helpers.ErrorReporterForTests
import prog8tests.helpers.compileText
import prog8tests.helpers.shouldContainInOrder
import prog8tests.helpers.simulate
import kotlin.io.path.readText
class TestVariousCodeGen: FunSpec({
@@ -904,4 +905,93 @@ main {
asm shouldContain "sta (p8v_s),y"
}
}
test("IfExpression string to pointer typecast") {
val src = """
%option no_sysinit
%launcher none
%address $1000
main {
sub start() {
str a = "hello"
str b = "world"
bool flag = true
^^ubyte p = if flag then a else b
}
}
""".trimIndent()
// This should not crash during compilation
val compileResult = compileText(Cx16Target(), false, src, outputDir)
compileResult shouldNotBe null
// Also test with virtual target
val compileResultVM = compileText(VMTarget(), false, src, outputDir)
compileResultVM shouldNotBe null
}
test("BranchConditionExpression string to pointer typecast") {
val src = """
%option no_sysinit
%launcher none
%address $1000
main {
sub start() {
str a = "hello"
str b = "world"
^^ubyte p = if_z then a else b
}
}
""".trimIndent()
// This should not crash during compilation
val compileResult = compileText(Cx16Target(), false, src, outputDir)
compileResult shouldNotBe null
}
test("pointer assignment to variable in 6502 backend") {
val src = """
%option no_sysinit
%launcher none
%address $1000
main {
ubyte value = 42
^^ubyte @shared ptr = 0
sub start() {
ptr = &value
}
}
""".trimIndent()
// This should not crash during compilation
val compileResult = compileText(Cx16Target(), false, src, outputDir)
compileResult shouldNotBe null
// Verify it actually works by simulating
compileResult!!.simulate()
}
test("pointer assignment to array element in 6502 backend") {
val src = """
%option no_sysinit
%launcher none
%address $1000
main {
ubyte value = 42
^^ubyte[2] ptrs
sub start() {
ptrs[1] = &value
}
}
""".trimIndent()
val compileResult = compileText(Cx16Target(), false, src, outputDir)
compileResult shouldNotBe null
}
})
+7 -4
View File
@@ -18,7 +18,8 @@ internal fun compileFile(
errors: IErrorReporter? = null,
writeAssembly: Boolean = true,
varshigh: Int? = null,
slabshigh: Int? = null
slabshigh: Int? = null,
experimentalCodegen: Boolean = false
) : CompilationResult? {
val filepath = fileDir.resolve(fileName)
assumeReadableFile(filepath)
@@ -33,7 +34,7 @@ internal fun compileFile(
showTimings = false,
asmListfile = false,
includeSourcelines = false,
experimentalCodegen = false,
experimentalCodegen = experimentalCodegen,
dumpVariables = false,
dumpSymbols = false,
varsHighBank = varshigh,
@@ -67,11 +68,13 @@ internal fun compileText(
errors: IErrorReporter? = null,
writeAssembly: Boolean = true,
varshigh: Int? = null,
slabshigh: Int? = null
slabshigh: Int? = null,
experimentalCodegen: Boolean = false
) : CompilationResult? {
val filePath = outputDir.resolve("on_the_fly_test_${sourceText.hashCode().toUInt().toString(16)}.p8")
// we don't assumeNotExists(filePath) - should be ok to just overwrite it
filePath.toFile().writeText(sourceText)
return compileFile(platform, optimize, filePath.parent, filePath.name, outputDir,
errors=errors, writeAssembly=writeAssembly, varshigh=varshigh, slabshigh=slabshigh)
errors=errors, writeAssembly=writeAssembly, varshigh=varshigh, slabshigh=slabshigh,
experimentalCodegen = experimentalCodegen)
}
@@ -0,0 +1,49 @@
package prog8tests.optimizer
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.shouldBe
import prog8.ast.expressions.BinaryExpression
import prog8.ast.expressions.IdentifierReference
import prog8.ast.expressions.NumericLiteral
import prog8.ast.statements.Assignment
import prog8.code.target.Cx16Target
import prog8tests.helpers.compileText
class TestExpressionSimplifier : FunSpec({
val outputDir = tempdir().toPath()
test("long & -1 is correctly simplified") {
val text = """
main {
sub start() {
long @shared x = 1000
long @shared y = x & -1
}
}
""".trimIndent()
val result = compileText(Cx16Target(), true, text, outputDir, writeAssembly = false)!!
val s = result.compilerAst.entrypoint.statements
val expr = (s[3] as Assignment).value as BinaryExpression
expr.operator shouldBe "&"
(expr.left as IdentifierReference).nameInSource shouldBe listOf("x")
(expr.right as NumericLiteral).number shouldBe -1.0
}
test("long & 255 is correctly handled") {
val text = """
main {
sub start() {
long @shared x = 1000
long @shared y = x & 255
}
}
""".trimIndent()
val result = compileText(Cx16Target(), true, text, outputDir, writeAssembly = false)!!
val s = result.compilerAst.entrypoint.statements
val expr = (s[3] as Assignment).value as BinaryExpression
expr.operator shouldBe "&"
(expr.left as IdentifierReference).nameInSource shouldBe listOf("x")
(expr.right as NumericLiteral).number shouldBe 255.0
}
})
+46 -1
View File
@@ -1090,5 +1090,50 @@ main {
result shouldNotBe null
}
}
test("some dummy support for rombank() rambank() in virtual target") {
val src = """
main {
sub start() {
cx16.r0 = cx16.numbanks()
cx16.rambank(0)
cx16.rombank(0)
}
}
""".trimIndent()
val result = compileText(VMTarget(), false, src, outputDir)
result shouldNotBe null
}
test("typed pointer var init not removed by optimizer when immediately dereferenced") {
val src = """
%zeropage basicsafe
%option no_sysinit
main {
uword data = $1234
uword @shared result
sub start() {
result = read_data()
}
sub read_data() -> uword {
^^uword ptr = &main.data
ptr = ptr^^
return ptr
}
}
""".trimIndent()
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irContent = virtfile.readText()
val irProgram = IRFileReader().read(irContent)
val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations
VmRunner().runAndTestProgram(irContent) { vm ->
vm.memory.getUW(allocations["main.result"]!!) shouldBe 0x1234u
}
}
})
+28 -4
View File
@@ -102,6 +102,11 @@ interface IStatementContainer {
if(found!=null)
return found
}
is Defer -> {
val found = stmt.scope.searchSymbol(name)
if(found!=null)
return found
}
is IfElse -> {
val found = stmt.truepart.searchSymbol(name) ?: stmt.elsepart.searchSymbol(name)
if(found!=null)
@@ -154,6 +159,7 @@ interface IStatementContainer {
is When -> stmt.choices.any { it.statements.hasReturnStatement() }
is ConditionalBranch -> stmt.truepart.hasReturnStatement() || stmt.elsepart.hasReturnStatement()
is UnrollLoop -> stmt.body.hasReturnStatement()
is Defer -> stmt.scope.hasReturnStatement()
is Return -> true
else -> false
}
@@ -161,6 +167,24 @@ interface IStatementContainer {
return statements.any { hasReturnStatement(it) }
}
fun hasDeferStatement(): Boolean {
fun hasDeferStatement(stmt: Statement): Boolean = when(stmt) {
is AnonymousScope -> stmt.statements.any { hasDeferStatement(it) }
is ForLoop -> stmt.body.hasDeferStatement()
is IfElse -> stmt.truepart.hasDeferStatement() || stmt.elsepart.hasDeferStatement()
is WhileLoop -> stmt.body.hasDeferStatement()
is RepeatLoop -> stmt.body.hasDeferStatement()
is UntilLoop -> stmt.body.hasDeferStatement()
is When -> stmt.choices.any { it.statements.hasDeferStatement() }
is ConditionalBranch -> stmt.truepart.hasDeferStatement() || stmt.elsepart.hasDeferStatement()
is UnrollLoop -> stmt.body.hasDeferStatement()
is Defer -> true
else -> false
}
return statements.any { hasDeferStatement(it) }
}
val allDefinedSymbols: Sequence<Pair<String, Statement>>
get() {
return statements.asSequence().filterIsInstance<INamedStatement>().map { Pair(it.name, it as Statement) }
@@ -234,7 +258,7 @@ interface INameScope: IStatementContainer, INamedStatement {
if(struct!=null) {
for ((idx, field) in scopedName.withIndex()) {
val fieldDt = struct!!.getFieldType(field) ?:
return null
return null
if (idx == scopedName.size - 1) {
// was last path element
val pointer = IdentifierReference(scopedName, localSymbol.position)
@@ -362,9 +386,9 @@ open class Module(final override val statements: MutableList<Statement>,
lateinit var program: Program
override val name = source.origin
.substringBeforeLast(".")
.substringAfterLast("/")
.substringAfterLast("\\")
.substringBeforeLast(".")
.substringAfterLast("/")
.substringAfterLast("\\")
val loadAddress: Pair<UInt, Position>? by lazy {
val address = (statements.singleOrNull { it is Directive && it.directive == "%address" } as? Directive)
+14 -13
View File
@@ -74,7 +74,7 @@ class Program(val name: String,
private val internedStringsReferenceCounts = mutableMapOf<VarDecl, Int>()
fun internString(string: StringLiteral): List<String> {
fun internString(string: StringLiteral, deduplicate: Boolean = true): List<String> {
// Move a string literal into the internal, deduplicated, string pool
// replace it with a variable declaration that points to the entry in the pool.
@@ -100,19 +100,20 @@ class Program(val name: String,
return Pair(listOf(INTERNED_STRINGS_MODULENAME, decl.name), decl)
}
val existingDecl = internedStringsBlock.statements.filterIsInstance<VarDecl>().singleOrNull {
val declString = it.value as StringLiteral
declString.encoding == string.encoding && declString.value == string.value
}
return if (existingDecl != null) {
internedStringsReferenceCounts[existingDecl] = internedStringsReferenceCounts.getValue(existingDecl)+1
existingDecl.scopedName
}
else {
val (newName, newDecl) = addNewInternedStringvar(string)
internedStringsReferenceCounts[newDecl] = 1
newName
if (deduplicate) {
val existingDecl = internedStringsBlock.statements.filterIsInstance<VarDecl>().singleOrNull {
val declString = it.value as StringLiteral
declString.encoding == string.encoding && declString.value == string.value
}
if (existingDecl != null) {
internedStringsReferenceCounts[existingDecl] = internedStringsReferenceCounts.getValue(existingDecl)+1
return existingDecl.scopedName
}
}
val (newName, newDecl) = addNewInternedStringvar(string)
internedStringsReferenceCounts[newDecl] = 1
return newName
}
fun removeInternedStringsFromRemovedSubroutine(sub: Subroutine) {
@@ -27,6 +27,8 @@ sealed class Expression: Node {
abstract fun isIORead(target: ICompilationTarget): Boolean
abstract val isSimple: Boolean
open fun hasSideEffects(target: ICompilationTarget): Boolean = isIORead(target)
infix fun isSameAs(assigntarget: AssignTarget) = assigntarget.isSameAs(this)
infix fun isSameAs(other: Expression): Boolean {
@@ -104,6 +106,7 @@ class PrefixExpression(val operator: String, var expression: Expression, overrid
}
override fun copy() = PrefixExpression(operator, expression.copy(), position)
override fun hasSideEffects(target: ICompilationTarget): Boolean = expression.hasSideEffects(target)
override fun constValue(program: Program): NumericLiteral? {
val constval = expression.constValue(program) ?: return null
val converted = when(operator) {
@@ -186,6 +189,7 @@ class BinaryExpression(
}
override fun copy() = BinaryExpression(left.copy(), operator, right.copy(), position)
override fun hasSideEffects(target: ICompilationTarget): Boolean = left.hasSideEffects(target) || right.hasSideEffects(target)
override fun toString() = "[$left $operator $right]"
override val isSimple = false
@@ -539,6 +543,8 @@ class ArrayIndexedExpression(var plainarrayvar: IdentifierReference?,
return ArrayIndexedExpression(plainarrayvar?.copy(), copyNested, pointerderef?.copy(), indexer.copy(), position)
}
override fun hasSideEffects(target: ICompilationTarget): Boolean = true
fun isSameArrayIndexedAs(other: Expression): Boolean {
if(other !is ArrayIndexedExpression || !(other.indexer.indexExpr isSameAs indexer.indexExpr))
return false
@@ -574,6 +580,7 @@ class TypecastExpression(var expression: Expression, var type: DataType, val imp
}
override fun copy() = TypecastExpression(expression.copy(), type, implicit, position)
override fun hasSideEffects(target: ICompilationTarget): Boolean = expression.hasSideEffects(target)
override fun accept(visitor: IAstVisitor) = visitor.visit(this)
override fun accept(visitor: AstWalker, parent: Node)= visitor.visit(this, parent)
@@ -654,7 +661,7 @@ data class AddressOf(var identifier: IdentifierReference?, var arrayIndex: Array
val target = this.identifier?.targetStatement()
val targetVar = target as? VarDecl
if(targetVar!=null) {
if (targetVar.type == VarDeclType.MEMORY || targetVar.type == VarDeclType.CONST) {
if (targetVar.type == VarDeclType.MEMORY || (targetVar.type == VarDeclType.CONST && arrayIndex != null)) {
var address = targetVar.value?.constValue(program)?.number
if (address != null) {
if (arrayIndex != null) {
@@ -719,13 +726,19 @@ class DirectMemoryRead(var addressExpression: Expression, override val position:
}
override fun copy() = DirectMemoryRead(addressExpression.copy(), position)
override fun hasSideEffects(target: ICompilationTarget): Boolean = isIORead(target)
override fun accept(visitor: IAstVisitor) = visitor.visit(this)
override fun accept(visitor: AstWalker, parent: Node)= visitor.visit(this, parent)
override fun referencesIdentifier(nameInSource: List<String>) = addressExpression.referencesIdentifier(nameInSource)
override fun inferType(program: Program) = InferredTypes.knownFor(BaseDataType.UBYTE)
override fun constValue(program: Program): NumericLiteral? = null
override fun isIORead(target: ICompilationTarget) = addressExpression.isIORead(target)
override fun isIORead(target: ICompilationTarget): Boolean {
if (addressExpression is NumericLiteral) {
return target.isIOAddress((addressExpression as NumericLiteral).number.toUInt())
}
return true // assume worst case if address is not constant
}
override fun toString(): String {
return "DirectMemoryRead($addressExpression)"
}
@@ -1207,6 +1220,7 @@ class ArrayLiteral(val type: InferredTypes.InferredType, // inferred because
}
override fun copy(): ArrayLiteral = ArrayLiteral(type, value.map { it.copy() }.toTypedArray(), position)
override fun hasSideEffects(target: ICompilationTarget): Boolean = value.any { it.hasSideEffects(target) }
override val isSimple = true
override fun isIORead(target: ICompilationTarget) = false
@@ -1222,6 +1236,7 @@ class ArrayLiteral(val type: InferredTypes.InferredType, // inferred because
override fun accept(visitor: IAstVisitor) = visitor.visit(this)
override fun accept(visitor: AstWalker, parent: Node)= visitor.visit(this, parent)
@Suppress("KotlinArrayToString")
override fun toString(): String = "$value"
override fun inferType(program: Program): InferredTypes.InferredType = if(type.isKnown) type else guessDatatype(program)
@@ -1472,28 +1487,34 @@ data class IdentifierReference(val nameInSource: List<String>, override val posi
}
else
definingScope.lookup(nameInSource)
fun targetVarDecl(): VarDecl? {
fun targetVarDecl(visitedAliases: MutableSet<Alias> = mutableSetOf()): VarDecl? {
// follows aliases
val t = targetStatement()
return if(t is Alias)
t.target.targetVarDecl()
else
return if (t is Alias) {
if (t in visitedAliases) return null
visitedAliases.add(t)
t.target.targetVarDecl(visitedAliases)
} else
t as? VarDecl
}
fun targetSubroutine(): Subroutine? {
fun targetSubroutine(visitedAliases: MutableSet<Alias> = mutableSetOf()): Subroutine? {
// follows aliases
val t = targetStatement()
return if(t is Alias)
t.target.targetSubroutine()
else
return if (t is Alias) {
if (t in visitedAliases) return null
visitedAliases.add(t)
t.target.targetSubroutine(visitedAliases)
} else
t as? Subroutine
}
fun targetStructDecl(): StructDecl? {
fun targetStructDecl(visitedAliases: MutableSet<Alias> = mutableSetOf()): StructDecl? {
// follows aliases
val t = targetStatement()
return if(t is Alias)
t.target.targetStructDecl()
else
return if (t is Alias) {
if (t in visitedAliases) return null
visitedAliases.add(t)
t.target.targetStructDecl(visitedAliases)
} else
t as? StructDecl
}
@@ -1659,6 +1680,7 @@ class FunctionCallExpression(override var target: IdentifierReference,
}
override fun copy() = FunctionCallExpression(target.copy(), args.map { it.copy() }.toMutableList(), position)
override fun hasSideEffects(target: ICompilationTarget): Boolean = true
override val isSimple = if (target.nameInSource.singleOrNull() in SimpleBuiltinFunctions) this.args.all { it.isSimple } else false
override fun replaceChildNode(node: Node, replacement: Node) {
if(node===target)
@@ -1711,6 +1733,8 @@ class FunctionCallExpression(override var target: IdentifierReference,
val constVal = constValue(program ,false)
if(constVal!=null)
return InferredTypes.knownFor(constVal.type)
if (args.any { it.inferType(program).isUnknown })
return InferredTypes.unknown()
val stmt = target.targetStatement(program.builtinFunctions) ?: return InferredTypes.unknown()
when (stmt) {
is BuiltinFunctionPlaceholder -> {
@@ -1847,6 +1871,8 @@ class IfExpression(var condition: Expression, var truevalue: Expression, var fal
override fun copy(): Expression = IfExpression(condition.copy(), truevalue.copy(), falsevalue.copy(), position)
override fun hasSideEffects(target: ICompilationTarget): Boolean = condition.hasSideEffects(target) || truevalue.hasSideEffects(target) || falsevalue.hasSideEffects(target)
override fun constValue(program: Program): NumericLiteral? {
val cond = condition.constValue(program)
if(cond!=null) {
@@ -1955,6 +1981,7 @@ class PtrDereference(
}
override val isSimple = false
override fun hasSideEffects(target: ICompilationTarget): Boolean = true
override fun isIORead(target: ICompilationTarget) = false
override fun copy(): PtrDereference = PtrDereference(chain.toList(), derefLast, position)
override fun constValue(program: Program): NumericLiteral? {
@@ -2070,6 +2097,7 @@ class ArrayIndexedPtrDereference(
}
override val isSimple = false
override fun hasSideEffects(target: ICompilationTarget): Boolean = true
override fun isIORead(target: ICompilationTarget) = false
override fun replaceChildNode(node: Node, replacement: Node) = throw FatalAstException("can't replace here")
override fun referencesIdentifier(nameInSource: List<String>) = chain.size==1 && chain==nameInSource
@@ -840,6 +840,19 @@ data class AssignTarget(
fun accept(visitor: IAstVisitor) = visitor.visit(this)
fun accept(visitor: AstWalker, parent: Node) = visitor.visit(this, parent)
fun hasSideEffects(target: ICompilationTarget): Boolean {
if (void) return false
if (identifier != null) {
val symbol = definingScope.lookup(identifier!!.nameInSource)
if (symbol is VarDecl && symbol.type == VarDeclType.MEMORY && symbol.value is NumericLiteral) {
return target.isIOAddress((symbol.value as NumericLiteral).number.toUInt())
}
return false
}
return true // memoryAddress, arrayindexed, pointerDereference, arrayIndexedDereference
}
override fun copy() = AssignTarget(
identifier?.copy(),
arrayindexed?.copy(),
@@ -1,6 +1,5 @@
Prog8 compiler v12.2-BETA2 by Irmen de Jong (irmen@razorvine.net)
Prerelease version from git commit 1dbaa6e0 in branch master
Prog8 compiler v12.2.1 by Irmen de Jong (irmen@razorvine.net)
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
Compiling program import-all-c128.p8
@@ -1,6 +1,5 @@
Prog8 compiler v12.2-BETA2 by Irmen de Jong (irmen@razorvine.net)
Prerelease version from git commit 1dbaa6e0 in branch master
Prog8 compiler v12.2.1 by Irmen de Jong (irmen@razorvine.net)
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
Compiling program import-all-c64.p8
@@ -1,6 +1,5 @@
Prog8 compiler v12.2-BETA2 by Irmen de Jong (irmen@razorvine.net)
Prerelease version from git commit 1dbaa6e0 in branch master
Prog8 compiler v12.2.1 by Irmen de Jong (irmen@razorvine.net)
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
Compiling program import-all-cx16.p8
@@ -765,14 +764,7 @@ psg2 {
const ubyte SAWTOOTH
const ubyte SQUARE
const ubyte TRIANGLE
ubyte[] envelope_attacks
ubyte[] envelope_maxvolumes
ubyte[] envelope_releases
ubyte[] envelope_states
ubyte[] envelope_sustains
uword[] envelope_volumes
^^psg2.Voice voices
^^psg2.Voice vptr
envelope (ubyte voice_num, ubyte attack, ubyte sustain, ubyte release)
frequency (ubyte voice_num, uword freq)
getvoice (ubyte voice_num) -> ^^psg2.Voice
@@ -788,6 +780,21 @@ LIBRARY MODULE NAME: serial
---------------------------
serial {
const uword BAUD::B115200
const uword BAUD::B1200
const uword BAUD::B14400
const uword BAUD::B19200
const uword BAUD::B230400
const uword BAUD::B2400
const uword BAUD::B28800
const uword BAUD::B300
const uword BAUD::B38400
const uword BAUD::B460800
const uword BAUD::B4800
const uword BAUD::B57600
const uword BAUD::B600
const uword BAUD::B921600
const uword BAUD::B9600
const ubyte LSR::BI
const ubyte LSR::DR
const ubyte LSR::FE
@@ -808,14 +815,18 @@ serial {
const ubyte REG_SCRATCH
const ubyte REG_TXRX_BUFFER
detect_uarts () -> uword, uword
discard_until (uword uart_addr, str match)
initialize_uart (uword uart_addr)
read_until (uword uart_addr, str match, ^^ubyte buffer, uword max_size) -> uword
write (uword uart_addr, str data)
zi_end_get_file ()
zi_get_file_chunk (^^ubyte buffer, uword buffer_size, long remaining_file_size) -> uword
zi_get_file_chunk_hexmode (^^ubyte buffer, uword buffer_size) -> uword
zi_get_ip_address () -> str
zi_initialize (uword uart_addr)
zi_reset ()
zi_start_get_file (str filename) -> long
zi_start_get_file_hexmode (str filename) -> bool
zi_write_cmd (str command)
}
@@ -1,6 +1,5 @@
Prog8 compiler v12.2-BETA2 by Irmen de Jong (irmen@razorvine.net)
Prerelease version from git commit 1dbaa6e0 in branch master
Prog8 compiler v12.2.1 by Irmen de Jong (irmen@razorvine.net)
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
Compiling program import-all-pet32.p8
@@ -1,6 +1,5 @@
Prog8 compiler v12.2-BETA2 by Irmen de Jong (irmen@razorvine.net)
Prerelease version from git commit 1dbaa6e0 in branch master
Prog8 compiler v12.2.1 by Irmen de Jong (irmen@razorvine.net)
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
Compiling program import-all-virtual.p8
@@ -612,7 +611,12 @@ cx16 {
&word r9s
&byte r9sH
&byte r9sL
getrambank () -> ubyte
getrombank () -> ubyte
numbanks () -> uword
rambank (ubyte bank)
restore_virtual_registers ()
rombank (ubyte bank)
save_virtual_registers ()
}
+15 -14
View File
@@ -42,7 +42,7 @@ No system initialization and startup code
Variable initialization
The library still has to initialize any variables it might use and clear
uninitialized "BSS" variables! Otherwise the code will not run predictably as prog8 code.
So, the library must still have a "start" entrypoint subroutine like any outher prog8 program,
So, the library must still have a "start" entrypoint subroutine like any other prog8 program,
that must be called before any other library routine can be called.
Binary output and loaded into a fixed memory address
@@ -79,7 +79,7 @@ Jump table
For ease of use, libraries should probably have a fixed "jump table" where the offsets of the
library routines stay the same across different versions of the library. Without needing new syntax,
there's a trick in Prog8 that you can use to build such a jumptable:
add a non-splitted word array at the top of the library main block that contains JMP instructions
add a non-split word array at the top of the library main block that contains JMP instructions
and the addresses of the individual library subroutines. Do NOT change the order of the subroutines
in this table!
Also note that the Prog8 compiler will insert a single JMP instruction at the very start of the library,
@@ -105,13 +105,14 @@ Assuming the load address of the library is $A000:
BLOAD "LIBRARY.BIN",8,1,$A000
SYS $A000 : REM TO INITIALIZE VARIABLES, REQUIRED!
SYS $A004 : REM CALL FIRST ROUTINE
SYS $A008 : REM CALL SECOND ROUTINE, ETC.
SYS $A003 : REM CALL FIRST ROUTINE
SYS $A006 : REM CALL SECOND ROUTINE, ETC.
**From Prog8**
.. index:: single: Examples; Library from Prog8
**From Prog8:**
The ``diskio`` module actually provides a convenience routine called ``loadlib`` that loads a Prog8-compiled
library blob into memory. It internally automatically uses either regular load() or load_raw(),
as required by the compilation target (so you don't have to bother with target machine
@@ -122,8 +123,8 @@ differences if you want to write portable code)::
main {
extsub $A000 = lib_init() clobbers(A)
extsub $A004 = lib_func1() clobbers(A,X,Y)
extsub $A008 = lib_func2() clobbers(A,X,Y)
extsub $A003 = lib_func1() clobbers(A,X,Y)
extsub $A006 = lib_func2() clobbers(A,X,Y)
sub start() {
if diskio.loadlib("library.bin", $a000) != 0 {
@@ -144,12 +145,12 @@ differences if you want to write portable code)::
int main() {
void (*lib_init)(void) = (void (*)()) 0xa000;
void (*lib_func1)(void) = (void (*)()) 0xa004;
void (*lib_func2)(void) = (void (*)()) 0xa008;
void (*lib_func1)(void) = (void (*)()) 0xa003;
void (*lib_func2)(void) = (void (*)()) 0xa006;
cbm_k_setlfs(0, 8, 2);
cbm_k_setnam("library.bin");
cbm_k_load(0, 0xa000);
cbm_k_setlfs(0, 8, 2);
cbm_k_setnam("library.bin");
cbm_k_load(0, 0xa000);
lib_init();
lib_func1();
@@ -178,8 +179,8 @@ differences if you want to write portable code)::
jsr $ffd2 ; CHROUT
jsr $A000 ; library init
jsr $A004 ; lib func 1
jsr $A008 ; lib func 2
jsr $A003 ; lib func 1
jsr $A006 ; lib func 2
rts
+6 -7
View File
@@ -16,7 +16,7 @@ The language
- It is meant to sit well above low level assembly code, but still allows that low level access to the system it runs on.
Via language features, or even simply by using inline hand-written assembly code.
- Prog8 is targeting very CPU and memory constrained 8-bit systems, this reflects many design choices to work within those limitations
(single digit Megaherz cpu clock speeds, and memory capacity counted in Kilobytes)
(single digit Megahertz cpu clock speeds, and memory capacity counted in Kilobytes)
- Identifiers and string literals can contain non-ASCII characters so for example ``knäckebröd`` and ``見せしめ`` are valid identifiers.
- There's usually a single statement per line. There is no statement separator.
- Semicolon ``;`` is used to start a line comment. Multi-line comments are also possible by enclosing it all in ``/*`` and ``*/``.
@@ -43,7 +43,7 @@ Data types
- **There is no automatic type enlargement:** all calculations remain within the data type of the operands. Any overflow silently wraps or truncates.
You'll have to add explicit casts to increase the size of the value if required.
For example when adding two byte variables having values 100 and 200, the result won't be 300, because that doesn't fit in a byte. It will be 44.
You'll have to cast one or both of the *operands* to a word type first if you want to accomodate the actual result value of 300.
You'll have to cast one or both of the *operands* to a word type first if you want to accommodate the actual result value of 300.
Similarly, ``long v = w1 * w2`` doesn't automatically give you the full 32 bits multiplication result, instead it is still constrained in the word range.
If you need the full 32 bits result you'll have to call a specialized routine such as ``math.mul32`` or ``math.mul16_last_upper()``.
- strings and arrays are allocated once, statically, and never resized.
@@ -66,7 +66,7 @@ Variables
are moved to the top of the subroutine. A for loop, or if/else blocks do not introduce a new scope.
A subroutine (also nested ones) *do* introduce a new scope.
- All variables are initialized at the start of the program. There is no random garbage in them: they are zero or any other initialization value you provide.
- This als means you can run a Prog8 program multiple times without having to reload it from disk, unlike programs produced by most other compilers targeting these 8 bit platforms.
- This also means you can run a Prog8 program multiple times without having to reload it from disk, unlike programs produced by most other compilers targeting these 8 bit platforms.
Subroutines
@@ -102,7 +102,7 @@ Pointers and Structs
Variables of the ``uword`` datatype can be used as a pointer to one of the possible 65536 memory locations,
so the value it points to is always a single byte. This is similar to ``uint8_t*`` from C.
You have to deal with the uword manually if the object it points to is something different.
- Note that there is the ``peekw`` builtin function that *does* allow you to directy obtain the *word* value at the given memory location.
- Note that there is the ``peekw`` builtin function that *does* allow you to directly obtain the *word* value at the given memory location.
So if you use this, you can use uword pointers as pointers to word values without much hassle.
- "dereferencing" a uword pointer is done via array indexing ``ptr[index]`` (where index value can be 0-65535!) or via the memory read operator ``@(ptr)``, or ``peek/peekw(ptr)``.
- Pointers don't have to be a variable, you can immediately access the value of a given memory location using ``@($d020)`` for instance.
@@ -124,15 +124,14 @@ Foreign function interface (external/ROM calls)
.. index:: single: Subroutines; Foreign/ROM
- You can use the ``extsub`` keyword to define the call signature of foreign functions (ROM routines or external routines elsewhere in RAM) in a natural way.
Calling those generates code that is as efficient or even more efficient as calling regular subroutines.
No additional stubs are needed. Y
Calling those generates code that is as efficient or even more efficient as calling regular subroutines. No additional stubs are needed.
- High level support of memory banking: an ``extsub`` can be defined with the memory bank number (constant or variable) where the routine is located in,
and then when you call it as usual, the compiler takes care of the required bank switching.
Optimizations
-------------
- Prog8 contains many compiler optimizations to generate efficient code, but also lacks many optimizations that modern compilers do have.
While empirical evidence shows that Prog8 generates more efficent code than some C compilers that also target the same 8 bit systems,
While empirical evidence shows that Prog8 generates more efficient code than some C compilers that also target the same 8 bit systems,
the optimizations it makes on your code aren't super sophisticated.
- For time critical code, it may be worth it to inspect the generated assembly code to see if you can write things differently
to help the compiler generate more efficient code (or even replace it with hand written inline assembly altogether).
+13 -11
View File
@@ -23,7 +23,7 @@ Then you can choose a few ways to get a compiler:
Arch Linux:
Currently, it's available on `AUR <https://wiki.archlinux.org/title/Arch_User_Repository>`_ for Arch Linux and derivative systems.
The package is called `"prog8" <https://aur.archlinux.org/packages/prog8>`_. There should be no need to install aything else as
The package is called `"prog8" <https://aur.archlinux.org/packages/prog8>`_. There should be no need to install anything else as
it can automatically pull in the required dependencies.
This package, alongside the compiler itself, also globally installs syntax highlighting for ``vim`` and ``nano``.
@@ -65,12 +65,12 @@ The most interesting gradle commands to run are probably the ones listed below.
``./gradlew installShadowDist``
Creates a 'fat-jar' that contains the compiler and all dependencies, in a single
executable .jar file, and includes few start scripts to run it.
The output can be found in ``.compiler/build/install/prog8c-shadow/``
The output can be found in ``./compiler/build/install/prog8c-shadow/``
``./gradlew shadowDistZip``
Creates a zipfile with the above in it, for easy distribution.
This file can be found in ``./compiler/build/distributions/``
For normal use, the ``installDist`` task should suffice and after succesful completion, you can start the compiler with:
For normal use, the ``installDist`` task should suffice and after successful completion, you can start the compiler with:
``./compiler/build/install/prog8c/bin/prog8c <options> <sourcefile>``
@@ -163,7 +163,7 @@ One or more .p8 module files
This can be useful on emulators/systems that don't parse the breakpoint information in the list file,
such as the X16Emu emulator for the Commander X16.
Useful instructions to consider are ``brk`` and ``stp``.
For example for the Commander X16 emulator, ``stp`` is useful because it can actually tyrigger
For example for the Commander X16 emulator, ``stp`` is useful because it can actually trigger
a breakpoint halt in the debugger when this is enabled by running the emulator with -debug.
``-bytes2float <bytes>``
@@ -219,7 +219,9 @@ One or more .p8 module files
With this option you're basically saying: "Yes, I know I am treading on mighty thin ice here, but I don't want to be reminded about that".
``-libsearch pattern``
Search in the embedded library files for occurences of the given regex pattern.
Search in the embedded library files for occurrences of the given regex pattern.
If no exact matches are found, the compiler automatically attempts a "fuzzy" search
by inserting wildcards between the characters of the search pattern.
``-libdump path``
Extract all embedded library files into the given output directory.
@@ -269,7 +271,7 @@ One or more .p8 module files
``-nosourcelines``
Do not include the original prog8 source code lines as comments in the generated assembly code file,
mixed in between the actual generated assembly code. The default behavior is to include the sourcel lines.
mixed in between the actual generated assembly code. The default behavior is to include the source lines.
``-srcdirs <pathlist>``
Specify a list of extra paths (separated with ':'), to search in for imported modules.
@@ -280,7 +282,7 @@ One or more .p8 module files
Sets the target output of the compiler. This option is required.
``c64`` = Commodore 64, ``c128`` = Commodore 128, ``cx16`` = Commander X16, ``pet32`` - Commodore PET model 4032,
``virtual`` = builtin virtual machine.
You can also specify a file name as target, prog8 will when try to read the target
You can also specify a file name as target, prog8 will then try to read the target
machine's configuration and properties from that configuration file instead of using one of the built-in targets.
See :ref:`customizable_target` for details about this.
@@ -303,7 +305,7 @@ One or more .p8 module files
cx16: $A000 - $BFFF ; 8 kB in the specified HIRAM bank (note: no auto bank switching is done, you must make sure yourself that this HIRAM bank is active when accessing these variables!)
If you use this option, you can no longer use the part of the above memory area that is
alotted to the variables, for your own purposes. The output of the 64tass assembler step at the
allotted to the variables, for your own purposes. The output of the 64tass assembler step at the
end of compilation shows precise details of where and how much memory is used by the variables
(it's called 'BSS' section or Gap at the address mentioned above).
Assembling the program will fail if there are too many variables to fit in a single high ram bank.
@@ -435,7 +437,7 @@ WARNING
a compiled program is still produced, but it is a good idea to investigate the warnings that are printed.
ERROR
unrecoverable problem that prevented succesful compilation
unrecoverable problem that prevented successful compilation
Compiler doesn't run, complains about "UnsupportedClassVersionError"
@@ -486,7 +488,7 @@ you don't use symbols named just a single letter (especially 'a', 'x' and 'y').
Sometimes these are interpreted as the CPU register of that name. To avoid such confusions,
always use 2 or more letters for symbols in your assembly code.
'shadowing' warnings form the assembler
'shadowing' warnings from the assembler
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Avoid using 'a', 'x' or 'y' as symbols in your inlined assembly code.
Also avoid using 64tass' built-in function or type names as symbols in your inlined assembly code.
@@ -513,7 +515,7 @@ Examples
.. index:: single: Examples; Examples in source tree
A bunch of example programs can be found in the 'examples' directory of the source tree on Github.
A bunch of example programs can be found in the :source:`examples` directory of the source tree on Github.
(Or download the source archive from there to get all the files at once).
There are cross-platform examples that can be compiled for various systems unaltered,
+21 -3
View File
@@ -39,11 +39,29 @@ version = read_properties("gradle.properties")["version"].strip()
release = version
if release.endswith("SNAPSHOT"):
commit_id = subprocess.run(
["git", "rev-parse", "HEAD"], capture_output=True, text=True, check=True
).stdout.strip()
try:
commit_id = subprocess.run(
["git", "rev-parse", "HEAD"], capture_output=True, text=True, check=True
).stdout.strip()
except Exception:
commit_id = "master"
else:
commit_id = f"v{release}"
try:
# If a tag v{version} exists, we use that.
# Otherwise, we try to use the current branch name.
subprocess.run(["git", "rev-parse", "--verify", commit_id], capture_output=True, check=True)
except Exception:
# Tag doesn't exist, try to get the branch name
try:
branch = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True, text=True, check=True
).stdout.strip()
if branch != "HEAD":
commit_id = branch
except Exception:
# Git failed or detached HEAD, stay with v{release}
pass
# -- extensions
+22 -2
View File
@@ -54,6 +54,12 @@ Summary of Major Language Milestones
| 12.0 | **``long`` datatype**, **structs reintroduced**, |
| | **typed pointers** |
+-----------+-------------------------------------------------------------+
| 12.1 | ``swap()`` builtin, ``psg2`` module, PET32 floats/gfx/snd, |
| | ``strings.split()``/``next_token()``, C128 2MHz mode |
+-----------+-------------------------------------------------------------+
| 12.2 | Long loop support, ``private``, enum syntax, 2D arrays, |
| | new modules (serial, wavfile, adpcm, lineclip) |
+-----------+-------------------------------------------------------------+
Breaking Changes Summary
@@ -72,6 +78,8 @@ Major breaking changes that require code modifications when upgrading:
- **v10.2**: Stricter boolean types
- **v10.4**: Namespace reorganization (``cx16`` to ``cbm``)
- **v12.0**: Structs reintroduced (different from v1.11), typed pointers
- **v12.1**: Combined virtual register renames (``R0R1_32`` → ``R0R1``, etc.)
- **v12.2**: ``swap()`` is now a statement (not a function), ``math.crc16()`` and ``math.crc16_start()`` require new parameters, ``private`` is now a reserved keyword
2019–2022 — Early Development (Python to Kotlin Transition)
@@ -216,15 +224,27 @@ Major breaking changes that require code modifications when upgrading:
- **Float to int casts** (truncating)
- **Double buffering** in monogfx module
**v12.0–v12.2** — November 2024–Latest
**v12.0–v12.1** — November 2024–February 2026
- **``long`` datatype** — native 32-bit signed integers
- **Structs reintroduced** — grouping multiple fields together
- **Typed pointers** — can point to structs and specific types (not just ``uword`` addresses)
- **PET32 modules**: ``petgfx``, ``petsnd``, ``diskio``
- **New builtins**: ``swap()``, ``offsetof()``
- **New builtin**: ``swap()``
- **C128 enhancements**: ``fast()``/``slow()`` for 2MHz mode
- **String operations**: case-insensitive comparison, ``strings.split()``, ``strings.next_token()``
- **Virtual target improvements**: ``f_seek()``, ``f_tell()``, ``diskname()``, ``loadlib()``
**v12.2** — May 2026
- **Long loop support** — for-loops with long counters on 6502, long math (division, sqrt, min/max/clamp/abs)
- **``private`` keyword** — for structs, enums, aliases, variables, and subroutines
- **Const pointers** — compile-time constant-folding for const pointers
- **``enum`` syntax** — concise constant list declarations
- **2D array support** — ``ubyte[3][4] matrix``
- **``swap()`` is now a statement** — more efficient than the previous builtin function
- **``math.crc16()`` signature changed** — now requires ``initvalue`` and ``xorout`` parameters for flexibility
- **New compiler options**: ``-daemon`` (IDE integration), ``-nostdlib``
- **New modules**: ``serial`` (CX16 UART+ZiModem), ``lineclip``, ``wavfile``, ``adpcm``
- **``-libsearch`` fuzzy search fallback** — finds libraries even with partial names
*This document summarizes major and minor releases. Bugfix releases (e.g., v12.0.1, v12.1.1) are omitted for brevity.*
+1 -1
View File
@@ -8,7 +8,7 @@ Prog8 documentation - |version|
|
|
Prog8 is is a compiled programming language targeting the 8-bit 6502 CPU family.
Prog8 is a compiled programming language targeting the 8-bit 6502 CPU family.
It aims to provide many conveniences over raw assembly code (even when using a macro assembler),
while still being low level enough to create high performance programs.
+5 -4
View File
@@ -10,17 +10,17 @@ while still being low level enough to create high performance programs.
The targeted CPUs are the `6502 <https://en.wikipedia.org/wiki/MOS_Technology_6502>`_ /
`6510 <https://en.wikipedia.org/wiki/MOS_Technology_6510>`_ /
`65c02 <https://en.wikipedia.org/wiki/MOS_Technology_65C02>`_ microprocessors.
They are from the late 1970's and early 1980's and was used in many home computers from that era,
They are from the late 1970's and early 1980's and were used in many home computers from that era,
such as the `Commodore 64 <https://en.wikipedia.org/wiki/Commodore_64>`_.
You can compile programs for various machines that are built in into the compiler:
You can compile programs for various machines that are built into the compiler:
* Commander X16 (with 65c02 cpu, 65816 cpu specifics are currently not supported by prog8 itself)
* Commodore 64
* Commodore 128 (limited support)
* Commodore PET (limited support)
* any other 65(C)02 target machine or setup can be configured to a great extent in a user written configuration file.
There are some examples included for the Atari 800 XL, NEO6502, Foenix F256, and such.
There are some for the Atari 800 XL, NEO6502, Foenix F256, and such.
* some users have been experimenting with a NES and a C64 OS target as well.
Some language features are mentioned below, and you can also read :ref:`comparingprog8` if you
@@ -79,6 +79,7 @@ Language :index:`Features`
the compiled program in an emulator and provide debugging information to this emulator.
- the language looks like a mix of Python and C so should be quite easy to learn
- Modular programming, scoping via module source files, code blocks, and subroutines. No need for forward declarations.
- Can make vars and routines private, as a simple way to restrict access and hide implementation details
- Provides high level programming constructs but at the same time stay close to the metal;
still able to directly use memory addresses and ROM subroutines,
and inline assembly to have full control when every register, cycle or byte matters
@@ -227,7 +228,7 @@ To use it, configure your editor to launch ``prog8-language-server`` as the LSP
.. index:: pair: Tools; CTags
**CTags:** for editors that support them, such as Vim, there is an effort by a member of the cummunity to provide "ctags" files for Prog8.
**CTags:** for editors that support them, such as Vim, there is an effort by a member of the community to provide "ctags" files for Prog8.
The basic purpose of ctags is to index the definitions from your source code and let you find them easily.
It is not really to find each use of print for example, but to find where print is defined.
Visit `the Github repository <https://github.com/gillham/prog8ctags>`_ for the files and detailed usage information.
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff. Show more