mirror of
https://github.com/irmen/prog8.git
synced 2026-10-24 03:02:20 +00:00
Compare commits
59
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 | ||
|
|
6fad90af6e | ||
|
|
3076318d7e | ||
|
|
9c99110738 | ||
|
|
1457d36997 | ||
|
|
43fb9296dd | ||
|
|
8bf744cc4a | ||
|
|
f1f5f070cf | ||
|
|
191a240d0d | ||
|
|
cafb2e355b | ||
|
|
c99c5751f8 | ||
|
|
435b7e56fa | ||
|
|
14f13fac53 | ||
|
|
f8b5037924 | ||
|
|
d2870cb02e | ||
|
|
8e43fc426c | ||
|
|
ba1b6e127a | ||
|
|
4f36a6958c | ||
|
|
df8f08b4ab | ||
|
|
fd5746d3fc | ||
|
|
6ab74fd51d | ||
|
|
245082ca1d | ||
|
|
845e58ea10 | ||
|
|
2a5dbc1ef7 |
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,15 +269,11 @@ Used to call routines at fixed memory addresses (like ROM KERNAL routines or thi
|
||||
- **No `elif`**: use nested `else { if ... }`
|
||||
- Type casting: `expression as type` (e.g., `bytevar as word`). `as` has very low precedence (lower than arithmetic)
|
||||
- **No automatic type widening**: `byte*byte=byte` (overflow possible!). Cast explicitly
|
||||
- **No block scope**: `for`/`if` don't introduce scope. Only subroutines do. Variables in blocks are hoisted to subroutine level
|
||||
- **No bare `{ }` blocks** like C/Java
|
||||
- Indentation: 4 spaces for .p8 and .asm files (no tabs)
|
||||
- Character encoding: 6502 targets use PETSCII by default (call `txt.lowercase()` at start for lowercase). Virtual target uses ISO (`%encoding iso` + `txt.iso()`)
|
||||
- Array size inferred from initializer: `str[] types = ["a", "b", "c"]`
|
||||
- Enums: `Enum::Value` syntax (double colon), declared inside a block. Use enums for related values, `const` for standalone
|
||||
- Enums: `Enum::Value` syntax (double colon), declared inside a block. They are syntactic sugar for a list of `const` declarations, not a type. Use enums for related values, `const` for standalone
|
||||
- Avoid `globals.XXXX` — move constants closer to where they're used
|
||||
- Member access through pointers: use `.` for both direct and pointer access. The compiler infers the type. For complex assignment targets, `^^` may be needed: `ptr^^.field = value`
|
||||
- Qualified names: must use full path from top level (e.g., `cx16.r0`, not relative)
|
||||
- **No block scope** — variables in `if`/`for`/`repeat` blocks are hoisted to subroutine level
|
||||
- **No reassigning strings** after declaration — use `strings.copy()`/`strings.ncopy()`
|
||||
- **Pre-allocate string buffers** — `""` allocates nothing
|
||||
@@ -12,13 +12,11 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: build and install recent 64tass
|
||||
- name: install 64tass
|
||||
run: |
|
||||
sudo apt-get install -y make build-essential
|
||||
git clone --depth=1 https://github.com/irmen/64tass
|
||||
cd 64tass
|
||||
make -j4
|
||||
sudo make install
|
||||
sudo apt update
|
||||
sudo apt install 64tass
|
||||
64tass --version
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
@@ -26,9 +24,12 @@ jobs:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
|
||||
- name: Build and test with Gradle
|
||||
run: |
|
||||
./gradlew build shadowJar --no-daemon
|
||||
./gradlew build shadowJar --no-daemon --console=plain
|
||||
sha256sum -b compiler/build/libs/*-all.jar > compiler/build/libs/hash.txt
|
||||
|
||||
- name: Create compiler shadowJar artifact
|
||||
|
||||
+1
-1
@@ -5,6 +5,7 @@
|
||||
.idea/shelf/
|
||||
.junie/
|
||||
.junie/memory/
|
||||
.serena
|
||||
build/
|
||||
dist/
|
||||
output/
|
||||
@@ -38,7 +39,6 @@ parsetab.py
|
||||
.pytest_cache/
|
||||
.attach_pid*
|
||||
compiler/lib/
|
||||
|
||||
.gradle
|
||||
sd*.img
|
||||
*.d64
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -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
|
||||
-------------
|
||||
@@ -63,6 +65,7 @@ What does Prog8 provide?
|
||||
- programs run very fast because it's compiled to native machine code
|
||||
- compiled code is very small; much smaller than equivalent C code compiled with CC65, and usually runs faster as well
|
||||
- modularity, symbol scoping, subroutines. No need for forward declarations.
|
||||
- Can make vars and routines private, as a simple way to restrict access and hide implementation details
|
||||
- various data types other than just bytes (16-bit words, long integers, floats, strings)
|
||||
- Structs and typed pointers
|
||||
- 2D arrays (`matrix[row][col]` syntax)
|
||||
|
||||
@@ -73,6 +73,9 @@ val BaseDataType.isWordOrByteOrBool get() = this in setOf(BaseDataType.UBYTE, Ba
|
||||
val BaseDataType.isNumeric get() = this == BaseDataType.FLOAT || this.isInteger
|
||||
val BaseDataType.isNumericOrBool get() = this == BaseDataType.BOOL || this.isNumeric
|
||||
val BaseDataType.isSigned get() = this in setOf(BaseDataType.BYTE, BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
|
||||
val BaseDataType.isUnsigned get() = !this.isSigned
|
||||
val BaseDataType.isSignedInteger get() = this.isSigned && this.isInteger
|
||||
val BaseDataType.isUnsignedInteger get() = this.isUnsigned && this.isInteger
|
||||
val BaseDataType.isArray get() = this == BaseDataType.ARRAY || this == BaseDataType.ARRAY_SPLITW || this == BaseDataType.ARRAY_POINTER
|
||||
val BaseDataType.isPointer get() = this == BaseDataType.POINTER
|
||||
val BaseDataType.isStructInstance get() = this == BaseDataType.STRUCT_INSTANCE
|
||||
|
||||
@@ -13,9 +13,10 @@ import kotlin.io.path.Path
|
||||
|
||||
object ImportFileSystem {
|
||||
|
||||
val userHome: Path get() = Path.of(System.getProperty("user.home"))
|
||||
|
||||
fun expandTilde(path: String): String = if (path.startsWith("~")) {
|
||||
val userHome = System.getProperty("user.home")
|
||||
userHome + path.drop(1)
|
||||
userHome.toString() + path.drop(1)
|
||||
} else {
|
||||
path
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import java.io.CharConversionException
|
||||
object PetsciiEncoding {
|
||||
|
||||
// decoding: from Petscii/Screencodes (0-255) to Unicode
|
||||
// character tables used from https://github.com/irmen/cbmcodecs2
|
||||
// character tables used from https://codeberg.org/irmen/cbmcodecs2.git
|
||||
|
||||
private val decodingPetsciiLowercase = charArrayOf(
|
||||
'\u0000', // 0x00 -> \u0000
|
||||
|
||||
@@ -1024,7 +1024,7 @@ class AsmGen6502Internal (
|
||||
assignExpressionToRegister(value, target.register!!)
|
||||
} else {
|
||||
assignExpressionToRegister(value, RegisterOrPair.AX)
|
||||
pointerGen.assignWordReg(PtrTarget(target), RegisterOrPair.AX)
|
||||
assignmentAsmGen.assignRegisterpairWord(target, RegisterOrPair.AX)
|
||||
}
|
||||
}
|
||||
target.datatype.isWord || target.datatype.isPassByRef -> {
|
||||
@@ -1175,6 +1175,7 @@ class AsmGen6502Internal (
|
||||
when {
|
||||
iterations == 0 -> {}
|
||||
iterations == 1 -> translate(stmt.statements)
|
||||
iterations > 65536 -> repeatLongCount(iterations.toLong(), stmt)
|
||||
iterations !in 0..65536 -> throw AssemblyError("invalid number of iterations")
|
||||
iterations <= 256 -> repeatByteCount(iterations, stmt)
|
||||
else -> repeatWordCount(iterations, stmt)
|
||||
@@ -1186,26 +1187,36 @@ class AsmGen6502Internal (
|
||||
val name = asmVariableName(stmt.count as PtIdentifier)
|
||||
when {
|
||||
vardecl.type.isByte -> {
|
||||
assignVariableToRegister(name, RegisterOrPair.Y, stmt.definingISub(), stmt.count.position)
|
||||
assignVariableToRegister(name, RegisterOrPair.Y, stmt.definingISub(), stmt.count.position, vardecl.type.isSigned)
|
||||
repeatCountInY(stmt, endLabel)
|
||||
}
|
||||
vardecl.type.isWord -> {
|
||||
assignVariableToRegister(name, RegisterOrPair.AY, stmt.definingISub(), stmt.count.position)
|
||||
assignVariableToRegister(name, RegisterOrPair.AY, stmt.definingISub(), stmt.count.position, vardecl.type.isSigned)
|
||||
repeatWordCountInAY(endLabel, stmt)
|
||||
}
|
||||
vardecl.type.isLong -> {
|
||||
val reg = CombinedLongRegisters.last()
|
||||
assignVariableToRegister(name, reg, stmt.definingISub(), (stmt.count as PtIdentifier).position, vardecl.type.isSigned)
|
||||
repeatLongCountInReg(reg, endLabel, stmt)
|
||||
}
|
||||
else -> throw AssemblyError("invalid loop variable datatype ${vardecl.type}")
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
when {
|
||||
stmt.count.type.isByte -> {
|
||||
assignExpressionToRegister(stmt.count, RegisterOrPair.Y)
|
||||
assignExpressionToRegister(stmt.count, RegisterOrPair.Y, stmt.count.type.isSigned)
|
||||
repeatCountInY(stmt, endLabel)
|
||||
}
|
||||
stmt.count.type.isWord -> {
|
||||
assignExpressionToRegister(stmt.count, RegisterOrPair.AY)
|
||||
assignExpressionToRegister(stmt.count, RegisterOrPair.AY, stmt.count.type.isSigned)
|
||||
repeatWordCountInAY(endLabel, stmt)
|
||||
}
|
||||
stmt.count.type.isLong -> {
|
||||
val reg = CombinedLongRegisters.last()
|
||||
assignExpressionToRegister(stmt.count, reg, stmt.count.type.isSigned)
|
||||
repeatLongCountInReg(reg, endLabel, stmt)
|
||||
}
|
||||
else -> throw AssemblyError("invalid loop expression datatype ${stmt.count.type}")
|
||||
}
|
||||
}
|
||||
@@ -1213,6 +1224,75 @@ class AsmGen6502Internal (
|
||||
|
||||
loopEndLabels.removeLast()
|
||||
}
|
||||
|
||||
private fun repeatLongCount(iterations: Long, stmt: PtRepeatLoop) {
|
||||
require(iterations > 65536) { "invalid repeat count ${stmt.position}" }
|
||||
val repeatLabel = makeLabel("repeat")
|
||||
val counterVar = createTempVarReused(BaseDataType.LONG, true, stmt)
|
||||
|
||||
val bytes = mutableListOf<Int>()
|
||||
var currentN = iterations
|
||||
while (currentN > 0) {
|
||||
val b = (currentN % 256).toInt()
|
||||
bytes.add(b)
|
||||
currentN = if (b == 0) currentN / 256 else currentN / 256 + 1
|
||||
if (currentN == 1L) break
|
||||
}
|
||||
|
||||
for (i in bytes.indices) {
|
||||
val v = bytes[i]
|
||||
val suffix = if (i > 0) "+$i" else ""
|
||||
if (v == 0 && isTargetCpu(CpuType.CPU65C02)) {
|
||||
out(" stz $counterVar$suffix")
|
||||
} else {
|
||||
out(" lda #$v")
|
||||
out(" sta $counterVar$suffix")
|
||||
}
|
||||
}
|
||||
|
||||
out(repeatLabel)
|
||||
translate(stmt.statements)
|
||||
|
||||
for (i in bytes.indices) {
|
||||
val suffix = if (i > 0) "+$i" else ""
|
||||
out(" dec $counterVar$suffix")
|
||||
out(" bne $repeatLabel")
|
||||
}
|
||||
}
|
||||
|
||||
private fun repeatLongCountInReg(reg: RegisterOrPair, endLabel: String, stmt: PtRepeatLoop) {
|
||||
require(reg in CombinedLongRegisters)
|
||||
val repeatLabel = makeLabel("repeat")
|
||||
val regName = "cx16." + reg.startregname()
|
||||
val d1 = makeLabel("skip_dec1")
|
||||
val d2 = makeLabel("skip_dec2")
|
||||
val d3 = makeLabel("skip_dec3")
|
||||
out("""
|
||||
lda $regName
|
||||
ora $regName+1
|
||||
ora $regName+2
|
||||
ora $regName+3
|
||||
beq $endLabel
|
||||
$repeatLabel""")
|
||||
translate(stmt.statements)
|
||||
out("""
|
||||
lda $regName
|
||||
bne $d3
|
||||
lda $regName+1
|
||||
bne $d2
|
||||
lda $regName+2
|
||||
bne $d1
|
||||
dec $regName+3
|
||||
$d1 dec $regName+2
|
||||
$d2 dec $regName+1
|
||||
$d3 dec $regName
|
||||
lda $regName
|
||||
ora $regName+1
|
||||
ora $regName+2
|
||||
ora $regName+3
|
||||
bne $repeatLabel""")
|
||||
out(endLabel)
|
||||
}
|
||||
|
||||
private fun repeatWordCount(iterations: Int, stmt: PtRepeatLoop) {
|
||||
require(iterations in 257..65536) { "invalid repeat count ${stmt.position}" }
|
||||
|
||||
@@ -136,9 +136,27 @@ internal class AssemblyProgram(
|
||||
}
|
||||
|
||||
val proc = ProcessBuilder(assemblerCommand)
|
||||
if(!options.quiet)
|
||||
proc.inheritIO()
|
||||
val process = proc.start()
|
||||
.redirectErrorStream(true)
|
||||
|
||||
if (options.quiet) {
|
||||
proc.redirectOutput(ProcessBuilder.Redirect.DISCARD)
|
||||
}
|
||||
|
||||
val process = try {
|
||||
proc.start()
|
||||
} catch (e: Exception) {
|
||||
System.err.println("daemon: assembler failed to start: ${e.message}")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!options.quiet) {
|
||||
process.inputStream.bufferedReader().use { reader ->
|
||||
reader.forEachLine {
|
||||
println(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val result = process.waitFor()
|
||||
if (result == 0) {
|
||||
removeGeneratedLabelsFromMonlist()
|
||||
|
||||
@@ -275,8 +275,14 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
asmgen.assignWordOperandsToAYAndVar(fcall.args[1], fcall.args[0], "P8ZP_SCRATCH_W1")
|
||||
// math.divmod_uw_asm: -- divide two unsigned words (16 bit each) into 16 bit results
|
||||
// input: P8ZP_SCRATCH_W1 in ZP: 16-bit number, A/Y: 16 bit divisor
|
||||
// output: cx16.r15: 16-bit remainder, A/Y: 16 bit division result
|
||||
// output: P8ZP_SCRATCH_W2: 16-bit remainder, A/Y: 16 bit division result
|
||||
asmgen.out(" jsr prog8_math.divmod_uw_asm")
|
||||
// copy remainder from P8ZP_SCRATCH_W2 to R15 (needed as second return value)
|
||||
asmgen.out("""
|
||||
ldx P8ZP_SCRATCH_W2
|
||||
stx cx16.r15L
|
||||
ldx P8ZP_SCRATCH_W2+1
|
||||
stx cx16.r15H""".trimIndent())
|
||||
return arrayOf(RegisterOrPair.AY, RegisterOrPair.R15)
|
||||
}
|
||||
|
||||
@@ -494,83 +500,115 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
val arg2 = fcall.args[1]
|
||||
if(arg1.type.isByte) {
|
||||
if(arg2.type.isByte) {
|
||||
val op = if (arg1.type.isSigned || arg2.type.isSigned) "sec | sbc" else "cmp"
|
||||
when (arg2) {
|
||||
is PtIdentifier -> {
|
||||
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.A)
|
||||
asmgen.out(" cmp ${asmgen.asmVariableName(arg2)}")
|
||||
asmgen.out(" $op ${asmgen.asmVariableName(arg2)}")
|
||||
}
|
||||
is PtNumber -> {
|
||||
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.A)
|
||||
asmgen.out(" cmp #${arg2.number.toInt()}")
|
||||
asmgen.out(" $op #${arg2.number.toInt()}")
|
||||
}
|
||||
is PtBool -> {
|
||||
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.A)
|
||||
asmgen.out(" cmp #${arg2.asInt()}")
|
||||
asmgen.out(" $op #${arg2.asInt()}")
|
||||
}
|
||||
is PtMemoryByte -> {
|
||||
if(arg2.address is PtNumber) {
|
||||
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.A)
|
||||
asmgen.out(" cmp ${arg2.address.asConstInteger()!!.toHex()}")
|
||||
asmgen.out(" $op ${arg2.address.asConstInteger()!!.toHex()}")
|
||||
} else {
|
||||
asmgen.assignByteOperandsToAAndVar(arg1, arg2, "P8ZP_SCRATCH_B1")
|
||||
asmgen.out(" cmp P8ZP_SCRATCH_B1")
|
||||
asmgen.out(" $op P8ZP_SCRATCH_B1")
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
asmgen.assignByteOperandsToAAndVar(arg1, arg2, "P8ZP_SCRATCH_B1")
|
||||
asmgen.out(" cmp P8ZP_SCRATCH_B1")
|
||||
asmgen.out(" $op P8ZP_SCRATCH_B1")
|
||||
}
|
||||
}
|
||||
} else
|
||||
throw AssemblyError("args for cmp() should have same dt")
|
||||
} else if(arg1.type.isWord) {
|
||||
if(arg2.type.isWord) {
|
||||
if(arg1.type.isSigned) {
|
||||
if(arg1.type.isSigned || arg2.type.isSigned) {
|
||||
when (arg2) {
|
||||
is PtIdentifier -> {
|
||||
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.AY)
|
||||
val var2 = asmgen.asmVariableName(arg2)
|
||||
asmgen.out("""
|
||||
sec
|
||||
sbc ${asmgen.asmVariableName(arg2)}
|
||||
sbc $var2
|
||||
sta P8ZP_SCRATCH_B1
|
||||
tya
|
||||
sbc ${asmgen.asmVariableName(arg2)}+1""")
|
||||
sbc $var2+1
|
||||
php
|
||||
bne +
|
||||
lda P8ZP_SCRATCH_B1
|
||||
beq +
|
||||
pla
|
||||
and #%11111101
|
||||
pha
|
||||
+ plp""")
|
||||
}
|
||||
is PtBool -> TODO("word compare against bool ${arg2.position}")
|
||||
is PtNumber -> {
|
||||
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.AY)
|
||||
val num = arg2.number.toInt()
|
||||
asmgen.out("""
|
||||
sec
|
||||
sbc #<${arg2.number.toInt()}
|
||||
sbc #<($num)
|
||||
sta P8ZP_SCRATCH_B1
|
||||
tya
|
||||
sbc #>${arg2.number.toInt()}""")
|
||||
sbc #>($num)
|
||||
php
|
||||
bne +
|
||||
lda P8ZP_SCRATCH_B1
|
||||
beq +
|
||||
pla
|
||||
and #%11111101
|
||||
pha
|
||||
+ plp""")
|
||||
}
|
||||
else -> {
|
||||
asmgen.assignWordOperandsToAYAndVar(arg1, arg2, "P8ZP_SCRATCH_W1")
|
||||
asmgen.out("""
|
||||
sec
|
||||
sbc P8ZP_SCRATCH_W1
|
||||
sta P8ZP_SCRATCH_B1
|
||||
tya
|
||||
sbc P8ZP_SCRATCH_W1+1""")
|
||||
sbc P8ZP_SCRATCH_W1+1
|
||||
php
|
||||
bne +
|
||||
lda P8ZP_SCRATCH_B1
|
||||
beq +
|
||||
pla
|
||||
and #%11111101
|
||||
pha
|
||||
+ plp""")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when (arg2) {
|
||||
is PtIdentifier -> {
|
||||
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.AY)
|
||||
val var2 = asmgen.asmVariableName(arg2)
|
||||
asmgen.out("""
|
||||
cpy ${asmgen.asmVariableName(arg2)}+1
|
||||
cpy $var2+1
|
||||
bne +
|
||||
cmp ${asmgen.asmVariableName(arg2)}
|
||||
+""")
|
||||
cmp $var2
|
||||
+""")
|
||||
}
|
||||
is PtBool -> TODO("word compare against bool ${arg2.position}")
|
||||
is PtNumber -> {
|
||||
asmgen.assignExpressionToRegister(arg1, RegisterOrPair.AY)
|
||||
val num = arg2.number.toInt()
|
||||
asmgen.out("""
|
||||
cpy #>${arg2.number.toInt()}
|
||||
cpy #>($num)
|
||||
bne +
|
||||
cmp #<${arg2.number.toInt()}
|
||||
+""")
|
||||
cmp #<($num)
|
||||
+""")
|
||||
}
|
||||
else -> {
|
||||
asmgen.assignWordOperandsToAYAndVar(arg1, arg2, "P8ZP_SCRATCH_W1")
|
||||
@@ -578,7 +616,7 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
cpy P8ZP_SCRATCH_W1+1
|
||||
bne +
|
||||
cmp P8ZP_SCRATCH_W1
|
||||
+""")
|
||||
+""")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -593,12 +631,25 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
sec
|
||||
lda $var1
|
||||
sbc #$${hex.substring(6, 8)}
|
||||
sta P8ZP_SCRATCH_B1
|
||||
lda $var1+1
|
||||
sbc #$${hex.substring(4, 6)}
|
||||
ora P8ZP_SCRATCH_B1
|
||||
sta P8ZP_SCRATCH_B1
|
||||
lda $var1+2
|
||||
sbc #$${hex.substring(2, 4)}
|
||||
ora P8ZP_SCRATCH_B1
|
||||
sta P8ZP_SCRATCH_B1
|
||||
lda $var1+3
|
||||
sbc #$${hex.take(2)}""")
|
||||
sbc #$${hex.take(2)}
|
||||
php
|
||||
bne +
|
||||
lda P8ZP_SCRATCH_B1
|
||||
beq +
|
||||
pla
|
||||
and #%11111101
|
||||
pha
|
||||
+ plp""")
|
||||
} else if(arg1 is PtIdentifier && arg2 is PtIdentifier) {
|
||||
val var1 = asmgen.asmVariableName(arg1)
|
||||
val var2 = asmgen.asmVariableName(arg2)
|
||||
@@ -606,12 +657,25 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
sec
|
||||
lda $var1
|
||||
sbc $var2
|
||||
sta P8ZP_SCRATCH_B1
|
||||
lda $var1+1
|
||||
sbc $var2+1
|
||||
ora P8ZP_SCRATCH_B1
|
||||
sta P8ZP_SCRATCH_B1
|
||||
lda $var1+2
|
||||
sbc $var2+2
|
||||
ora P8ZP_SCRATCH_B1
|
||||
sta P8ZP_SCRATCH_B1
|
||||
lda $var1+3
|
||||
sbc $var2+3""")
|
||||
sbc $var2+3
|
||||
php
|
||||
bne +
|
||||
lda P8ZP_SCRATCH_B1
|
||||
beq +
|
||||
pla
|
||||
and #%11111101
|
||||
pha
|
||||
+ plp""")
|
||||
} else {
|
||||
assignAsmGen.assignExpressionToRegister(arg2, RegisterOrPair.R14R15, true)
|
||||
if(!assignAsmGen.isRightTrivial(arg1))
|
||||
@@ -623,12 +687,25 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
sec
|
||||
lda cx16.r12
|
||||
sbc cx16.r14
|
||||
sta P8ZP_SCRATCH_B1
|
||||
lda cx16.r12+1
|
||||
sbc cx16.r14+1
|
||||
ora P8ZP_SCRATCH_B1
|
||||
sta P8ZP_SCRATCH_B1
|
||||
lda cx16.r12+2
|
||||
sbc cx16.r14+2
|
||||
ora P8ZP_SCRATCH_B1
|
||||
sta P8ZP_SCRATCH_B1
|
||||
lda cx16.r12+3
|
||||
sbc cx16.r14+3""")
|
||||
sbc cx16.r14+3
|
||||
php
|
||||
bne +
|
||||
lda P8ZP_SCRATCH_B1
|
||||
beq +
|
||||
pla
|
||||
and #%11111101
|
||||
pha
|
||||
+ plp""")
|
||||
}
|
||||
} else
|
||||
throw AssemblyError("args for cmp() should have same dt")
|
||||
@@ -1121,19 +1198,16 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
if(arg is PtIdentifier) {
|
||||
val varname = asmgen.asmVariableName(arg)
|
||||
when(val dt = arg.type.base) {
|
||||
BaseDataType.UBYTE -> asmgen.out(" lda $varname | jsr prog8_lib.func_sign_ub_into_A")
|
||||
BaseDataType.BYTE -> asmgen.out(" lda $varname | jsr prog8_lib.func_sign_b_into_A")
|
||||
BaseDataType.UWORD -> asmgen.out(" lda $varname+1 | jsr prog8_lib.func_sign_ub_into_A")
|
||||
BaseDataType.WORD -> asmgen.out(" lda $varname+1 | jsr prog8_lib.func_sign_b_into_A")
|
||||
BaseDataType.LONG -> asmgen.out(" lda $varname+3 | jsr prog8_lib.func_sign_b_into_A")
|
||||
BaseDataType.FLOAT -> {
|
||||
translateArguments(fcall, null, scope)
|
||||
asmgen.out(" jsr floats.func_sign_f_into_A")
|
||||
BaseDataType.UBYTE -> {
|
||||
asmgen.out(" lda $varname | jsr prog8_lib.func_sign_ub_into_A")
|
||||
return arrayOf(RegisterOrPair.A)
|
||||
}
|
||||
else -> throw AssemblyError("weird type $dt")
|
||||
BaseDataType.BYTE -> {
|
||||
asmgen.out(" lda $varname | jsr prog8_lib.func_sign_b_into_A")
|
||||
return arrayOf(RegisterOrPair.A)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
// result in A
|
||||
return arrayOf(RegisterOrPair.A)
|
||||
}
|
||||
|
||||
when (val dt = arg.type.base) {
|
||||
|
||||
@@ -42,12 +42,129 @@ internal class ForLoopsAsmGen(
|
||||
when {
|
||||
iterableDt.isByteArray -> forOverNonconstByteRange(stmt, iterableDt, range)
|
||||
iterableDt.isWordArray && !iterableDt.isSplitWordArray -> forOverNonconstWordRange(stmt, iterableDt, range)
|
||||
else -> throw AssemblyError("range expression can only be byte or word")
|
||||
iterableDt.isLongArray -> forOverNonconstLongRange(stmt, iterableDt, range)
|
||||
else -> throw AssemblyError("range expression can only be byte, word or long")
|
||||
}
|
||||
|
||||
asmgen.loopEndLabels.removeLast()
|
||||
}
|
||||
|
||||
private fun forOverNonconstLongRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
|
||||
val stepsize = range.step.asConstInteger()!!
|
||||
val loopLabel = asmgen.makeLabel("for_loop")
|
||||
val endLabel = asmgen.makeLabel("for_end")
|
||||
asmgen.loopEndLabels.add(endLabel)
|
||||
val varname = asmgen.asmVariableName(stmt.variable)
|
||||
|
||||
asmgen.assignExpressionToVariable(range.from, varname, iterableDt.elementType())
|
||||
|
||||
if (stepsize == 1 || stepsize == -1)
|
||||
forOverLongsRangeStepOne(range, varname, iterableDt, loopLabel, endLabel, stmt)
|
||||
else
|
||||
forOverLongsRangeStepGreaterOne(range, varname, iterableDt, loopLabel, endLabel, stmt)
|
||||
}
|
||||
|
||||
private fun forOverLongsRangeStepOne(range: PtRange, varname: String, iterableDt: DataType, loopLabel: String, endLabel: String, forloop: PtForLoop) {
|
||||
val stepsize = range.step.asConstInteger()!!
|
||||
|
||||
// Use temporary variable to store the limit
|
||||
val toValueVar = asmgen.createTempVarReused(iterableDt.elementType().base, false, range)
|
||||
asmgen.assignExpressionToVariable(range.to, toValueVar, iterableDt.elementType())
|
||||
|
||||
if (stepsize == 1) {
|
||||
// Increment toValueVar to make the loop inclusive (0 to N runs N+1 times)
|
||||
asmgen.out("""
|
||||
inc $toValueVar
|
||||
bne +
|
||||
inc $toValueVar+1
|
||||
bne +
|
||||
inc $toValueVar+2
|
||||
bne +
|
||||
inc $toValueVar+3
|
||||
+
|
||||
""")
|
||||
} else {
|
||||
// Decrement toValueVar to make the loop inclusive (N downto 0 runs N+1 times)
|
||||
val label1 = asmgen.makeLabel("dec_1")
|
||||
val label2 = asmgen.makeLabel("dec_2")
|
||||
val label3 = asmgen.makeLabel("dec_3")
|
||||
asmgen.out("""
|
||||
lda $toValueVar
|
||||
bne $label1
|
||||
lda $toValueVar+1
|
||||
bne $label2
|
||||
lda $toValueVar+2
|
||||
bne $label3
|
||||
dec $toValueVar+3
|
||||
$label3: dec $toValueVar+2
|
||||
$label2: dec $toValueVar+1
|
||||
$label1: dec $toValueVar
|
||||
""")
|
||||
}
|
||||
|
||||
asmgen.out(loopLabel)
|
||||
asmgen.translate(forloop.statements)
|
||||
|
||||
if (stepsize == 1) {
|
||||
asmgen.out("""
|
||||
inc $varname
|
||||
bne +
|
||||
inc $varname+1
|
||||
bne +
|
||||
inc $varname+2
|
||||
bne +
|
||||
inc $varname+3
|
||||
+ lda $varname
|
||||
cmp $toValueVar
|
||||
bne $loopLabel
|
||||
lda $varname+1
|
||||
cmp $toValueVar+1
|
||||
bne $loopLabel
|
||||
lda $varname+2
|
||||
cmp $toValueVar+2
|
||||
bne $loopLabel
|
||||
lda $varname+3
|
||||
cmp $toValueVar+3
|
||||
bne $loopLabel
|
||||
""")
|
||||
} else {
|
||||
asmgen.out("""
|
||||
lda $varname
|
||||
bne +++
|
||||
lda $varname+1
|
||||
bne ++
|
||||
lda $varname+2
|
||||
bne +
|
||||
dec $varname+3
|
||||
+ dec $varname+2
|
||||
+ dec $varname+1
|
||||
+ dec $varname
|
||||
lda $varname
|
||||
cmp $toValueVar
|
||||
bne $loopLabel
|
||||
lda $varname+1
|
||||
cmp $toValueVar+1
|
||||
bne $loopLabel
|
||||
lda $varname+2
|
||||
cmp $toValueVar+2
|
||||
bne $loopLabel
|
||||
lda $varname+3
|
||||
cmp $toValueVar+3
|
||||
bne $loopLabel
|
||||
""")
|
||||
}
|
||||
asmgen.out(endLabel)
|
||||
}
|
||||
|
||||
private fun forOverLongsRangeStepGreaterOne(range: PtRange, varname: String, iterableDt: DataType, loopLabel: String, endLabel: String, forloop: PtForLoop) {
|
||||
// TODO: implement 32-bit loop
|
||||
asmgen.romableError("for loops over long ranges with step != 1 not yet fully implemented", forloop.position)
|
||||
asmgen.out(loopLabel)
|
||||
asmgen.translate(forloop.statements)
|
||||
asmgen.jmp(loopLabel)
|
||||
asmgen.out(endLabel)
|
||||
}
|
||||
|
||||
private fun forOverNonconstByteRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
|
||||
val stepsize = range.step.asConstInteger()!!
|
||||
val loopLabel = asmgen.makeLabel("for_loop")
|
||||
|
||||
@@ -83,7 +83,7 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
|
||||
asmgen.out(endLabel)
|
||||
assignmentAsmGen.assignRegisterByte(target, CpuRegister.A, false, false)
|
||||
}
|
||||
expr.type.isWord || expr.type.isString -> {
|
||||
expr.type.isWord || expr.type.isString || expr.type.isPointer -> {
|
||||
asmgen.assignExpressionToRegister(expr.falsevalue, RegisterOrPair.AY)
|
||||
asmgen.jmp(endLabel)
|
||||
asmgen.out(trueLabel)
|
||||
|
||||
@@ -125,10 +125,13 @@ internal class AnyExprAsmGen(
|
||||
}
|
||||
|
||||
private fun assignFloatBinExpr(expr: PtBinaryExpression, assign: AsmAssignment): Boolean {
|
||||
// C64/PET32 ROM T-variant entries skip CONUPK and need arisgn+Z flag set up beforehand.
|
||||
// CX16 ROM entries are already fixed and handle this internally.
|
||||
val tSuffix = if(asmgen.options.compTarget.name in listOf(C64Target.NAME, PETTarget.NAME)) "_NZ" else ""
|
||||
when(expr.operator) {
|
||||
"+" -> {
|
||||
assignFloatOperandsToFACandARG(expr.left, expr.right)
|
||||
asmgen.out(" jsr floats.FADDT")
|
||||
asmgen.out(" jsr floats.FADDT$tSuffix")
|
||||
asmgen.assignRegister(RegisterOrPair.FAC1, assign.target)
|
||||
return true
|
||||
}
|
||||
@@ -140,13 +143,13 @@ internal class AnyExprAsmGen(
|
||||
}
|
||||
"*" -> {
|
||||
assignFloatOperandsToFACandARG(expr.left, expr.right)
|
||||
asmgen.out(" jsr floats.FMULTT")
|
||||
asmgen.out(" jsr floats.FMULTT$tSuffix")
|
||||
asmgen.assignRegister(RegisterOrPair.FAC1, assign.target)
|
||||
return true
|
||||
}
|
||||
"/" -> {
|
||||
assignFloatOperandsToFACandARG(expr.right, expr.left)
|
||||
asmgen.out(" jsr floats.FDIVT")
|
||||
asmgen.out(" jsr floats.FDIVT$tSuffix")
|
||||
asmgen.assignRegister(RegisterOrPair.FAC1, assign.target)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ internal class AssignmentAsmGen(
|
||||
}
|
||||
targetDt.isByte -> assignVariableByte(assign.target, variable)
|
||||
targetDt.isSignedWord -> assignVariableWord(assign.target, variable, assign.source.datatype)
|
||||
targetDt.isUnsignedWord -> {
|
||||
targetDt.isUnsignedWord || targetDt.isPointer -> {
|
||||
if(assign.source.datatype.isPassByRef)
|
||||
assignAddressOf(assign.target, variable, false, assign.source.datatype, assign.source.array ?: PtNumber(BaseDataType.UBYTE, 0.0, assign.position))
|
||||
else
|
||||
@@ -236,7 +236,6 @@ internal class AssignmentAsmGen(
|
||||
targetDt.isLong -> assignVariableLong(assign.target, variable, assign.source.datatype)
|
||||
targetDt.isFloat -> assignVariableFloat(assign.target, variable)
|
||||
targetDt.isString -> assignVariableString(assign.target, variable)
|
||||
targetDt.isPointer -> assignVariableWord(assign.target, variable, assign.source.datatype)
|
||||
else -> throw AssemblyError("unsupported assignment target type ${assign.target.datatype} ${assign.position}")
|
||||
}
|
||||
}
|
||||
@@ -493,13 +492,7 @@ internal class AssignmentAsmGen(
|
||||
val identifier = value.identifier!!
|
||||
val source = asmgen.symbolTable.lookup(identifier.name)
|
||||
require(source !is StConstant) { "addressOf of a constant should have been rewritten to a simple addition expression" }
|
||||
val sourceName =
|
||||
if (value.isMsbForSplitArray)
|
||||
asmgen.asmSymbolName(identifier) + "_msb"
|
||||
else if (identifier.type.isSplitWordArray)
|
||||
asmgen.asmSymbolName(identifier) + "_lsb" // the _lsb split array comes first in memory
|
||||
else
|
||||
asmgen.asmSymbolName(identifier)
|
||||
val sourceName = asmgen.asmSymbolName(identifier)
|
||||
assignAddressOf(assign.target, sourceName, value.isMsbForSplitArray, identifier.type, value.arrayIndexExpr)
|
||||
} else {
|
||||
val ptrderef = value.dereference!!
|
||||
@@ -3661,17 +3654,26 @@ $endLabel""")
|
||||
}
|
||||
|
||||
private fun assignAddressOf(target: AsmAssignTarget, sourceName: String, msb: Boolean, arrayDt: DataType?, arrayIndexExpr: PtExpression?) {
|
||||
var actualSourceName = sourceName
|
||||
var actualMsb = msb
|
||||
if (arrayDt?.isSplitWordArray == true) {
|
||||
if (!sourceName.endsWith("_lsb") && !sourceName.endsWith("_msb")) {
|
||||
actualSourceName = if (msb) sourceName + "_msb" else sourceName + "_lsb"
|
||||
}
|
||||
actualMsb = false
|
||||
}
|
||||
|
||||
if(arrayIndexExpr!=null) {
|
||||
if(arrayDt?.isPointer==true) {
|
||||
require(!msb)
|
||||
return pointergen.assignAddressOfIndexedPointer(target, sourceName, arrayDt, arrayIndexExpr)
|
||||
require(!actualMsb)
|
||||
return pointergen.assignAddressOfIndexedPointer(target, actualSourceName, arrayDt, arrayIndexExpr)
|
||||
}
|
||||
val constIndex = arrayIndexExpr.asConstInteger()
|
||||
if(constIndex!=null) {
|
||||
if (arrayDt!!.isUnsignedWord) {
|
||||
// using a UWORD pointer with array indexing, always bytes
|
||||
require(!msb)
|
||||
assignVariableToRegister(sourceName, RegisterOrPair.AY, false, arrayIndexExpr.definingISub(), arrayIndexExpr.position)
|
||||
require(!actualMsb)
|
||||
assignVariableToRegister(actualSourceName, RegisterOrPair.AY, false, arrayIndexExpr.definingISub(), arrayIndexExpr.position)
|
||||
if(constIndex in 1..255)
|
||||
asmgen.out("""
|
||||
clc
|
||||
@@ -3693,9 +3695,9 @@ $endLabel""")
|
||||
else {
|
||||
if(constIndex>0) {
|
||||
val offset = if(arrayDt.isSplitWordArray) constIndex else program.memsizer.memorySize(arrayDt, constIndex) // add arrayIndexExpr * elementsize to the address of the array variable.
|
||||
asmgen.out(" lda #<($sourceName + $offset) | ldy #>($sourceName + $offset)")
|
||||
asmgen.out(" lda #<($actualSourceName + $offset) | ldy #>($actualSourceName + $offset)")
|
||||
} else {
|
||||
asmgen.out(" lda #<$sourceName | ldy #>$sourceName")
|
||||
asmgen.out(" lda #<$actualSourceName | ldy #>$actualSourceName")
|
||||
}
|
||||
}
|
||||
assignRegisterpairWord(target, RegisterOrPair.AY)
|
||||
@@ -3703,8 +3705,8 @@ $endLabel""")
|
||||
} else {
|
||||
if (arrayDt!!.isUnsignedWord) {
|
||||
// using a UWORD pointer with array indexing, always bytes
|
||||
require(!msb)
|
||||
assignVariableToRegister(sourceName, RegisterOrPair.AY, false, arrayIndexExpr.definingISub(), arrayIndexExpr.position)
|
||||
require(!actualMsb)
|
||||
assignVariableToRegister(actualSourceName, RegisterOrPair.AY, false, arrayIndexExpr.definingISub(), arrayIndexExpr.position)
|
||||
asmgen.saveRegisterStack(CpuRegister.A, false)
|
||||
asmgen.saveRegisterStack(CpuRegister.Y, false)
|
||||
if(arrayIndexExpr.type.isWord) {
|
||||
@@ -3757,9 +3759,9 @@ $endLabel""")
|
||||
)
|
||||
} else throw AssemblyError("weird type $subtype")
|
||||
asmgen.out("""
|
||||
ldy #>$sourceName
|
||||
ldy #>$actualSourceName
|
||||
clc
|
||||
adc #<$sourceName
|
||||
adc #<$actualSourceName
|
||||
bcc +
|
||||
iny
|
||||
+""")
|
||||
@@ -3770,12 +3772,12 @@ $endLabel""")
|
||||
}
|
||||
|
||||
// address of a normal variable
|
||||
require(!msb)
|
||||
require(!actualMsb)
|
||||
when(target.kind) {
|
||||
TargetStorageKind.VARIABLE -> {
|
||||
asmgen.out("""
|
||||
lda #<$sourceName
|
||||
ldy #>$sourceName
|
||||
lda #<$actualSourceName
|
||||
ldy #>$actualSourceName
|
||||
sta ${target.asmVarname}
|
||||
sty ${target.asmVarname}+1""")
|
||||
}
|
||||
@@ -3783,25 +3785,25 @@ $endLabel""")
|
||||
throw AssemblyError("can't store word into memory byte")
|
||||
}
|
||||
TargetStorageKind.ARRAY -> {
|
||||
asmgen.out(" lda #<$sourceName | ldy #>$sourceName")
|
||||
asmgen.out(" lda #<$actualSourceName | ldy #>$actualSourceName")
|
||||
assignRegisterpairWord(target, RegisterOrPair.AY)
|
||||
}
|
||||
TargetStorageKind.REGISTER -> {
|
||||
when(target.register!!) {
|
||||
RegisterOrPair.AX -> asmgen.out(" ldx #>$sourceName | lda #<$sourceName")
|
||||
RegisterOrPair.AY -> asmgen.out(" ldy #>$sourceName | lda #<$sourceName")
|
||||
RegisterOrPair.XY -> asmgen.out(" ldy #>$sourceName | ldx #<$sourceName")
|
||||
RegisterOrPair.AX -> asmgen.out(" ldx #>$actualSourceName | lda #<$actualSourceName")
|
||||
RegisterOrPair.AY -> asmgen.out(" ldy #>$actualSourceName | lda #<$actualSourceName")
|
||||
RegisterOrPair.XY -> asmgen.out(" ldy #>$actualSourceName | ldx #<$actualSourceName")
|
||||
in Cx16VirtualRegisters -> {
|
||||
asmgen.out("""
|
||||
lda #<$sourceName
|
||||
ldy #>$sourceName
|
||||
lda #<$actualSourceName
|
||||
ldy #>$actualSourceName
|
||||
sta cx16.${target.register.toString().lowercase()}
|
||||
sty cx16.${target.register.toString().lowercase()}+1""")
|
||||
}
|
||||
else -> throw AssemblyError("can only load address into 16 bit register")
|
||||
}
|
||||
}
|
||||
TargetStorageKind.POINTER -> pointergen.assignAddressOf(PtrTarget(target), sourceName)
|
||||
TargetStorageKind.POINTER -> pointergen.assignAddressOf(PtrTarget(target), actualSourceName)
|
||||
TargetStorageKind.VOID -> { /* do nothing */ }
|
||||
}
|
||||
}
|
||||
|
||||
+136
-14
@@ -318,14 +318,13 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
|
||||
SourceStorageKind.REGISTER -> inplacemodificationLongWithVariable(targetVarName, operator, regName(value))
|
||||
SourceStorageKind.MEMORY -> TODO("inplace long modification with memread value ${target.position}")
|
||||
SourceStorageKind.ARRAY -> TODO("inplace long modification with array value ${target.position}")
|
||||
SourceStorageKind.EXPRESSION -> {
|
||||
SourceStorageKind.EXPRESSION ->
|
||||
if(value.expression is PtTypeCast) {
|
||||
if (tryInplaceModifyWithRemovedRedundantCast(value.expression, target, operator)) return
|
||||
TODO("inplace long modification ${target.position}")
|
||||
} else {
|
||||
TODO("inplace long modification ${target.position}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,24 +576,40 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
|
||||
internal fun inplacemodificationLongWithVariable(targetVar: String, operator: String, sourceVar: String) {
|
||||
when(operator) {
|
||||
"+" -> {
|
||||
// need to unroll to correctly keep Carry flag for multi-byte arithmetic
|
||||
asmgen.out("""
|
||||
clc
|
||||
ldx #252
|
||||
- lda $targetVar+4,x
|
||||
adc $sourceVar+4,x
|
||||
sta $targetVar+4,x
|
||||
inx
|
||||
bne -""")
|
||||
lda $targetVar+0
|
||||
adc $sourceVar+0
|
||||
sta $targetVar+0
|
||||
lda $targetVar+1
|
||||
adc $sourceVar+1
|
||||
sta $targetVar+1
|
||||
lda $targetVar+2
|
||||
adc $sourceVar+2
|
||||
sta $targetVar+2
|
||||
lda $targetVar+3
|
||||
adc $sourceVar+3
|
||||
sta $targetVar+3
|
||||
""")
|
||||
}
|
||||
"-" -> {
|
||||
// need to unroll to correctly keep Carry flag for multi-byte arithmetic
|
||||
asmgen.out("""
|
||||
sec
|
||||
ldx #252
|
||||
- lda $targetVar+4,x
|
||||
sbc $sourceVar+4,x
|
||||
sta $targetVar+4,x
|
||||
inx
|
||||
bne -""")
|
||||
lda $targetVar+0
|
||||
sbc $sourceVar+0
|
||||
sta $targetVar+0
|
||||
lda $targetVar+1
|
||||
sbc $sourceVar+1
|
||||
sta $targetVar+1
|
||||
lda $targetVar+2
|
||||
sbc $sourceVar+2
|
||||
sta $targetVar+2
|
||||
lda $targetVar+3
|
||||
sbc $sourceVar+3
|
||||
sta $targetVar+3
|
||||
""")
|
||||
}
|
||||
"<<" -> {
|
||||
asmgen.out("""
|
||||
@@ -667,6 +682,53 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
|
||||
dey
|
||||
bpl -""")
|
||||
}
|
||||
"/" -> {
|
||||
asmgen.out("""
|
||||
; Copy target to R12R13 (dividend)
|
||||
ldy #3
|
||||
- lda $targetVar,y
|
||||
sta cx16.r12,y
|
||||
dey
|
||||
bpl -
|
||||
; Copy source to R14R15 (divisor)
|
||||
ldy #3
|
||||
- lda $sourceVar,y
|
||||
sta cx16.r14,y
|
||||
dey
|
||||
bpl -
|
||||
jsr prog8_math.div_longs
|
||||
; Copy result from R14R15 back to target
|
||||
ldy #3
|
||||
- lda cx16.r14,y
|
||||
sta $targetVar,y
|
||||
dey
|
||||
bpl -""")
|
||||
}
|
||||
"%" -> {
|
||||
asmgen.out("""
|
||||
; Copy target to R12R13 (dividend)
|
||||
ldy #3
|
||||
- lda $targetVar,y
|
||||
sta cx16.r12,y
|
||||
dey
|
||||
bpl -
|
||||
; Copy source to R14R15 (divisor)
|
||||
ldy #3
|
||||
- lda $sourceVar,y
|
||||
sta cx16.r14,y
|
||||
dey
|
||||
bpl -
|
||||
jsr prog8_math.div_longs
|
||||
; Copy remainder from P8ZP_SCRATCH_W1/W2 back to target
|
||||
lda P8ZP_SCRATCH_W1
|
||||
sta $targetVar
|
||||
lda P8ZP_SCRATCH_W1+1
|
||||
sta $targetVar+1
|
||||
lda P8ZP_SCRATCH_W2
|
||||
sta $targetVar+2
|
||||
lda P8ZP_SCRATCH_W2+1
|
||||
sta $targetVar+3""")
|
||||
}
|
||||
else -> {
|
||||
TODO("in-place modify LONG with variable, operator=$operator")
|
||||
}
|
||||
@@ -1021,6 +1083,65 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
|
||||
dey
|
||||
bpl -""")
|
||||
}
|
||||
"/" -> {
|
||||
// long /= constant - use division routine
|
||||
val lo = value and 0xFFFF
|
||||
val hi = (value ushr 16) and 0xFFFF
|
||||
asmgen.out("""
|
||||
; Copy variable to R12R13 (dividend)
|
||||
ldy #3
|
||||
- lda $variable,y
|
||||
sta cx16.r12,y
|
||||
dey
|
||||
bpl -
|
||||
; Load constant into R14R15 (divisor)
|
||||
lda #<${lo}
|
||||
sta cx16.r14
|
||||
lda #>${lo}
|
||||
sta cx16.r14+1
|
||||
lda #<${hi}
|
||||
sta cx16.r15
|
||||
lda #>${hi}
|
||||
sta cx16.r15+1
|
||||
jsr prog8_math.div_longs
|
||||
; Copy result from R14R15 back to variable
|
||||
ldy #3
|
||||
- lda cx16.r14,y
|
||||
sta $variable,y
|
||||
dey
|
||||
bpl -""")
|
||||
}
|
||||
"%" -> {
|
||||
// long %= constant - use division routine, remainder is in P8ZP_SCRATCH_W1/W2
|
||||
val lo = value and 0xFFFF
|
||||
val hi = (value ushr 16) and 0xFFFF
|
||||
asmgen.out("""
|
||||
; Copy variable to R12R13 (dividend)
|
||||
ldy #3
|
||||
- lda $variable,y
|
||||
sta cx16.r12,y
|
||||
dey
|
||||
bpl -
|
||||
; Load constant into R14R15 (divisor)
|
||||
lda #<${lo}
|
||||
sta cx16.r14
|
||||
lda #>${lo}
|
||||
sta cx16.r14+1
|
||||
lda #<${hi}
|
||||
sta cx16.r15
|
||||
lda #>${hi}
|
||||
sta cx16.r15+1
|
||||
jsr prog8_math.div_longs
|
||||
; Copy remainder from P8ZP_SCRATCH_W1/W2 back to variable
|
||||
lda P8ZP_SCRATCH_W1
|
||||
sta $variable
|
||||
lda P8ZP_SCRATCH_W1+1
|
||||
sta $variable+1
|
||||
lda P8ZP_SCRATCH_W2
|
||||
sta $variable+2
|
||||
lda P8ZP_SCRATCH_W2+1
|
||||
sta $variable+3""")
|
||||
}
|
||||
"<<" -> if (value > 0) inplaceLongShiftLeft()
|
||||
">>" -> if (value > 0) inplaceLongShiftRight()
|
||||
"|" -> {
|
||||
@@ -3149,6 +3270,7 @@ $shortcutLabel:""")
|
||||
lda $name+1
|
||||
sta P8ZP_SCRATCH_W1+1
|
||||
txa
|
||||
oweroff
|
||||
jsr prog8_math.divmod_uw_asm
|
||||
lda P8ZP_SCRATCH_W2
|
||||
ldy P8ZP_SCRATCH_W2+1
|
||||
|
||||
@@ -33,10 +33,10 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
extsub.returns.zip(assignmentTargets).forEach { pair ->
|
||||
val returns = pair.first
|
||||
val target = pair.second as PtAssignTarget
|
||||
// consume the register even for void targets to keep indices aligned
|
||||
val isFpRegister = returns.register.registerOrPair in listOf(RegisterOrPair.FAC1, RegisterOrPair.FAC2)
|
||||
val regNumber = if(isFpRegister) fpRegs.removeAt(0) else cpuRegs.removeAt(0)
|
||||
if (!target.void) {
|
||||
// Determine if this return uses an FP register or CPU register
|
||||
val isFpRegister = returns.register.registerOrPair in listOf(RegisterOrPair.FAC1, RegisterOrPair.FAC2)
|
||||
val regNumber = if(isFpRegister) fpRegs.removeAt(0) else cpuRegs.removeAt(0)
|
||||
result += assignCpuRegister(returns, regNumber, target)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,6 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
is PtContainmentCheck -> translate(expr)
|
||||
is PtPointerDeref -> translate(expr)
|
||||
is PtConstant -> {
|
||||
val name = expr.name
|
||||
val slab = expr.memorySlab
|
||||
// Check if the constant has a numeric value or is a memory slab reference
|
||||
if(slab != null) {
|
||||
@@ -1176,8 +1175,22 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
address = address.address.toAddress(),
|
||||
fcallArgs = FunctionCallArgs(argRegisters, returnRegisters)
|
||||
)
|
||||
} else if(address.constbank!=null) {
|
||||
IRInstruction(
|
||||
Opcode.CALLFAR,
|
||||
address = address.address.toAddress(),
|
||||
immediate = address.constbank!!.toInt()
|
||||
)
|
||||
} else {
|
||||
val tr = translateExpression(address.varbank!!)
|
||||
require(tr.dt==IRDataType.BYTE)
|
||||
result += tr.chunks
|
||||
IRInstruction(
|
||||
Opcode.CALLFARVB,
|
||||
address = address.address.toAddress(),
|
||||
reg1 = tr.resultReg
|
||||
)
|
||||
}
|
||||
else TODO("extsub with banked address got called ${callTarget.name} ${fcall.position}")
|
||||
}
|
||||
addInstr(result, call, null)
|
||||
val resultRegs = returnRegisters.filter{it.dt!=IRDataType.FLOAT}.map{it.registerNum.value}
|
||||
|
||||
@@ -347,6 +347,12 @@ internal class ConstantIdentifierReplacer(
|
||||
override fun before(addressOf: AddressOf, parent: Node): Iterable<AstModification> {
|
||||
val constValue = addressOf.constValue(program)
|
||||
if(constValue!=null) {
|
||||
// don't replace &constVar (without array index); leave it for AstChecker to reject as invalid address-of.
|
||||
// But still allow &constArray[idx] (those are valid address-of operations).
|
||||
val target = addressOf.identifier?.targetVarDecl()
|
||||
if(target?.type==VarDeclType.CONST && addressOf.arrayIndex==null) {
|
||||
return noModifications
|
||||
}
|
||||
return listOf(AstReplaceNode(addressOf, constValue, parent))
|
||||
}
|
||||
return noModifications
|
||||
@@ -355,7 +361,7 @@ internal class ConstantIdentifierReplacer(
|
||||
override fun after(identifier: IdentifierReference, parent: Node): Iterable<AstModification> {
|
||||
// replace identifiers that refer to const value, with the value itself
|
||||
// if it's a simple type and if it's not a left hand side variable
|
||||
if (identifier.parent is AssignTarget || identifier.parent is Alias)
|
||||
if (identifier.parent is AssignTarget || identifier.parent is Alias || identifier.parent is AddressOf)
|
||||
return noModifications
|
||||
var forloop = identifier.parent as? ForLoop
|
||||
if (forloop == null)
|
||||
|
||||
@@ -12,7 +12,7 @@ import kotlin.math.floor
|
||||
import kotlin.math.log2
|
||||
|
||||
|
||||
class ExpressionSimplifier(private val program: Program, private val errors: IErrorReporter) : AstWalker() {
|
||||
class ExpressionSimplifier(private val program: Program, private val errors: IErrorReporter, private val options: CompilationOptions) : AstWalker() {
|
||||
override fun after(typecast: TypecastExpression, parent: Node): Iterable<AstModification> {
|
||||
val mods = mutableListOf<AstModification>()
|
||||
|
||||
@@ -177,7 +177,8 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
|
||||
}
|
||||
|
||||
// X <= Y-1 ---> X<Y , X >= Y+1 ---> X>Y
|
||||
if(leftDt.isInteger && rightDt.isInteger) {
|
||||
// Only for signed integers to avoid unsigned wraparound issues.
|
||||
if(leftDt.isSignedInteger && rightDt.isSignedInteger) {
|
||||
val rightExpr = expr.right as? BinaryExpression
|
||||
if(rightExpr!=null && rightExpr.right.constValue(program)?.number==1.0) {
|
||||
if (expr.operator == "<=" && rightExpr.operator == "-") {
|
||||
@@ -463,12 +464,12 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
|
||||
when {
|
||||
leftVal?.number==0.0 -> expr.right
|
||||
rightVal?.number==0.0 -> expr.left
|
||||
rightIDt.isBytes && rightVal?.number==255.0 -> NumericLiteral(BaseDataType.UBYTE, 255.0, rightVal.position)
|
||||
rightIDt.isWords && rightVal?.number==65535.0 -> NumericLiteral(BaseDataType.UWORD, 65535.0, rightVal.position)
|
||||
rightIDt.isLong && rightVal?.number==-1.0 -> NumericLiteral(BaseDataType.LONG, -1.0, rightVal.position)
|
||||
leftIDt.isBytes && leftVal?.number==255.0 -> NumericLiteral(BaseDataType.UBYTE, 255.0, leftVal.position)
|
||||
leftIDt.isWords && leftVal?.number==65535.0 -> NumericLiteral(BaseDataType.UWORD, 65535.0, leftVal.position)
|
||||
leftIDt.isLong && leftVal?.number==-1.0 -> NumericLiteral(BaseDataType.LONG, -1.0, leftVal.position)
|
||||
leftIDt.isBytes && rightIDt.isBytes && rightVal?.number==255.0 -> NumericLiteral(BaseDataType.UBYTE, 255.0, rightVal.position)
|
||||
leftIDt.isWords && rightIDt.isWords && rightVal?.number==65535.0 -> NumericLiteral(BaseDataType.UWORD, 65535.0, rightVal.position)
|
||||
leftIDt.isLong && rightIDt.isLong && rightVal?.number==-1.0 -> NumericLiteral(BaseDataType.LONG, -1.0, rightVal.position)
|
||||
leftIDt.isBytes && rightIDt.isBytes && leftVal?.number==255.0 -> NumericLiteral(BaseDataType.UBYTE, 255.0, leftVal.position)
|
||||
leftIDt.isWords && rightIDt.isWords && leftVal?.number==65535.0 -> NumericLiteral(BaseDataType.UWORD, 65535.0, leftVal.position)
|
||||
leftIDt.isLong && rightIDt.isLong && leftVal?.number==-1.0 -> NumericLiteral(BaseDataType.LONG, -1.0, leftVal.position)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -476,12 +477,12 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
|
||||
when {
|
||||
leftVal?.number==0.0 -> expr.right
|
||||
rightVal?.number==0.0 -> expr.left
|
||||
rightIDt.isBytes && rightVal?.number==255.0 -> PrefixExpression("~", expr.left, expr.left.position)
|
||||
rightIDt.isWords && rightVal?.number==65535.0 -> PrefixExpression("~", expr.left, expr.left.position)
|
||||
rightIDt.isLong && rightVal?.number==-1.0 -> PrefixExpression("~", expr.left, expr.left.position)
|
||||
leftIDt.isBytes && leftVal?.number==255.0 -> PrefixExpression("~", expr.right, expr.right.position)
|
||||
leftIDt.isWords && leftVal?.number==65535.0 -> PrefixExpression("~", expr.right, expr.right.position)
|
||||
leftIDt.isLong && leftVal?.number==-1.0 -> PrefixExpression("~", expr.right, expr.right.position)
|
||||
leftIDt.isBytes && rightIDt.isBytes && rightVal?.number==255.0 -> PrefixExpression("~", expr.left, expr.left.position)
|
||||
leftIDt.isWords && rightIDt.isWords && rightVal?.number==65535.0 -> PrefixExpression("~", expr.left, expr.left.position)
|
||||
leftIDt.isLong && rightIDt.isLong && rightVal?.number==-1.0 -> PrefixExpression("~", expr.left, expr.left.position)
|
||||
leftIDt.isBytes && rightIDt.isBytes && leftVal?.number==255.0 -> PrefixExpression("~", expr.right, expr.right.position)
|
||||
leftIDt.isWords && rightIDt.isWords && leftVal?.number==65535.0 -> PrefixExpression("~", expr.right, expr.right.position)
|
||||
leftIDt.isLong && rightIDt.isLong && leftVal?.number==-1.0 -> PrefixExpression("~", expr.right, expr.right.position)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -489,12 +490,12 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
|
||||
when {
|
||||
leftVal?.number==0.0 -> constFalse
|
||||
rightVal?.number==0.0 -> constFalse
|
||||
rightIDt.isBytes && rightVal?.number==255.0 -> expr.left
|
||||
rightIDt.isWords && rightVal?.number==65535.0 -> expr.left
|
||||
rightIDt.isLong && rightVal?.number==-1.0 -> expr.left
|
||||
leftIDt.isBytes && leftVal?.number==255.0 -> expr.right
|
||||
leftIDt.isWords && leftVal?.number==65535.0 -> expr.right
|
||||
leftIDt.isLong && leftVal?.number==-1.0 -> expr.right
|
||||
leftIDt.isBytes && rightIDt.isBytes && rightVal?.number==255.0 -> expr.left
|
||||
leftIDt.isWords && rightIDt.isWords && rightVal?.number==65535.0 -> expr.left
|
||||
leftIDt.isLong && rightIDt.isLong && rightVal?.number==-1.0 -> expr.left
|
||||
leftIDt.isBytes && rightIDt.isBytes && leftVal?.number==255.0 -> expr.right
|
||||
leftIDt.isWords && rightIDt.isWords && leftVal?.number==65535.0 -> expr.right
|
||||
leftIDt.isLong && rightIDt.isLong && leftVal?.number==-1.0 -> expr.right
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -679,23 +680,23 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
|
||||
return null
|
||||
// absorption laws: a or (a and b) --> a, a and (a or b) --> a
|
||||
if(expr.operator=="or" && rightB.operator=="and") {
|
||||
if(expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) {
|
||||
if((expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) && !expr.right.hasSideEffects(options.compTarget)) {
|
||||
return expr.left
|
||||
}
|
||||
}
|
||||
else if(expr.operator=="and" && rightB.operator=="or") {
|
||||
if(expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) {
|
||||
if((expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) && !expr.right.hasSideEffects(options.compTarget)) {
|
||||
return expr.left
|
||||
}
|
||||
}
|
||||
else if(expr.operator=="or" && rightB.operator=="or") {
|
||||
if(expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) {
|
||||
if((expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) && !expr.left.hasSideEffects(options.compTarget)) {
|
||||
// a or (a or b) -> a or b
|
||||
return expr.right
|
||||
}
|
||||
}
|
||||
else if(expr.operator=="and" && rightB.operator=="and") {
|
||||
if(expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) {
|
||||
if((expr.left isSameAs rightB.left || expr.left isSameAs rightB.right) && !expr.left.hasSideEffects(options.compTarget)) {
|
||||
// a and (a and b) -> a and b
|
||||
return expr.right
|
||||
}
|
||||
@@ -707,23 +708,23 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
|
||||
return null
|
||||
// absorption laws: (a and b) or a --> a, (a or b) and a --> a
|
||||
if(expr.operator=="or" && leftB.operator=="and") {
|
||||
if(expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) {
|
||||
if((expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) && !expr.left.hasSideEffects(options.compTarget)) {
|
||||
return expr.right
|
||||
}
|
||||
}
|
||||
else if(expr.operator=="and" && leftB.operator=="or") {
|
||||
if(expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) {
|
||||
if((expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) && !expr.left.hasSideEffects(options.compTarget)) {
|
||||
return expr.right
|
||||
}
|
||||
}
|
||||
else if(expr.operator=="or" && leftB.operator=="or") {
|
||||
if(expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) {
|
||||
if((expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) && !expr.right.hasSideEffects(options.compTarget)) {
|
||||
// (a or b) or a -> a or b
|
||||
return expr.left
|
||||
}
|
||||
}
|
||||
else if(expr.operator=="and" && leftB.operator=="and") {
|
||||
if(expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) {
|
||||
if((expr.right isSameAs leftB.left || expr.right isSameAs leftB.right) && !expr.right.hasSideEffects(options.compTarget)) {
|
||||
// (a and b) or a -> a and b
|
||||
return expr.left
|
||||
}
|
||||
@@ -844,7 +845,7 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
|
||||
}
|
||||
|
||||
private fun optimizeAdd(expr: BinaryExpression, leftVal: NumericLiteral?, rightVal: NumericLiteral?): Expression? {
|
||||
if(expr.left.isSameAs(expr.right)) {
|
||||
if(expr.left.isSameAs(expr.right) && !expr.left.hasSideEffects(options.compTarget)) {
|
||||
// optimize X+X into X *2
|
||||
expr.operator = "*"
|
||||
expr.right = NumericLiteral.optimalInteger(2, expr.right.position)
|
||||
@@ -879,7 +880,7 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
|
||||
}
|
||||
|
||||
private fun optimizeSub(expr: BinaryExpression, leftVal: NumericLiteral?, rightVal: NumericLiteral?): Expression? {
|
||||
if(expr.left.isSameAs(expr.right)) {
|
||||
if(expr.left.isSameAs(expr.right) && !expr.left.hasSideEffects(options.compTarget)) {
|
||||
// optimize X-X into 0
|
||||
return NumericLiteral.optimalInteger(0, expr.position)
|
||||
}
|
||||
@@ -924,12 +925,12 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
|
||||
val cv = rightVal?.number?.toInt()?.toDouble()
|
||||
when (expr.operator) {
|
||||
"%" -> {
|
||||
if (cv == 1.0) {
|
||||
if (cv == 1.0 && !expr.left.hasSideEffects(options.compTarget)) {
|
||||
val idt = expr.inferType(program)
|
||||
if(!idt.isKnown)
|
||||
throw FatalAstException("unknown dt")
|
||||
return NumericLiteral(idt.getOrUndef().base, 0.0, expr.position)
|
||||
} else if (cv in powersOfTwoFloat) {
|
||||
} else if (cv in powersOfTwoFloat && leftVal?.type?.isUnsignedInteger == true) {
|
||||
expr.operator = "&"
|
||||
expr.right = NumericLiteral.optimalInteger(cv!!.toInt()-1, expr.position)
|
||||
return null
|
||||
@@ -969,7 +970,7 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
|
||||
}
|
||||
256.0 -> {
|
||||
when(leftDt.base) {
|
||||
BaseDataType.UBYTE -> return NumericLiteral(BaseDataType.UBYTE, 0.0, expr.position)
|
||||
BaseDataType.UBYTE -> if (!expr.left.hasSideEffects(options.compTarget)) return NumericLiteral(BaseDataType.UBYTE, 0.0, expr.position) else return null
|
||||
BaseDataType.BYTE -> return null // is either 0 or -1 we cannot tell here
|
||||
BaseDataType.UWORD, BaseDataType.WORD -> {
|
||||
// just use: msb(value) as type
|
||||
@@ -992,11 +993,11 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
|
||||
}
|
||||
|
||||
if (leftDt.isUnsignedByte) {
|
||||
if (abs(rightConst.number) >= 256.0) {
|
||||
if (abs(rightConst.number) >= 256.0 && !expr.left.hasSideEffects(options.compTarget)) {
|
||||
return NumericLiteral(BaseDataType.UBYTE, 0.0, expr.position)
|
||||
}
|
||||
} else if (leftDt.isUnsignedWord) {
|
||||
if (abs(rightConst.number) >= 65536.0) {
|
||||
if (abs(rightConst.number) >= 65536.0 && !expr.left.hasSideEffects(options.compTarget)) {
|
||||
return NumericLiteral(BaseDataType.UBYTE, 0.0, expr.position)
|
||||
}
|
||||
}
|
||||
@@ -1006,8 +1007,9 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
|
||||
// left value is a constant, see if we can optimize
|
||||
when (leftVal.number) {
|
||||
0.0 -> {
|
||||
// 0
|
||||
return NumericLiteral(leftVal.type, 0.0, expr.position)
|
||||
// 0 (only if not float and no side effects)
|
||||
if (!expr.inferType(program).isFloat && !expr.right.hasSideEffects(options.compTarget))
|
||||
return NumericLiteral(leftVal.type, 0.0, expr.position)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1030,8 +1032,11 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
|
||||
return PrefixExpression("-", leftValue, expr.position)
|
||||
}
|
||||
0.0 -> {
|
||||
// 0
|
||||
return NumericLiteral(rightConst.type, 0.0, expr.position)
|
||||
// 0 (only if not float and no side effects)
|
||||
if (!expr.inferType(program).isFloat && !leftValue.hasSideEffects(options.compTarget))
|
||||
return NumericLiteral(rightConst.type, 0.0, expr.position)
|
||||
else
|
||||
return null
|
||||
}
|
||||
1.0 -> {
|
||||
// left
|
||||
|
||||
@@ -64,8 +64,8 @@ fun Program.inlineSubroutines(options: CompilationOptions): Int {
|
||||
return inliner.applyModifications()
|
||||
}
|
||||
|
||||
fun Program.simplifyExpressions(errors: IErrorReporter) : Int {
|
||||
val opti = ExpressionSimplifier(this, errors)
|
||||
fun Program.simplifyExpressions(errors: IErrorReporter, options: CompilationOptions) : Int {
|
||||
val opti = ExpressionSimplifier(this, errors, options)
|
||||
opti.visit(this)
|
||||
return opti.applyModifications()
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -14,7 +14,7 @@ txt {
|
||||
const ubyte DEFAULT_WIDTH = 40
|
||||
const ubyte DEFAULT_HEIGHT = 25
|
||||
|
||||
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly ofcourse. Note: takes a PETSCII encoded character.
|
||||
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly of course. Note: takes a PETSCII encoded character.
|
||||
|
||||
|
||||
sub bell() {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ txt {
|
||||
const ubyte DEFAULT_WIDTH = 40
|
||||
const ubyte DEFAULT_HEIGHT = 25
|
||||
|
||||
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly ofcourse. Note: takes a PETSCII encoded character.
|
||||
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly of course. Note: takes a PETSCII encoded character.
|
||||
|
||||
|
||||
sub bell() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -872,7 +872,7 @@ exit:
|
||||
}
|
||||
|
||||
sub get_loadaddress(str filename) -> uword {
|
||||
; get the load adress from a PRG file (usually $0801 but it can be different)
|
||||
; get the load address from a PRG file (usually $0801 but it can be different)
|
||||
|
||||
cbm.SETNAM(strings.length(filename), filename)
|
||||
cbm.SETLFS(READ_IO_CHANNEL, drivenumber, READ_IO_CHANNEL)
|
||||
|
||||
@@ -185,7 +185,7 @@ gfx_hires {
|
||||
|
||||
sub vertical_line(uword xx, uword yy, uword lheight, ubyte color) {
|
||||
; highres 4c
|
||||
; use TWO vera adress pointers simultaneously one for reading, one for writing, so auto-increment is possible
|
||||
; use TWO vera address pointers simultaneously one for reading, one for writing, so auto-increment is possible
|
||||
if lheight==0
|
||||
return
|
||||
position2(xx,yy,true)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
; optimized graphics routines for just the single screen mode: lores 320*240, 256c (8bpp)
|
||||
; bitmap image needs to start at VRAM addres $00000.
|
||||
; bitmap image needs to start at VRAM address $00000.
|
||||
; This is compatible with the CX16's screen mode 128.
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ gfx_lores {
|
||||
|
||||
sub clear_screen(ubyte color) {
|
||||
if verafx.available() {
|
||||
; use verafx cache writes to quicly clear the screen
|
||||
; use verafx cache writes to quickly clear the screen
|
||||
const ubyte vbank = 0
|
||||
const uword vaddr = 0
|
||||
cx16.VERA_CTRL = 0
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,26 @@ serial {
|
||||
const ubyte REG_DIVISOR_LATCH_LOW = 0 ;same as TXRX, but when bit-7 of line control high
|
||||
const ubyte REG_DIVISOR_LATCH_HI = 1 ;same as IRQE, but when bit-7 of line control high
|
||||
|
||||
; Baud rate divisor values for TL16C2550 with 14.7456 MHz clock
|
||||
; Formula: divisor = 14745600 / (16 * baudrate)
|
||||
enum BAUD {
|
||||
B921600 = $0001,
|
||||
B460800 = $0002,
|
||||
B230400 = $0004,
|
||||
B115200 = $0008,
|
||||
B57600 = $0010,
|
||||
B38400 = $0018,
|
||||
B28800 = $0020,
|
||||
B19200 = $0030,
|
||||
B14400 = $0040,
|
||||
B9600 = $0060,
|
||||
B4800 = $00C0,
|
||||
B2400 = $0180,
|
||||
B1200 = $0300,
|
||||
B600 = $0600,
|
||||
B300 = $0C00,
|
||||
}
|
||||
|
||||
; Bitfield masks for TL16C2550's Line Status Register
|
||||
enum LSR {
|
||||
DR = %00000001, ; Data Ready
|
||||
@@ -54,13 +74,12 @@ serial {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uart1, uart2
|
||||
|
||||
private sub probe(uword address) -> bool {
|
||||
sys.set_irqd()
|
||||
address[REG_INTERRUPT_ENABLE] = $ff
|
||||
bool found = address[REG_INTERRUPT_ENABLE] == $0f ; The high nybble of IER is always clear
|
||||
address[REG_INTERRUPT_ENABLE] = $f0
|
||||
bool found = address[REG_INTERRUPT_ENABLE] & $f0 == 0 ; The high nybble of IER is always clear
|
||||
address[REG_INTERRUPT_ENABLE] = 0
|
||||
if found {
|
||||
address[REG_MODEM_CONTROL] = $ff
|
||||
@@ -73,15 +92,14 @@ serial {
|
||||
}
|
||||
|
||||
sub initialize_uart(uword uart_addr) {
|
||||
; TODO baud rate is currently fixed at 921600, make this configurable
|
||||
; 921600 baud, 8,N,1, AutoFlow Control, FIFOS, no interrupts
|
||||
uart_addr[REG_INTERRUPT_ENABLE] = $00 ; No Interrupts
|
||||
; 921600 baud, 8,N,1, AutoFlow Control, FIFOS, no interrupts
|
||||
uart_addr[REG_LINE_CONTROL] = $80 ; Set DLAB
|
||||
uart_addr[REG_DIVISOR_LATCH_HI] = $00
|
||||
uart_addr[REG_DIVISOR_LATCH_LOW] = $01 ; $0001 = 921600
|
||||
uart_addr[REG_LINE_CONTROL] = $03 ; 8,N,1
|
||||
uart_addr[REG_FIFO_CONTROL] = $C7 ; FIFO enable & reset
|
||||
uart_addr[REG_MODEM_CONTROL] = $23 ; DTR/RTS & AutoFlow Control
|
||||
uart_addr[REG_DIVISOR_LATCH_HI] = msb(BAUD::B921600)
|
||||
uart_addr[REG_DIVISOR_LATCH_LOW] = lsb(BAUD::B921600)
|
||||
uart_addr[REG_LINE_CONTROL] = $03 ; 8,N,1, DLAB off
|
||||
uart_addr[REG_FIFO_CONTROL] = $C3 ; FIFO enable & reset, trigger 14
|
||||
uart_addr[REG_MODEM_CONTROL] = $23 ; DTR/RTS + AutoFlow Control
|
||||
uart_addr[REG_INTERRUPT_ENABLE] = $00 ; No interrupts
|
||||
}
|
||||
|
||||
sub write(uword @zp uart_addr, str data) {
|
||||
@@ -98,7 +116,8 @@ serial {
|
||||
}
|
||||
|
||||
sub read_until(uword @zp uart_addr, str match, ^^ubyte buffer, uword max_size) -> uword {
|
||||
; Reads up to and including the match string from the uart, into buffer.
|
||||
; Reads bytes from the uart into buffer until the match string is found.
|
||||
; The match string itself is also included in the buffer.
|
||||
; Stops when the match string is found or max_size bytes have been read.
|
||||
; Returns the number of bytes read.
|
||||
|
||||
@@ -119,6 +138,7 @@ serial {
|
||||
}
|
||||
@(buffer_ptr) = data = @(uart_addr)
|
||||
buffer_ptr++
|
||||
read_size++
|
||||
if data == @(match_ptr) {
|
||||
match_ptr++
|
||||
if @(match_ptr)==0
|
||||
@@ -131,96 +151,9 @@ serial {
|
||||
return read_size
|
||||
}
|
||||
|
||||
; zimodem routines:
|
||||
|
||||
sub zi_initialize(uword uart_addr) {
|
||||
; initializes the ZiModem on the given uart address
|
||||
zi_uart = uart_addr
|
||||
if zi_uart == 0
|
||||
return ; no uart present
|
||||
|
||||
initialize_uart(zi_uart)
|
||||
; ZiModem sends a version banner of the `ati` command when the ESP32 boots up.
|
||||
; Read it off if present. It is not always ready immediately so wait a tiny bit
|
||||
sys.wait(10)
|
||||
zi_write_cmd(iso:"\x03ate0") ; first send CTRL-C to abort any previous file streams
|
||||
sys.wait(5)
|
||||
if zi_uart[REG_LINE_STATUS] & LSR::DR != 0
|
||||
discard_until(zi_uart, iso:"OK\x0d\x0a")
|
||||
zi_write_cmd("atq0v1x1f0r1s45=0s40=4096&p0b921600")
|
||||
discard_until(zi_uart, iso:"OK\x0d\x0a")
|
||||
}
|
||||
|
||||
sub zi_reset() {
|
||||
; reset all zimodem connections
|
||||
serial.zi_write_cmd(iso:"atz")
|
||||
discard_until(zi_uart, iso:"OK\x0d\x0a")
|
||||
}
|
||||
|
||||
sub zi_write_cmd(str command) {
|
||||
; writes the zimodem command (followed by a proper line ending)
|
||||
write(zi_uart, command)
|
||||
write_eol(zi_uart)
|
||||
}
|
||||
|
||||
sub zi_start_get_file(str filename) -> long {
|
||||
; starts a file download from the given url, returns the size of the file to download.
|
||||
; you can download chunks of the file by repeatedly calling zi_get_file_chunk() until that returns 0
|
||||
write(zi_uart, iso:"at&g\"")
|
||||
write(zi_uart, filename)
|
||||
write(zi_uart, "\"")
|
||||
write_eol(zi_uart)
|
||||
|
||||
if read_one(zi_uart)=='[' {
|
||||
; parse header [ 0 <filesize> <sum> ]
|
||||
ubyte[25] header_buf
|
||||
void read_until(zi_uart, iso:"]\x0d\x0a", &header_buf, sizeof(header_buf))
|
||||
return conv.str2long(&header_buf + 3)
|
||||
} else {
|
||||
discard_until(zi_uart, iso:"RROR\x0d\x0a")
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
sub zi_get_file_chunk(^^ubyte @zp buffer, uword buffer_size, long remaining_file_size) -> uword {
|
||||
; read the next chunk of data from the file, into buffer, up to buffer_size bytes.
|
||||
; returns the number of bytes read. 0 if no more data was available.
|
||||
|
||||
uword bytes_to_read = min(remaining_file_size, buffer_size) as uword
|
||||
if bytes_to_read == 0
|
||||
return 0
|
||||
|
||||
repeat bytes_to_read {
|
||||
while zi_uart[REG_LINE_STATUS] & LSR::DR == 0 {
|
||||
; wait until data is present
|
||||
}
|
||||
@(buffer) = @(zi_uart)
|
||||
buffer++
|
||||
}
|
||||
|
||||
return bytes_to_read
|
||||
}
|
||||
|
||||
sub zi_get_ip_address() -> str {
|
||||
; returns the ip address of the zimodem wifi connection
|
||||
ubyte[30] ip_buffer
|
||||
zi_write_cmd(iso:"ati2")
|
||||
void read_until(zi_uart, iso:"OK\x0d\x0a", &ip_buffer, len(ip_buffer))
|
||||
cx16.r0 = &ip_buffer
|
||||
while not strings.isspace(@(cx16.r0)) {
|
||||
cx16.r0++
|
||||
}
|
||||
@(cx16.r0) = 0
|
||||
return &ip_buffer
|
||||
}
|
||||
|
||||
|
||||
; private stuff
|
||||
|
||||
private uword @zp zi_uart ; the uart address to use for ZiModem
|
||||
|
||||
private sub discard_until(uword @zp uart_addr, str match) {
|
||||
; Reads up to and including the match string from the uart, discards all data.
|
||||
sub discard_until(uword @zp uart_addr, str match) {
|
||||
; Reads bytes from the uart until the match string is found, discards all data.
|
||||
; The match string itself is also discarded.
|
||||
|
||||
alias match_ptr = cx16.r0
|
||||
match_ptr = match
|
||||
@@ -239,6 +172,163 @@ serial {
|
||||
}
|
||||
}
|
||||
|
||||
; zimodem routines:
|
||||
|
||||
sub zi_initialize(uword uart_addr) {
|
||||
; initializes the ZiModem on the given uart address
|
||||
zi_uart = uart_addr
|
||||
if zi_uart == 0
|
||||
return ; no uart present
|
||||
|
||||
initialize_uart(zi_uart)
|
||||
; ZiModem sends a version banner of the `ati` command when the ESP32 boots up.
|
||||
; Read it off if present. It is not always ready immediately so wait a tiny bit
|
||||
sys.wait(5)
|
||||
write(zi_uart, "\x03") ; first send CTRL-C (+ no echo) to abort any previous file streams
|
||||
sys.wait(10)
|
||||
zi_write_cmd(iso:"ate0q0v1x1f0r1s45=3&p0&k3")
|
||||
discard_until(zi_uart, iso:"OK\x0d\x0a")
|
||||
}
|
||||
|
||||
sub zi_reset() {
|
||||
; reset all zimodem connections
|
||||
write(zi_uart, "\x03") ; first send CTRL-C (+ no echo) to abort any previous file streams
|
||||
sys.wait(10)
|
||||
serial.zi_write_cmd(iso:"atz")
|
||||
discard_until(zi_uart, iso:"OK\x0d\x0a")
|
||||
}
|
||||
|
||||
sub zi_write_cmd(str command) {
|
||||
; writes the zimodem command (followed by a proper line ending)
|
||||
write(zi_uart, command)
|
||||
write_eol(zi_uart)
|
||||
}
|
||||
|
||||
sub zi_start_get_file_hexmode(str filename) -> bool {
|
||||
; starts a file download from the given url. Returns true if ok, false if file could not be found
|
||||
; you can download chunks of the file by repeatedly calling zi_get_file_chunk_hexmode() until that returns 0.
|
||||
; Note: this uses zimodem hex mode transfer for now because binary mode transfer is broken at the moment.
|
||||
zi_write_cmd(iso:"ats45=1") ; enable hex mode transfer (unfortunately this also forces caching mode on the esp32)
|
||||
discard_until(zi_uart, iso:"OK\x0d\x0a")
|
||||
write(zi_uart, iso:"at&g\"")
|
||||
write(zi_uart, filename)
|
||||
write(zi_uart, "\"\x0d\x0a")
|
||||
|
||||
if read_one(zi_uart)=='[' {
|
||||
; note: zimodem file size in hex mode is bogus (only can go up to $FFFF), just skip the whole header line.
|
||||
discard_until(zi_uart, "\x0d\x0a")
|
||||
return true
|
||||
} else {
|
||||
discard_until(zi_uart, iso:"RROR\x0d\x0a")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
sub zi_start_get_file(str filename) -> long {
|
||||
; TODO: binary file transfer seems unreliable at the moment (text files are still ok, but often freezes on binary files. Could be a problem with my particular X16 serial card or zimodem firmware though), use hex mode transfer for reliable results.
|
||||
; starts a file download from the given url, returns the size of the file to download.
|
||||
; you can download chunks of the file by repeatedly calling zi_get_file_chunk() until that returns 0
|
||||
write(zi_uart, iso:"at&g\"")
|
||||
write(zi_uart, filename)
|
||||
write(zi_uart, "\"\x0d\x0a")
|
||||
|
||||
if read_one(zi_uart)=='[' {
|
||||
; parse header [ 0 <filesize> <sum> ]
|
||||
ubyte[25] header_buf
|
||||
void read_until(zi_uart, iso:"]\x0d\x0a", &header_buf, sizeof(header_buf))
|
||||
return conv.str2long(&header_buf + 3)
|
||||
} else {
|
||||
discard_until(zi_uart, iso:"RROR\x0d\x0a")
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
sub zi_get_file_chunk_hexmode(^^ubyte @zp buffer, uword buffer_size) -> uword {
|
||||
; read the next chunk of data from the file (opened in hex mode), into buffer, up to buffer_size bytes (minimum 40 bytes!!)
|
||||
; returns the number of bytes read. 0 if no more data was available.
|
||||
ubyte[90] linebuffer
|
||||
cx16.r0 = read_until(zi_uart, "\x0d\x0a", linebuffer, sizeof(linebuffer))
|
||||
if cx16.r0 == 4 and linebuffer[0] == iso:'O' ; OK\r\n was received -- end of transmission
|
||||
return 0
|
||||
|
||||
ubyte digits = lsb(cx16.r0)-2
|
||||
decode(digits)
|
||||
return digits/2
|
||||
|
||||
private sub decode(ubyte numdigits) {
|
||||
cx16.r1L = 0
|
||||
for cx16.r0L in 0 to numdigits-1 step 2 {
|
||||
buffer[cx16.r1L] = decodehex(cx16.r0L)
|
||||
cx16.r1L++
|
||||
}
|
||||
|
||||
asmsub decodehex(ubyte digitindex @Y) -> ubyte @A {
|
||||
%asm {{
|
||||
lda p8v_linebuffer,y
|
||||
sec
|
||||
sbc #$30
|
||||
cmp #$0a
|
||||
bmi +
|
||||
sbc #$07
|
||||
+ asl a
|
||||
asl a
|
||||
asl a
|
||||
asl a
|
||||
sta P8ZP_SCRATCH_REG
|
||||
lda p8v_linebuffer+1,y
|
||||
sec
|
||||
sbc #$30
|
||||
cmp #$0a
|
||||
bmi +
|
||||
sbc #$07
|
||||
+ ora P8ZP_SCRATCH_REG
|
||||
rts
|
||||
}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub zi_get_file_chunk(^^ubyte @zp buffer, uword buffer_size, long remaining_file_size) -> uword {
|
||||
; TODO: binary file transfer seems unreliable at the moment (text files are still ok, but often freezes on binary files. Could be a problem with my particular X16 serial card or zimodem firmware though), use hex mode transfer for reliable results.
|
||||
; read the next chunk of data from the file, into buffer, up to buffer_size bytes.
|
||||
; returns the number of bytes read. 0 if no more data was available.
|
||||
|
||||
uword bytes_to_read = if msw(remaining_file_size)!=0 then buffer_size else min(lsw(remaining_file_size), buffer_size)
|
||||
|
||||
repeat bytes_to_read {
|
||||
while zi_uart[REG_LINE_STATUS] & LSR::DR == 0 {
|
||||
; wait until data is present
|
||||
}
|
||||
@(buffer) = @(zi_uart)
|
||||
buffer++
|
||||
}
|
||||
|
||||
return bytes_to_read
|
||||
}
|
||||
|
||||
sub zi_end_get_file() {
|
||||
; makes sure the 'OK' response after the actual file data is also read away
|
||||
discard_until(zi_uart, iso:"OK\x0d\x0a")
|
||||
}
|
||||
|
||||
sub zi_get_ip_address() -> str {
|
||||
; returns the ip address of the zimodem wifi connection. Assumes IPV4 only.
|
||||
ubyte[25] ip_buffer
|
||||
zi_write_cmd(iso:"ati2")
|
||||
void read_until(zi_uart, iso:"OK\x0d\x0a", &ip_buffer, len(ip_buffer))
|
||||
cx16.r0 = &ip_buffer
|
||||
while not strings.isspace(@(cx16.r0)) {
|
||||
cx16.r0++
|
||||
}
|
||||
@(cx16.r0) = 0
|
||||
return &ip_buffer
|
||||
}
|
||||
|
||||
|
||||
; private stuff
|
||||
|
||||
private uword @zp zi_uart ; the uart address to use for ZiModem
|
||||
|
||||
private sub read_one(uword @zp uart_addr) -> ubyte {
|
||||
while uart_addr[REG_LINE_STATUS] & LSR::DR == 0 {
|
||||
; wait until data is present
|
||||
|
||||
@@ -14,7 +14,7 @@ const ubyte DEFAULT_WIDTH = 80
|
||||
const ubyte DEFAULT_HEIGHT = 60
|
||||
const long VERA_TEXTMATRIX = $1b000
|
||||
|
||||
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly ofcourse. Note: takes a PETSCII encoded character.
|
||||
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly of course. Note: takes a PETSCII encoded character.
|
||||
|
||||
|
||||
sub bell() {
|
||||
|
||||
@@ -434,20 +434,21 @@ divmod_w_asm .proc
|
||||
divmod_uw_asm .proc
|
||||
; -- divide two unsigned words (16 bit each) into 16 bit results
|
||||
; input: P8ZP_SCRATCH_W1 in ZP: 16 bit number, A/Y: 16 bit divisor
|
||||
; output: cx16.r15: 16 bit remainder, A/Y: 16 bit division result
|
||||
; output: P8ZP_SCRATCH_W2: 16 bit remainder, A/Y: 16 bit division result
|
||||
; division by zero will result in quotient = 65535 and remainder = 0
|
||||
; NOTE: does not clobber any cx16.rX registers.
|
||||
|
||||
dividend = P8ZP_SCRATCH_W1
|
||||
remainder = P8ZP_SCRATCH_W2
|
||||
result = dividend ;save memory by reusing divident to store the result
|
||||
result = dividend ;save memory by reusing dividend to store the result
|
||||
|
||||
cpy #0
|
||||
bne _nonzero
|
||||
ora #0
|
||||
bne _nonzero
|
||||
lda #0 ; remainder = 0
|
||||
sta cx16.r15L
|
||||
sta cx16.r15H
|
||||
sta remainder
|
||||
sta remainder+1
|
||||
lda #$ff ; quotient = 65535 (A/Y)
|
||||
tay
|
||||
rts
|
||||
@@ -478,10 +479,6 @@ _nonzero
|
||||
+ dex
|
||||
bne -
|
||||
|
||||
lda P8ZP_SCRATCH_W2
|
||||
ldy P8ZP_SCRATCH_W2+1
|
||||
sta cx16.r15L
|
||||
sty cx16.r15H
|
||||
lda result
|
||||
ldy result+1
|
||||
rts
|
||||
|
||||
@@ -105,6 +105,7 @@ _sinecosR8 .char trunc(127.0 * sin(range(180+45) * rad(360.0/180.0)))
|
||||
; -- return random number uniformly distributed from 0 to n-1
|
||||
; NOTE: does not work for code in ROM, use randrange_rom instead for that
|
||||
; why this works: https://www.youtube.com/watch?v=3DvlLUWTNMY&t=347s
|
||||
; Uses cx16.r0 as temporary
|
||||
cx16.r0 = math.rnd() * (n as uword)
|
||||
return cx16.r0H
|
||||
}
|
||||
@@ -113,6 +114,7 @@ _sinecosR8 .char trunc(127.0 * sin(range(180+45) * rad(360.0/180.0)))
|
||||
; -- return random number uniformly distributed from 0 to n-1
|
||||
; NOTE: works for code in ROM, make sure you have initialized the seed using rndseed_rom
|
||||
; why this works: https://www.youtube.com/watch?v=3DvlLUWTNMY&t=347s
|
||||
; Uses cx16.r0 as temporary
|
||||
cx16.r0 = math.rnd_rom() * (n as uword)
|
||||
return cx16.r0H
|
||||
}
|
||||
@@ -177,6 +179,7 @@ _sinecosR8 .char trunc(127.0 * sin(range(180+45) * rad(360.0/180.0)))
|
||||
}
|
||||
|
||||
asmsub log2w(uword value @AY) -> ubyte @Y {
|
||||
; Clobbers cx16.r0
|
||||
%asm {{
|
||||
sta P8ZP_SCRATCH_W1
|
||||
sty P8ZP_SCRATCH_W1+1
|
||||
@@ -212,7 +215,7 @@ _sinecosR8 .char trunc(127.0 * sin(range(180+45) * rad(360.0/180.0)))
|
||||
; some simpler multiplications will be optimized away into faster routines. These will not set the upper 16 bits at all!
|
||||
; - THE RESULT IS ONLY VALID IF THE MULTIPLICATION WAS DONE WITH UWORD ARGUMENTS (or two positive WORD arguments)
|
||||
; as soon as a negative word value (or 2) was used in the multiplication, these upper 16 bits are not valid!!
|
||||
; Suggestion (if you are on the Commander X16): use verafx.muls() to get a hardware accelerated 32 bit signed multplication.
|
||||
; Suggestion (if you are on the Commander X16): use verafx.muls() to get a hardware accelerated 32 bit signed multiplication.
|
||||
%asm {{
|
||||
lda prog8_math.multiply_words.result+2
|
||||
ldy prog8_math.multiply_words.result+3
|
||||
@@ -267,6 +270,7 @@ asmsub direction_qd(ubyte quadrant @A, ubyte xdelta @X, ubyte ydelta @Y) -> ubyt
|
||||
; .reg:x @in x_delta Delta for x direction.
|
||||
; .reg:y @in y_delta Delta for y direction.
|
||||
; Returns A as the direction (0-23).
|
||||
; Clobbers cx16.r0 through cx16.r5 (used as temporary variables).
|
||||
|
||||
%asm {{
|
||||
x_delta = cx16.r0L
|
||||
@@ -385,9 +389,10 @@ _quadrant_region_to_direction:
|
||||
|
||||
asmsub atan2(ubyte x1 @R0, ubyte y1 @R1, ubyte x2 @R2, ubyte y2 @R3) -> ubyte @A {
|
||||
;; Calculate the angle, in a 256-degree circle, between two points into A.
|
||||
;; The points (x1, y1) and (x2, y2) have to use *unsigned coordinates only* from the positive quadrant in the carthesian plane!
|
||||
;; The points (x1, y1) and (x2, y2) have to use *unsigned coordinates only* from the positive quadrant in the cartesian plane!
|
||||
;; http://codebase64.net/doku.php?id=base:8bit_atan2_8-bit_angle
|
||||
;; This uses 2 large lookup tables so uses a lot of memory but is super fast.
|
||||
;; Clobbers cx16.r0 through cx16.r4 (r0-r3 are input params, r4 is temporary).
|
||||
|
||||
%asm {{
|
||||
|
||||
@@ -531,6 +536,7 @@ log2_tab
|
||||
|
||||
asmsub diffw(uword w1 @R0, uword w2 @AY) -> uword @AY {
|
||||
; -- returns the (absolute) difference, or distance, between the two words
|
||||
; clobbers cx16.r0 (used as input and temporary)
|
||||
%asm {{
|
||||
sec
|
||||
sbc cx16.r0L
|
||||
|
||||
@@ -416,7 +416,7 @@ exit:
|
||||
}
|
||||
|
||||
sub get_loadaddress(str filename) -> uword {
|
||||
; get the load adress from a PRG file (usually $0801 but it can be different)
|
||||
; get the load address from a PRG file (usually $0801 but it can be different)
|
||||
|
||||
cbm.SETNAM(strings.length(filename), filename)
|
||||
cbm.SETLFS(READ_IO_CHANNEL, drivenumber, 0)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,7 +12,7 @@ txt {
|
||||
const ubyte DEFAULT_WIDTH = 40
|
||||
const ubyte DEFAULT_HEIGHT = 25
|
||||
|
||||
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly ofcourse. Note: takes a PETSCII encoded character.
|
||||
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly of course. Note: takes a PETSCII encoded character.
|
||||
|
||||
|
||||
sub bell() {
|
||||
|
||||
@@ -482,7 +482,7 @@ from_scratchW1
|
||||
.pend
|
||||
|
||||
func_peekl .proc
|
||||
; -- read the ;pmg value on the address in AY, into R14:R15
|
||||
; -- read the long value on the address in AY, into R14:R15
|
||||
sta P8ZP_SCRATCH_W1
|
||||
sty P8ZP_SCRATCH_W1+1
|
||||
from_scratchW1
|
||||
|
||||
@@ -716,7 +716,7 @@ exit:
|
||||
}
|
||||
|
||||
sub get_loadaddress(str filename) -> uword {
|
||||
; get the load adress from a PRG file (usually $0801 but it can be different)
|
||||
; get the load address from a PRG file (usually $0801 but it can be different)
|
||||
|
||||
cbm.SETNAM(strings.length(filename), filename)
|
||||
cbm.SETLFS(READ_IO_CHANNEL, drivenumber, READ_IO_CHANNEL)
|
||||
|
||||
@@ -293,7 +293,7 @@ sub interpolate(float v, float inputMin, float inputMax, float outputMin, float
|
||||
}
|
||||
|
||||
asmsub internal_long_AY_to_FAC() {
|
||||
; Used by the compiler to cast a long to a a float in FAC
|
||||
; Used by the compiler to cast a long to a float in FAC
|
||||
; convert the long pointed to by AY into a float and store it in FAC
|
||||
%asm {{
|
||||
sta cx16.r1L
|
||||
@@ -308,7 +308,7 @@ sub interpolate(float v, float inputMin, float inputMax, float outputMin, float
|
||||
}
|
||||
|
||||
private sub internal_long_R1_to_float_AY() {
|
||||
; Used by the compiler to cast a long to a a float variable:
|
||||
; Used by the compiler to cast a long to a float variable:
|
||||
; make the float pointed to by AY equal to the long pointed to by R1 (as float)
|
||||
%asm {{
|
||||
sta cx16.r0L
|
||||
|
||||
@@ -124,14 +124,13 @@ strings {
|
||||
|
||||
sub startswith(str st, str prefix) -> bool {
|
||||
ubyte prefix_len = length(prefix)
|
||||
ubyte str_len = length(st)
|
||||
if prefix_len > str_len
|
||||
if prefix_len > length(st)
|
||||
return false
|
||||
cx16.r9L = st[prefix_len]
|
||||
push(st[prefix_len])
|
||||
st[prefix_len] = 0
|
||||
cx16.r9H = compare(st, prefix) as ubyte
|
||||
st[prefix_len] = cx16.r9L
|
||||
return cx16.r9H==0
|
||||
bool result = compare(st, prefix) == 0
|
||||
st[prefix_len] = pop()
|
||||
return result
|
||||
}
|
||||
|
||||
sub endswith(str st, str suffix) -> bool {
|
||||
|
||||
@@ -44,7 +44,7 @@ _done
|
||||
}
|
||||
|
||||
sub gnomesort_by_ub(^^ubyte @requirezp ub_keys, ^^uword wordvalues, ubyte num_elements) {
|
||||
; sorts the 'wordvalues' array (no-split array of words) according to the 'ub_keys' array (which also gets sorted ofcourse).
|
||||
; sorts the 'wordvalues' array (no-split array of words) according to the 'ub_keys' array (which also gets sorted of course).
|
||||
ubyte @zp pos=1
|
||||
while pos != num_elements {
|
||||
if ub_keys[pos]>=ub_keys[pos-1]
|
||||
@@ -88,7 +88,7 @@ _done
|
||||
}
|
||||
|
||||
sub gnomesort_by_uw(uword @requirezp uw_keys, uword wordvalues, ubyte num_elements) {
|
||||
; Sorts the 'wordvalues' array according to the 'uw_keys' array (which also gets sorted ofcourse).
|
||||
; Sorts the 'wordvalues' array according to the 'uw_keys' array (which also gets sorted of course).
|
||||
; both arrays should be no-split array of words. uw_keys are unsigned.
|
||||
; Max number of elements is 128 to keep indexing code size small and fast. Clobbers R0 and R1.
|
||||
ubyte @zp pos=2
|
||||
@@ -161,7 +161,7 @@ _done
|
||||
}
|
||||
|
||||
sub shellsort_by_ub(^^ubyte @requirezp ub_keys, ^^uword @requirezp wordvalues, ubyte num_elements) {
|
||||
; sorts the 'wordvalues' array (no-split array of words) according to the 'ub_keys' array (which also gets sorted ofcourse).
|
||||
; sorts the 'wordvalues' array (no-split array of words) according to the 'ub_keys' array (which also gets sorted of course).
|
||||
num_elements--
|
||||
ubyte @zp gap
|
||||
for gap in [132, 57, 23, 10, 4, 1] {
|
||||
@@ -187,7 +187,7 @@ _done
|
||||
}
|
||||
|
||||
sub shellsort_by_uw(^^uword @requirezp uw_keys, ^^uword @requirezp wordvalues, ubyte num_elements) {
|
||||
; sorts the 'wordvalues' array according to the 'uw_keys' array (which also gets sorted ofcourse).
|
||||
; sorts the 'wordvalues' array according to the 'uw_keys' array (which also gets sorted of course).
|
||||
; both arrays should be no-split array of words. uw_keys are unsigned.
|
||||
num_elements--
|
||||
ubyte gap
|
||||
|
||||
@@ -7,7 +7,7 @@ strings {
|
||||
|
||||
asmsub length(str string @AY) clobbers(A) -> ubyte @Y {
|
||||
; Returns the number of bytes in the string.
|
||||
; This value is determined during runtime and counts upto the first terminating 0 byte in the string,
|
||||
; This value is determined during runtime and counts up to the first terminating 0 byte in the string,
|
||||
; regardless of the size of the string during compilation time. Don’t confuse this with len and sizeof!
|
||||
|
||||
; uses P8ZP_SCRATCH_W1 to store the string address, do not change this, other routines here may depend on it (as optimization implementation detail)
|
||||
@@ -29,7 +29,7 @@ strings {
|
||||
; Also, you have to make sure yourself that length is smaller or equal to the length of the source string.
|
||||
; Modifies in-place, doesn’t return a value (so can’t be used in an expression).
|
||||
%asm {{
|
||||
; need to copy the the cx16 virtual registers to zeropage to be compatible with C64...
|
||||
; need to copy the cx16 virtual registers to zeropage to be compatible with C64...
|
||||
sta P8ZP_SCRATCH_W1
|
||||
stx P8ZP_SCRATCH_W1+1
|
||||
lda cx16.r1
|
||||
@@ -56,7 +56,7 @@ _loop dey
|
||||
; Also, you have to make sure yourself that length is smaller or equal to the length of the source string.
|
||||
; Modifies in-place, doesn’t return a value (so can’t be used in an expression).
|
||||
%asm {{
|
||||
; need to copy the the cx16 virtual registers to zeropage to be compatible with C64...
|
||||
; need to copy the cx16 virtual registers to zeropage to be compatible with C64...
|
||||
stx P8ZP_SCRATCH_B1
|
||||
sta cx16.r0
|
||||
sty cx16.r0+1
|
||||
@@ -96,7 +96,7 @@ _loop dey
|
||||
; Also, you have to make sure yourself that start and length are within bounds of the strings.
|
||||
; Modifies in-place, doesn’t return a value (so can’t be used in an expression).
|
||||
%asm {{
|
||||
; need to copy the the cx16 virtual registers to zeropage to be compatible with C64...
|
||||
; need to copy the cx16 virtual registers to zeropage to be compatible with C64...
|
||||
; substr(source, target, start, length)
|
||||
sta P8ZP_SCRATCH_B1
|
||||
lda cx16.r0
|
||||
@@ -131,7 +131,7 @@ _startloop dey
|
||||
; Locates the first position of the given character in the string,
|
||||
; returns Carry set if found + index in A, or Carry clear if not found (and A will be 255, an invalid index).
|
||||
%asm {{
|
||||
; need to copy the the cx16 virtual registers to zeropage to make this run on C64...
|
||||
; need to copy the cx16 virtual registers to zeropage to make this run on C64...
|
||||
sta P8ZP_SCRATCH_W1
|
||||
sty P8ZP_SCRATCH_W1+1
|
||||
stx P8ZP_SCRATCH_B1
|
||||
@@ -157,7 +157,7 @@ _found tya
|
||||
; This is a convenience function that looks for both a CR or LF (byte 13 or byte 10) as being a possible Line Ending.
|
||||
; returns Carry set if found + index in A, or Carry clear if not found (and A will be 255, an invalid index).
|
||||
%asm {{
|
||||
; need to copy the the cx16 virtual registers to zeropage to make this run on C64...
|
||||
; need to copy the cx16 virtual registers to zeropage to make this run on C64...
|
||||
sta P8ZP_SCRATCH_W1
|
||||
sty P8ZP_SCRATCH_W1+1
|
||||
ldy #0
|
||||
@@ -297,7 +297,7 @@ _max_too_small
|
||||
asmsub compare(str string1 @R0, str string2 @AY) clobbers(Y) -> byte @A {
|
||||
; Compares two strings for sorting, case-sensitively.
|
||||
; Returns -1 (255), 0 or 1, meaning: string1 sorts before, equal or after string2.
|
||||
; Note that you can also directly compare strings and string values with eachother using
|
||||
; Note that you can also directly compare strings and string values with each other using
|
||||
; comparison operators ==, < etcetera (this will use strcmp automatically).
|
||||
%asm {{
|
||||
sta P8ZP_SCRATCH_W2
|
||||
@@ -311,7 +311,7 @@ _max_too_small
|
||||
asmsub compare_nocase(str string1 @R0, str string2 @AY) clobbers(Y) -> byte @A {
|
||||
; Compares two strings for sorting, case-insensitively for PETSCII strings.
|
||||
; Returns -1 (255), 0 or 1, meaning: string1 sorts before, equal or after string2.
|
||||
; Note that you can also directly compare strings and string values with eachother using
|
||||
; Note that you can also directly compare strings and string values with each other using
|
||||
; comparison operators ==, < etcetera (this will use strcmp automatically).
|
||||
%asm {{
|
||||
sta P8ZP_SCRATCH_W2
|
||||
@@ -350,7 +350,7 @@ _c1_zero
|
||||
asmsub compare_nocase_iso(str string1 @R0, str string2 @AY) clobbers(Y) -> byte @A {
|
||||
; Compares two strings for sorting, case-insensitively for ISO strings.
|
||||
; Returns -1 (255), 0 or 1, meaning: string1 sorts before, equal or after string2.
|
||||
; Note that you can also directly compare strings and string values with eachother using
|
||||
; Note that you can also directly compare strings and string values with each other using
|
||||
; comparison operators ==, < etcetera (this will use strcmp automatically).
|
||||
%asm {{
|
||||
sta P8ZP_SCRATCH_W2
|
||||
@@ -525,7 +525,7 @@ _done rts
|
||||
; ? matches any one character. For example, F?? matches FOO but not FU, and ?? matches all two-character strings.
|
||||
; * matches any string, including the empty string.
|
||||
; For example, F* matches all strings starting with F. *O*O* matches all strings with at least two Os. Finally, ?* matches all non-empty strings.
|
||||
; Both the pattern and the string must be NUL-terminated (that it, followed with a 00 byte) and at most 255 characters long (excluding the NUL).
|
||||
; Both the pattern and the string must be NUL-terminated (that is, followed with a 00 byte) and at most 255 characters long (excluding the NUL).
|
||||
; Code taken from http://6502.org/source/strings/patmatch.htm
|
||||
;
|
||||
; Input: cx16.r0: A NUL-terminated, <255-length pattern
|
||||
@@ -562,7 +562,7 @@ next lda $ffff,x ; look at next pattern character MODIFIED
|
||||
cmp #'*' ; is it a star?
|
||||
beq star ; yes, do the complicated stuff
|
||||
iny ; no, let's look at the string
|
||||
cmp #'?' ; is the pattern caracter a ques?
|
||||
cmp #'?' ; is the pattern character a ques?
|
||||
bne reg ; no, it's a regular character
|
||||
lda (strptr),y ; yes, so it will match anything
|
||||
beq fail ; except the end of string
|
||||
@@ -602,7 +602,7 @@ fail clc ; yes, no match found, return with c=0
|
||||
; ? matches any one character. For example, F?? matches FOO but not FU, and ?? matches all two-character strings.
|
||||
; * matches any string, including the empty string.
|
||||
; For example, F* matches all strings starting with F. *O*O* matches all strings with at least two Os. Finally, ?* matches all non-empty strings.
|
||||
; Both the pattern and the string must be NUL-terminated (that it, followed with a 00 byte) and at most 255 characters long (excluding the NUL).
|
||||
; Both the pattern and the string must be NUL-terminated (that is, followed with a 00 byte) and at most 255 characters long (excluding the NUL).
|
||||
; Code taken from http://6502.org/source/strings/patmatch.htm
|
||||
;
|
||||
; Input: cx16.r0: A NUL-terminated, <255-length pattern
|
||||
@@ -651,7 +651,7 @@ next lda $ffff,x ; look at next pattern character MODIFIED
|
||||
cmp #'*' ; is it a star?
|
||||
beq star ; yes, do the complicated stuff
|
||||
iny ; no, let's look at the string
|
||||
cmp #'?' ; is the pattern caracter a ques?
|
||||
cmp #'?' ; is the pattern character a ques?
|
||||
bne reg ; no, it's a regular character
|
||||
lda (strptr),y ; yes, so it will match anything
|
||||
beq fail ; except the end of string
|
||||
|
||||
@@ -300,7 +300,7 @@ diskio {
|
||||
}
|
||||
|
||||
sub get_loadaddress(str filename) -> uword {
|
||||
; get the load adress from a PRG file (usually $0801 but it can be different)
|
||||
; get the load address from a PRG file (usually $0801 but it can be different)
|
||||
if f_open(filename) {
|
||||
uword address
|
||||
void f_read(&address, 2)
|
||||
|
||||
@@ -277,7 +277,7 @@ math {
|
||||
|
||||
sub atan2(ubyte x1, ubyte y1, ubyte x2, ubyte y2) -> ubyte {
|
||||
;; Calculate the angle, in a 256-degree circle, between two points into A.
|
||||
;; The points (x1, y1) and (x2, y2) have to use *unsigned coordinates only* from the positive quadrant in the carthesian plane!
|
||||
;; The points (x1, y1) and (x2, y2) have to use *unsigned coordinates only* from the positive quadrant in the cartesian plane!
|
||||
%ir {{
|
||||
loadm.b r99100,math.atan2.x1
|
||||
loadm.b r99101,math.atan2.y1
|
||||
|
||||
@@ -50,7 +50,7 @@ sorting {
|
||||
|
||||
|
||||
sub shellsort_by_ub(^^ubyte @requirezp ub_keys, ^^uword @requirezp wordvalues, ubyte num_elements) {
|
||||
; sorts the 'wordvalues' array (no-split array of words) according to the 'ub_keys' array (which also gets sorted ofcourse).
|
||||
; sorts the 'wordvalues' array (no-split array of words) according to the 'ub_keys' array (which also gets sorted of course).
|
||||
num_elements--
|
||||
ubyte @zp gap
|
||||
for gap in [132, 57, 23, 10, 4, 1] {
|
||||
@@ -76,7 +76,7 @@ sorting {
|
||||
}
|
||||
|
||||
sub shellsort_by_uw(^^uword @requirezp uw_keys, ^^uword @requirezp wordvalues, ubyte num_elements) {
|
||||
; sorts the 'wordvalues' array according to the 'uw_keys' array (which also gets sorted ofcourse).
|
||||
; sorts the 'wordvalues' array according to the 'uw_keys' array (which also gets sorted of course).
|
||||
; both arrays should be no-split array of words. uw_keys are unsigned.
|
||||
num_elements--
|
||||
ubyte gap
|
||||
|
||||
@@ -147,7 +147,7 @@ strings {
|
||||
sub compare(str st1, str st2) -> byte {
|
||||
; Compares two strings for sorting (case sensitive).
|
||||
; Returns -1 (255), 0 or 1, meaning: string1 sorts before, equal or after string2.
|
||||
; Note that you can also directly compare strings and string values with eachother using
|
||||
; Note that you can also directly compare strings and string values with each other using
|
||||
; comparison operators ==, < etcetera (this will use strcmp automatically).
|
||||
%ir {{
|
||||
loadm.w r99000,strings.compare.st1
|
||||
@@ -160,7 +160,7 @@ strings {
|
||||
sub compare_nocase(str st1, str st2) -> byte {
|
||||
; Compares two strings for sorting (case insensitive).
|
||||
; Returns -1 (255), 0 or 1, meaning: string1 sorts before, equal or after string2.
|
||||
; Note that you can also directly compare strings and string values with eachother using
|
||||
; Note that you can also directly compare strings and string values with each other using
|
||||
; comparison operators ==, < etcetera (this will use strcmp automatically).
|
||||
%ir {{
|
||||
loadm.w r99000,strings.compare_nocase.st1
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -274,8 +274,7 @@ internal class AstChecker(private val program: Program,
|
||||
}
|
||||
|
||||
BaseDataType.LONG -> {
|
||||
if(!iterableDt.elementType().isInteger)
|
||||
errors.err("long loop variable can only loop over integers", forLoop.position)
|
||||
errors.warn("for loop using a long counter could be very slow", forLoop.position)
|
||||
}
|
||||
|
||||
BaseDataType.FLOAT -> {
|
||||
@@ -698,7 +697,12 @@ internal class AstChecker(private val program: Program,
|
||||
val iterations = repeatLoop.iterations?.constValue(program)
|
||||
if (iterations != null) {
|
||||
require(floor(iterations.number)==iterations.number)
|
||||
if (iterations.number.toInt() > 65536) errors.err("repeat cannot exceed 65536 iterations", iterations.position)
|
||||
if (iterations.number.toInt() > 65536)
|
||||
errors.warn("repeat using a long counter could be very slow", iterations.position)
|
||||
}
|
||||
|
||||
if(repeatLoop.iterations?.inferType(program)?.isLong==true) {
|
||||
errors.warn("repeat using a long counter could be very slow", repeatLoop.iterations!!.position)
|
||||
}
|
||||
|
||||
val ident = repeatLoop.iterations as? IdentifierReference
|
||||
@@ -907,7 +911,7 @@ internal class AstChecker(private val program: Program,
|
||||
val variable=addressOf.identifier?.targetVarDecl()
|
||||
if (variable!=null) {
|
||||
if (variable.type == VarDeclType.CONST && addressOf.arrayIndex == null)
|
||||
errors.err("invalid pointer-of operand type", addressOf.position)
|
||||
errors.err("invalid address-of operand type", addressOf.position)
|
||||
}
|
||||
|
||||
if(addressOf.msb) {
|
||||
@@ -975,7 +979,9 @@ internal class AstChecker(private val program: Program,
|
||||
|
||||
// CONST can only occur on simple types (byte, word, float) and pointers
|
||||
if(decl.type==VarDeclType.CONST) {
|
||||
if (!decl.datatype.isNumericOrBool && !decl.datatype.isPointer)
|
||||
if(decl.datatype.isString)
|
||||
err("strings cannot be made const they are always potentially mutable")
|
||||
else if (!decl.datatype.isNumericOrBool && !decl.datatype.isPointer)
|
||||
err("const can only be used on numbers, booleans and pointers")
|
||||
}
|
||||
|
||||
|
||||
@@ -47,8 +47,36 @@ internal class BeforeAsmTypecastCleaner(val program: Program,
|
||||
}
|
||||
} else if(sourceDt.isString && typecast.type.isPointer && typecast.type.sub==BaseDataType.UBYTE) {
|
||||
// casting a string to a ^^ubyte is just taking the address of the string.
|
||||
val addr = AddressOf(typecast.expression as IdentifierReference, null, null, false, true, typecast.position)
|
||||
return listOf(AstReplaceNode(typecast, addr, parent))
|
||||
when (typecast.expression) {
|
||||
is IfExpression -> {
|
||||
val ifExpr = typecast.expression as IfExpression
|
||||
val trueCast = TypecastExpression(ifExpr.truevalue, typecast.type, typecast.implicit, typecast.position)
|
||||
val falseCast = TypecastExpression(ifExpr.falsevalue, typecast.type, typecast.implicit, typecast.position)
|
||||
trueCast.linkParents(ifExpr)
|
||||
falseCast.linkParents(ifExpr)
|
||||
ifExpr.truevalue = trueCast
|
||||
ifExpr.falsevalue = falseCast
|
||||
return listOf(AstReplaceNode(typecast, ifExpr, parent))
|
||||
}
|
||||
is BranchConditionExpression -> {
|
||||
val branchExpr = typecast.expression as BranchConditionExpression
|
||||
val trueCast = TypecastExpression(branchExpr.truevalue, typecast.type, typecast.implicit, typecast.position)
|
||||
val falseCast = TypecastExpression(branchExpr.falsevalue, typecast.type, typecast.implicit, typecast.position)
|
||||
trueCast.linkParents(branchExpr)
|
||||
falseCast.linkParents(branchExpr)
|
||||
branchExpr.truevalue = trueCast
|
||||
branchExpr.falsevalue = falseCast
|
||||
return listOf(AstReplaceNode(typecast, branchExpr, parent))
|
||||
}
|
||||
else -> {
|
||||
val identifier = typecast.expression as? IdentifierReference
|
||||
if (identifier != null) {
|
||||
val addr = AddressOf(identifier, null, null, false, true, typecast.position)
|
||||
return listOf(AstReplaceNode(typecast, addr, parent))
|
||||
}
|
||||
errors.err("cannot take address of this expression", typecast.position)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errors.err("cannot cast pass-by-reference value to type ${typecast.type} (only to UWORD)", typecast.position)
|
||||
}
|
||||
|
||||
@@ -1232,33 +1232,6 @@ _after:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// consolidates simple assignment followed by augmented assignment into a single combined assignment.
|
||||
// a = simplevalue | a += expression --> a = simplevalue + expression (for all augmented operators)
|
||||
// NOTE: this is NOT SAFE to do when dealing with pointer arithmetic because the semantics of the combined expression is different!
|
||||
// NOTE: this is also NOT SAFE when the augmented assignment's RHS references the target variable (e.g., a = b; a *= a)
|
||||
if(!assignment.isAugmentable && assignment.value.isSimple) {
|
||||
val next = assignment.nextSibling() as? Assignment
|
||||
if(next?.isAugmentable==true && next.value is BinaryExpression && next.target.isSameAs(assignment.target, program)) {
|
||||
val combined = next.value as BinaryExpression
|
||||
if(combined.left isSameAs assignment.target) {
|
||||
// Check if RHS also references the target variable - if so, can't optimize
|
||||
if(!(combined.right isSameAs assignment.target)) {
|
||||
if(!assignment.target.inferType(program).isPointer && (assignment.value.inferType(program).isPointer || combined.right.inferType(program).isPointer)) {
|
||||
// avoid pointer arithmetic: a = (simplevalue as uword) + expression
|
||||
combined.left = TypecastExpression(assignment.value, DataType.UWORD, true, combined.left.position)
|
||||
} else {
|
||||
// no pointer arithmetic done, just transform it into a = simplevalue + expression
|
||||
combined.left = assignment.value
|
||||
}
|
||||
assignment.value = combined
|
||||
combined.linkParents(assignment)
|
||||
return listOf(AstRemove(next, parent as IStatementContainer))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(assignment.target.pointerDereference!=null && assignment.value is PtrDereference) {
|
||||
val targetPtr = assignment.target.pointerDereference!!
|
||||
val sourcePtr = assignment.value as PtrDereference
|
||||
|
||||
@@ -142,9 +142,15 @@ internal object DeferProcessor {
|
||||
}
|
||||
|
||||
// complex return value, need to store it before calling the defer block
|
||||
val pushAndPopCalls = ret.children.map { makePushPopFunctionCalls(it as PtExpression) }
|
||||
val pushAndPopCalls = ret.children.map {
|
||||
val expr = it as PtExpression
|
||||
if (expr.type == DataType.UNDEFINED)
|
||||
Pair(expr, null)
|
||||
else
|
||||
makePushPopFunctionCalls(expr)
|
||||
}
|
||||
val pushCalls = pushAndPopCalls.map { it.first }.reversed() // push in reverse order
|
||||
val popCalls = pushAndPopCalls.map { it.second }
|
||||
val popCalls = pushAndPopCalls.mapNotNull { it.second }
|
||||
val newRet = PtReturn(ret.position)
|
||||
val group = PtNodeGroup()
|
||||
pushCalls.forEach { group.add(it) }
|
||||
|
||||
+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)
|
||||
|
||||
@@ -2,7 +2,6 @@ package prog8tests.compiler
|
||||
|
||||
import io.kotest.common.ExperimentalKotest
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.datatest.withData
|
||||
import io.kotest.engine.spec.tempdir
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.shouldNotBe
|
||||
@@ -146,47 +145,45 @@ private fun verifyIrRegisterCount(result: CompilationResult, expectedCount: Int)
|
||||
class TestCompilerOnExamplesC64: FunSpec({
|
||||
|
||||
@OptIn(ExperimentalKotest::class)
|
||||
//testExecutionMode = TestExecutionMode.LimitedConcurrency(6)
|
||||
this.concurrency = 4
|
||||
|
||||
val outputDir = tempdir().toPath()
|
||||
this.threads = 4
|
||||
|
||||
val onlyC64 = cartesianProduct(
|
||||
listOf(
|
||||
ExampleSizes("balloonflight", c64SizeOptimized=1674, c64SizeUnoptimized=1745),
|
||||
ExampleSizes("balloonflight", c64SizeOptimized=1674, c64SizeUnoptimized=1737),
|
||||
ExampleSizes("banking", c64SizeOptimized=680, c64SizeUnoptimized=1451),
|
||||
ExampleSizes("bdmusic-irq", c64SizeOptimized=1001, c64SizeUnoptimized=1019),
|
||||
ExampleSizes("bdmusic", c64SizeOptimized=938, c64SizeUnoptimized=1011),
|
||||
ExampleSizes("charset", c64SizeOptimized=835, c64SizeUnoptimized=860),
|
||||
ExampleSizes("cube3d-sprites", c64SizeOptimized=2712, c64SizeUnoptimized=2764),
|
||||
ExampleSizes("bdmusic", c64SizeOptimized=944, c64SizeUnoptimized=1011),
|
||||
ExampleSizes("charset", c64SizeOptimized=841, c64SizeUnoptimized=860),
|
||||
ExampleSizes("cube3d-sprites", c64SizeOptimized=2704, c64SizeUnoptimized=2756),
|
||||
ExampleSizes("fileselector", c64SizeOptimized=3568, c64SizeUnoptimized=4758),
|
||||
ExampleSizes("library/main", c64SizeOptimized=321, c64SizeUnoptimized=1237),
|
||||
ExampleSizes("multiplexer", c64SizeOptimized=1356, c64SizeUnoptimized=1507),
|
||||
ExampleSizes("petswirl", c64SizeOptimized=959, c64SizeUnoptimized=1029),
|
||||
ExampleSizes("plasma", c64SizeOptimized=1690, c64SizeUnoptimized=1731),
|
||||
ExampleSizes("petswirl", c64SizeOptimized=953, c64SizeUnoptimized=1021),
|
||||
ExampleSizes("plasma", c64SizeOptimized=1688, c64SizeUnoptimized=1723),
|
||||
ExampleSizes("rasterbars", c64SizeOptimized=845, c64SizeUnoptimized=892),
|
||||
ExampleSizes("simplemultiplexer", c64SizeOptimized=843, c64SizeUnoptimized=914),
|
||||
ExampleSizes("sprites", c64SizeOptimized=963, c64SizeUnoptimized=1052),
|
||||
ExampleSizes("simplemultiplexer", c64SizeOptimized=843, c64SizeUnoptimized=902),
|
||||
ExampleSizes("sprites", c64SizeOptimized=963, c64SizeUnoptimized=1040),
|
||||
ExampleSizes("starfield", c64SizeOptimized=743, c64SizeUnoptimized=758),
|
||||
ExampleSizes("turtle-gfx", c64SizeOptimized=2745, c64SizeUnoptimized=2861),
|
||||
ExampleSizes("wizzine", c64SizeOptimized=1124, c64SizeUnoptimized=1236),
|
||||
ExampleSizes("turtle-gfx", c64SizeOptimized=2758, c64SizeUnoptimized=2860),
|
||||
ExampleSizes("wizzine", c64SizeOptimized=1124, c64SizeUnoptimized=1224),
|
||||
// boingball, cube3d, tehtriz are in BothC64andCx16
|
||||
),
|
||||
listOf(false, true)
|
||||
)
|
||||
|
||||
val target = C64Target()
|
||||
withData(
|
||||
nameFn = { "${it.first.first.name}: ${target.name}, optimize=${it.first.second}" },
|
||||
onlyC64.map { it to prepareTestFiles(it.first.name, it.second, target) }
|
||||
) { (params, prep) ->
|
||||
val filepath = prep.second
|
||||
val optimize = params.second
|
||||
val expectedSize = if (optimize) params.first.c64SizeOptimized else params.first.c64SizeUnoptimized
|
||||
val result = compileTheThing(filepath, optimize, target, outputDir)
|
||||
result shouldNotBe null
|
||||
if (expectedSize > 0) {
|
||||
verifyOutputFileSize(result!!, expectedSize)
|
||||
onlyC64.forEach { (params, optimize) ->
|
||||
val prep = prepareTestFiles(params.name, optimize, target)
|
||||
test(prep.first) {
|
||||
val testOutputDir = tempdir().toPath()
|
||||
val filepath = prep.second
|
||||
val expectedSize = if (optimize) params.c64SizeOptimized else params.c64SizeUnoptimized
|
||||
val result = compileTheThing(filepath, optimize, target, testOutputDir)
|
||||
result shouldNotBe null
|
||||
if (expectedSize > 0) {
|
||||
verifyOutputFileSize(result!!, expectedSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -194,21 +191,19 @@ class TestCompilerOnExamplesC64: FunSpec({
|
||||
class TestCompilerOnExamplesCx16: FunSpec({
|
||||
|
||||
@OptIn(ExperimentalKotest::class)
|
||||
//testExecutionMode = TestExecutionMode.LimitedConcurrency(6)
|
||||
this.concurrency = 4
|
||||
|
||||
val outputDir = tempdir().toPath()
|
||||
this.threads = 4
|
||||
|
||||
val onlyCx16 = cartesianProduct(
|
||||
listOf(
|
||||
ExampleSizes("vtui/testvtui", cx16SizeOptimized=3041, cx16SizeUnoptimized=3041),
|
||||
ExampleSizes("pcmaudio/play-adpcm", cx16SizeOptimized=36147, cx16SizeUnoptimized=36645),
|
||||
ExampleSizes("pcmaudio/stream-wav", cx16SizeOptimized=5627, cx16SizeUnoptimized=7863),
|
||||
ExampleSizes("pcmaudio/stream-simple-aflow", cx16SizeOptimized=1850, cx16SizeUnoptimized=3753),
|
||||
ExampleSizes("pcmaudio/stream-simple-poll", cx16SizeOptimized=1564, cx16SizeUnoptimized=3462),
|
||||
ExampleSizes("pcmaudio/play-adpcm", cx16SizeOptimized=36147, cx16SizeUnoptimized=36637),
|
||||
ExampleSizes("pcmaudio/stream-wav", cx16SizeOptimized=5636, cx16SizeUnoptimized=7878),
|
||||
ExampleSizes("pcmaudio/stream-simple-aflow", cx16SizeOptimized=1842, cx16SizeUnoptimized=3745),
|
||||
ExampleSizes("pcmaudio/stream-simple-poll", cx16SizeOptimized=1556, cx16SizeUnoptimized=3454),
|
||||
ExampleSizes("pcmaudio/vumeter", cx16SizeOptimized=3888, cx16SizeUnoptimized=6381),
|
||||
ExampleSizes("sprites/dragon", cx16SizeOptimized=1858, cx16SizeUnoptimized=4374),
|
||||
ExampleSizes("sprites/dragons", cx16SizeOptimized=1511, cx16SizeUnoptimized=3686),
|
||||
ExampleSizes("sprites/dragons", cx16SizeOptimized=1503, cx16SizeUnoptimized=3678),
|
||||
ExampleSizes("zsmkit_v1/demo1", cx16SizeOptimized=7546, cx16SizeUnoptimized=7984),
|
||||
ExampleSizes("zsmkit_v1/demo2", cx16SizeOptimized=708, cx16SizeUnoptimized=2579),
|
||||
// zsmkit_v1/zsmkit_high and zsmkit_v1/zsmkit_low don't compile (empty source files)
|
||||
@@ -223,54 +218,54 @@ class TestCompilerOnExamplesCx16: FunSpec({
|
||||
ExampleSizes("audioroutines", cx16SizeOptimized=948, cx16SizeUnoptimized=1244),
|
||||
ExampleSizes("automatons", cx16SizeOptimized=1037, cx16SizeUnoptimized=1342),
|
||||
ExampleSizes("balloonflight", cx16SizeOptimized=3187, cx16SizeUnoptimized=4163),
|
||||
ExampleSizes("bdmusic", cx16SizeOptimized=2386, cx16SizeUnoptimized=2761),
|
||||
ExampleSizes("bobs", cx16SizeOptimized=2800, cx16SizeUnoptimized=3414),
|
||||
ExampleSizes("bdmusic", cx16SizeOptimized=2427, cx16SizeUnoptimized=2792),
|
||||
ExampleSizes("bobs", cx16SizeOptimized=2800, cx16SizeUnoptimized=3406),
|
||||
ExampleSizes("bubbleuniverse", cx16SizeOptimized=1450, cx16SizeUnoptimized=1958),
|
||||
ExampleSizes("charfade", cx16SizeOptimized=1054, cx16SizeUnoptimized=1362),
|
||||
ExampleSizes("charsets", cx16SizeOptimized=1125, cx16SizeUnoptimized=1417),
|
||||
ExampleSizes("circles", cx16SizeOptimized=2385, cx16SizeUnoptimized=4547),
|
||||
ExampleSizes("cobramk3-gfx", cx16SizeOptimized=7705, cx16SizeUnoptimized=10612),
|
||||
ExampleSizes("circles", cx16SizeOptimized=2384, cx16SizeUnoptimized=4539),
|
||||
ExampleSizes("cobramk3-gfx", cx16SizeOptimized=7697, cx16SizeUnoptimized=10604),
|
||||
ExampleSizes("colorbars", cx16SizeOptimized=1606, cx16SizeUnoptimized=1904),
|
||||
ExampleSizes("cxlogo", cx16SizeOptimized=599, cx16SizeUnoptimized=895),
|
||||
ExampleSizes("diskspeed", cx16SizeOptimized=6280, cx16SizeUnoptimized=8283),
|
||||
ExampleSizes("fileseek", cx16SizeOptimized=2468, cx16SizeUnoptimized=4496),
|
||||
ExampleSizes("floatparse", cx16SizeOptimized=8303, cx16SizeUnoptimized=9190),
|
||||
ExampleSizes("interpolation", cx16SizeOptimized=3791, cx16SizeUnoptimized=4768),
|
||||
ExampleSizes("kefrenbars", cx16SizeOptimized=2386, cx16SizeUnoptimized=4654),
|
||||
ExampleSizes("interpolation", cx16SizeOptimized=3783, cx16SizeUnoptimized=4760),
|
||||
ExampleSizes("kefrenbars", cx16SizeOptimized=2393, cx16SizeUnoptimized=4654),
|
||||
ExampleSizes("keyboardhandler", cx16SizeOptimized=412, cx16SizeUnoptimized=717),
|
||||
ExampleSizes("landscape", cx16SizeOptimized=1673, cx16SizeUnoptimized=1997),
|
||||
ExampleSizes("landscape", cx16SizeOptimized=1665, cx16SizeUnoptimized=1989),
|
||||
ExampleSizes("life", cx16SizeOptimized=1320, cx16SizeUnoptimized=1751),
|
||||
ExampleSizes("mandelbrot", cx16SizeOptimized=1120, cx16SizeUnoptimized=1434),
|
||||
ExampleSizes("multi-irq-new", cx16SizeOptimized=1073, cx16SizeUnoptimized=1519),
|
||||
ExampleSizes("multi-irq-old", cx16SizeOptimized=962, cx16SizeUnoptimized=1410),
|
||||
ExampleSizes("plasma", cx16SizeOptimized=1739, cx16SizeUnoptimized=2066),
|
||||
ExampleSizes("plasma", cx16SizeOptimized=1731, cx16SizeUnoptimized=2058),
|
||||
ExampleSizes("rasterbars", cx16SizeOptimized=1268, cx16SizeUnoptimized=1685),
|
||||
ExampleSizes("serialdownload", cx16SizeOptimized=2471, cx16SizeUnoptimized=3649),
|
||||
ExampleSizes("showbmx", cx16SizeOptimized=2745, cx16SizeUnoptimized=5269),
|
||||
ExampleSizes("snow", cx16SizeOptimized=3120, cx16SizeUnoptimized=5295),
|
||||
ExampleSizes("serialdownload", cx16SizeOptimized=2516, cx16SizeUnoptimized=3945),
|
||||
ExampleSizes("showbmx", cx16SizeOptimized=2745, cx16SizeUnoptimized=5261),
|
||||
ExampleSizes("snow", cx16SizeOptimized=3119, cx16SizeUnoptimized=5287),
|
||||
ExampleSizes("sortingbench", cx16SizeOptimized=2928, cx16SizeUnoptimized=3847),
|
||||
ExampleSizes("spotlight", cx16SizeOptimized=908, cx16SizeUnoptimized=1364),
|
||||
ExampleSizes("starszoom", cx16SizeOptimized=2399, cx16SizeUnoptimized=4863),
|
||||
ExampleSizes("starszoom", cx16SizeOptimized=2406, cx16SizeUnoptimized=4855),
|
||||
ExampleSizes("test_gfx_hires", cx16SizeOptimized=4835, cx16SizeUnoptimized=7514),
|
||||
ExampleSizes("test_gfx_lores", cx16SizeOptimized=5841, cx16SizeUnoptimized=7188),
|
||||
ExampleSizes("testmonogfx", cx16SizeOptimized=10463, cx16SizeUnoptimized=11982),
|
||||
ExampleSizes("test_gfx_lores", cx16SizeOptimized=5849, cx16SizeUnoptimized=7188),
|
||||
ExampleSizes("testmonogfx", cx16SizeOptimized=10480, cx16SizeUnoptimized=11974),
|
||||
ExampleSizes("textspotlight", cx16SizeOptimized=3431, cx16SizeUnoptimized=3788),
|
||||
),
|
||||
listOf(false, true)
|
||||
)
|
||||
|
||||
val target = Cx16Target()
|
||||
withData(
|
||||
nameFn = { "${it.first.first.name}: ${target.name}, optimize=${it.first.second}" },
|
||||
onlyCx16.map { it to prepareTestFiles(it.first.name, it.second, target) }
|
||||
) { (params, prep) ->
|
||||
val filepath = prep.second
|
||||
val optimize = params.second
|
||||
val expectedSize = if (optimize) params.first.cx16SizeOptimized else params.first.cx16SizeUnoptimized
|
||||
val result = compileTheThing(filepath, optimize, target, outputDir)
|
||||
result shouldNotBe null
|
||||
if (expectedSize > 0) {
|
||||
verifyOutputFileSize(result!!, expectedSize)
|
||||
onlyCx16.forEach { (params, optimize) ->
|
||||
val prep = prepareTestFiles(params.name, optimize, target)
|
||||
test(prep.first) {
|
||||
val testOutputDir = tempdir().toPath()
|
||||
val filepath = prep.second
|
||||
val expectedSize = if (optimize) params.cx16SizeOptimized else params.cx16SizeUnoptimized
|
||||
val result = compileTheThing(filepath, optimize, target, testOutputDir)
|
||||
result shouldNotBe null
|
||||
if (expectedSize > 0) {
|
||||
verifyOutputFileSize(result!!, expectedSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -278,102 +273,97 @@ class TestCompilerOnExamplesCx16: FunSpec({
|
||||
class TestCompilerOnExamplesBothC64andCx16: FunSpec({
|
||||
|
||||
@OptIn(ExperimentalKotest::class)
|
||||
//testExecutionMode = TestExecutionMode.LimitedConcurrency(6)
|
||||
this.concurrency = 4
|
||||
|
||||
val outputDir = tempdir().toPath()
|
||||
this.threads = 4
|
||||
|
||||
val bothCx16AndC64 = cartesianProduct(
|
||||
listOf(
|
||||
ExampleSizes("balls", c64SizeOptimized=885, c64SizeUnoptimized=919, cx16SizeOptimized=874, cx16SizeUnoptimized=1188),
|
||||
ExampleSizes("boingball", c64SizeOptimized=1478, c64SizeUnoptimized=1544, cx16SizeOptimized=1486, cx16SizeUnoptimized=1957),
|
||||
ExampleSizes("cube3d", c64SizeOptimized=2444, c64SizeUnoptimized=2509, cx16SizeOptimized=2381, cx16SizeUnoptimized=2712),
|
||||
ExampleSizes("cube3d-float", c64SizeOptimized=2619, c64SizeUnoptimized=2668, cx16SizeOptimized=2511, cx16SizeUnoptimized=2835),
|
||||
ExampleSizes("cube3d-gfx", c64SizeOptimized=3536, c64SizeUnoptimized=3589, cx16SizeOptimized=2206, cx16SizeUnoptimized=2522),
|
||||
ExampleSizes("boingball", c64SizeOptimized=1513, c64SizeUnoptimized=1579, cx16SizeOptimized=1486, cx16SizeUnoptimized=1957),
|
||||
ExampleSizes("cube3d", c64SizeOptimized=2436, c64SizeUnoptimized=2501, cx16SizeOptimized=2373, cx16SizeUnoptimized=2704),
|
||||
ExampleSizes("cube3d-float", c64SizeOptimized=2654, c64SizeUnoptimized=2703, cx16SizeOptimized=2511, cx16SizeUnoptimized=2835),
|
||||
ExampleSizes("cube3d-gfx", c64SizeOptimized=3528, c64SizeUnoptimized=3581, cx16SizeOptimized=2198, cx16SizeUnoptimized=2514),
|
||||
ExampleSizes("dirlist", c64SizeOptimized=1543, c64SizeUnoptimized=2405, cx16SizeOptimized=1525, cx16SizeUnoptimized=3372),
|
||||
ExampleSizes("fibonacci", c64SizeOptimized=570, c64SizeUnoptimized=585, cx16SizeOptimized=626, cx16SizeUnoptimized=925),
|
||||
ExampleSizes("fractal-tree", c64SizeOptimized=2808, c64SizeUnoptimized=2845, cx16SizeOptimized=1220, cx16SizeUnoptimized=1511),
|
||||
ExampleSizes("fractal-tree", c64SizeOptimized=2832, c64SizeUnoptimized=2869, cx16SizeOptimized=1220, cx16SizeUnoptimized=1511),
|
||||
ExampleSizes("maze", c64SizeOptimized=3088, c64SizeUnoptimized=3189, cx16SizeOptimized=2873, cx16SizeUnoptimized=3251),
|
||||
ExampleSizes("mandelbrot", c64SizeOptimized=1138, c64SizeUnoptimized=1172, cx16SizeOptimized=1120, cx16SizeUnoptimized=1434),
|
||||
ExampleSizes("mandelbrot-gfx", c64SizeOptimized=1457, c64SizeUnoptimized=1479, cx16SizeOptimized=957, cx16SizeUnoptimized=1250),
|
||||
ExampleSizes("mandelbrot", c64SizeOptimized=1173, c64SizeUnoptimized=1207, cx16SizeOptimized=1120, cx16SizeUnoptimized=1434),
|
||||
ExampleSizes("mandelbrot-gfx", c64SizeOptimized=1492, c64SizeUnoptimized=1514, cx16SizeOptimized=957, cx16SizeUnoptimized=1250),
|
||||
ExampleSizes("multitasking", c64SizeOptimized=1956, c64SizeUnoptimized=2029, cx16SizeOptimized=1826, cx16SizeUnoptimized=2159),
|
||||
ExampleSizes("numbergame", c64SizeOptimized=1034, c64SizeUnoptimized=1050, cx16SizeOptimized=1079, cx16SizeUnoptimized=1382),
|
||||
ExampleSizes("primes", c64SizeOptimized=520, c64SizeUnoptimized=541, cx16SizeOptimized=565, cx16SizeUnoptimized=870),
|
||||
ExampleSizes("queens", c64SizeOptimized=1061, c64SizeUnoptimized=1093, cx16SizeOptimized=1111, cx16SizeUnoptimized=1427),
|
||||
ExampleSizes("queens", c64SizeOptimized=1053, c64SizeUnoptimized=1085, cx16SizeOptimized=1103, cx16SizeUnoptimized=1419),
|
||||
ExampleSizes("screencodes", c64SizeOptimized=667, c64SizeUnoptimized=687, cx16SizeOptimized=655, cx16SizeUnoptimized=959),
|
||||
ExampleSizes("swirl", c64SizeOptimized=828, c64SizeUnoptimized=831, cx16SizeOptimized=923, cx16SizeUnoptimized=1208),
|
||||
ExampleSizes("swirl-float", c64SizeOptimized=708, c64SizeUnoptimized=735, cx16SizeOptimized=595, cx16SizeUnoptimized=906),
|
||||
ExampleSizes("tehtriz", c64SizeOptimized=4333, c64SizeUnoptimized=4504, cx16SizeOptimized=6211, cx16SizeUnoptimized=6742),
|
||||
ExampleSizes("textelite", c64SizeOptimized=11440, c64SizeUnoptimized=12423, cx16SizeOptimized=10778, cx16SizeUnoptimized=13046),
|
||||
ExampleSizes("swirl-float", c64SizeOptimized=732, c64SizeUnoptimized=759, cx16SizeOptimized=595, cx16SizeUnoptimized=906),
|
||||
ExampleSizes("tehtriz", c64SizeOptimized=4331, c64SizeUnoptimized=4496, cx16SizeOptimized=6244, cx16SizeUnoptimized=6765),
|
||||
ExampleSizes("textelite", c64SizeOptimized=11428, c64SizeUnoptimized=12411, cx16SizeOptimized=10770, cx16SizeUnoptimized=13038),
|
||||
ExampleSizes("pointers/animalgame", c64SizeOptimized=1570, c64SizeUnoptimized=2126, cx16SizeOptimized=1605, cx16SizeUnoptimized=2412),
|
||||
ExampleSizes("pointers/binarytree", c64SizeOptimized=2562, c64SizeUnoptimized=2640, cx16SizeOptimized=2085, cx16SizeUnoptimized=2389),
|
||||
ExampleSizes("pointers/hashtable", c64SizeOptimized=3294, c64SizeUnoptimized=4140, cx16SizeOptimized=2832, cx16SizeUnoptimized=3786),
|
||||
ExampleSizes("pointers/sortedlist", c64SizeOptimized=1274, c64SizeUnoptimized=1319, cx16SizeOptimized=1223, cx16SizeUnoptimized=1532),
|
||||
ExampleSizes("pointers/sortedlist", c64SizeOptimized=1262, c64SizeUnoptimized=1307, cx16SizeOptimized=1215, cx16SizeUnoptimized=1524),
|
||||
ExampleSizes("pointers/sorting", c64SizeOptimized=1776, c64SizeUnoptimized=2676, cx16SizeOptimized=1788, cx16SizeUnoptimized=2659),
|
||||
),
|
||||
listOf(false, true),
|
||||
listOf(C64Target(), Cx16Target())
|
||||
)
|
||||
|
||||
withData(
|
||||
nameFn = { "${it.first.name}: ${it.third.first.name}, optimize=${it.second}" },
|
||||
bothCx16AndC64.map {
|
||||
val prep = prepareTestFiles(it.first.name, it.second, it.third)
|
||||
Triple(it.first, it.second, Triple(it.third, prep.first, prep.second))
|
||||
}
|
||||
) { params ->
|
||||
val filepath = params.third.third
|
||||
val optimize = params.second
|
||||
val target = params.third.first
|
||||
val exampleSizes = params.first
|
||||
val expectedSize = when (target) {
|
||||
is C64Target -> if (optimize) exampleSizes.c64SizeOptimized else exampleSizes.c64SizeUnoptimized
|
||||
is Cx16Target -> if (optimize) exampleSizes.cx16SizeOptimized else exampleSizes.cx16SizeUnoptimized
|
||||
else -> 0
|
||||
}
|
||||
val result = compileTheThing(filepath, optimize, target, outputDir)
|
||||
result shouldNotBe null
|
||||
if (expectedSize > 0) {
|
||||
verifyOutputFileSize(result!!, expectedSize)
|
||||
bothCx16AndC64.forEach { (exampleSizes, optimize, target) ->
|
||||
val prep = prepareTestFiles(exampleSizes.name, optimize, target)
|
||||
test(prep.first) {
|
||||
val testOutputDir = tempdir().toPath()
|
||||
val filepath = prep.second
|
||||
val expectedSize = when (target) {
|
||||
is C64Target -> if (optimize) exampleSizes.c64SizeOptimized else exampleSizes.c64SizeUnoptimized
|
||||
is Cx16Target -> if (optimize) exampleSizes.cx16SizeOptimized else exampleSizes.cx16SizeUnoptimized
|
||||
else -> 0
|
||||
}
|
||||
val result = compileTheThing(filepath, optimize, target, testOutputDir)
|
||||
result shouldNotBe null
|
||||
if (expectedSize > 0) {
|
||||
verifyOutputFileSize(result!!, expectedSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
class TestCompilerOnExamplesVirtual: FunSpec({
|
||||
|
||||
val outputDir = tempdir().toPath()
|
||||
@OptIn(ExperimentalKotest::class)
|
||||
this.concurrency = 4
|
||||
this.threads = 4
|
||||
|
||||
val onlyVirtual = cartesianProduct(
|
||||
listOf(
|
||||
ExampleSizes("bouncegfx", virtualInstrCountOptimized=186, virtualInstrCountUnoptimized=824, virtualRegCountOptimized=92, virtualRegCountUnoptimized=380),
|
||||
ExampleSizes("bsieve", virtualInstrCountOptimized=158, virtualInstrCountUnoptimized=1939, virtualRegCountOptimized=66, virtualRegCountUnoptimized=860),
|
||||
ExampleSizes("fountain", virtualInstrCountOptimized=150, virtualInstrCountUnoptimized=791, virtualRegCountOptimized=75, virtualRegCountUnoptimized=363),
|
||||
ExampleSizes("pixelshader", virtualInstrCountOptimized=43, virtualInstrCountUnoptimized=286, virtualRegCountOptimized=15, virtualRegCountUnoptimized=109),
|
||||
ExampleSizes("sincos", virtualInstrCountOptimized=276, virtualInstrCountUnoptimized=898, virtualRegCountOptimized=133, virtualRegCountUnoptimized=403),
|
||||
ExampleSizes("pointers/animalgame", virtualInstrCountOptimized=221, virtualInstrCountUnoptimized=2083, virtualRegCountOptimized=103, virtualRegCountUnoptimized=933),
|
||||
ExampleSizes("pointers/binarytree", virtualInstrCountOptimized=510, virtualInstrCountUnoptimized=2237, virtualRegCountOptimized=251, virtualRegCountUnoptimized=1020),
|
||||
ExampleSizes("pointers/hashtable", virtualInstrCountOptimized=578, virtualInstrCountUnoptimized=2255, virtualRegCountOptimized=293, virtualRegCountUnoptimized=1045),
|
||||
ExampleSizes("pointers/sortedlist", virtualInstrCountOptimized=267, virtualInstrCountUnoptimized=2406, virtualRegCountOptimized=118, virtualRegCountUnoptimized=1086),
|
||||
ExampleSizes("pointers/fountain-virtual", virtualInstrCountOptimized=145, virtualInstrCountUnoptimized=785, virtualRegCountOptimized=70, virtualRegCountUnoptimized=357),
|
||||
ExampleSizes("pointers/sorting", virtualInstrCountOptimized=369, virtualInstrCountUnoptimized=2251, virtualRegCountOptimized=173, virtualRegCountUnoptimized=1010)
|
||||
ExampleSizes("bouncegfx", virtualInstrCountOptimized=186, virtualInstrCountUnoptimized=846, virtualRegCountOptimized=92, virtualRegCountUnoptimized=386),
|
||||
ExampleSizes("bsieve", virtualInstrCountOptimized=158, virtualInstrCountUnoptimized=1961, virtualRegCountOptimized=66, virtualRegCountUnoptimized=866),
|
||||
ExampleSizes("fountain", virtualInstrCountOptimized=150, virtualInstrCountUnoptimized=813, virtualRegCountOptimized=75, virtualRegCountUnoptimized=369),
|
||||
ExampleSizes("pixelshader", virtualInstrCountOptimized=43, virtualInstrCountUnoptimized=308, virtualRegCountOptimized=15, virtualRegCountUnoptimized=115),
|
||||
ExampleSizes("sincos", virtualInstrCountOptimized=276, virtualInstrCountUnoptimized=920, virtualRegCountOptimized=133, virtualRegCountUnoptimized=409),
|
||||
ExampleSizes("pointers/animalgame", virtualInstrCountOptimized=221, virtualInstrCountUnoptimized=2105, virtualRegCountOptimized=103, virtualRegCountUnoptimized=939),
|
||||
ExampleSizes("pointers/binarytree", virtualInstrCountOptimized=510, virtualInstrCountUnoptimized=2259, virtualRegCountOptimized=251, virtualRegCountUnoptimized=1026),
|
||||
ExampleSizes("pointers/hashtable", virtualInstrCountOptimized=578, virtualInstrCountUnoptimized=2277, virtualRegCountOptimized=293, virtualRegCountUnoptimized=1051),
|
||||
ExampleSizes("pointers/sortedlist", virtualInstrCountOptimized=267, virtualInstrCountUnoptimized=2428, virtualRegCountOptimized=118, virtualRegCountUnoptimized=1092),
|
||||
ExampleSizes("pointers/fountain-virtual", virtualInstrCountOptimized=145, virtualInstrCountUnoptimized=807, virtualRegCountOptimized=70, virtualRegCountUnoptimized=363),
|
||||
ExampleSizes("pointers/sorting", virtualInstrCountOptimized=369, virtualInstrCountUnoptimized=2273, virtualRegCountOptimized=173, virtualRegCountUnoptimized=1016)
|
||||
),
|
||||
listOf(false, true)
|
||||
)
|
||||
|
||||
val target = VMTarget()
|
||||
withData(
|
||||
nameFn = { "${it.first.first.name}: ${target.name}, optimize=${it.first.second}" },
|
||||
onlyVirtual.map { it to prepareTestFiles(it.first.name, it.second, target) }
|
||||
) { (params, prep) ->
|
||||
val filepath = prep.second
|
||||
val optimize = params.second
|
||||
val expectedInstrCount = if (optimize) params.first.virtualInstrCountOptimized else params.first.virtualInstrCountUnoptimized
|
||||
val expectedRegCount = if (optimize) params.first.virtualRegCountOptimized else params.first.virtualRegCountUnoptimized
|
||||
val result = compileTheThing(filepath, optimize, target, outputDir)
|
||||
result shouldNotBe null
|
||||
if (expectedInstrCount > 0) {
|
||||
verifyIrInstructionCount(result!!, expectedInstrCount)
|
||||
verifyIrRegisterCount(result, expectedRegCount)
|
||||
onlyVirtual.forEach { (params, optimize) ->
|
||||
val prep = prepareTestFiles(params.name, optimize, target)
|
||||
test(prep.first) {
|
||||
val testOutputDir = tempdir().toPath()
|
||||
val filepath = prep.second
|
||||
val expectedInstrCount = if (optimize) params.virtualInstrCountOptimized else params.virtualInstrCountUnoptimized
|
||||
val expectedRegCount = if (optimize) params.virtualRegCountOptimized else params.virtualRegCountUnoptimized
|
||||
val result = compileTheThing(filepath, optimize, target, testOutputDir)
|
||||
result shouldNotBe null
|
||||
if (expectedInstrCount > 0) {
|
||||
verifyIrInstructionCount(result!!, expectedInstrCount)
|
||||
verifyIrRegisterCount(result, expectedRegCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -381,31 +371,29 @@ class TestCompilerOnExamplesVirtual: FunSpec({
|
||||
class TestCompilerOnExamplesPET32: FunSpec({
|
||||
|
||||
@OptIn(ExperimentalKotest::class)
|
||||
//testExecutionMode = TestExecutionMode.LimitedConcurrency(6)
|
||||
this.concurrency = 4
|
||||
|
||||
val outputDir = tempdir().toPath()
|
||||
this.threads = 4
|
||||
|
||||
val onlyPET32 = cartesianProduct(
|
||||
listOf(
|
||||
ExampleSizes("boingball", pet32SizeOptimized=1497, pet32SizeUnoptimized=1586),
|
||||
ExampleSizes("petswirl", pet32SizeOptimized=915, pet32SizeUnoptimized=985),
|
||||
ExampleSizes("boingball", pet32SizeOptimized=1532, pet32SizeUnoptimized=1621),
|
||||
ExampleSizes("petswirl", pet32SizeOptimized=909, pet32SizeUnoptimized=977),
|
||||
),
|
||||
listOf(false, true)
|
||||
)
|
||||
|
||||
val target = PETTarget()
|
||||
withData(
|
||||
nameFn = { "${it.first.first.name}: ${target.name}, optimize=${it.first.second}" },
|
||||
onlyPET32.map { it to prepareTestFiles(it.first.name, it.second, target) }
|
||||
) { (params, prep) ->
|
||||
val filepath = prep.second
|
||||
val optimize = params.second
|
||||
val expectedSize = if (optimize) params.first.pet32SizeOptimized else params.first.pet32SizeUnoptimized
|
||||
val result = compileTheThing(filepath, optimize, target, outputDir)
|
||||
result shouldNotBe null
|
||||
if (expectedSize > 0) {
|
||||
verifyOutputFileSize(result!!, expectedSize)
|
||||
onlyPET32.forEach { (params, optimize) ->
|
||||
val prep = prepareTestFiles(params.name, optimize, target)
|
||||
test(prep.first) {
|
||||
val testOutputDir = tempdir().toPath()
|
||||
val filepath = prep.second
|
||||
val expectedSize = if (optimize) params.pet32SizeOptimized else params.pet32SizeUnoptimized
|
||||
val result = compileTheThing(filepath, optimize, target, testOutputDir)
|
||||
result shouldNotBe null
|
||||
if (expectedSize > 0) {
|
||||
verifyOutputFileSize(result!!, expectedSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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 {
|
||||
|
||||
@@ -23,7 +23,11 @@ class TestComparisonIssues : FunSpec({
|
||||
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
|
||||
val asmFile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm")
|
||||
val asm = asmFile.readText()
|
||||
asm.shouldContainInOrder("ldx", "#252", "lda", "p8v_a+4,x", "adc", "p8v_b+4,x", "sta", "p8v_a+4,x", "inx", "bne")
|
||||
asm.shouldContainInOrder("clc",
|
||||
"lda", "p8v_a+0", "adc", "p8v_b+0", "sta", "p8v_a+0",
|
||||
"lda", "p8v_a+1", "adc", "p8v_b+1", "sta", "p8v_a+1",
|
||||
"lda", "p8v_a+2", "adc", "p8v_b+2", "sta", "p8v_a+2",
|
||||
"lda", "p8v_a+3", "adc", "p8v_b+3", "sta", "p8v_a+3")
|
||||
}
|
||||
|
||||
test("long augmented subtraction generates loop") {
|
||||
@@ -39,7 +43,28 @@ class TestComparisonIssues : FunSpec({
|
||||
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
|
||||
val asmFile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm")
|
||||
val asm = asmFile.readText()
|
||||
asm.shouldContainInOrder("ldx", "#252", "lda", "p8v_a+4,x", "sbc", "p8v_b+4,x", "sta", "p8v_a+4,x", "inx", "bne")
|
||||
asm.shouldContainInOrder("sec",
|
||||
"lda", "p8v_a+0", "sbc", "p8v_b+0", "sta", "p8v_a+0",
|
||||
"lda", "p8v_a+1", "sbc", "p8v_b+1", "sta", "p8v_a+1",
|
||||
"lda", "p8v_a+2", "sbc", "p8v_b+2", "sta", "p8v_a+2",
|
||||
"lda", "p8v_a+3", "sbc", "p8v_b+3", "sta", "p8v_a+3")
|
||||
}
|
||||
|
||||
test("long augmented addition generates loop with nozp variables") {
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
long @nozp a = 1000
|
||||
long @nozp b = 2000
|
||||
a += b
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
|
||||
val asmFile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm")
|
||||
val asm = asmFile.readText()
|
||||
// verify unrolled structure
|
||||
asm.shouldContainInOrder("clc", "lda", "adc", "sta", "lda", "adc", "sta", "lda", "adc", "sta", "lda", "adc", "sta")
|
||||
}
|
||||
|
||||
test("word <= 0 signed comparison (identifier)") {
|
||||
|
||||
@@ -119,6 +119,43 @@ class TestExecution6502 : FunSpec({
|
||||
machine.cpu.totalCycles shouldBeLessThan 200
|
||||
}
|
||||
|
||||
test("long repeat loops") {
|
||||
val src = $$"""
|
||||
%option no_sysinit
|
||||
%launcher none
|
||||
%address $1000
|
||||
|
||||
main {
|
||||
&ubyte poweroff = $f203
|
||||
&long result = $02 ; result is stored in memory at $02
|
||||
|
||||
sub start() {
|
||||
long @shared qq = 0
|
||||
repeat 70000 {
|
||||
qq++
|
||||
}
|
||||
result = qq
|
||||
|
||||
long @shared iters = 70000
|
||||
repeat iters {
|
||||
qq++
|
||||
}
|
||||
result = qq
|
||||
|
||||
poweroff = 1
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
||||
// 140000 iterations * ~40 cycles = 5.6 million. Let's use 10 million to be safe.
|
||||
val machine = compileResult!!.simulate(maxCycles = 10000000L)
|
||||
machine.assertMemory(0x02, (140000L and 0xff).toInt())
|
||||
machine.assertMemory(0x03, ((140000L shr 8) and 0xff).toInt())
|
||||
machine.assertMemory(0x04, ((140000L shr 16) and 0xff).toInt())
|
||||
machine.assertMemory(0x05, ((140000L shr 24) and 0xff).toInt())
|
||||
}
|
||||
|
||||
test("top level RTS exits simulator") {
|
||||
val src = $$"""
|
||||
%option no_sysinit
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package prog8tests.codegeneration
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.engine.spec.tempdir
|
||||
import io.kotest.matchers.shouldNotBe
|
||||
import prog8.code.target.Cx16Target
|
||||
import prog8tests.helpers.compileText
|
||||
|
||||
class TestExperiCodeGen: FunSpec({
|
||||
|
||||
val outputDir = tempdir().toPath()
|
||||
|
||||
test("banked extsub with multiple return values (constbank)") {
|
||||
val src = $$"""
|
||||
main {
|
||||
sub start() {
|
||||
uword ptr
|
||||
ubyte bank
|
||||
void, ptr, bank = mymodule.myfunc(0)
|
||||
}
|
||||
}
|
||||
|
||||
mymodule {
|
||||
const ubyte MYBANK = 1
|
||||
extsub @bank MYBANK $A000 = myfunc(ubyte x @X) -> bool @Pc, uword @XY, ubyte @A
|
||||
}"""
|
||||
compileText(Cx16Target(), false, src, outputDir, experimentalCodegen = true) shouldNotBe null
|
||||
}
|
||||
|
||||
test("banked extsub with multiple return values (varbank)") {
|
||||
val src = $$"""
|
||||
main {
|
||||
sub start() {
|
||||
uword ptr
|
||||
ubyte bank
|
||||
void, ptr, bank = mymodule.myfunc(0)
|
||||
}
|
||||
}
|
||||
|
||||
mymodule {
|
||||
ubyte @shared bankvar = 1
|
||||
extsub @bank bankvar $A000 = myfunc(ubyte x @X) -> bool @Pc, uword @XY, ubyte @A
|
||||
}"""
|
||||
compileText(Cx16Target(), false, src, outputDir, experimentalCodegen = true) shouldNotBe null
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
package prog8tests.codegeneration
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.engine.spec.tempdir
|
||||
import prog8.code.target.Cx16Target
|
||||
import prog8tests.helpers.compileText
|
||||
|
||||
class TestLongAugmentedAssignment : FunSpec({
|
||||
val outputDir = tempdir().toPath()
|
||||
|
||||
test("long augmented assignment with literal") {
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
long @shared a = 1000
|
||||
a += 500
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
// Should compile successfully
|
||||
compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)
|
||||
}
|
||||
|
||||
test("long augmented assignment with expression") {
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
long @shared a = 1000
|
||||
long @shared b = 500
|
||||
a += b + 100
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)
|
||||
}
|
||||
|
||||
test("long augmented assignment with ubyte") {
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
long @shared a = 1000
|
||||
ubyte @shared b = 200
|
||||
a += b
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)
|
||||
}
|
||||
})
|
||||
@@ -18,6 +18,7 @@ import prog8.code.target.*
|
||||
import prog8tests.helpers.ErrorReporterForTests
|
||||
import prog8tests.helpers.compileText
|
||||
import prog8tests.helpers.shouldContainInOrder
|
||||
import prog8tests.helpers.simulate
|
||||
import kotlin.io.path.readText
|
||||
|
||||
class TestVariousCodeGen: FunSpec({
|
||||
@@ -904,4 +905,93 @@ main {
|
||||
asm shouldContain "sta (p8v_s),y"
|
||||
}
|
||||
}
|
||||
|
||||
test("IfExpression string to pointer typecast") {
|
||||
val src = """
|
||||
%option no_sysinit
|
||||
%launcher none
|
||||
%address $1000
|
||||
|
||||
main {
|
||||
sub start() {
|
||||
str a = "hello"
|
||||
str b = "world"
|
||||
bool flag = true
|
||||
^^ubyte p = if flag then a else b
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
// This should not crash during compilation
|
||||
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
||||
compileResult shouldNotBe null
|
||||
|
||||
// Also test with virtual target
|
||||
val compileResultVM = compileText(VMTarget(), false, src, outputDir)
|
||||
compileResultVM shouldNotBe null
|
||||
}
|
||||
|
||||
test("BranchConditionExpression string to pointer typecast") {
|
||||
val src = """
|
||||
%option no_sysinit
|
||||
%launcher none
|
||||
%address $1000
|
||||
|
||||
main {
|
||||
sub start() {
|
||||
str a = "hello"
|
||||
str b = "world"
|
||||
^^ubyte p = if_z then a else b
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
// This should not crash during compilation
|
||||
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
||||
compileResult shouldNotBe null
|
||||
}
|
||||
|
||||
test("pointer assignment to variable in 6502 backend") {
|
||||
val src = """
|
||||
%option no_sysinit
|
||||
%launcher none
|
||||
%address $1000
|
||||
|
||||
main {
|
||||
ubyte value = 42
|
||||
^^ubyte @shared ptr = 0
|
||||
|
||||
sub start() {
|
||||
ptr = &value
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
// This should not crash during compilation
|
||||
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
||||
compileResult shouldNotBe null
|
||||
|
||||
// Verify it actually works by simulating
|
||||
compileResult!!.simulate()
|
||||
}
|
||||
|
||||
test("pointer assignment to array element in 6502 backend") {
|
||||
val src = """
|
||||
%option no_sysinit
|
||||
%launcher none
|
||||
%address $1000
|
||||
|
||||
main {
|
||||
ubyte value = 42
|
||||
^^ubyte[2] ptrs
|
||||
|
||||
sub start() {
|
||||
ptrs[1] = &value
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
||||
compileResult shouldNotBe null
|
||||
}
|
||||
})
|
||||
@@ -18,7 +18,8 @@ internal fun compileFile(
|
||||
errors: IErrorReporter? = null,
|
||||
writeAssembly: Boolean = true,
|
||||
varshigh: Int? = null,
|
||||
slabshigh: Int? = null
|
||||
slabshigh: Int? = null,
|
||||
experimentalCodegen: Boolean = false
|
||||
) : CompilationResult? {
|
||||
val filepath = fileDir.resolve(fileName)
|
||||
assumeReadableFile(filepath)
|
||||
@@ -33,7 +34,7 @@ internal fun compileFile(
|
||||
showTimings = false,
|
||||
asmListfile = false,
|
||||
includeSourcelines = false,
|
||||
experimentalCodegen = false,
|
||||
experimentalCodegen = experimentalCodegen,
|
||||
dumpVariables = false,
|
||||
dumpSymbols = false,
|
||||
varsHighBank = varshigh,
|
||||
@@ -67,11 +68,13 @@ internal fun compileText(
|
||||
errors: IErrorReporter? = null,
|
||||
writeAssembly: Boolean = true,
|
||||
varshigh: Int? = null,
|
||||
slabshigh: Int? = null
|
||||
slabshigh: Int? = null,
|
||||
experimentalCodegen: Boolean = false
|
||||
) : CompilationResult? {
|
||||
val filePath = outputDir.resolve("on_the_fly_test_${sourceText.hashCode().toUInt().toString(16)}.p8")
|
||||
// we don't assumeNotExists(filePath) - should be ok to just overwrite it
|
||||
filePath.toFile().writeText(sourceText)
|
||||
return compileFile(platform, optimize, filePath.parent, filePath.name, outputDir,
|
||||
errors=errors, writeAssembly=writeAssembly, varshigh=varshigh, slabshigh=slabshigh)
|
||||
errors=errors, writeAssembly=writeAssembly, varshigh=varshigh, slabshigh=slabshigh,
|
||||
experimentalCodegen = experimentalCodegen)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package prog8tests.optimizer
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.engine.spec.tempdir
|
||||
import io.kotest.matchers.shouldBe
|
||||
import prog8.ast.expressions.BinaryExpression
|
||||
import prog8.ast.expressions.IdentifierReference
|
||||
import prog8.ast.expressions.NumericLiteral
|
||||
import prog8.ast.statements.Assignment
|
||||
import prog8.code.target.Cx16Target
|
||||
import prog8tests.helpers.compileText
|
||||
|
||||
class TestExpressionSimplifier : FunSpec({
|
||||
val outputDir = tempdir().toPath()
|
||||
|
||||
test("long & -1 is correctly simplified") {
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
long @shared x = 1000
|
||||
long @shared y = x & -1
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = compileText(Cx16Target(), true, text, outputDir, writeAssembly = false)!!
|
||||
val s = result.compilerAst.entrypoint.statements
|
||||
val expr = (s[3] as Assignment).value as BinaryExpression
|
||||
expr.operator shouldBe "&"
|
||||
(expr.left as IdentifierReference).nameInSource shouldBe listOf("x")
|
||||
(expr.right as NumericLiteral).number shouldBe -1.0
|
||||
}
|
||||
|
||||
test("long & 255 is correctly handled") {
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
long @shared x = 1000
|
||||
long @shared y = x & 255
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = compileText(Cx16Target(), true, text, outputDir, writeAssembly = false)!!
|
||||
val s = result.compilerAst.entrypoint.statements
|
||||
val expr = (s[3] as Assignment).value as BinaryExpression
|
||||
expr.operator shouldBe "&"
|
||||
(expr.left as IdentifierReference).nameInSource shouldBe listOf("x")
|
||||
(expr.right as NumericLiteral).number shouldBe 255.0
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
@@ -654,7 +661,7 @@ data class AddressOf(var identifier: IdentifierReference?, var arrayIndex: Array
|
||||
val target = this.identifier?.targetStatement()
|
||||
val targetVar = target as? VarDecl
|
||||
if(targetVar!=null) {
|
||||
if (targetVar.type == VarDeclType.MEMORY || targetVar.type == VarDeclType.CONST) {
|
||||
if (targetVar.type == VarDeclType.MEMORY || (targetVar.type == VarDeclType.CONST && arrayIndex != null)) {
|
||||
var address = targetVar.value?.constValue(program)?.number
|
||||
if (address != null) {
|
||||
if (arrayIndex != null) {
|
||||
@@ -719,13 +726,19 @@ class DirectMemoryRead(var addressExpression: Expression, override val position:
|
||||
}
|
||||
|
||||
override fun copy() = DirectMemoryRead(addressExpression.copy(), position)
|
||||
override fun hasSideEffects(target: ICompilationTarget): Boolean = isIORead(target)
|
||||
override fun accept(visitor: IAstVisitor) = visitor.visit(this)
|
||||
override fun accept(visitor: AstWalker, parent: Node)= visitor.visit(this, parent)
|
||||
|
||||
override fun referencesIdentifier(nameInSource: List<String>) = addressExpression.referencesIdentifier(nameInSource)
|
||||
override fun inferType(program: Program) = InferredTypes.knownFor(BaseDataType.UBYTE)
|
||||
override fun constValue(program: Program): NumericLiteral? = null
|
||||
override fun isIORead(target: ICompilationTarget) = addressExpression.isIORead(target)
|
||||
override fun isIORead(target: ICompilationTarget): Boolean {
|
||||
if (addressExpression is NumericLiteral) {
|
||||
return target.isIOAddress((addressExpression as NumericLiteral).number.toUInt())
|
||||
}
|
||||
return true // assume worst case if address is not constant
|
||||
}
|
||||
override fun toString(): String {
|
||||
return "DirectMemoryRead($addressExpression)"
|
||||
}
|
||||
@@ -1207,6 +1220,7 @@ class ArrayLiteral(val type: InferredTypes.InferredType, // inferred because
|
||||
}
|
||||
|
||||
override fun copy(): ArrayLiteral = ArrayLiteral(type, value.map { it.copy() }.toTypedArray(), position)
|
||||
override fun hasSideEffects(target: ICompilationTarget): Boolean = value.any { it.hasSideEffects(target) }
|
||||
override val isSimple = true
|
||||
override fun isIORead(target: ICompilationTarget) = false
|
||||
|
||||
@@ -1222,6 +1236,7 @@ class ArrayLiteral(val type: InferredTypes.InferredType, // inferred because
|
||||
override fun accept(visitor: IAstVisitor) = visitor.visit(this)
|
||||
override fun accept(visitor: AstWalker, parent: Node)= visitor.visit(this, parent)
|
||||
|
||||
@Suppress("KotlinArrayToString")
|
||||
override fun toString(): String = "$value"
|
||||
override fun inferType(program: Program): InferredTypes.InferredType = if(type.isKnown) type else guessDatatype(program)
|
||||
|
||||
@@ -1472,28 +1487,34 @@ data class IdentifierReference(val nameInSource: List<String>, override val posi
|
||||
}
|
||||
else
|
||||
definingScope.lookup(nameInSource)
|
||||
fun targetVarDecl(): VarDecl? {
|
||||
fun targetVarDecl(visitedAliases: MutableSet<Alias> = mutableSetOf()): VarDecl? {
|
||||
// follows aliases
|
||||
val t = targetStatement()
|
||||
return if(t is Alias)
|
||||
t.target.targetVarDecl()
|
||||
else
|
||||
return if (t is Alias) {
|
||||
if (t in visitedAliases) return null
|
||||
visitedAliases.add(t)
|
||||
t.target.targetVarDecl(visitedAliases)
|
||||
} else
|
||||
t as? VarDecl
|
||||
}
|
||||
fun targetSubroutine(): Subroutine? {
|
||||
fun targetSubroutine(visitedAliases: MutableSet<Alias> = mutableSetOf()): Subroutine? {
|
||||
// follows aliases
|
||||
val t = targetStatement()
|
||||
return if(t is Alias)
|
||||
t.target.targetSubroutine()
|
||||
else
|
||||
return if (t is Alias) {
|
||||
if (t in visitedAliases) return null
|
||||
visitedAliases.add(t)
|
||||
t.target.targetSubroutine(visitedAliases)
|
||||
} else
|
||||
t as? Subroutine
|
||||
}
|
||||
fun targetStructDecl(): StructDecl? {
|
||||
fun targetStructDecl(visitedAliases: MutableSet<Alias> = mutableSetOf()): StructDecl? {
|
||||
// follows aliases
|
||||
val t = targetStatement()
|
||||
return if(t is Alias)
|
||||
t.target.targetStructDecl()
|
||||
else
|
||||
return if (t is Alias) {
|
||||
if (t in visitedAliases) return null
|
||||
visitedAliases.add(t)
|
||||
t.target.targetStructDecl(visitedAliases)
|
||||
} else
|
||||
t as? StructDecl
|
||||
}
|
||||
|
||||
@@ -1659,6 +1680,7 @@ class FunctionCallExpression(override var target: IdentifierReference,
|
||||
}
|
||||
|
||||
override fun copy() = FunctionCallExpression(target.copy(), args.map { it.copy() }.toMutableList(), position)
|
||||
override fun hasSideEffects(target: ICompilationTarget): Boolean = true
|
||||
override val isSimple = if (target.nameInSource.singleOrNull() in SimpleBuiltinFunctions) this.args.all { it.isSimple } else false
|
||||
override fun replaceChildNode(node: Node, replacement: Node) {
|
||||
if(node===target)
|
||||
@@ -1711,6 +1733,8 @@ class FunctionCallExpression(override var target: IdentifierReference,
|
||||
val constVal = constValue(program ,false)
|
||||
if(constVal!=null)
|
||||
return InferredTypes.knownFor(constVal.type)
|
||||
if (args.any { it.inferType(program).isUnknown })
|
||||
return InferredTypes.unknown()
|
||||
val stmt = target.targetStatement(program.builtinFunctions) ?: return InferredTypes.unknown()
|
||||
when (stmt) {
|
||||
is BuiltinFunctionPlaceholder -> {
|
||||
@@ -1847,6 +1871,8 @@ class IfExpression(var condition: Expression, var truevalue: Expression, var fal
|
||||
|
||||
override fun copy(): Expression = IfExpression(condition.copy(), truevalue.copy(), falsevalue.copy(), position)
|
||||
|
||||
override fun hasSideEffects(target: ICompilationTarget): Boolean = condition.hasSideEffects(target) || truevalue.hasSideEffects(target) || falsevalue.hasSideEffects(target)
|
||||
|
||||
override fun constValue(program: Program): NumericLiteral? {
|
||||
val cond = condition.constValue(program)
|
||||
if(cond!=null) {
|
||||
@@ -1955,6 +1981,7 @@ class PtrDereference(
|
||||
}
|
||||
|
||||
override val isSimple = false
|
||||
override fun hasSideEffects(target: ICompilationTarget): Boolean = true
|
||||
override fun isIORead(target: ICompilationTarget) = false
|
||||
override fun copy(): PtrDereference = PtrDereference(chain.toList(), derefLast, position)
|
||||
override fun constValue(program: Program): NumericLiteral? {
|
||||
@@ -2070,6 +2097,7 @@ class ArrayIndexedPtrDereference(
|
||||
}
|
||||
|
||||
override val isSimple = false
|
||||
override fun hasSideEffects(target: ICompilationTarget): Boolean = true
|
||||
override fun isIORead(target: ICompilationTarget) = false
|
||||
override fun replaceChildNode(node: Node, replacement: Node) = throw FatalAstException("can't replace here")
|
||||
override fun referencesIdentifier(nameInSource: List<String>) = chain.size==1 && chain==nameInSource
|
||||
|
||||
@@ -840,6 +840,19 @@ data class AssignTarget(
|
||||
|
||||
fun accept(visitor: IAstVisitor) = visitor.visit(this)
|
||||
fun accept(visitor: AstWalker, parent: Node) = visitor.visit(this, parent)
|
||||
|
||||
fun hasSideEffects(target: ICompilationTarget): Boolean {
|
||||
if (void) return false
|
||||
if (identifier != null) {
|
||||
val symbol = definingScope.lookup(identifier!!.nameInSource)
|
||||
if (symbol is VarDecl && symbol.type == VarDeclType.MEMORY && symbol.value is NumericLiteral) {
|
||||
return target.isIOAddress((symbol.value as NumericLiteral).number.toUInt())
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true // memoryAddress, arrayindexed, pointerDereference, arrayIndexedDereference
|
||||
}
|
||||
|
||||
override fun copy() = AssignTarget(
|
||||
identifier?.copy(),
|
||||
arrayindexed?.copy(),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
Prog8 compiler v12.2-BETA2 by Irmen de Jong (irmen@razorvine.net)
|
||||
Prerelease version from git commit 1dbaa6e0 in branch master
|
||||
Prog8 compiler v12.2.1 by Irmen de Jong (irmen@razorvine.net)
|
||||
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
|
||||
|
||||
Compiling program import-all-c128.p8
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
Prog8 compiler v12.2-BETA2 by Irmen de Jong (irmen@razorvine.net)
|
||||
Prerelease version from git commit 1dbaa6e0 in branch master
|
||||
Prog8 compiler v12.2.1 by Irmen de Jong (irmen@razorvine.net)
|
||||
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
|
||||
|
||||
Compiling program import-all-c64.p8
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
Prog8 compiler v12.2-BETA2 by Irmen de Jong (irmen@razorvine.net)
|
||||
Prerelease version from git commit 1dbaa6e0 in branch master
|
||||
Prog8 compiler v12.2.1 by Irmen de Jong (irmen@razorvine.net)
|
||||
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
|
||||
|
||||
Compiling program import-all-cx16.p8
|
||||
@@ -765,14 +764,7 @@ psg2 {
|
||||
const ubyte SAWTOOTH
|
||||
const ubyte SQUARE
|
||||
const ubyte TRIANGLE
|
||||
ubyte[] envelope_attacks
|
||||
ubyte[] envelope_maxvolumes
|
||||
ubyte[] envelope_releases
|
||||
ubyte[] envelope_states
|
||||
ubyte[] envelope_sustains
|
||||
uword[] envelope_volumes
|
||||
^^psg2.Voice voices
|
||||
^^psg2.Voice vptr
|
||||
envelope (ubyte voice_num, ubyte attack, ubyte sustain, ubyte release)
|
||||
frequency (ubyte voice_num, uword freq)
|
||||
getvoice (ubyte voice_num) -> ^^psg2.Voice
|
||||
@@ -788,6 +780,21 @@ LIBRARY MODULE NAME: serial
|
||||
---------------------------
|
||||
|
||||
serial {
|
||||
const uword BAUD::B115200
|
||||
const uword BAUD::B1200
|
||||
const uword BAUD::B14400
|
||||
const uword BAUD::B19200
|
||||
const uword BAUD::B230400
|
||||
const uword BAUD::B2400
|
||||
const uword BAUD::B28800
|
||||
const uword BAUD::B300
|
||||
const uword BAUD::B38400
|
||||
const uword BAUD::B460800
|
||||
const uword BAUD::B4800
|
||||
const uword BAUD::B57600
|
||||
const uword BAUD::B600
|
||||
const uword BAUD::B921600
|
||||
const uword BAUD::B9600
|
||||
const ubyte LSR::BI
|
||||
const ubyte LSR::DR
|
||||
const ubyte LSR::FE
|
||||
@@ -808,14 +815,18 @@ serial {
|
||||
const ubyte REG_SCRATCH
|
||||
const ubyte REG_TXRX_BUFFER
|
||||
detect_uarts () -> uword, uword
|
||||
discard_until (uword uart_addr, str match)
|
||||
initialize_uart (uword uart_addr)
|
||||
read_until (uword uart_addr, str match, ^^ubyte buffer, uword max_size) -> uword
|
||||
write (uword uart_addr, str data)
|
||||
zi_end_get_file ()
|
||||
zi_get_file_chunk (^^ubyte buffer, uword buffer_size, long remaining_file_size) -> uword
|
||||
zi_get_file_chunk_hexmode (^^ubyte buffer, uword buffer_size) -> uword
|
||||
zi_get_ip_address () -> str
|
||||
zi_initialize (uword uart_addr)
|
||||
zi_reset ()
|
||||
zi_start_get_file (str filename) -> long
|
||||
zi_start_get_file_hexmode (str filename) -> bool
|
||||
zi_write_cmd (str command)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
Prog8 compiler v12.2-BETA2 by Irmen de Jong (irmen@razorvine.net)
|
||||
Prerelease version from git commit 1dbaa6e0 in branch master
|
||||
Prog8 compiler v12.2.1 by Irmen de Jong (irmen@razorvine.net)
|
||||
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
|
||||
|
||||
Compiling program import-all-pet32.p8
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
Prog8 compiler v12.2-BETA2 by Irmen de Jong (irmen@razorvine.net)
|
||||
Prerelease version from git commit 1dbaa6e0 in branch master
|
||||
Prog8 compiler v12.2.1 by Irmen de Jong (irmen@razorvine.net)
|
||||
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
|
||||
|
||||
Compiling program import-all-virtual.p8
|
||||
@@ -612,7 +611,12 @@ cx16 {
|
||||
&word r9s
|
||||
&byte r9sH
|
||||
&byte r9sL
|
||||
getrambank () -> ubyte
|
||||
getrombank () -> ubyte
|
||||
numbanks () -> uword
|
||||
rambank (ubyte bank)
|
||||
restore_virtual_registers ()
|
||||
rombank (ubyte bank)
|
||||
save_virtual_registers ()
|
||||
}
|
||||
|
||||
|
||||
+15
-14
@@ -42,7 +42,7 @@ No system initialization and startup code
|
||||
Variable initialization
|
||||
The library still has to initialize any variables it might use and clear
|
||||
uninitialized "BSS" variables! Otherwise the code will not run predictably as prog8 code.
|
||||
So, the library must still have a "start" entrypoint subroutine like any outher prog8 program,
|
||||
So, the library must still have a "start" entrypoint subroutine like any other prog8 program,
|
||||
that must be called before any other library routine can be called.
|
||||
|
||||
Binary output and loaded into a fixed memory address
|
||||
@@ -79,7 +79,7 @@ Jump table
|
||||
For ease of use, libraries should probably have a fixed "jump table" where the offsets of the
|
||||
library routines stay the same across different versions of the library. Without needing new syntax,
|
||||
there's a trick in Prog8 that you can use to build such a jumptable:
|
||||
add a non-splitted word array at the top of the library main block that contains JMP instructions
|
||||
add a non-split word array at the top of the library main block that contains JMP instructions
|
||||
and the addresses of the individual library subroutines. Do NOT change the order of the subroutines
|
||||
in this table!
|
||||
Also note that the Prog8 compiler will insert a single JMP instruction at the very start of the library,
|
||||
@@ -105,13 +105,14 @@ Assuming the load address of the library is $A000:
|
||||
|
||||
BLOAD "LIBRARY.BIN",8,1,$A000
|
||||
SYS $A000 : REM TO INITIALIZE VARIABLES, REQUIRED!
|
||||
SYS $A004 : REM CALL FIRST ROUTINE
|
||||
SYS $A008 : REM CALL SECOND ROUTINE, ETC.
|
||||
SYS $A003 : REM CALL FIRST ROUTINE
|
||||
SYS $A006 : REM CALL SECOND ROUTINE, ETC.
|
||||
|
||||
|
||||
**From Prog8**
|
||||
.. index:: single: Examples; Library from Prog8
|
||||
|
||||
**From Prog8:**
|
||||
|
||||
The ``diskio`` module actually provides a convenience routine called ``loadlib`` that loads a Prog8-compiled
|
||||
library blob into memory. It internally automatically uses either regular load() or load_raw(),
|
||||
as required by the compilation target (so you don't have to bother with target machine
|
||||
@@ -122,8 +123,8 @@ differences if you want to write portable code)::
|
||||
main {
|
||||
|
||||
extsub $A000 = lib_init() clobbers(A)
|
||||
extsub $A004 = lib_func1() clobbers(A,X,Y)
|
||||
extsub $A008 = lib_func2() clobbers(A,X,Y)
|
||||
extsub $A003 = lib_func1() clobbers(A,X,Y)
|
||||
extsub $A006 = lib_func2() clobbers(A,X,Y)
|
||||
|
||||
sub start() {
|
||||
if diskio.loadlib("library.bin", $a000) != 0 {
|
||||
@@ -144,12 +145,12 @@ differences if you want to write portable code)::
|
||||
|
||||
int main() {
|
||||
void (*lib_init)(void) = (void (*)()) 0xa000;
|
||||
void (*lib_func1)(void) = (void (*)()) 0xa004;
|
||||
void (*lib_func2)(void) = (void (*)()) 0xa008;
|
||||
void (*lib_func1)(void) = (void (*)()) 0xa003;
|
||||
void (*lib_func2)(void) = (void (*)()) 0xa006;
|
||||
|
||||
cbm_k_setlfs(0, 8, 2);
|
||||
cbm_k_setnam("library.bin");
|
||||
cbm_k_load(0, 0xa000);
|
||||
cbm_k_setlfs(0, 8, 2);
|
||||
cbm_k_setnam("library.bin");
|
||||
cbm_k_load(0, 0xa000);
|
||||
|
||||
lib_init();
|
||||
lib_func1();
|
||||
@@ -178,8 +179,8 @@ differences if you want to write portable code)::
|
||||
jsr $ffd2 ; CHROUT
|
||||
|
||||
jsr $A000 ; library init
|
||||
jsr $A004 ; lib func 1
|
||||
jsr $A008 ; lib func 2
|
||||
jsr $A003 ; lib func 1
|
||||
jsr $A006 ; lib func 2
|
||||
|
||||
rts
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ The language
|
||||
- It is meant to sit well above low level assembly code, but still allows that low level access to the system it runs on.
|
||||
Via language features, or even simply by using inline hand-written assembly code.
|
||||
- Prog8 is targeting very CPU and memory constrained 8-bit systems, this reflects many design choices to work within those limitations
|
||||
(single digit Megaherz cpu clock speeds, and memory capacity counted in Kilobytes)
|
||||
(single digit Megahertz cpu clock speeds, and memory capacity counted in Kilobytes)
|
||||
- Identifiers and string literals can contain non-ASCII characters so for example ``knäckebröd`` and ``見せしめ`` are valid identifiers.
|
||||
- There's usually a single statement per line. There is no statement separator.
|
||||
- Semicolon ``;`` is used to start a line comment. Multi-line comments are also possible by enclosing it all in ``/*`` and ``*/``.
|
||||
@@ -43,7 +43,7 @@ Data types
|
||||
- **There is no automatic type enlargement:** all calculations remain within the data type of the operands. Any overflow silently wraps or truncates.
|
||||
You'll have to add explicit casts to increase the size of the value if required.
|
||||
For example when adding two byte variables having values 100 and 200, the result won't be 300, because that doesn't fit in a byte. It will be 44.
|
||||
You'll have to cast one or both of the *operands* to a word type first if you want to accomodate the actual result value of 300.
|
||||
You'll have to cast one or both of the *operands* to a word type first if you want to accommodate the actual result value of 300.
|
||||
Similarly, ``long v = w1 * w2`` doesn't automatically give you the full 32 bits multiplication result, instead it is still constrained in the word range.
|
||||
If you need the full 32 bits result you'll have to call a specialized routine such as ``math.mul32`` or ``math.mul16_last_upper()``.
|
||||
- strings and arrays are allocated once, statically, and never resized.
|
||||
@@ -66,7 +66,7 @@ Variables
|
||||
are moved to the top of the subroutine. A for loop, or if/else blocks do not introduce a new scope.
|
||||
A subroutine (also nested ones) *do* introduce a new scope.
|
||||
- All variables are initialized at the start of the program. There is no random garbage in them: they are zero or any other initialization value you provide.
|
||||
- This als means you can run a Prog8 program multiple times without having to reload it from disk, unlike programs produced by most other compilers targeting these 8 bit platforms.
|
||||
- This also means you can run a Prog8 program multiple times without having to reload it from disk, unlike programs produced by most other compilers targeting these 8 bit platforms.
|
||||
|
||||
|
||||
Subroutines
|
||||
@@ -102,7 +102,7 @@ Pointers and Structs
|
||||
Variables of the ``uword`` datatype can be used as a pointer to one of the possible 65536 memory locations,
|
||||
so the value it points to is always a single byte. This is similar to ``uint8_t*`` from C.
|
||||
You have to deal with the uword manually if the object it points to is something different.
|
||||
- Note that there is the ``peekw`` builtin function that *does* allow you to directy obtain the *word* value at the given memory location.
|
||||
- Note that there is the ``peekw`` builtin function that *does* allow you to directly obtain the *word* value at the given memory location.
|
||||
So if you use this, you can use uword pointers as pointers to word values without much hassle.
|
||||
- "dereferencing" a uword pointer is done via array indexing ``ptr[index]`` (where index value can be 0-65535!) or via the memory read operator ``@(ptr)``, or ``peek/peekw(ptr)``.
|
||||
- Pointers don't have to be a variable, you can immediately access the value of a given memory location using ``@($d020)`` for instance.
|
||||
@@ -124,15 +124,14 @@ Foreign function interface (external/ROM calls)
|
||||
.. index:: single: Subroutines; Foreign/ROM
|
||||
|
||||
- You can use the ``extsub`` keyword to define the call signature of foreign functions (ROM routines or external routines elsewhere in RAM) in a natural way.
|
||||
Calling those generates code that is as efficient or even more efficient as calling regular subroutines.
|
||||
No additional stubs are needed. Y
|
||||
Calling those generates code that is as efficient or even more efficient as calling regular subroutines. No additional stubs are needed.
|
||||
- High level support of memory banking: an ``extsub`` can be defined with the memory bank number (constant or variable) where the routine is located in,
|
||||
and then when you call it as usual, the compiler takes care of the required bank switching.
|
||||
|
||||
Optimizations
|
||||
-------------
|
||||
- Prog8 contains many compiler optimizations to generate efficient code, but also lacks many optimizations that modern compilers do have.
|
||||
While empirical evidence shows that Prog8 generates more efficent code than some C compilers that also target the same 8 bit systems,
|
||||
While empirical evidence shows that Prog8 generates more efficient code than some C compilers that also target the same 8 bit systems,
|
||||
the optimizations it makes on your code aren't super sophisticated.
|
||||
- For time critical code, it may be worth it to inspect the generated assembly code to see if you can write things differently
|
||||
to help the compiler generate more efficient code (or even replace it with hand written inline assembly altogether).
|
||||
|
||||
+13
-11
@@ -23,7 +23,7 @@ Then you can choose a few ways to get a compiler:
|
||||
|
||||
Arch Linux:
|
||||
Currently, it's available on `AUR <https://wiki.archlinux.org/title/Arch_User_Repository>`_ for Arch Linux and derivative systems.
|
||||
The package is called `"prog8" <https://aur.archlinux.org/packages/prog8>`_. There should be no need to install aything else as
|
||||
The package is called `"prog8" <https://aur.archlinux.org/packages/prog8>`_. There should be no need to install anything else as
|
||||
it can automatically pull in the required dependencies.
|
||||
|
||||
This package, alongside the compiler itself, also globally installs syntax highlighting for ``vim`` and ``nano``.
|
||||
@@ -65,12 +65,12 @@ The most interesting gradle commands to run are probably the ones listed below.
|
||||
``./gradlew installShadowDist``
|
||||
Creates a 'fat-jar' that contains the compiler and all dependencies, in a single
|
||||
executable .jar file, and includes few start scripts to run it.
|
||||
The output can be found in ``.compiler/build/install/prog8c-shadow/``
|
||||
The output can be found in ``./compiler/build/install/prog8c-shadow/``
|
||||
``./gradlew shadowDistZip``
|
||||
Creates a zipfile with the above in it, for easy distribution.
|
||||
This file can be found in ``./compiler/build/distributions/``
|
||||
|
||||
For normal use, the ``installDist`` task should suffice and after succesful completion, you can start the compiler with:
|
||||
For normal use, the ``installDist`` task should suffice and after successful completion, you can start the compiler with:
|
||||
|
||||
``./compiler/build/install/prog8c/bin/prog8c <options> <sourcefile>``
|
||||
|
||||
@@ -163,7 +163,7 @@ One or more .p8 module files
|
||||
This can be useful on emulators/systems that don't parse the breakpoint information in the list file,
|
||||
such as the X16Emu emulator for the Commander X16.
|
||||
Useful instructions to consider are ``brk`` and ``stp``.
|
||||
For example for the Commander X16 emulator, ``stp`` is useful because it can actually tyrigger
|
||||
For example for the Commander X16 emulator, ``stp`` is useful because it can actually trigger
|
||||
a breakpoint halt in the debugger when this is enabled by running the emulator with -debug.
|
||||
|
||||
``-bytes2float <bytes>``
|
||||
@@ -219,7 +219,9 @@ One or more .p8 module files
|
||||
With this option you're basically saying: "Yes, I know I am treading on mighty thin ice here, but I don't want to be reminded about that".
|
||||
|
||||
``-libsearch pattern``
|
||||
Search in the embedded library files for occurences of the given regex pattern.
|
||||
Search in the embedded library files for occurrences of the given regex pattern.
|
||||
If no exact matches are found, the compiler automatically attempts a "fuzzy" search
|
||||
by inserting wildcards between the characters of the search pattern.
|
||||
|
||||
``-libdump path``
|
||||
Extract all embedded library files into the given output directory.
|
||||
@@ -269,7 +271,7 @@ One or more .p8 module files
|
||||
|
||||
``-nosourcelines``
|
||||
Do not include the original prog8 source code lines as comments in the generated assembly code file,
|
||||
mixed in between the actual generated assembly code. The default behavior is to include the sourcel lines.
|
||||
mixed in between the actual generated assembly code. The default behavior is to include the source lines.
|
||||
|
||||
``-srcdirs <pathlist>``
|
||||
Specify a list of extra paths (separated with ':'), to search in for imported modules.
|
||||
@@ -280,7 +282,7 @@ One or more .p8 module files
|
||||
Sets the target output of the compiler. This option is required.
|
||||
``c64`` = Commodore 64, ``c128`` = Commodore 128, ``cx16`` = Commander X16, ``pet32`` - Commodore PET model 4032,
|
||||
``virtual`` = builtin virtual machine.
|
||||
You can also specify a file name as target, prog8 will when try to read the target
|
||||
You can also specify a file name as target, prog8 will then try to read the target
|
||||
machine's configuration and properties from that configuration file instead of using one of the built-in targets.
|
||||
See :ref:`customizable_target` for details about this.
|
||||
|
||||
@@ -303,7 +305,7 @@ One or more .p8 module files
|
||||
cx16: $A000 - $BFFF ; 8 kB in the specified HIRAM bank (note: no auto bank switching is done, you must make sure yourself that this HIRAM bank is active when accessing these variables!)
|
||||
|
||||
If you use this option, you can no longer use the part of the above memory area that is
|
||||
alotted to the variables, for your own purposes. The output of the 64tass assembler step at the
|
||||
allotted to the variables, for your own purposes. The output of the 64tass assembler step at the
|
||||
end of compilation shows precise details of where and how much memory is used by the variables
|
||||
(it's called 'BSS' section or Gap at the address mentioned above).
|
||||
Assembling the program will fail if there are too many variables to fit in a single high ram bank.
|
||||
@@ -435,7 +437,7 @@ WARNING
|
||||
a compiled program is still produced, but it is a good idea to investigate the warnings that are printed.
|
||||
|
||||
ERROR
|
||||
unrecoverable problem that prevented succesful compilation
|
||||
unrecoverable problem that prevented successful compilation
|
||||
|
||||
|
||||
Compiler doesn't run, complains about "UnsupportedClassVersionError"
|
||||
@@ -486,7 +488,7 @@ you don't use symbols named just a single letter (especially 'a', 'x' and 'y').
|
||||
Sometimes these are interpreted as the CPU register of that name. To avoid such confusions,
|
||||
always use 2 or more letters for symbols in your assembly code.
|
||||
|
||||
'shadowing' warnings form the assembler
|
||||
'shadowing' warnings from the assembler
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Avoid using 'a', 'x' or 'y' as symbols in your inlined assembly code.
|
||||
Also avoid using 64tass' built-in function or type names as symbols in your inlined assembly code.
|
||||
@@ -513,7 +515,7 @@ Examples
|
||||
|
||||
.. index:: single: Examples; Examples in source tree
|
||||
|
||||
A bunch of example programs can be found in the 'examples' directory of the source tree on Github.
|
||||
A bunch of example programs can be found in the :source:`examples` directory of the source tree on Github.
|
||||
(Or download the source archive from there to get all the files at once).
|
||||
|
||||
There are cross-platform examples that can be compiled for various systems unaltered,
|
||||
|
||||
+21
-3
@@ -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.*
|
||||
@@ -8,7 +8,7 @@ Prog8 documentation - |version|
|
||||
|
|
||||
|
|
||||
|
||||
Prog8 is is a compiled programming language targeting the 8-bit 6502 CPU family.
|
||||
Prog8 is a compiled programming language targeting the 8-bit 6502 CPU family.
|
||||
It aims to provide many conveniences over raw assembly code (even when using a macro assembler),
|
||||
while still being low level enough to create high performance programs.
|
||||
|
||||
|
||||
@@ -10,17 +10,17 @@ while still being low level enough to create high performance programs.
|
||||
The targeted CPUs are the `6502 <https://en.wikipedia.org/wiki/MOS_Technology_6502>`_ /
|
||||
`6510 <https://en.wikipedia.org/wiki/MOS_Technology_6510>`_ /
|
||||
`65c02 <https://en.wikipedia.org/wiki/MOS_Technology_65C02>`_ microprocessors.
|
||||
They are from the late 1970's and early 1980's and was used in many home computers from that era,
|
||||
They are from the late 1970's and early 1980's and were used in many home computers from that era,
|
||||
such as the `Commodore 64 <https://en.wikipedia.org/wiki/Commodore_64>`_.
|
||||
|
||||
You can compile programs for various machines that are built in into the compiler:
|
||||
You can compile programs for various machines that are built into the compiler:
|
||||
|
||||
* Commander X16 (with 65c02 cpu, 65816 cpu specifics are currently not supported by prog8 itself)
|
||||
* Commodore 64
|
||||
* Commodore 128 (limited support)
|
||||
* Commodore PET (limited support)
|
||||
* any other 65(C)02 target machine or setup can be configured to a great extent in a user written configuration file.
|
||||
There are some examples included for the Atari 800 XL, NEO6502, Foenix F256, and such.
|
||||
There are some for the Atari 800 XL, NEO6502, Foenix F256, and such.
|
||||
* some users have been experimenting with a NES and a C64 OS target as well.
|
||||
|
||||
Some language features are mentioned below, and you can also read :ref:`comparingprog8` if you
|
||||
@@ -79,6 +79,7 @@ Language :index:`Features`
|
||||
the compiled program in an emulator and provide debugging information to this emulator.
|
||||
- the language looks like a mix of Python and C so should be quite easy to learn
|
||||
- Modular programming, scoping via module source files, code blocks, and subroutines. No need for forward declarations.
|
||||
- Can make vars and routines private, as a simple way to restrict access and hide implementation details
|
||||
- Provides high level programming constructs but at the same time stay close to the metal;
|
||||
still able to directly use memory addresses and ROM subroutines,
|
||||
and inline assembly to have full control when every register, cycle or byte matters
|
||||
@@ -227,7 +228,7 @@ To use it, configure your editor to launch ``prog8-language-server`` as the LSP
|
||||
|
||||
.. index:: pair: Tools; CTags
|
||||
|
||||
**CTags:** for editors that support them, such as Vim, there is an effort by a member of the cummunity to provide "ctags" files for Prog8.
|
||||
**CTags:** for editors that support them, such as Vim, there is an effort by a member of the community to provide "ctags" files for Prog8.
|
||||
The basic purpose of ctags is to index the definitions from your source code and let you find them easily.
|
||||
It is not really to find each use of print for example, but to find where print is defined.
|
||||
Visit `the Github repository <https://github.com/gillham/prog8ctags>`_ for the files and detailed usage information.
|
||||
|
||||
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user