mirror of
https://github.com/irmen/prog8.git
synced 2026-10-22 05:02:58 +00:00
Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86f9b6aa69 | ||
|
|
33f30d925e | ||
|
|
db227679a8 | ||
|
|
0f3475128a | ||
|
|
d451953e40 | ||
|
|
d505c42d15 | ||
|
|
5a593b9ef3 | ||
|
|
b411914624 | ||
|
|
0f80b695e7 | ||
|
|
276627abcb | ||
|
|
34225d10aa | ||
|
|
282c3aa00b | ||
|
|
19f3b03ac3 | ||
|
|
6cb6f37871 | ||
|
|
d0edc0b484 | ||
|
|
e9a3ed457e | ||
|
|
6001da30d8 | ||
|
|
e983c0c2b9 | ||
|
|
131134d24b | ||
|
|
f2d1dffbe0 | ||
|
|
4008280a62 | ||
|
|
2e7235ec02 | ||
|
|
a4330f5048 | ||
|
|
f65aeff231 | ||
|
|
505243aff7 | ||
|
|
d0f2fc13ba | ||
|
|
53d51dcd38 | ||
|
|
c1aa0e49fe | ||
|
|
e1e72d4317 | ||
|
|
feec343cbc | ||
|
|
5cdc4233f3 | ||
|
|
1ae89d18cc | ||
|
|
eb19d00a1e | ||
|
|
e01ee10ef0 | ||
|
|
f4b3cb12e1 | ||
|
|
77ced60ad6 |
No files matched your search
@@ -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
|
||||
```
|
||||
@@ -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,7 +269,6 @@ 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()`)
|
||||
@@ -248,6 +277,3 @@ Used to call routines at fixed memory addresses (like ROM KERNAL routines or thi
|
||||
- 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
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
[](https://jb.gg/OpenSource)
|
||||
I am using JetBrains products to develop the Prog8 compiler.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 */ }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,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")
|
||||
}
|
||||
@@ -1036,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()
|
||||
"|" -> {
|
||||
@@ -3164,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
|
||||
|
||||
@@ -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 == "-") {
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
}
|
||||
}
|
||||
@@ -434,8 +434,9 @@ 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
|
||||
@@ -446,8 +447,8 @@ result = dividend ;save memory by reusing dividend to store the result
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -388,6 +392,7 @@ asmsub atan2(ubyte x1 @R0, ubyte y1 @R1, ubyte x2 @R2, ubyte y2 @R3) -> ubyte @A
|
||||
;; 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -979,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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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) }
|
||||
|
||||
+2
-1
@@ -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)
|
||||
|
||||
@@ -150,23 +150,23 @@ class TestCompilerOnExamplesC64: FunSpec({
|
||||
|
||||
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)
|
||||
@@ -197,13 +197,13 @@ class TestCompilerOnExamplesCx16: FunSpec({
|
||||
val onlyCx16 = cartesianProduct(
|
||||
listOf(
|
||||
ExampleSizes("vtui/testvtui", cx16SizeOptimized=3041, cx16SizeUnoptimized=3041),
|
||||
ExampleSizes("pcmaudio/play-adpcm", cx16SizeOptimized=36147, cx16SizeUnoptimized=36645),
|
||||
ExampleSizes("pcmaudio/play-adpcm", cx16SizeOptimized=36147, cx16SizeUnoptimized=36637),
|
||||
ExampleSizes("pcmaudio/stream-wav", cx16SizeOptimized=5636, cx16SizeUnoptimized=7878),
|
||||
ExampleSizes("pcmaudio/stream-simple-aflow", cx16SizeOptimized=1850, cx16SizeUnoptimized=3753),
|
||||
ExampleSizes("pcmaudio/stream-simple-poll", cx16SizeOptimized=1564, cx16SizeUnoptimized=3462),
|
||||
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)
|
||||
@@ -218,37 +218,37 @@ 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=2516, cx16SizeUnoptimized=3945),
|
||||
ExampleSizes("showbmx", cx16SizeOptimized=2745, cx16SizeUnoptimized=5269),
|
||||
ExampleSizes("snow", cx16SizeOptimized=3120, cx16SizeUnoptimized=5295),
|
||||
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)
|
||||
@@ -279,29 +279,29 @@ class TestCompilerOnExamplesBothC64andCx16: FunSpec({
|
||||
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),
|
||||
@@ -335,17 +335,17 @@ class TestCompilerOnExamplesVirtual: FunSpec({
|
||||
|
||||
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)
|
||||
)
|
||||
@@ -376,8 +376,8 @@ class TestCompilerOnExamplesPET32: FunSpec({
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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 "/"
|
||||
}
|
||||
})
|
||||
@@ -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") {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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-SNAPSHOT by Irmen de Jong (irmen@razorvine.net)
|
||||
Prerelease version from git commit 8bf744cc 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
|
||||
|
||||
@@ -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 ()
|
||||
}
|
||||
|
||||
|
||||
@@ -124,8 +124,7 @@ 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.
|
||||
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.
|
||||
|
||||
|
||||
@@ -220,6 +220,8 @@ One or more .p8 module files
|
||||
|
||||
``-libsearch 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.
|
||||
@@ -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
@@ -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
@@ -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.*
|
||||
@@ -20,7 +20,7 @@ You can compile programs for various machines that are built into the compiler:
|
||||
* 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
|
||||
|
||||
@@ -67,10 +67,13 @@ Math
|
||||
Returns quotient and remainder of the division as two ubyte or uword values.
|
||||
Performs the division only once. Using '/' and '%' separately
|
||||
would perform the division twice, so using divmod is much more efficient for this.
|
||||
**Note:** Clobbers ``cx16.r15`` (the remainder is stored there for the word variant).
|
||||
The ``%`` operator on its own used to also clobber ``cx16.r15`` but now preserves it.
|
||||
|
||||
:index:`gcd` (a, b)
|
||||
Returns the GCD (greatest common divisor) of uwords a and b
|
||||
The routine is efficient and uses bit shifts instead of divisions.
|
||||
**Clobbers:** ``cx16.r0`` and ``cx16.r1``.
|
||||
|
||||
:index:`max` (x, y)
|
||||
Returns the largest of x and y. Supported for integer types only, for floats use ``floats.maxf()`` instead.
|
||||
@@ -938,6 +941,7 @@ miscellaneous
|
||||
``direction_qd (ubyte quadrant, ubyte xdelta, ubyte ydelta)``
|
||||
If you already know the quadrant and x/y deltas, calculate discrete direction between 0 and 23.
|
||||
This is a heavily optimized routine (small and fast).
|
||||
**Clobbers:** ``cx16.r0`` through ``cx16.r5`` (used as temporary variables).
|
||||
|
||||
``diff (ubyte b1, ubyte b2) -> ubyte``
|
||||
Returns the absolute difference, or distance, between the two byte values.
|
||||
@@ -946,6 +950,7 @@ miscellaneous
|
||||
``diffw (uword w1, uword w2) -> uword``
|
||||
Returns the absolute difference, or distance, between the two word values.
|
||||
(This routine is more efficient than doing a compare and a subtract separately, or using abs)
|
||||
**Clobbers:** ``cx16.r0``
|
||||
|
||||
|
||||
random numbers
|
||||
@@ -998,6 +1003,7 @@ random numbers
|
||||
|
||||
``log2w (uword v)``
|
||||
Returns the 2-Log of the word value v.
|
||||
**Clobbers:** ``cx16.r0``
|
||||
|
||||
trigonometry
|
||||
''''''''''''
|
||||
@@ -1014,6 +1020,7 @@ trigonometry
|
||||
``atan2 (ubyte x1, ubyte y1, ubyte x2, ubyte y2)``
|
||||
Fast arctan routine that uses more memory because of large lookup tables.
|
||||
Calculate the angle, in a 256-degree circle, between two points in the positive quadrant.
|
||||
**Clobbers:** ``cx16.r0`` through ``cx16.r4`` (used as temporary variables).
|
||||
|
||||
``sin8u (x)``
|
||||
Fast 8-bit ubyte sine using a lookup table.
|
||||
@@ -1156,9 +1163,52 @@ Available for the Cx16 target.
|
||||
Contains an abstraction for the Vera's PSG (programmable sound generator) to play simple waveforms.
|
||||
It has better consistent envelope timings than the older ``psg`` module, and easier access to all voice parameters if desired.
|
||||
It includes an interrupt handler routine for handling automatic ASR volume envelopes as well.
|
||||
See the examples/cx16/bdmusic.p8 program for ideas how to use it.
|
||||
|
||||
Read the :source:`psg source code <compiler/res/prog8lib/cx16/psg2.p8>` to see what's in there.
|
||||
Envelope timing (one tick = one call to ``update()``):
|
||||
|
||||
- Attack and Release are linear volume ramps: ``duration_ticks = ceil(256 / speed)``
|
||||
- Higher speed = faster ramp (shorter duration)
|
||||
- speed=1 -> 256 ticks; speed=4 -> 64 ticks; speed=255 -> 1 tick
|
||||
- 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 then transitions to Release
|
||||
- speed=0 stalls at that phase (useful for infinite sustain)
|
||||
- Actual real-time duration depends on how often ``update()`` is called.
|
||||
If called every vsync at 60Hz, each tick is ~16.7ms.
|
||||
|
||||
Available routines:
|
||||
|
||||
``init ()``
|
||||
Initialize the module, set all voices to default off state.
|
||||
|
||||
``off ()``
|
||||
Turn off all voices immediately, then call ``update`` automatically.
|
||||
|
||||
``voice (voice_num, channels, volume, waveform, pulsewidth)``
|
||||
Set all parameters for a voice (frequency unchanged). Disables any active envelope.
|
||||
|
||||
``frequency (voice_num, freq)``
|
||||
Set frequency word for the voice. This is the raw VERA PSG register value (no scaling, written directly to the hardware): ``freq = HERZ(Hz) / 0.3725290298461914``.
|
||||
|
||||
``volume (voice_num, vol)``
|
||||
Set volume directly (0-63). Disables any active envelope. Also sets the max volume for subsequent envelope use.
|
||||
|
||||
``envelope (voice_num, attack, sustain, release)``
|
||||
Start ASR envelope on the voice (volume starts from 0). ``attack`` and ``release`` are speeds 0-255, ``sustain`` is duration in ticks.
|
||||
Speed 255 means "instant".
|
||||
|
||||
``getvoice (voice_num) -> ^Voice``
|
||||
Return pointer to a voice's parameter struct (for direct field access).
|
||||
|
||||
``update () -> bool``
|
||||
Advance all active envelopes by one tick, then write all 16 voices to the VERA PSG registers. This call is IRQ-safe!
|
||||
|
||||
Waveform constants: ``PULSE``, ``SAWTOOTH``, ``TRIANGLE``, ``NOISE``
|
||||
Channel constants: ``LEFT``, ``RIGHT``, ``BOTH``, ``DISABLED``
|
||||
|
||||
See the :source:`examples/cx16/bdmusic.p8` program for ideas how to use it.
|
||||
|
||||
Read the :source:`psg2 source code <compiler/res/prog8lib/cx16/psg2.p8>` to see everything that's in there.
|
||||
|
||||
|
||||
serial (cx16 only)
|
||||
@@ -1240,7 +1290,7 @@ Various sorting routines (gnome sort and shell sort variants) for byte, word and
|
||||
**NOTE:** all word and str arrays have to be @nosplit! Words and pointers need to be consecutive in memory for now.
|
||||
**NOTE:** sorting is done in ascending order.
|
||||
Read the :source:`sorting source code <compiler/res/prog8lib/sorting.p8>`
|
||||
to see what's in there. Also check out the `sortingbench` example.
|
||||
to see what's in there. Also check out the :source:`sortingbench <examples/cx16/sortingbench.p8>` example.
|
||||
|
||||
|
||||
sprites (cx16 only)
|
||||
@@ -1251,7 +1301,7 @@ Available for the Cx16 target. Simple routines to manipulate sprites.
|
||||
They're not written for high performance, but for simplicity.
|
||||
That's why they control one sprite at a time. The exception is the ``pos_batch`` routine,
|
||||
which is quite efficient to update sprite positions of multiple sprites in one go.
|
||||
See the examples/cx16/sprites/dragon.p8 and dragons.p8 programs for ideas how to use it.
|
||||
See the :source:`examples/cx16/sprites/dragon.p8` and :source:`dragons.p8 <examples/cx16/sprites/dragons.p8>` programs for ideas how to use it.
|
||||
|
||||
Read the :source:`sprites source code <compiler/res/prog8lib/cx16/sprites.p8>`
|
||||
to see what's in there.
|
||||
@@ -1544,6 +1594,7 @@ miscellaneous
|
||||
- 8 = Atari 8 bits
|
||||
- 16 = Commander X16
|
||||
- 25 = Foenix F256 family
|
||||
- 32 = Commodore PET
|
||||
- 64 = Commodore 64
|
||||
- 128 = Commodore 128
|
||||
- 255 = Virtual machine
|
||||
|
||||
@@ -1352,6 +1352,27 @@ flag such as Carry (Pc).
|
||||
For example, you can use R0+R1, R2+R3, R4+R5 and so on to take a long value instead.
|
||||
The syntax to use as a 'register' name for those pairs is ``R0R1``, ``R2R3``, ``R4R5`` and so on.
|
||||
|
||||
.. caution::
|
||||
**Virtual register clobbering by compiler operations:**
|
||||
|
||||
Various compiler operations and builtin routines use the virtual registers R0-R15 as
|
||||
temporary storage or for placing return values. If you are using the virtual registers
|
||||
directly in your program (or via ``@R0..@R15`` parameter annotations), be aware of the
|
||||
fact that they will not always preserve their value!
|
||||
|
||||
========================== ================================================
|
||||
Register(s) When clobbered
|
||||
========================== ================================================
|
||||
``R0-R3`` Many library routines use these as scratch
|
||||
``R12-R15`` Several long value operations and arithmetic expressions
|
||||
``R14-R15`` Several long value operations / word '%', '/', divmod
|
||||
``R0..R15`` if specified: multi-value subroutine return values, parameter reuse.
|
||||
several library routines may clobber one or more of these registers too.
|
||||
========================== ================================================
|
||||
|
||||
Additionally, on all targets, the virtual registers R0-R15 are **not preserved** across IRQ
|
||||
handler calls. Use ``cx16.save_virtual_registers()`` / ``cx16.restore_virtual_registers()``
|
||||
if your IRQ handler needs to use them.
|
||||
|
||||
External subroutines
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
+5
-10
@@ -10,16 +10,11 @@ Dead Code Elimination bug in 64tass, for nested subroutines
|
||||
that only used other gfx_lores functions (like ``circle()``). Fixed by moving it to a separate ``internal_line_plot()``.
|
||||
|
||||
|
||||
Weird Heisenbug
|
||||
^^^^^^^^^^^^^^^
|
||||
- BUG: examples/cube3d-float crashes with div by zero error on C64 (works on cx16. Already broken in v11, v10 still worked)
|
||||
caused by the RTS after JMP removal in optimizeJsrRtsAndOtherCombinations (replacing it with a NOP makes the problem disappear !??!?).
|
||||
Also observed in the boingball example for the C64 when some code was removed from the start and end.
|
||||
|
||||
|
||||
Future Things and Ideas
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- make enums strongly typed instead of just syntactic sugar for ints (see enum-strong-type.md for the plan)
|
||||
- make enums strongly typed instead of just syntactic sugar for ints (see ideas/enum-strong-type.md for the plan)
|
||||
- arrays inline in structs (see ideas/arrays-in-structs.md)
|
||||
- add %option private_symbols to make access mode private by default; need (new) 'public' keyword to explicitly mark symbols public.
|
||||
- symboldump: some sort of javadocs generated from the p8 source files (instead of just the function signatures). Use markdown for formatting, not html.
|
||||
- why are (interned) strings stored as initialization value in the SymbolTable AND as string nodes in the interned string block? Something seems redundant here?
|
||||
- when implementing unsigned longs: remove the (multiple?) "TODO "hack" to allow unsigned long constants to be used as values for signed longs, without needing a cast
|
||||
@@ -35,7 +30,7 @@ Future Things and Ideas
|
||||
- sizeof(pointer) is now always 2 (an uword), make this a variable in the ICompilationTarget so that it could be 4 at the time we might ad a 32-bits 68000 target for example. Much code assumes word size addresses though.
|
||||
- add float support to the configurable compiler targets. Restrictions: just have "cbm-style floats" as an option (to that it can slot into the current float codegen), where all you have to specify is the addresses of AYINT and GIVAYF and FADDT and all their friends.
|
||||
- Change scoping rules for qualified symbols so that they don't always start from the root but behave like other programming languages (look in local scope first), maybe only when qualified symbol starts with '.' such as: .local.value = 33
|
||||
- something to reduce the need to use fully qualified names all the time. 'with' ? Or 'using <prefix>'?
|
||||
- something to reduce the need to use fully qualified names all the time. 'with <prefix>' or 'using <prefix>'?
|
||||
- Improve register load order in subroutine call args assignments:
|
||||
in certain situations (need examples!), the "wrong" order of evaluation of function call arguments is done which results
|
||||
in overwriting registers that already got their value, which requires a lot of stack juggling (especially on plain 6502 cpu!)
|
||||
@@ -65,7 +60,6 @@ Romable (%option romable)
|
||||
|
||||
IR/VM
|
||||
^^^^^
|
||||
- getting it in shape for code generation: the IR file should be able to encode every detail about a prog8 program (the VM doesn't have to actually be able to run all of it though!)
|
||||
- maybe change all branch instructions to have 2 exits (label if branch condition true, and label if false) instead of 1, and get rid of the implicit "next code chunk" link between chunks.
|
||||
- implement more TODOs in AssignmentGen?
|
||||
- add more optimizations in IRPeepholeOptimizer?
|
||||
@@ -91,6 +85,7 @@ Libraries
|
||||
Optimizations
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
- Inline subroutines with 1 (or even more) simple argument, BE VERY CAREFUL though because this is a complicated inlining action that could cause endless loops. Currently inlining is limited to subroutines with 0 params.
|
||||
- Port more benchmarks from https://thred.github.io/c-bench-64/ to prog8 and see how it stacks up. (see benchmark-c/ directory)
|
||||
- Compilation speed: try to join multiple modifications in 1 result in the AST processors instead of returning it straight away every time
|
||||
- VariableAllocator: can we think of a smarter strategy for allocating variables into zeropage, rather than first-come-first-served?
|
||||
|
||||
@@ -327,7 +327,7 @@ to be done on word values, and don't want to explicitly have to cast everything
|
||||
**longs and cx16.R12,R13,R14,R15**:
|
||||
**Some operations on long values require the use of the R12-R15 virtual register as temporary storage**
|
||||
So if you are working with long values, you should assume that the contents of R12-R15 could be destroyed.
|
||||
()Many operations preserve the values, but not all because of reasons)
|
||||
(Many operations preserve the values, but not all, because of reasons)
|
||||
**Using R12,R13,R14,R15 in expressions that work with longs, will sometimes give a corrupted result, without
|
||||
a warning of the compiler!** It is strongly advised to *not* use R12-R15 at all when dealing with longs.
|
||||
|
||||
|
||||
+261
-10
@@ -1,18 +1,269 @@
|
||||
%encoding iso
|
||||
%option no_sysinit
|
||||
%import textio
|
||||
%zeropage basicsafe
|
||||
%option no_sysinit
|
||||
%encoding iso
|
||||
|
||||
main {
|
||||
; test values
|
||||
ubyte @shared ub0 = 0
|
||||
ubyte @shared ub1 = 1
|
||||
ubyte @shared ub127 = 127
|
||||
ubyte @shared ub128 = 128
|
||||
ubyte @shared ub255 = 255
|
||||
|
||||
byte @shared bmin = -128
|
||||
byte @shared b_1 = -1
|
||||
byte @shared b0 = 0
|
||||
byte @shared b1 = 1
|
||||
byte @shared bmax = 127
|
||||
|
||||
uword @shared uw0 = 0
|
||||
uword @shared uw1 = 1
|
||||
uword @shared uw32767 = 32767
|
||||
uword @shared uw32768 = 32768
|
||||
uword @shared uwmax = 65535
|
||||
|
||||
word @shared wmin = -32768
|
||||
word @shared w_1 = -1
|
||||
word @shared w0 = 0
|
||||
word @shared w1 = 1
|
||||
word @shared wmax = 32767
|
||||
|
||||
long @shared lmin = -2147483647
|
||||
long @shared l_1 = -1
|
||||
long @shared l0 = 0
|
||||
long @shared l1 = 1
|
||||
long @shared lmax = 2147483647
|
||||
uword @shared failures = 0
|
||||
|
||||
sub start() {
|
||||
long @shared @nozp size = 147320
|
||||
while size>0 {
|
||||
size--
|
||||
if size & $ff == 0 {
|
||||
txt.chrout('.')
|
||||
}
|
||||
txt.iso()
|
||||
txt.print("--- sgn tests ---\n")
|
||||
test_sgn()
|
||||
txt.print("--- cmp ubyte ---\n")
|
||||
test_ubyte()
|
||||
txt.print("--- cmp byte ---\n")
|
||||
test_byte()
|
||||
txt.print("--- cmp uword ---\n")
|
||||
test_uword()
|
||||
txt.print("--- cmp word ---\n")
|
||||
test_word()
|
||||
txt.print("--- cmp long ---\n")
|
||||
test_long()
|
||||
txt.print("\n")
|
||||
txt.print_uw(failures)
|
||||
txt.print(" failures!\n")
|
||||
sys.poweroff_system()
|
||||
}
|
||||
|
||||
sub print_flags(ubyte f) {
|
||||
if ((f & 128) != 0) txt.print("n") else txt.print(".")
|
||||
if ((f & 64) != 0) txt.print("v") else txt.print(".")
|
||||
if ((f & 2) != 0) txt.print("z") else txt.print(".")
|
||||
if ((f & 1) != 0) txt.print("c") else txt.print(".")
|
||||
}
|
||||
|
||||
sub test_sgn() {
|
||||
check_sgn_b(b_1, "-1")
|
||||
check_sgn_b(b0, "0")
|
||||
check_sgn_b(b1, "1")
|
||||
check_sgn_l(l_1, "-1")
|
||||
check_sgn_l(l0, "0")
|
||||
check_sgn_l(l1, "1")
|
||||
}
|
||||
|
||||
sub check_sgn_b(byte v, uword s) {
|
||||
txt.print("sgn(")
|
||||
txt.print(s)
|
||||
txt.print(") -> ")
|
||||
byte res = sgn(v)
|
||||
ubyte flags = sys.read_flags() & $c3
|
||||
txt.print_b(res)
|
||||
txt.print(" flags: ")
|
||||
print_flags(flags)
|
||||
txt.print("\n")
|
||||
}
|
||||
|
||||
sub check_sgn_l(long v, uword s) {
|
||||
txt.print("sgn(")
|
||||
txt.print(s)
|
||||
txt.print(") -> ")
|
||||
byte res = sgn(v)
|
||||
ubyte flags = sys.read_flags() & $c3
|
||||
txt.print_b(res)
|
||||
txt.print(" flags: ")
|
||||
print_flags(flags)
|
||||
txt.print("\n")
|
||||
}
|
||||
|
||||
sub check_ub(ubyte a, ubyte b, uword sa, uword sb) {
|
||||
txt.print("cmp(")
|
||||
txt.print(sa)
|
||||
txt.print(",")
|
||||
txt.print(sb)
|
||||
txt.print(") -> ")
|
||||
cmp(a, b)
|
||||
ubyte actual = sys.read_flags() & $83
|
||||
print_flags(actual)
|
||||
ubyte res = a - b
|
||||
ubyte expected = 0
|
||||
if res == 0 expected |= 2
|
||||
if (res & 128) != 0 expected |= 128
|
||||
if a >= b expected |= 1
|
||||
if actual != expected {
|
||||
txt.print(" fail!")
|
||||
failures += 1
|
||||
}
|
||||
txt.nl()
|
||||
;;sys.poweroff_system()
|
||||
txt.print("\n")
|
||||
}
|
||||
|
||||
sub test_ubyte() {
|
||||
do_ub(ub0, ub0, "0", "0")
|
||||
do_ub(ub0, ub1, "0", "1")
|
||||
do_ub(ub127, ub128, "127", "128")
|
||||
do_ub(ub0, ub255, "0", "255")
|
||||
do_ub(ub255, ub255, "255", "255")
|
||||
}
|
||||
sub do_ub(ubyte a, ubyte b, uword sa, uword sb) {
|
||||
check_ub(a, b, sa, sb)
|
||||
if a != b check_ub(b, a, sb, sa)
|
||||
}
|
||||
|
||||
sub check_b(byte a, byte b, uword sa, uword sb) {
|
||||
txt.print("cmp(")
|
||||
txt.print(sa)
|
||||
txt.print(",")
|
||||
txt.print(sb)
|
||||
txt.print(") -> ")
|
||||
cmp(a, b)
|
||||
ubyte actual = sys.read_flags() & $c3
|
||||
print_flags(actual)
|
||||
byte res = a - b
|
||||
ubyte expected = 0
|
||||
if res == 0 expected |= 2
|
||||
if (res & 128) != 0 expected |= 128
|
||||
if ((a as ubyte) >= (b as ubyte)) { expected |= 1 }
|
||||
if (((a ^ b) & (a ^ res) & 128) != 0) expected |= 64
|
||||
if actual != expected {
|
||||
txt.print(" fail!")
|
||||
failures += 1
|
||||
}
|
||||
txt.print("\n")
|
||||
}
|
||||
|
||||
sub test_byte() {
|
||||
do_b(b0, b0, "0", "0")
|
||||
do_b(b0, b1, "0", "1")
|
||||
do_b(b_1, b0, "-1", "0")
|
||||
do_b(bmin, bmax, "-128", "127")
|
||||
do_b(b_1, b1, "-1", "1")
|
||||
do_b(bmin, bmin, "-128", "-128")
|
||||
}
|
||||
sub do_b(byte a, byte b, uword sa, uword sb) {
|
||||
check_b(a, b, sa, sb)
|
||||
if a != b check_b(b, a, sb, sa)
|
||||
}
|
||||
|
||||
sub check_uw(uword a, uword b, uword sa, uword sb) {
|
||||
txt.print("cmp(")
|
||||
txt.print(sa)
|
||||
txt.print(",")
|
||||
txt.print(sb)
|
||||
txt.print(") -> ")
|
||||
cmp(a, b)
|
||||
ubyte actual = sys.read_flags() & $83
|
||||
print_flags(actual)
|
||||
uword res = a - b
|
||||
ubyte expected = 0
|
||||
if res == 0 expected |= 2
|
||||
if (res & $8000) != 0 expected |= 128
|
||||
if a >= b expected |= 1
|
||||
if actual != expected {
|
||||
txt.print(" fail!")
|
||||
failures += 1
|
||||
}
|
||||
txt.print("\n")
|
||||
}
|
||||
|
||||
sub test_uword() {
|
||||
do_uw(uw0, uw0, "0", "0")
|
||||
do_uw(uw0, uw1, "0", "1")
|
||||
do_uw(uw32767, uw32768, "32767", "32768")
|
||||
do_uw(uw0, uwmax, "0", "65535")
|
||||
do_uw(uwmax, uwmax, "65535", "65535")
|
||||
}
|
||||
sub do_uw(uword a, uword b, uword sa, uword sb) {
|
||||
check_uw(a, b, sa, sb)
|
||||
if a != b check_uw(b, a, sb, sa)
|
||||
}
|
||||
|
||||
sub check_w(word a, word b, uword sa, uword sb) {
|
||||
txt.print("cmp(")
|
||||
txt.print(sa)
|
||||
txt.print(",")
|
||||
txt.print(sb)
|
||||
txt.print(") -> ")
|
||||
cmp(a, b)
|
||||
ubyte actual = sys.read_flags() & $c3
|
||||
print_flags(actual)
|
||||
word res = a - b
|
||||
ubyte expected = 0
|
||||
if res == 0 expected |= 2
|
||||
if (res & $8000) != 0 expected |= 128
|
||||
if ((a as uword) >= (b as uword)) { expected |= 1 }
|
||||
if (((a ^ b) & (a ^ res) & $8000) != 0) expected |= 64
|
||||
if actual != expected {
|
||||
txt.print(" fail!")
|
||||
failures += 1
|
||||
}
|
||||
txt.print("\n")
|
||||
}
|
||||
|
||||
sub test_word() {
|
||||
do_w(w0, w0, "0", "0")
|
||||
do_w(w0, w1, "0", "1")
|
||||
do_w(w_1, w0, "-1", "0")
|
||||
do_w(wmin, wmax, "-32768", "32767")
|
||||
do_w(w_1, w1, "-1", "1")
|
||||
do_w(wmin, wmin, "-32768", "-32768")
|
||||
}
|
||||
sub do_w(word a, word b, uword sa, uword sb) {
|
||||
check_w(a, b, sa, sb)
|
||||
if a != b check_w(b, a, sb, sa)
|
||||
}
|
||||
|
||||
sub check_l(long a, long b, uword sa, uword sb) {
|
||||
txt.print("cmp(")
|
||||
txt.print(sa)
|
||||
txt.print(",")
|
||||
txt.print(sb)
|
||||
txt.print(") -> ")
|
||||
cmp(a, b)
|
||||
ubyte actual = sys.read_flags() & $c3
|
||||
print_flags(actual)
|
||||
long res = a - b
|
||||
ubyte expected = 0
|
||||
if res == 0 expected |= 2
|
||||
if (res & $80000000) != 0 expected |= 128
|
||||
if (a ^ $80000000) >= (b ^ $80000000) expected |= 1
|
||||
if (((a ^ b) & (a ^ res) & $80000000) != 0) expected |= 64
|
||||
if actual != expected {
|
||||
txt.print(" fail!")
|
||||
failures += 1
|
||||
}
|
||||
txt.print("\n")
|
||||
}
|
||||
|
||||
sub test_long() {
|
||||
do_l(l0, l0, "0", "0")
|
||||
do_l(l0, l1, "0", "1")
|
||||
do_l(l_1, l0, "-1", "0")
|
||||
do_l(lmin, lmax, "lmin", "lmax")
|
||||
do_l(l_1, l1, "-1", "1")
|
||||
do_l(lmin, lmin, "lmin", "lmin")
|
||||
}
|
||||
sub do_l(long a, long b, uword sa, uword sb) {
|
||||
check_l(a, b, sa, sb)
|
||||
if a != b check_l(b, a, sb, sa)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -4,4 +4,4 @@ org.gradle.parallel=true
|
||||
org.gradle.daemon=true
|
||||
org.gradle.configuration-cache=false
|
||||
kotlin.code.style=official
|
||||
version=12.2-BETA3
|
||||
version=12.2.1
|
||||
@@ -0,0 +1,282 @@
|
||||
|
||||
# Requirements
|
||||
|
||||
### Overview & Goals
|
||||
Support inline arrays by-value inside struct fields. Currently, struct fields can only hold scalar types (bool, byte, ubyte, word, uword, long, float) and pointers. Arrays embedded directly in a struct's memory layout are not supported. The goal is to allow:
|
||||
|
||||
```prog8
|
||||
struct Entry {
|
||||
ubyte age
|
||||
ubyte[10] name ; 10 bytes inline, not a pointer
|
||||
word[4] scores
|
||||
}
|
||||
```
|
||||
|
||||
When a struct instance is created, the array data is stored contiguously within the struct's memory block, not as a separate pointer to external data. This differs from `str` fields, which only store a 2-byte pointer.
|
||||
|
||||
### Scope
|
||||
|
||||
#### In Scope
|
||||
- 1D arrays of basic types (`ubyte[N]`, `word[N]`, `uword[N]`, `long[N]`, `float[N]`, `bool[N]`) as struct fields
|
||||
- `@nosplit` tag for word arrays in structs
|
||||
- `ubyte[][] name` (empty array sig, size inferred from initializer)
|
||||
- Reading and writing individual elements: `entry.name[i] = val`, `val = entry.name[i]`
|
||||
- Initialization of struct instances with array fields via `^^Entry : [...]`
|
||||
- `sizeof(Entry)` and `offsetof(Entry.name)` for structs with array fields
|
||||
- Struct total size still limited to 256 bytes
|
||||
|
||||
#### Out of Scope
|
||||
- 2D arrays as struct fields initially (but grammar can accept them for future use)
|
||||
- Entire-array operations on array fields (e.g., `copy entry.name, source` on the field) -- use element-by-element copying
|
||||
- Arrays of structs inside structs
|
||||
- `str` fields changed from pointer to inline (keep `str` as pointer)
|
||||
- Variable-length array fields
|
||||
- Array fields of struct pointer types (`^^Foo[N]`)
|
||||
|
||||
### User Stories
|
||||
- As a Prog8 developer, I want to store small fixed-size arrays inline in structs so that I can model data structures like file system entries, game objects with stats, or C-style structs without needing separate memory allocations.
|
||||
- As a Prog8 developer, I want `sizeof` and `offsetof` to correctly reflect the full inline storage of array fields so that I can use them in assembly and memory layout calculations.
|
||||
- As a Prog8 developer, I want to read and write individual array elements in a struct field using natural `instance.field[i]` syntax.
|
||||
|
||||
|
||||
# Technical Design
|
||||
|
||||
### Current Implementation
|
||||
Struct fields are stored as `Array<Pair<DataType, String>>` in `StructDecl` (compiler AST) and `List<Pair<DataType, String>>` in `StStruct` (simple AST). The `DataType` does not store array dimensions; that information lives separately in `VarDecl.arraysize`. For structs, there is no such separate storage.
|
||||
|
||||
Key restrictions currently in place:
|
||||
1. **Grammar** (`Prog8ANTLR.g4:174`): `structfielddecl: datatype identifierlist` -- no array index syntax
|
||||
2. **AstChecker** (`AstChecker.kt:2301`): rejects non-basic, non-numeric, non-pointer field types
|
||||
3. **Memory layout** (`StructDecl.memsize()`:527, `offsetof()`:533): passes `numElements=1` to `sizer.memorySize()` -- correct for scalar fields, but for array fields would need to pass the actual array dimension instead
|
||||
|
||||
Field access `ptr.field[idx]` on a struct pointer hits an error/TODO at `AstChecker.kt:1583-1605` because the `.` operator with an array-indexed right side is not handled for struct pointers.
|
||||
|
||||
### Key Decisions
|
||||
- **`StructField` data class**: Introduce a proper data class for fields to carry array dimension alongside type and name, rather than continuing with `Pair<DataType, String>`. This provides a clean extension point and avoids parallel arrays.
|
||||
- **Grammar mirrors `vardecl`**: The `(arrayindex arrayindex? | EMPTYARRAYSIG)?` suffix follows the exact pattern used for variable declarations, keeping the language consistent.
|
||||
- **Array dimension in DataType**: Use `DataType.elementToArray()` to create array types (with `isArray=true`, `sub=elementType`) for array fields, just like `VarDecl` does. The concrete dimension lives in `StructField.arraySize`.
|
||||
- **Element-level access only**: `ptr.field` on its own (without index) is not a valid rvalue for array fields; you must write `ptr.field[i]`. This keeps code generation simple and avoids the need for struct-field-as-array-reference semantics.
|
||||
- **Initialization**: Use nested ArrayLiteral values for array fields in `^^Type : [...]` initializer. The invariant `args.size == fields.size` is preserved -- each field gets one argument. For `ubyte[N]` fields, string literals are also accepted as initializer shorthand, auto-expanded to individual bytes.
|
||||
|
||||
### Proposed Changes
|
||||
|
||||
#### 1. Grammar (`Prog8ANTLR.g4:174`)
|
||||
```
|
||||
- structfielddecl: datatype identifierlist ;
|
||||
+ structfielddecl: datatype (arrayindex arrayindex? | EMPTYARRAYSIG)? identifierlist ;
|
||||
```
|
||||
|
||||
#### 2. `StructField` data class (new, or inline in `StructDecl`)
|
||||
```kotlin
|
||||
data class StructField(
|
||||
val type: DataType,
|
||||
val name: String,
|
||||
val arraySize: ArrayIndex? = null // null = scalar
|
||||
) {
|
||||
val isArray: Boolean get() = arraySize != null
|
||||
}
|
||||
```
|
||||
|
||||
**`StructDecl` changes** (`AstStatements.kt`):
|
||||
- `fields: Array<StructField>` replaces `Array<Pair<DataType, String>>`
|
||||
- `memsize()`: element-count-aware: `if(field.isArray) sizer.memorySize(field.type, resolvedSize) else sizer.memorySize(field.type, 1)`
|
||||
- `offsetof()`: same treatment for field offset accumulation
|
||||
- `getFieldType()`: unchanged (already delegates to `DataType`)
|
||||
- `copy()`, `sameas()`: updated for new field type
|
||||
- `offsetof()` constraint: field offsets must remain ≤ 255 (Y-register limit)
|
||||
|
||||
#### 3. `Antlr2KotlinVisitor.getStructField()` (`Antlr2KotlinVisitor.kt:801`)
|
||||
- Read `ctx.arrayindex()` from context
|
||||
- If array indices present, convert base DataType via `elementToArray()` (same as line 616/926)
|
||||
- Return `StructField(dt, name, arraySize)` instead of `Pair<DataType, String>`
|
||||
|
||||
#### 4. `StStruct` changes (`SymbolTable.kt`)
|
||||
- `fields: List<StructField>` replaces `List<Pair<DataType, String>>`
|
||||
- `getField()`: compute offsets with element-count-aware `sizer.memorySize()`
|
||||
- Update `size` calculation in constructor and `SymbolTableMaker`
|
||||
|
||||
#### 5. AstChecker changes (`AstChecker.kt`)
|
||||
|
||||
**Field type validation (line 2301)**:
|
||||
```kotlin
|
||||
struct.fields.forEach { field ->
|
||||
val dt = field.type
|
||||
val allowed = dt.isBasic
|
||||
|| dt.base.isNumericOrBool
|
||||
|| dt.isPointer
|
||||
|| (dt.isArray && dt.sub!!.isNumericOrBool)
|
||||
if(!allowed)
|
||||
errors.err("only booleans, numeric and pointer fields allowed in a struct: '${field.name}'", struct.position)
|
||||
}
|
||||
```
|
||||
|
||||
**`.` operator with array field (lines 1583-1605)**:
|
||||
Handle the case where `left` is a struct pointer and `right` is `ArrayIndexedExpression("field", idx)`. Validate the field exists and is an array type. Set the result type to the element type of the array.
|
||||
|
||||
#### 6. 6502 Code Generator (`PointerAssignmentsGen.kt`)
|
||||
|
||||
`deref()` and `operatorDereference()` for `ptr.field[idx]`:
|
||||
- Compute address as `base_pointer + field_offset + (index * element_size)`
|
||||
- If `field_offset + (max_index * element_size) > 255`, the Y-register can't hold the full offset; emit code to add the high byte to the pointer
|
||||
- This is already handled for general array indexing on pointers; struct field offset just needs to be added to the base
|
||||
|
||||
#### 7. IR Code Generator (`ExpressionGen.kt`, `AssignmentGen.kt`)
|
||||
|
||||
`traverseRestOfDerefChainToCalculateFinalAddress()`:
|
||||
- When a field in the deref chain is an array and has an index, add `index * element_size` to the computed address
|
||||
- Scale the index by element size before adding to the field offset
|
||||
|
||||
#### 8. StaticStructInitializer changes (`AstChecker.kt:2681`)
|
||||
|
||||
Current validation at line 2698 enforces `args.size == struct.fields.size`. With array fields, the invariant is preserved: each array field receives a single initializer value which is an ArrayLiteral (or string literal for `ubyte[N]`).
|
||||
|
||||
- Accept ArrayLiteral or string literal as initializer value for array fields
|
||||
- Validate element count in ArrayLiteral matches the array dimension
|
||||
- For `ubyte[N]` fields, accept a string literal as shorthand; expand/crop to N bytes
|
||||
- Allow `[]` (empty array sig) for uninitialized array fields
|
||||
- Update `SymbolTableMaker.handleStructAllocation()` to process nested initializer values
|
||||
|
||||
Example:
|
||||
```prog8
|
||||
struct Entry {
|
||||
ubyte age
|
||||
ubyte[10] name
|
||||
word[3] scores
|
||||
}
|
||||
|
||||
^^Entry entry = ^^Entry : [25, "Hello", [100, 200, 300]]
|
||||
```
|
||||
|
||||
#### 9. `AstToplevel.searchStructFieldRef()` (line 227)
|
||||
- Uses `getFieldType()` which returns `DataType` already -- should work if `getFieldType` returns the array DataType
|
||||
- The `StructFieldRef` node gets the field's type; subsequent array indexing should resolve the element type
|
||||
|
||||
#### 10. `AstToSourceTextConverter`
|
||||
- Update `visit(StructDecl)` to emit array dimensions for struct fields
|
||||
|
||||
### Data Models
|
||||
|
||||
```kotlin
|
||||
data class StructField(
|
||||
val type: DataType,
|
||||
val name: String,
|
||||
val arraySize: ArrayIndex? = null
|
||||
)
|
||||
```
|
||||
|
||||
Used in:
|
||||
- `StructDecl.fields: Array<StructField>` (compiler AST)
|
||||
- `StStruct.fields: List<StructField>` (simple AST)
|
||||
- `Antlr2KotlinVisitor.getStructField()` return type
|
||||
|
||||
The `ArrayIndex` class already exists (`AstStatements.kt:600`) with `indexExpr: Expression` and supports const-evaluation.
|
||||
|
||||
|
||||
# Migration / Compatibility
|
||||
|
||||
No breaking changes to existing code. All existing struct declarations continue to work unchanged. The change is purely additive:
|
||||
- Existing scalar/pointer fields: `isArray=false` in `StructField`, behavior identical to current `Pair<DataType, String>`
|
||||
- New array fields: only accessible when explicitly declared with `type[N] varname`
|
||||
|
||||
Existing `AstChecker` tests that check the "only booleans, numeric and pointer fields" error message need updating to also permit array types.
|
||||
|
||||
Existing tests accessing `struct.fields[i].first` / `.second` need updating to use `.type` / `.name` instead (or add backward-compat extension properties).
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
### Validation Approach
|
||||
Primary testing uses the **virtual target** and prog8c's IR debugging features. This allows fast iteration without needing 6502 emulators.
|
||||
|
||||
**Debugging workflow for each change:**
|
||||
1. **Syntax check**: `prog8c -check input.p8` -- quick semantic validation
|
||||
2. **AST inspection**: `prog8c -target virtual -printast1 input.p8` -- inspect compiler AST after parsing
|
||||
3. **IR inspection**: `prog8c -target virtual -out /tmp/out input.p8` then examine the generated `.p8ir` file
|
||||
4. **IR comparison**: `prog8c -target virtual -compareir baseline.p8ir new.p8ir` -- compare IR instruction changes
|
||||
5. **VM execution**: `prog8c -target virtual -emu input.p8` -- run and see stdout output
|
||||
6. **VM trace**: `prog8c -vm input.p8ir -vmtrace` -- step through IR instructions with location tracking
|
||||
7. **Disable optimizations**: `prog8c -target virtual -noopt -emu input.p8` -- isolate optimizer issues
|
||||
8. **Full regression**: `gradle build --console=plain` -- run all unit tests
|
||||
|
||||
**Important**: Always test with `-noopt` first to isolate optimizer issues from code generation bugs. Use `-compareir` to see what instructions change when optimizations are applied.
|
||||
|
||||
Final verification against the existing test suite via `gradle build --console=plain`.
|
||||
|
||||
### Key Scenarios
|
||||
|
||||
| Scenario | Test |
|
||||
|----------|------|
|
||||
| Basic array field | `struct S { ubyte[4] data }` -- create instance, write/read elements |
|
||||
| Word array | `struct S { word[3] vals }` -- test split word and `@nosplit` |
|
||||
| Mixed fields | `struct S { ubyte id; ubyte[8] name; word count }` -- correct offsets |
|
||||
| Nested struct with array | `struct Inner { ubyte[4] data }` then `struct Outer { ^^Inner ptr }` |
|
||||
| sizeof/offsetof | `sizeof(S)` = 8 for `ubyte[8]`, `offsetof(S.data)` = 1 if preceded by `ubyte id` |
|
||||
| Initialization | `^^S : [1, 'a','b','c','d', 1000]` -- array field from byte values |
|
||||
| Error: too large | `struct S { ubyte[300] data }` -- rejects (> 256 bytes) |
|
||||
| Error: invalid type | `struct S { str[3] names }` -- rejects arrays of strings (complex) |
|
||||
| Error: non-const array size | `struct S { ubyte[n] data }` -- rejects non-constant dimensions |
|
||||
| Field offset > 255 | struct with carefully laid out array fields that push offset past 255 (requires extra pointer math) |
|
||||
| Existing structs unchanged | All existing struct tests continue to pass |
|
||||
|
||||
### Test Changes
|
||||
- New VM test class or extend `TestCompilerVirtual.kt` for array-in-struct runtime tests
|
||||
- New codegen test in `TestExecution6502` for 6502 code generation with array fields
|
||||
- Update `TestPointers.kt` struct tests if field access patterns change
|
||||
- Update `TestAstChecks.kt` for relaxed field type validation
|
||||
- Update `TestConst.kt` for `sizeof`/`offsetof` with array fields
|
||||
|
||||
|
||||
# Delivery Steps
|
||||
|
||||
### Step 1: Grammar, Parser, and AST Changes
|
||||
The parser and AST now support array index declarations in struct fields.
|
||||
|
||||
- Update `structfielddecl` grammar rule in `Prog8ANTLR.g4`.
|
||||
- Create `StructField` data class to manage array dimensions.
|
||||
- Update `StructDecl` to store `StructField`s.
|
||||
- Update `Antlr2KotlinVisitor.getStructField()` to process array indices.
|
||||
- Update `AstToSourceTextConverter` to emit array dimensions.
|
||||
- **Verify**: parser successfully parses `struct S { ubyte[10] name }`.
|
||||
|
||||
### Step 2: Semantic Analysis and Validation
|
||||
The compiler correctly validates array field types and struct initializers.
|
||||
|
||||
- Relax field type check in `AstChecker.struct` visitor to permit array types.
|
||||
- Handle `.` operator with `field[idx]` for struct pointers.
|
||||
- Handle `StaticStructInitializer` validation for array field initializers.
|
||||
- Update `AstToplevel.searchStructFieldRef()` for proper symbol resolution.
|
||||
- **Verify**: valid structs compile, and invalid declarations produce appropriate errors.
|
||||
|
||||
### Step 3: Memory Layout and Symbol Table
|
||||
Struct field offsets and total sizes correctly account for array fields.
|
||||
|
||||
- Update `StructDecl.memsize()` and `offsetof()` to compute array field sizes.
|
||||
- Update `StStruct` and `SymbolTableMaker` to handle array field storage.
|
||||
- **Verify**: `sizeof` and `offsetof` return correct values based on element counts.
|
||||
|
||||
### Step 4: 6502 Code Generation
|
||||
The 6502 backend correctly handles array field offsets in pointer assignments.
|
||||
|
||||
- Update `PointerAssignmentsGen` to handle array field offsets with indexing.
|
||||
- Handle offset > 255 case by emitting extra pointer math.
|
||||
- Verify generated 64tass `.struct` directives and `.dstruct` data layouts.
|
||||
- **Verify**: compiled programs run correctly in the 6502 simulator/emulator.
|
||||
|
||||
### Step 5: IR + VM Code Generation
|
||||
The IR generator correctly calculates addresses for array field indexed access.
|
||||
|
||||
- Update IR expression and assignment generators to handle array field access.
|
||||
- **Verify using IR debugging workflow**:
|
||||
- `prog8c -check` for syntax errors.
|
||||
- `prog8c -target virtual -printast1` to inspect AST nodes.
|
||||
- `prog8c -target virtual -out` then inspect `.p8ir` for correct indexing opcodes.
|
||||
- `prog8c -target virtual -emu` to run and verify output.
|
||||
- `prog8c -vm file.p8ir -vmtrace` to trace execution and verify field access addresses.
|
||||
|
||||
### Step 6: Testing
|
||||
All new functionality is covered by automated unit and integration tests.
|
||||
|
||||
- Add VM runtime tests for array-in-struct field access.
|
||||
- Add 6502 simulator-based tests.
|
||||
- Add error-case coverage for invalid field declarations.
|
||||
- Run full suite: `gradle build --console=plain`.
|
||||
@@ -41,6 +41,21 @@ Currently, `Enumeration` nodes in the `compilerAst` are lowered into standard co
|
||||
# Testing
|
||||
|
||||
### Validation Approach
|
||||
Primary testing uses the **virtual target** and prog8c's IR debugging features. This allows fast iteration without needing 6502 emulators.
|
||||
|
||||
**Debugging workflow for each change:**
|
||||
1. **Syntax check**: `prog8c -check input.p8` -- quick semantic validation
|
||||
2. **AST inspection**: `prog8c -target virtual -printast1 input.p8` -- inspect compiler AST (verify enum nodes are preserved)
|
||||
3. **Simple AST inspection**: `prog8c -target virtual -printast2 input.p8` -- inspect Simple AST (verify enum erasure to integers)
|
||||
4. **IR inspection**: `prog8c -target virtual -out /tmp/out input.p8` then examine the generated `.p8ir` file
|
||||
5. **IR comparison**: `prog8c -target virtual -compareir baseline.p8ir new.p8ir` -- compare IR instruction changes when optimizing
|
||||
6. **VM execution**: `prog8c -target virtual -emu input.p8` -- run and see stdout output
|
||||
7. **VM trace**: `prog8c -vm input.p8ir -vmtrace` -- step through IR instructions with location tracking
|
||||
8. **Disable optimizations**: `prog8c -target virtual -noopt -emu input.p8` -- isolate optimizer issues
|
||||
9. **Full regression**: `gradle build --console=plain` -- run all unit tests
|
||||
|
||||
**Important**: Always test with `-noopt` first to isolate optimizer issues from code generation bugs. Use `-compareir` to see what instructions change when optimizations are applied.
|
||||
|
||||
- **Type Checking Tests**: Add test cases that explicitly verify:
|
||||
- Implicit assignment/comparison of incompatible types (expect failure).
|
||||
- Explicit cast (Enum ↔ Int) (expect success).
|
||||
@@ -49,3 +64,25 @@ Currently, `Enumeration` nodes in the `compilerAst` are lowered into standard co
|
||||
- Use an enum member without proper type qualification.
|
||||
- **AST Printing Verification**: Use `-printast1` and `-printast2` to verify that strongly typed enum nodes are correctly displayed in the AST output.
|
||||
- **Runtime Verification**: Ensure that the code generation produces correct code for enums (e.g., correct values, proper size) using the `virtual` target and `vmtrace`.
|
||||
|
||||
Final verification against the existing test suite via `gradle build --console=plain`.
|
||||
|
||||
### Delivery Plan
|
||||
|
||||
### Stage 1: Add Enum Type Support and Erasure Logic
|
||||
Enum types are introduced and correctly preserved in the compiler AST.
|
||||
|
||||
- Implement `EnumSubType` class in `codeCore` and add `BaseDataType.ENUM`.
|
||||
- Update `CompilerAST` and `SimplifiedAstMaker` to preserve and later erase enum declarations.
|
||||
|
||||
### Stage 2: Implement Strict Type Checking and Casting
|
||||
Strong typing for enums is enforced by the compiler.
|
||||
|
||||
- Update `TypeChecker` and `SymbolTable` to enforce strict type checking for enums.
|
||||
- Implement explicit casting rules for enums and disallow implicit/cross-enum conversions.
|
||||
|
||||
### Stage 3: Verification and Testing
|
||||
Enum type implementation is verified with unit and integration tests.
|
||||
|
||||
- Update AST printing for enums.
|
||||
- Add unit tests for type safety, casting, and AST representation verification.
|
||||
@@ -1,101 +0,0 @@
|
||||
|
||||
# Requirements
|
||||
|
||||
### Overview & Goals
|
||||
Extend the Prog8 `Inliner` to support subroutines with parameters for multi-value returns and expression context calls. Additionally, enable the `inline` keyword for regular subroutines, allowing developers to explicitly suggest inlining for performance-critical code. Currently, the inliner is limited to parameterless subroutines in expression context, and the `inline` keyword is forcefully disabled for regular subroutines with a misleading warning.
|
||||
|
||||
### Scope
|
||||
#### In Scope
|
||||
- Parameterized subroutines in multi-value return assignments (e.g., `a, b = foo(x, y)`).
|
||||
- Parameterized subroutines in expression context (e.g., `a = foo(x) + 1`).
|
||||
- Substitution of parameter references with actual argument values (literals or simple identifiers).
|
||||
- Expansion of supported return expressions to include simple binary/prefix/typecast operations.
|
||||
- Enabling `inline` keyword support for regular subroutines by removing the restriction in `AstChecker`.
|
||||
- Ensuring `Inliner` properly processes manually marked `inline` subroutines (including scoping analysis).
|
||||
|
||||
#### Out of Scope
|
||||
- Support for complex arguments with side effects (calls like `foo(bar())` will still be excluded from inlining).
|
||||
- Multi-statement subroutines in expression context.
|
||||
|
||||
### User Stories
|
||||
- As a Prog8 developer, I want to use `inline` subroutines with parameters and return values so that I can write clean, modular code without the performance penalty of subroutine calls.
|
||||
- As a Prog8 developer, I want the `inline` keyword to be respected for regular subroutines, so I can explicitly optimize specific parts of my code.
|
||||
- As a compiler, I want to automatically inline simple parameterized subroutines to reduce execution time and code size.
|
||||
|
||||
|
||||
# Technical Design
|
||||
|
||||
### Current Implementation
|
||||
The `Inliner` uses a two-phase approach:
|
||||
1. `DetermineInlineSubs` identifies simple subroutines and marks them as `inline`.
|
||||
2. The `Inliner` itself visits call sites and replaces calls to `inline` subroutines with their bodies.
|
||||
|
||||
Currently, `AstChecker` disables `inline` for regular subroutines and emits a warning stating it has no effect. However, the `Inliner` already performs automatic inlining for simple regular subroutines, making this warning misleading. Furthermore, the `Inliner` explicitly rejects parameterized subroutines in expression context.
|
||||
|
||||
### Key Decisions
|
||||
- **Parameter Substitution**: Use a specialized `AstWalker` (`ParameterSubstitutor`) to replace parameter references in the inlined body with copies of the call arguments.
|
||||
- **Simple Arguments Only**: Maintain the restriction that arguments must be `NumericLiteral` or `IdentifierReference` to safely allow duplication without side-effect issues.
|
||||
- **Scoping**: Ensure `makeFullyScoped` is called on all inlineable subroutines (including those manually marked `inline`) to prevent name collisions after inlining.
|
||||
- **Respect `inline` Keyword**: Stop unsetting the `inline` flag in `AstChecker` for regular subroutines. This allows the user to suggest inlining even for cases that might not be picked up by the automatic inliner's heuristic (though they must still meet basic inlineability criteria).
|
||||
|
||||
### Proposed Changes
|
||||
#### `Inliner.kt`
|
||||
- **`ParameterSubstitutor`**: A new private class to perform the replacement of parameters with arguments.
|
||||
- **`substitute(node, paramMap)`**: A helper function to apply substitution to a copied node.
|
||||
- **`after(Assignment)`**: Implement zipping of `sub.parameters` and `fcall.args` to perform substitution in all return values of a multi-assignment.
|
||||
- **`before(FunctionCallExpression)`**: Implement substitution for single-value returns.
|
||||
- **`possiblyInlineFunctionBody`**: Use substitution for void calls, fixing a bug where parameters used in the body were incorrectly handled.
|
||||
- **`DetermineInlineSubs`**:
|
||||
- Expand `isBodyInlineable` to allow `BinaryExpression`, `PrefixExpression`, and `TypecastExpression`.
|
||||
- Modify the visit condition to process all regular subroutines (not just those with `inline=false`), ensuring `makeFullyScoped` is called for manually marked `inline` subroutines.
|
||||
- If a subroutine is already `inline`, verify it meets the inlineability criteria; if not, it will naturally fail at call sites via `canInlineAtCallSite`.
|
||||
|
||||
#### `AstChecker.kt`
|
||||
- Remove the block that disables `inline` for regular subroutines and emits the "no effect" warning.
|
||||
|
||||
### Data Models / Contracts
|
||||
- `paramMap`: `Map<VarDecl, Expression>` where `VarDecl` is the parameter declaration and `Expression` is the argument passed at the call site.
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
### Validation Approach
|
||||
Verification will be performed using the automated test suite in `TestOptimization.kt`.
|
||||
|
||||
### Key Scenarios
|
||||
- **Multi-return with parameters**: `a, b = foo(1, 2)` where `foo(x, y)` returns `x+1, y+1`.
|
||||
- **Expression context with parameters**: `a = foo(10) + gv`.
|
||||
- **Void call with used parameters**: `foo(10)` where `foo(x)` calls `txt.print_ub(x)`.
|
||||
- **Nested inlining**: `a = foo(bar(10))` where both are inlineable.
|
||||
- **Manual `inline` keyword**: Verify that `inline sub foo() { return 1 }` is inlined and no longer emits a "no effect" warning.
|
||||
|
||||
### Test Changes
|
||||
- Enable `xtest("inline call with one return value and one parameter")`.
|
||||
- Enable `xtest("inline call with two return values and two parameters")`.
|
||||
- Add new tests for complex return expressions and void calls with parameters.
|
||||
|
||||
|
||||
# Delivery Steps
|
||||
|
||||
### Step 1: Relax inlining restrictions
|
||||
Relax inlining restrictions for regular subroutines.
|
||||
- Modify `AstChecker.kt` to remove the block that forcefully disables the `inline` flag on regular subroutines and emits a "no effect" warning.
|
||||
- Update `canInlineAtCallSiteWithReason` in `Inliner.kt` to allow expression context calls (including multi-value returns) for subroutines with parameters, provided all arguments are simple (literals or identifiers).
|
||||
- Update `DetermineInlineSubs` in `Inliner.kt` to ensure it processes all regular subroutines for scoping (calls `isBodyInlineable` even if already marked `inline`).
|
||||
|
||||
### Step 2: Implement parameter substitution logic
|
||||
Implement parameter substitution logic.
|
||||
- Add a private `ParameterSubstitutor` class in `Inliner.kt` that inherits from `AstWalker` and replaces `IdentifierReference` nodes that target subroutine parameters with copies of the corresponding argument expressions.
|
||||
- Expand `isBodyInlineable(Return)` in `DetermineInlineSubs` to allow `BinaryExpression`, `PrefixExpression`, and `TypecastExpression` as return values if their operands are also inlineable. This enables inlining of subroutines like `return x + 1`.
|
||||
|
||||
### Step 3: Complete inlining implementation
|
||||
Complete inlining implementation for all call contexts.
|
||||
- Update `Inliner.after(Assignment)` to handle multi-value returns from parameterized subroutines by zipping parameters with arguments and applying `ParameterSubstitutor` to each return value.
|
||||
- Update `Inliner.before(FunctionCallExpression)` to handle single-value returns from parameterized subroutines using `ParameterSubstitutor`.
|
||||
- Update `possiblyInlineFunctionBody` to handle void calls with parameters by applying `ParameterSubstitutor` to the inlined body statement. This fix ensures that parameters used in the body are correctly replaced by argument values.
|
||||
|
||||
### Step 4: Verification and Testing
|
||||
Verify the changes with tests.
|
||||
- Enable the previously disabled `xtest` cases in `TestOptimization.kt` that were blocked by missing parameter substitution.
|
||||
- Add new test cases to `TestOptimization.kt` covering void calls with used parameters, complex return expressions involving parameters, and the `inline` keyword on regular subroutines.
|
||||
- Run the full compiler test suite to ensure no regressions in other optimization passes or code generation.
|
||||
@@ -1,11 +1,11 @@
|
||||
package prog8lsp
|
||||
|
||||
import prog8.ast.IFunctionCall
|
||||
import prog8.ast.Module
|
||||
import prog8.ast.Node
|
||||
import prog8.ast.IFunctionCall
|
||||
import prog8.ast.walk.IAstVisitor
|
||||
import prog8.ast.expressions.*
|
||||
import prog8.ast.statements.*
|
||||
import prog8.ast.walk.IAstVisitor
|
||||
import prog8.code.core.BaseDataType
|
||||
import prog8.code.core.Position
|
||||
|
||||
@@ -479,6 +479,8 @@ object SymbolLookup {
|
||||
}
|
||||
}
|
||||
is Subroutine -> {
|
||||
if(stmt.isPrivate)
|
||||
return
|
||||
val params = stmt.parameters.joinToString(", ") { "${it.name}: ${it.type}" }
|
||||
symbols.add(CompletionSymbol(
|
||||
name = stmt.name,
|
||||
@@ -488,6 +490,8 @@ object SymbolLookup {
|
||||
))
|
||||
}
|
||||
is VarDecl -> {
|
||||
if(stmt.isPrivate)
|
||||
return
|
||||
val kind = if (stmt.type == VarDeclType.CONST) {
|
||||
org.eclipse.lsp4j.CompletionItemKind.Constant
|
||||
} else {
|
||||
@@ -501,6 +505,8 @@ object SymbolLookup {
|
||||
))
|
||||
}
|
||||
is StructDecl -> {
|
||||
if(stmt.isPrivate)
|
||||
return
|
||||
symbols.add(CompletionSymbol(
|
||||
name = stmt.name,
|
||||
kind = org.eclipse.lsp4j.CompletionItemKind.Struct,
|
||||
|
||||
@@ -112,6 +112,37 @@ sealed class PtExpression(val type: DataType, position: Position) : PtNode(posit
|
||||
|
||||
fun asConstValue(): Double? = (this as? PtNumber)?.number ?: (this as? PtBool)?.asInt()?.toDouble() ?: (this as? PtTypeCast)?.value?.asConstValue()
|
||||
|
||||
fun hasSideEffects(target: ICompilationTarget? = null): Boolean {
|
||||
return when(this) {
|
||||
is PtAddressOf -> arrayIndexExpr?.hasSideEffects(target) == true
|
||||
is PtArray -> children.any { (it as PtExpression).hasSideEffects(target) }
|
||||
is PtArrayIndexer -> true
|
||||
is PtBinaryExpression -> left.hasSideEffects(target) || right.hasSideEffects(target)
|
||||
is PtContainmentCheck -> children.any { it is PtExpression && it.hasSideEffects(target) }
|
||||
is PtFunctionCall -> !hasNoSideEffects || args.any { it.hasSideEffects(target) }
|
||||
is PtIdentifier -> false
|
||||
is PtIrRegister -> false
|
||||
is PtMemoryByte -> {
|
||||
val addr = address.asConstInteger()
|
||||
if (addr != null && target != null) {
|
||||
target.isIOAddress(addr.toUInt())
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
is PtBool -> false
|
||||
is PtNumber -> false
|
||||
is PtPrefix -> value.hasSideEffects(target)
|
||||
is PtRange -> from.hasSideEffects(target) || to.hasSideEffects(target)
|
||||
is PtString -> false
|
||||
is PtPointerDeref -> true
|
||||
is PtTypeCast -> value.hasSideEffects(target)
|
||||
is PtIfExpression -> condition.hasSideEffects(target) || truevalue.hasSideEffects(target) || falsevalue.hasSideEffects(target)
|
||||
is PtBranchCondExpression -> truevalue.hasSideEffects(target) || falsevalue.hasSideEffects(target)
|
||||
is PtConstant -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun isSimple(): Boolean {
|
||||
return when(this) {
|
||||
is PtAddressOf -> this.arrayIndexExpr==null || this.arrayIndexExpr?.isSimple()==true
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package prog8.code.optimize
|
||||
|
||||
import prog8.code.ast.*
|
||||
import prog8.code.core.BaseDataType
|
||||
import prog8.code.core.ComparisonOperators
|
||||
import prog8.code.core.DataType
|
||||
import prog8.code.core.invertedComparisonOperator
|
||||
import prog8.code.core.*
|
||||
|
||||
/**
|
||||
* Boolean and bitwise expression optimizations.
|
||||
@@ -15,7 +12,7 @@ internal object BooleanOptimizers {
|
||||
/**
|
||||
* Optimizes boolean expression patterns (and/or/xor with constants, double negation, etc.).
|
||||
*/
|
||||
fun optimizeBooleanExpressions(program: PtProgram): Int {
|
||||
fun optimizeBooleanExpressions(program: PtProgram, options: CompilationOptions): Int {
|
||||
var changes = 0
|
||||
walkAst(program) { node: PtNode, depth: Int ->
|
||||
// Handle PtPrefix (not, -, ~)
|
||||
@@ -67,28 +64,28 @@ internal object BooleanOptimizers {
|
||||
node.parent.setChild(index, right)
|
||||
changes++
|
||||
}
|
||||
// x and false -> false
|
||||
else if (right is PtBool && !right.value) {
|
||||
// x and false -> false (without side effects)
|
||||
else if (right is PtBool && !right.value && !left.hasSideEffects(options.compTarget)) {
|
||||
val replacement = PtBool(false, node.position)
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, replacement)
|
||||
changes++
|
||||
}
|
||||
// false and x -> false
|
||||
else if (left is PtBool && !left.value) {
|
||||
// false and x -> false (without side effects)
|
||||
else if (left is PtBool && !left.value && !right.hasSideEffects(options.compTarget)) {
|
||||
val replacement = PtBool(false, node.position)
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, replacement)
|
||||
changes++
|
||||
}
|
||||
// x and x -> x (idempotent)
|
||||
else if (left isSameAs right) {
|
||||
// x and x -> x (idempotent, without side effects)
|
||||
else if (left isSameAs right && !left.hasSideEffects(options.compTarget)) {
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, left)
|
||||
changes++
|
||||
}
|
||||
// x and not(x) -> false (complement)
|
||||
else if (Helpers.isNegationOf(left, right) || Helpers.isNegationOf(right, left)) {
|
||||
// x and not(x) -> false (complement, without side effects)
|
||||
else if ((Helpers.isNegationOf(left, right) || Helpers.isNegationOf(right, left)) && !left.hasSideEffects(options.compTarget) && !right.hasSideEffects(options.compTarget)) {
|
||||
val replacement = PtBool(false, node.position)
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, replacement)
|
||||
@@ -96,15 +93,15 @@ internal object BooleanOptimizers {
|
||||
}
|
||||
}
|
||||
"or" -> {
|
||||
// x or true -> true
|
||||
if (right is PtBool && right.value) {
|
||||
// x or true -> true (without side effects)
|
||||
if (right is PtBool && right.value && !left.hasSideEffects(options.compTarget)) {
|
||||
val replacement = PtBool(true, node.position)
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, replacement)
|
||||
changes++
|
||||
}
|
||||
// true or x -> true
|
||||
else if (left is PtBool && left.value) {
|
||||
// true or x -> true (without side effects)
|
||||
else if (left is PtBool && left.value && !right.hasSideEffects(options.compTarget)) {
|
||||
val replacement = PtBool(true, node.position)
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, replacement)
|
||||
@@ -122,14 +119,14 @@ internal object BooleanOptimizers {
|
||||
node.parent.setChild(index, right)
|
||||
changes++
|
||||
}
|
||||
// x or x -> x (idempotent)
|
||||
else if (left isSameAs right) {
|
||||
// x or x -> x (idempotent, without side effects)
|
||||
else if (left isSameAs right && !left.hasSideEffects(options.compTarget)) {
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, left)
|
||||
changes++
|
||||
}
|
||||
// x or not(x) -> true (complement)
|
||||
else if (Helpers.isNegationOf(left, right) || Helpers.isNegationOf(right, left)) {
|
||||
// x or not(x) -> true (complement, without side effects)
|
||||
else if ((Helpers.isNegationOf(left, right) || Helpers.isNegationOf(right, left)) && !left.hasSideEffects(options.compTarget) && !right.hasSideEffects(options.compTarget)) {
|
||||
val replacement = PtBool(true, node.position)
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, replacement)
|
||||
@@ -167,8 +164,8 @@ internal object BooleanOptimizers {
|
||||
node.parent.setChild(index, right)
|
||||
changes++
|
||||
}
|
||||
// x xor x -> false (idempotent)
|
||||
else if (left isSameAs right) {
|
||||
// x xor x -> false (idempotent, without side effects)
|
||||
else if (left isSameAs right && !left.hasSideEffects(options.compTarget)) {
|
||||
val replacement = PtBool(false, node.position)
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, replacement)
|
||||
@@ -206,7 +203,7 @@ internal object BooleanOptimizers {
|
||||
/**
|
||||
* Optimizes bitwise complement patterns: x & ~x -> 0, x | ~x -> -1
|
||||
*/
|
||||
fun optimizeBitwiseComplementBinary(program: PtProgram): Int {
|
||||
fun optimizeBitwiseComplementBinary(program: PtProgram, options: CompilationOptions): Int {
|
||||
var changes = 0
|
||||
walkAst(program) { node: PtNode, depth: Int ->
|
||||
// Handle x & ~x -> 0 and x | ~x -> -1
|
||||
@@ -215,16 +212,16 @@ internal object BooleanOptimizers {
|
||||
val right = node.right
|
||||
|
||||
if (node.operator == "&") {
|
||||
// x & ~x -> 0
|
||||
if (Helpers.isBitwiseNegationOf(left, right) || Helpers.isBitwiseNegationOf(right, left)) {
|
||||
// x & ~x -> 0 (without side effects)
|
||||
if ((Helpers.isBitwiseNegationOf(left, right) || Helpers.isBitwiseNegationOf(right, left)) && !left.hasSideEffects(options.compTarget) && !right.hasSideEffects(options.compTarget)) {
|
||||
val zero = PtNumber(node.type.base, 0.0, node.position)
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, zero)
|
||||
changes++
|
||||
}
|
||||
} else if (node.operator == "|") {
|
||||
// x | ~x -> -1 (all bits set)
|
||||
if (Helpers.isBitwiseNegationOf(left, right) || Helpers.isBitwiseNegationOf(right, left)) {
|
||||
// x | ~x -> -1 (all bits set, without side effects)
|
||||
if ((Helpers.isBitwiseNegationOf(left, right) || Helpers.isBitwiseNegationOf(right, left)) && !left.hasSideEffects(options.compTarget) && !right.hasSideEffects(options.compTarget)) {
|
||||
val allOnesValue = when (node.type.base) {
|
||||
BaseDataType.BYTE -> -1.0
|
||||
BaseDataType.UBYTE -> 255.0
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package prog8.code.optimize
|
||||
|
||||
import prog8.code.ast.*
|
||||
import prog8.code.core.BaseDataType
|
||||
import prog8.code.core.ComparisonOperators
|
||||
import prog8.code.core.DataType
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.code.core.*
|
||||
|
||||
/**
|
||||
* Comparison pattern optimizations.
|
||||
@@ -104,7 +101,8 @@ internal object ComparisonOptimizers {
|
||||
}
|
||||
|
||||
// Comparison with arithmetic: x <= y-1 -> x < y, x >= y+1 -> x > y
|
||||
if (leftType.isInteger && rightType.isInteger) {
|
||||
// Only for signed integers to avoid unsigned wraparound issues.
|
||||
if (leftType.isSignedInteger && rightType.isSignedInteger) {
|
||||
val rightExpr = right as? PtBinaryExpression
|
||||
if (rightExpr != null) {
|
||||
val rightRightConst = rightExpr.right.asConstValue()
|
||||
@@ -136,7 +134,7 @@ internal object ComparisonOptimizers {
|
||||
/**
|
||||
* Optimizes comparison identities (x==x, x<x, etc.).
|
||||
*/
|
||||
fun optimizeComparisonIdentities(program: PtProgram): Int {
|
||||
fun optimizeComparisonIdentities(program: PtProgram, options: CompilationOptions): Int {
|
||||
var changes = 0
|
||||
walkAst(program) { node: PtNode, depth: Int ->
|
||||
if (node is PtBinaryExpression && node.operator in ComparisonOperators) {
|
||||
@@ -147,7 +145,8 @@ internal object ComparisonOptimizers {
|
||||
// (constant-vs-constant is already folded in compilerAST phase)
|
||||
// NOTE: do NOT apply to floats - NaN comparisons don't follow these rules
|
||||
// (e.g. NaN == NaN is false, NaN <= NaN is false)
|
||||
if (left isSameAs right && !left.type.isFloat) {
|
||||
// NOTE: do NOT apply if expressions have side effects (e.g. peek(1000) == peek(1000))
|
||||
if (left isSameAs right && !left.type.isFloat && !left.hasSideEffects(options.compTarget)) {
|
||||
val result = when (node.operator) {
|
||||
"==" -> true
|
||||
"!=" -> false
|
||||
|
||||
@@ -16,41 +16,39 @@ internal object ControlFlowOptimizers {
|
||||
* if false {A} else {B} -> B
|
||||
*/
|
||||
fun optimizeConditionalExpressions(program: PtProgram, errors: IErrorReporter): Int {
|
||||
var changes = 0
|
||||
val toOptimize = mutableListOf<Triple<PtIfElse, Boolean, PtNode>>()
|
||||
|
||||
walkAst(program) { node: PtNode, depth: Int ->
|
||||
if (node is PtIfElse) {
|
||||
val condition = node.condition
|
||||
val trueConst = condition.asConstValue()
|
||||
val parent = node.parent
|
||||
val index = parent.children.indexOf(node)
|
||||
|
||||
// if true { A } else { B } -> A
|
||||
if (trueConst == 1.0 || (condition is PtBool && condition.value)) {
|
||||
val ifScope = node.ifScope
|
||||
// Replace if-else with the true part statements
|
||||
parent.removeChildAt(index)
|
||||
// Insert the if scope statements in reverse order to maintain order
|
||||
ifScope.children.reversed().forEach { stmt ->
|
||||
parent.add(index, stmt)
|
||||
stmt.parent = parent
|
||||
}
|
||||
changes++
|
||||
toOptimize.add(Triple(node, true, node.parent))
|
||||
}
|
||||
// if false { A } else { B } -> B
|
||||
else if (trueConst == 0.0 || (condition is PtBool && !condition.value)) {
|
||||
val elseScope = node.elseScope
|
||||
// Replace if-else with the false part statements
|
||||
parent.removeChildAt(index)
|
||||
// Insert the else scope statements in reverse order to maintain order
|
||||
elseScope.children.reversed().forEach { stmt ->
|
||||
parent.add(index, stmt)
|
||||
stmt.parent = parent
|
||||
}
|
||||
changes++
|
||||
toOptimize.add(Triple(node, false, node.parent))
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
var changes = 0
|
||||
for ((node, takeTrue, parent) in toOptimize) {
|
||||
val index = parent.children.indexOf(node)
|
||||
if (index == -1) continue
|
||||
|
||||
parent.removeChildAt(index)
|
||||
val scope = if (takeTrue) node.ifScope else node.elseScope
|
||||
// Insert the scope statements in reverse order to maintain order
|
||||
scope.children.reversed().forEach { stmt ->
|
||||
parent.add(index, stmt)
|
||||
stmt.parent = parent
|
||||
}
|
||||
changes++
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
@@ -58,26 +56,34 @@ internal object ControlFlowOptimizers {
|
||||
* Optimizes dead conditional branches in when statements.
|
||||
*/
|
||||
fun optimizeDeadConditionalBranches(program: PtProgram): Int {
|
||||
var changes = 0
|
||||
val toOptimize = mutableListOf<PtWhen>()
|
||||
|
||||
walkAst(program) { node: PtNode, depth: Int ->
|
||||
if (node is PtWhen) {
|
||||
val choices = node.choices.children
|
||||
// If we have a when with only an else choice, convert to if-else or just the else block
|
||||
if (choices.size == 1 && choices[0] is PtWhenChoice && (choices[0] as PtWhenChoice).isElse) {
|
||||
val elseChoice = choices[0] as PtWhenChoice
|
||||
val index = node.parent.children.indexOf(node)
|
||||
val parent = node.parent
|
||||
parent.removeChildAt(index)
|
||||
// Insert the else part statements
|
||||
elseChoice.statements.children.reversed().forEach { stmt ->
|
||||
parent.add(index, stmt)
|
||||
stmt.parent = parent
|
||||
}
|
||||
changes++
|
||||
toOptimize.add(node)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
var changes = 0
|
||||
for (node in toOptimize) {
|
||||
if (!node.parent.children.contains(node)) continue
|
||||
|
||||
val elseChoice = node.choices.children[0] as PtWhenChoice
|
||||
val index = node.parent.children.indexOf(node)
|
||||
val parent = node.parent
|
||||
parent.removeChildAt(index)
|
||||
// Insert the else part statements
|
||||
elseChoice.statements.children.reversed().forEach { stmt ->
|
||||
parent.add(index, stmt)
|
||||
stmt.parent = parent
|
||||
}
|
||||
changes++
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
@@ -85,42 +91,51 @@ internal object ControlFlowOptimizers {
|
||||
* Simplifies single-choice when statements to if-else.
|
||||
*/
|
||||
fun optimizeSingleWhens(program: PtProgram, errors: IErrorReporter): Int {
|
||||
var changes = 0
|
||||
val toOptimize = mutableListOf<PtWhen>()
|
||||
|
||||
walkAst(program) { node: PtNode, depth: Int ->
|
||||
if(node is PtWhen && node.choices.children.size==2) {
|
||||
val choice1 = node.choices.children[0] as PtWhenChoice
|
||||
val choice2 = node.choices.children[1] as PtWhenChoice
|
||||
if(choice1.isElse && choice2.values.children.size==1 || choice2.isElse && choice1.values.children.size==1) {
|
||||
errors.info("when can be simplified into an if-else", node.position)
|
||||
val truescope: PtNodeGroup
|
||||
val elsescope: PtNodeGroup
|
||||
val comparisonValue : PtNumber
|
||||
if(choice1.isElse) {
|
||||
truescope = choice2.statements
|
||||
elsescope = choice1.statements
|
||||
comparisonValue = choice2.values.children.single() as PtNumber
|
||||
} else {
|
||||
truescope = choice1.statements
|
||||
elsescope = choice2.statements
|
||||
comparisonValue = choice1.values.children.single() as PtNumber
|
||||
}
|
||||
val ifelse = PtIfElse(node.position)
|
||||
val condition = PtBinaryExpression("==", DataType.BOOL, node.position)
|
||||
condition.add(node.value)
|
||||
condition.add(comparisonValue)
|
||||
ifelse.add(condition)
|
||||
ifelse.add(truescope)
|
||||
ifelse.add(elsescope)
|
||||
ifelse.parent = node.parent
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, ifelse)
|
||||
changes++
|
||||
toOptimize.add(node)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
var changes = 0
|
||||
for (node in toOptimize) {
|
||||
if (!node.parent.children.contains(node)) continue
|
||||
|
||||
val choice1 = node.choices.children[0] as PtWhenChoice
|
||||
val choice2 = node.choices.children[1] as PtWhenChoice
|
||||
|
||||
errors.info("when can be simplified into an if-else", node.position)
|
||||
val truescope: PtNodeGroup
|
||||
val elsescope: PtNodeGroup
|
||||
val comparisonValue: PtNumber
|
||||
if (choice1.isElse) {
|
||||
truescope = choice2.statements
|
||||
elsescope = choice1.statements
|
||||
comparisonValue = choice2.values.children.single() as PtNumber
|
||||
} else {
|
||||
truescope = choice1.statements
|
||||
elsescope = choice2.statements
|
||||
comparisonValue = choice1.values.children.single() as PtNumber
|
||||
}
|
||||
val ifelse = PtIfElse(node.position)
|
||||
val condition = PtBinaryExpression("==", DataType.BOOL, node.position)
|
||||
condition.add(node.value)
|
||||
condition.add(comparisonValue)
|
||||
ifelse.add(condition)
|
||||
ifelse.add(truescope)
|
||||
ifelse.add(elsescope)
|
||||
ifelse.parent = node.parent
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, ifelse)
|
||||
changes++
|
||||
}
|
||||
return changes
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ internal object ExpressionOptimizers {
|
||||
/**
|
||||
* Optimizes algebraic identity patterns (x+0, x*1, x*0, etc.).
|
||||
*/
|
||||
fun optimizeAlgebraicIdentities(program: PtProgram): Int {
|
||||
fun optimizeAlgebraicIdentities(program: PtProgram, options: CompilationOptions): Int {
|
||||
var changes = 0
|
||||
walkAst(program) { node: PtNode, _: Int ->
|
||||
if (node is PtBinaryExpression) {
|
||||
@@ -96,15 +96,15 @@ internal object ExpressionOptimizers {
|
||||
}
|
||||
}
|
||||
"*" -> {
|
||||
// x * 0 -> 0
|
||||
if (rightConst == 0.0) {
|
||||
// x * 0 -> 0 (for non-floats and without side effects)
|
||||
if (rightConst == 0.0 && !node.type.isFloat && !node.left.hasSideEffects(options.compTarget)) {
|
||||
val zero = PtNumber(node.type.base, 0.0, node.position)
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, zero)
|
||||
changes++
|
||||
}
|
||||
// 0 * x -> 0
|
||||
else if (leftConst == 0.0) {
|
||||
// 0 * x -> 0 (for non-floats and without side effects)
|
||||
else if (leftConst == 0.0 && !node.type.isFloat && !node.right.hasSideEffects(options.compTarget)) {
|
||||
val zero = PtNumber(node.type.base, 0.0, node.position)
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, zero)
|
||||
@@ -164,8 +164,8 @@ internal object ExpressionOptimizers {
|
||||
}
|
||||
}
|
||||
"xor" -> {
|
||||
// x xor x -> 0
|
||||
if (left isSameAs right) {
|
||||
// x xor x -> 0 (without side effects)
|
||||
if (left isSameAs right && !left.hasSideEffects(options.compTarget)) {
|
||||
val zero = PtNumber(node.type.base, 0.0, node.position)
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, zero)
|
||||
@@ -199,8 +199,8 @@ internal object ExpressionOptimizers {
|
||||
node.parent.setChild(index, right)
|
||||
changes++
|
||||
}
|
||||
// x & x -> x (idempotent)
|
||||
else if (left isSameAs right) {
|
||||
// x & x -> x (idempotent, without side effects)
|
||||
else if (left isSameAs right && !left.hasSideEffects(options.compTarget)) {
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, left)
|
||||
changes++
|
||||
@@ -233,8 +233,8 @@ internal object ExpressionOptimizers {
|
||||
node.parent.setChild(index, allOnes)
|
||||
changes++
|
||||
}
|
||||
// x | x -> x (idempotent)
|
||||
else if (left isSameAs right) {
|
||||
// x | x -> x (idempotent, without side effects)
|
||||
else if (left isSameAs right && !left.hasSideEffects(options.compTarget)) {
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, left)
|
||||
changes++
|
||||
@@ -271,8 +271,8 @@ internal object ExpressionOptimizers {
|
||||
negation.parent = node.parent
|
||||
changes++
|
||||
}
|
||||
// x ^ x -> 0 (idempotent)
|
||||
else if (left isSameAs right) {
|
||||
// x ^ x -> 0 (idempotent, without side effects)
|
||||
else if (left isSameAs right && !left.hasSideEffects(options.compTarget)) {
|
||||
val zero = PtNumber(node.type.base, 0.0, node.position)
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, zero)
|
||||
@@ -376,7 +376,7 @@ internal object ExpressionOptimizers {
|
||||
* Strength reduction: replaces expensive operations with cheaper equivalents.
|
||||
* Division/modulo by powers of two become shifts/masks.
|
||||
*/
|
||||
fun optimizeStrengthReduction(program: PtProgram): Int {
|
||||
fun optimizeStrengthReduction(program: PtProgram, options: CompilationOptions): Int {
|
||||
var changes = 0
|
||||
walkAst(program) { node: PtNode, _: Int ->
|
||||
if (node is PtBinaryExpression && node.type.isInteger) {
|
||||
@@ -396,8 +396,8 @@ internal object ExpressionOptimizers {
|
||||
}
|
||||
}
|
||||
|
||||
// Modulo by power of two: x % 2^n -> x & (2^n - 1) (for all integers)
|
||||
if (node.operator == "%" && rightConst != null) {
|
||||
// Modulo by power of two: x % 2^n -> x & (2^n - 1) (for unsigned integers only)
|
||||
if (node.operator == "%" && rightConst != null && node.type.isUnsignedInteger) {
|
||||
if (rightConst in powersOfTwoFloat) {
|
||||
val mask = rightConst - 1.0
|
||||
val andExpr = PtBinaryExpression("&", node.type, node.position)
|
||||
@@ -408,8 +408,8 @@ internal object ExpressionOptimizers {
|
||||
andExpr.parent = node.parent
|
||||
changes++
|
||||
}
|
||||
// x % 1 -> 0
|
||||
else if (rightConst == 1.0) {
|
||||
// x % 1 -> 0 (without side effects)
|
||||
else if (rightConst == 1.0 && !node.left.hasSideEffects(options.compTarget)) {
|
||||
val zero = PtNumber(node.type.base, 0.0, node.position)
|
||||
val index = node.parent.children.indexOf(node)
|
||||
node.parent.setChild(index, zero)
|
||||
|
||||
@@ -120,7 +120,7 @@ fun referencesIdentifier(node: PtNode, identifier: PtIdentifier): Boolean {
|
||||
is PtIfExpression -> expr.children.any { referencesIdentifier(it, identifier) }
|
||||
is PtFunctionCall -> true
|
||||
is PtMemoryByte -> expr.children.any { referencesIdentifier(it, identifier) }
|
||||
is PtPointerDeref -> false
|
||||
is PtPointerDeref -> expr.children.any { referencesIdentifier(it, identifier) }
|
||||
is PtPrefix -> expr.children.any { referencesIdentifier(it, identifier) }
|
||||
is PtRange -> expr.children.any { referencesIdentifier(it, identifier) }
|
||||
is PtTypeCast -> expr.children.any { referencesIdentifier(it, identifier) }
|
||||
|
||||
@@ -88,7 +88,7 @@ fun optimizeSimplifiedAst(program: PtProgram, options: CompilationOptions, st: S
|
||||
runFixpointOptimizations(program, ctx)
|
||||
|
||||
// Run single-pass optimizations (only need to run once)
|
||||
runSinglePassOptimizations(program)
|
||||
runSinglePassOptimizations(program, options)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +105,7 @@ private fun runFixpointOptimizations(program: PtProgram, ctx: OptimizerContext)
|
||||
|
||||
while (ctx.errors.noErrors() &&
|
||||
VariableOptimizers.optimizeAssignTargets(program, ctx.st)
|
||||
+ ExpressionOptimizers.optimizeAlgebraicIdentities(program)
|
||||
+ ExpressionOptimizers.optimizeAlgebraicIdentities(program, ctx.options)
|
||||
+ ExpressionOptimizers.optimizeExpressionRearrangement(program)
|
||||
+ BooleanOptimizers.optimizeBitwisePrefix(program)
|
||||
+ MemoryOptimizers.optimizeAddressOfDereference(program)
|
||||
@@ -113,9 +113,9 @@ private fun runFixpointOptimizations(program: PtProgram, ctx: OptimizerContext)
|
||||
+ ComparisonOptimizers.optimizeFloatComparesToZero(program)
|
||||
+ ComparisonOptimizers.optimizeSgnComparisons(program, ctx.errors)
|
||||
+ ComparisonOptimizers.optimizeComparisonSimplifications(program)
|
||||
+ ComparisonOptimizers.optimizeComparisonIdentities(program)
|
||||
+ BooleanOptimizers.optimizeBooleanExpressions(program)
|
||||
+ BooleanOptimizers.optimizeBitwiseComplementBinary(program)
|
||||
+ ComparisonOptimizers.optimizeComparisonIdentities(program, ctx.options)
|
||||
+ BooleanOptimizers.optimizeBooleanExpressions(program, ctx.options)
|
||||
+ BooleanOptimizers.optimizeBitwiseComplementBinary(program, ctx.options)
|
||||
+ ExpressionOptimizers.optimizeBinaryExpressions(program, ctx.options)
|
||||
+ ExpressionOptimizers.optimizeOperandOrder(program)
|
||||
+ ControlFlowOptimizers.optimizeSingleWhens(program, ctx.errors)
|
||||
@@ -133,10 +133,10 @@ private fun runFixpointOptimizations(program: PtProgram, ctx: OptimizerContext)
|
||||
* Runs optimizations that only need to execute once.
|
||||
* These don't create opportunities for other optimizations, so no fixpoint loop needed.
|
||||
*/
|
||||
private fun runSinglePassOptimizations(program: PtProgram) {
|
||||
private fun runSinglePassOptimizations(program: PtProgram, options: CompilationOptions) {
|
||||
// Variable optimizations
|
||||
VariableOptimizers.optimizeRedundantVarInits(program)
|
||||
|
||||
// Strength reduction (x/2^n→x>>n, x%2^n→x&(2^n-1)) doesn't create opportunities for other opts
|
||||
ExpressionOptimizers.optimizeStrengthReduction(program)
|
||||
ExpressionOptimizers.optimizeStrengthReduction(program, options)
|
||||
}
|
||||
@@ -88,8 +88,12 @@ internal object VariableOptimizers {
|
||||
if (assignIndex>initializerIndex) {
|
||||
val inbetween = parent.children.subList(initializerIndex+1, assignIndex)
|
||||
if(!inbetween.any { stmt -> referencesIdentifier(stmt, identifier) }) {
|
||||
// var initializer is redundant, it will be overwritten by an assignment later. remove the initializer
|
||||
removeInitializations.add(parent to parent.children[initializerIndex] as PtAssignment)
|
||||
// also check that the overwriting assignment's value doesn't reference the variable
|
||||
val assignment = parent.children[assignIndex] as PtAssignment
|
||||
if(!referencesIdentifier(assignment.value, identifier)) {
|
||||
// var initializer is redundant, it will be overwritten by an assignment later. remove the initializer
|
||||
removeInitializations.add(parent to parent.children[initializerIndex] as PtAssignment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ package prog8tests.code
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import prog8.code.ast.*
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.DataType
|
||||
import prog8.code.core.Position
|
||||
import prog8.code.optimize.ComparisonOptimizers
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8tests.helpers.DummyMemsizer
|
||||
import prog8tests.helpers.DummyStringEncoder
|
||||
|
||||
@@ -15,6 +17,7 @@ import prog8tests.helpers.DummyStringEncoder
|
||||
* (because NaN comparisons don't follow the same rules).
|
||||
*/
|
||||
class TestComparisonOptimizers: FunSpec({
|
||||
val options = CompilationOptions.builder(VMTarget()).build()
|
||||
|
||||
// Helper to create a simple program with a binary expression in a sub
|
||||
fun createProgramWithComparison(operator: String, leftType: DataType): PtProgram {
|
||||
@@ -37,7 +40,7 @@ class TestComparisonOptimizers: FunSpec({
|
||||
|
||||
test("optimizeComparisonIdentities: integer x==x should be optimized to true") {
|
||||
val program = createProgramWithComparison("==", DataType.UWORD)
|
||||
val changes = ComparisonOptimizers.optimizeComparisonIdentities(program)
|
||||
val changes = ComparisonOptimizers.optimizeComparisonIdentities(program, options)
|
||||
|
||||
changes shouldBe 1
|
||||
// The comparison should be replaced by PtBool(true)
|
||||
@@ -49,7 +52,7 @@ class TestComparisonOptimizers: FunSpec({
|
||||
|
||||
test("optimizeComparisonIdentities: byte x<x should be optimized to false") {
|
||||
val program = createProgramWithComparison("<", DataType.UBYTE)
|
||||
val changes = ComparisonOptimizers.optimizeComparisonIdentities(program)
|
||||
val changes = ComparisonOptimizers.optimizeComparisonIdentities(program, options)
|
||||
|
||||
changes shouldBe 1
|
||||
val sub = program.children.filterIsInstance<PtBlock>().first()
|
||||
@@ -59,7 +62,7 @@ class TestComparisonOptimizers: FunSpec({
|
||||
|
||||
test("optimizeComparisonIdentities: word x<=x should be optimized to true") {
|
||||
val program = createProgramWithComparison("<=", DataType.WORD)
|
||||
val changes = ComparisonOptimizers.optimizeComparisonIdentities(program)
|
||||
val changes = ComparisonOptimizers.optimizeComparisonIdentities(program, options)
|
||||
|
||||
changes shouldBe 1
|
||||
val sub = program.children.filterIsInstance<PtBlock>().first()
|
||||
@@ -69,7 +72,7 @@ class TestComparisonOptimizers: FunSpec({
|
||||
|
||||
test("optimizeComparisonIdentities: float x==x should NOT be optimized (NaN)") {
|
||||
val program = createProgramWithComparison("==", DataType.FLOAT)
|
||||
val changes = ComparisonOptimizers.optimizeComparisonIdentities(program)
|
||||
val changes = ComparisonOptimizers.optimizeComparisonIdentities(program, options)
|
||||
|
||||
// NaN == NaN is false, so x==x should NOT be optimized to true for floats
|
||||
changes shouldBe 0
|
||||
@@ -81,7 +84,7 @@ class TestComparisonOptimizers: FunSpec({
|
||||
|
||||
test("optimizeComparisonIdentities: float x<=x should NOT be optimized (NaN)") {
|
||||
val program = createProgramWithComparison("<=", DataType.FLOAT)
|
||||
val changes = ComparisonOptimizers.optimizeComparisonIdentities(program)
|
||||
val changes = ComparisonOptimizers.optimizeComparisonIdentities(program, options)
|
||||
|
||||
// NaN <= NaN is false, so x<=x should NOT be optimized to true for floats
|
||||
changes shouldBe 0
|
||||
@@ -92,7 +95,7 @@ class TestComparisonOptimizers: FunSpec({
|
||||
|
||||
test("optimizeComparisonIdentities: float x>=x should NOT be optimized (NaN)") {
|
||||
val program = createProgramWithComparison(">=", DataType.FLOAT)
|
||||
val changes = ComparisonOptimizers.optimizeComparisonIdentities(program)
|
||||
val changes = ComparisonOptimizers.optimizeComparisonIdentities(program, options)
|
||||
|
||||
// NaN >= NaN is false, so x>=x should NOT be optimized to true for floats
|
||||
changes shouldBe 0
|
||||
@@ -103,7 +106,7 @@ class TestComparisonOptimizers: FunSpec({
|
||||
|
||||
test("optimizeComparisonIdentities: float x!=x should NOT be optimized (NaN)") {
|
||||
val program = createProgramWithComparison("!=", DataType.FLOAT)
|
||||
val changes = ComparisonOptimizers.optimizeComparisonIdentities(program)
|
||||
val changes = ComparisonOptimizers.optimizeComparisonIdentities(program, options)
|
||||
|
||||
// NaN != NaN is true, but we should not optimize this as it's NaN-specific behavior
|
||||
changes shouldBe 0
|
||||
|
||||
@@ -1411,26 +1411,14 @@ class VirtualMachine(irProgram: IRProgram) {
|
||||
}
|
||||
|
||||
private fun InsSGN(i: IRInstruction) {
|
||||
var sign: Int
|
||||
when(i.type!!) {
|
||||
IRDataType.BYTE -> {
|
||||
sign = registers.getSB(i.reg2!!).toInt().sign
|
||||
registers.setSB(i.reg1!!, sign.toByte())
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
sign = registers.getSW(i.reg2!!).toInt().sign
|
||||
registers.setSB(i.reg1!!, sign.toByte())
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
sign = registers.getSL(i.reg2!!).sign
|
||||
registers.setSB(i.reg1!!, sign.toByte())
|
||||
}
|
||||
IRDataType.FLOAT -> {
|
||||
sign = registers.getFloat(i.fpReg1!!).sign.toInt()
|
||||
registers.setSB(i.reg1!!, sign.toByte())
|
||||
}
|
||||
val sign: Int = when (i.type!!) {
|
||||
IRDataType.BYTE -> registers.getSB(i.reg2!!).toInt().sign
|
||||
IRDataType.WORD -> registers.getSW(i.reg2!!).toInt().sign
|
||||
IRDataType.LONG -> registers.getSL(i.reg2!!).sign
|
||||
IRDataType.FLOAT -> registers.getFloat(i.fpReg1!!).sign.toInt()
|
||||
}
|
||||
statusbitsComparison(sign, i.type!!)
|
||||
registers.setSB(i.reg1!!, sign.toByte())
|
||||
statusbitsComparisonWithOverflow(sign, 0, IRDataType.BYTE)
|
||||
nextPc()
|
||||
}
|
||||
|
||||
@@ -1472,57 +1460,38 @@ class VirtualMachine(irProgram: IRProgram) {
|
||||
}
|
||||
|
||||
private fun InsCMP(i: IRInstruction) {
|
||||
val leftOrig = when(i.type!!) {
|
||||
val type = i.type!!
|
||||
val left = when(type) {
|
||||
IRDataType.BYTE -> registers.getUB(i.reg1!!).toInt()
|
||||
IRDataType.WORD -> registers.getUW(i.reg1!!).toInt()
|
||||
IRDataType.LONG -> registers.getSL(i.reg1!!)
|
||||
IRDataType.FLOAT -> throw IllegalArgumentException("invalid float type for this instruction $i")
|
||||
}
|
||||
val comparison = when(i.type!!) {
|
||||
IRDataType.BYTE -> {
|
||||
val reg1 = registers.getUB(i.reg1!!)
|
||||
val reg2 = registers.getUB(i.reg2!!)
|
||||
reg1.toInt() - reg2.toInt()
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
val reg1 = registers.getUW(i.reg1!!)
|
||||
val reg2 = registers.getUW(i.reg2!!)
|
||||
reg1.toInt() - reg2.toInt()
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
val reg1 = registers.getSL(i.reg1!!)
|
||||
val reg2 = registers.getSL(i.reg2!!)
|
||||
reg1 - reg2
|
||||
}
|
||||
val right = when(type) {
|
||||
IRDataType.BYTE -> registers.getUB(i.reg2!!).toInt()
|
||||
IRDataType.WORD -> registers.getUW(i.reg2!!).toInt()
|
||||
IRDataType.LONG -> registers.getSL(i.reg2!!)
|
||||
IRDataType.FLOAT -> throw IllegalArgumentException("invalid float type for this instruction $i")
|
||||
}
|
||||
statusbitsComparisonWithOverflow(comparison, leftOrig, i.type!!)
|
||||
statusbitsComparisonWithOverflow(left, right, type)
|
||||
nextPc()
|
||||
}
|
||||
|
||||
private fun InsCMPI(i: IRInstruction) {
|
||||
val leftOrig = when(i.type!!) {
|
||||
val type = i.type!!
|
||||
val left = when(type) {
|
||||
IRDataType.BYTE -> registers.getUB(i.reg1!!).toInt()
|
||||
IRDataType.WORD -> registers.getUW(i.reg1!!).toInt()
|
||||
IRDataType.LONG -> registers.getSL(i.reg1!!)
|
||||
IRDataType.FLOAT -> throw IllegalArgumentException("invalid float type for this instruction $i")
|
||||
}
|
||||
val comparison = when(i.type!!) {
|
||||
IRDataType.BYTE -> {
|
||||
val reg1 = registers.getUB(i.reg1!!)
|
||||
reg1.toInt() - (i.immediate!! and 255)
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
val reg1 = registers.getUW(i.reg1!!)
|
||||
reg1.toInt() - (i.immediate!! and 65535)
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
val reg1 = registers.getSL(i.reg1!!)
|
||||
reg1 - i.immediate!!
|
||||
}
|
||||
val right = when(type) {
|
||||
IRDataType.BYTE -> i.immediate!! and 0xff
|
||||
IRDataType.WORD -> i.immediate!! and 0xffff
|
||||
IRDataType.LONG -> i.immediate!!
|
||||
IRDataType.FLOAT -> throw IllegalArgumentException("invalid float type for this instruction $i")
|
||||
}
|
||||
statusbitsComparisonWithOverflow(comparison, leftOrig, i.type!!)
|
||||
statusbitsComparisonWithOverflow(left, right, type)
|
||||
nextPc()
|
||||
}
|
||||
|
||||
|
||||
@@ -13,43 +13,45 @@ internal fun VirtualMachine.statusbitsNZ(value: Int, type: IRDataType) {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun VirtualMachine.statusbitsComparison(comparison: Int, type: IRDataType) {
|
||||
if(comparison==0) {
|
||||
statusZero = true
|
||||
statusCarry = true
|
||||
} else if(comparison>0) {
|
||||
statusZero = false
|
||||
statusCarry = true
|
||||
} else {
|
||||
statusZero = false
|
||||
statusCarry = false
|
||||
}
|
||||
when(type) {
|
||||
IRDataType.BYTE -> statusNegative = (comparison and 0x80)!=0
|
||||
IRDataType.WORD -> statusNegative = (comparison and 0x8000)!=0
|
||||
IRDataType.LONG -> statusNegative = comparison<0
|
||||
internal fun VirtualMachine.statusbitsComparison(left: Int, right: Int, type: IRDataType) {
|
||||
val comparison = left - right
|
||||
statusZero = comparison == 0
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
statusNegative = (comparison and 0x80) != 0
|
||||
statusCarry = (left and 0xff) >= (right and 0xff)
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
statusNegative = (comparison and 0x8000) != 0
|
||||
statusCarry = (left and 0xffff) >= (right and 0xffff)
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
statusNegative = comparison < 0
|
||||
statusCarry = Integer.compareUnsigned(left, right) >= 0
|
||||
}
|
||||
IRDataType.FLOAT -> { /* floats don't change the status bits */ }
|
||||
}
|
||||
}
|
||||
|
||||
internal fun VirtualMachine.statusbitsComparisonWithOverflow(comparison: Int, leftOrig: Int, type: IRDataType) {
|
||||
statusbitsComparison(comparison, type)
|
||||
when(type) {
|
||||
internal fun VirtualMachine.statusbitsComparisonWithOverflow(left: Int, right: Int, type: IRDataType) {
|
||||
statusbitsComparison(left, right, type)
|
||||
val comparison = left - right
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
val signBit = 0x80
|
||||
val leftSign = leftOrig and signBit
|
||||
val rightSign = (leftOrig - comparison) and signBit
|
||||
val leftSign = left and signBit
|
||||
val rightSign = right and signBit
|
||||
statusOverflow = ((leftSign xor rightSign) and (leftSign xor (comparison and signBit))) != 0
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
val signBit = 0x8000
|
||||
val leftSign = leftOrig and signBit
|
||||
val rightSign = (leftOrig - comparison) and signBit
|
||||
val leftSign = left and signBit
|
||||
val rightSign = right and signBit
|
||||
statusOverflow = ((leftSign xor rightSign) and (leftSign xor (comparison and signBit))) != 0
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
val leftSign = leftOrig < 0
|
||||
val rightSign = (leftOrig - comparison) < 0
|
||||
val leftSign = left < 0
|
||||
val rightSign = right < 0
|
||||
val resultSign = comparison < 0
|
||||
statusOverflow = leftSign != rightSign && leftSign != resultSign
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user