mirror of
https://github.com/irmen/prog8.git
synced 2026-10-22 20:02:27 +00:00
Compare commits
64
Commits
v12.2-beta
...
v12.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
02a1deb1e0 | ||
|
|
3ba8e5de10 | ||
|
|
819d1b4af7 | ||
|
|
cdd3d37376 | ||
|
|
d0bc01d618 | ||
|
|
ffab73334a | ||
|
|
83aeb8846c | ||
|
|
3a2087310b | ||
|
|
61b616701e | ||
|
|
bddf872906 | ||
|
|
48214a6fdb | ||
|
|
b7340856df | ||
|
|
2685854113 | ||
|
|
ad92f39469 | ||
|
|
d3fe041e01 | ||
|
|
f32bac5004 | ||
|
|
9a8e4ac105 | ||
|
|
78857e5c22 | ||
|
|
1bb828875d | ||
|
|
82aa67ffda | ||
|
|
7c44835772 | ||
|
|
8b9c63fe90 | ||
|
|
e77fcc8c16 | ||
|
|
08c12131d4 |
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.
|
||||
|
||||
@@ -49,6 +52,7 @@ You are writing **6502/65C02 assembly** using **64tass syntax**, in separate `*.
|
||||
- **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`
|
||||
- Check the target before using 65C02-specific instructions
|
||||
- **6502 / 65C02 instruction reference**: https://www.pagetable.com/c64ref/6502/?cpu=65c02&tab=2
|
||||
|
||||
## Calling Convention / Register Conventions
|
||||
- **Accumulator (A)**: 8-bit, used for most arithmetic, data movement, return values
|
||||
@@ -164,3 +168,8 @@ loop ; do work here
|
||||
lda p8v_myword ; loads LSB
|
||||
ldy p8v_myword+1 ; loads MSB (word variables are stored LSB-first)
|
||||
```
|
||||
|
||||
## 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
|
||||
@@ -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,8 +21,10 @@ 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
|
||||
- **`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 +35,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 +79,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
|
||||
@@ -76,7 +94,12 @@ No call stack for variable storage — recursion overwrites locals. To handle it
|
||||
- CPU status flag branches: `if_cs`, `if_cc`, `if_z`, `if_nz` (compile to single 6502 branch instructions)
|
||||
- Use `when` with choice blocks instead of multiple `if`
|
||||
- If-expressions for simple value assignments based on a choice
|
||||
- `defer` defers statement execution until scope exit
|
||||
- **Optional braces in if/else**: when the `if` or `else` body is a single statement, the `{ }` can be omitted. Place the statement on the next line, indented. Example:
|
||||
```
|
||||
if x < 5
|
||||
txt.print("small")
|
||||
```
|
||||
- `defer` defers statement execution until scope exit. Multiple defers fire in **reverse registration order** (LIFO / stack order — last deferred runs first). A defer is only registered if execution reaches that statement — conditional paths that skip the `defer` line will not register it.
|
||||
- `goto`, labels, jump lists allowed
|
||||
- **Common mistake**: `and`/`or` for bitmasking — use `&`/`|` instead!
|
||||
|
||||
@@ -164,30 +187,56 @@ unroll 80 {
|
||||
- Nested subroutines access parent scope variables directly
|
||||
|
||||
## Assembly Subroutines (asmsub)
|
||||
For kernel (ROM) routines or low-level assembly that gets arguments via registers.
|
||||
- Parameter passing via registers: `@A` (accu), `@X`, `@Y`, `@AX`/`@AY` (16-bit), `@R0`-`@R15` (virtual regs), `@FAC1`/`@FAC2` (float), `@Pc` (carry), `@Pz` (zero)
|
||||
- Return value: `-> type @register` (e.g., `-> ubyte @A`, `-> bool @Pz` for immediate branch use)
|
||||
- Clobbers: `clobbers (A, X, Y)` — list all modified registers
|
||||
- Parameter names are documentation only — use registers in assembly, not parameter names
|
||||
- Create symbolic aliases at assembly start: `x1 = cx16.r0`, `x1_lo = cx16.r0L`
|
||||
- Example:
|
||||
For low-level assembly that gets arguments via registers and returns values in registers.
|
||||
- **Syntax**: `[private] [inline] asmsub subname(params) [clobbers(regs)] [-> returns] { %asm {{ ... }} }`
|
||||
- **Body Restriction**: The body of an `asmsub` **must only contain** a single `%asm {{ ... }}` node. Regular Prog8 statements or nested blocks are NOT allowed.
|
||||
- **Parameters**: `type name @register` (e.g., `ubyte val @A`, `uword addr @AX`, `float f @FAC1`).
|
||||
- **Return Values**: `-> type @register` (e.g., `-> ubyte @A`, `-> bool @Pz` for immediate branch use).
|
||||
- **Registers**:
|
||||
- **8-bit**: `@A`, `@X`, `@Y`
|
||||
- **16-bit**: `@AX`, `@AY`, `@XY` (register pairs)
|
||||
- **Float**: `@FAC1`, `@FAC2` (Floating Point Accumulators)
|
||||
- **Virtual**: `@R0`–`@R15` (16-bit), `@R0R1`–`@R14R15` (32-bit combined)
|
||||
- **Status Flags (for returns)**: `@Pc` (Carry), `@Pz` (Zero), `@Pv` (Overflow), `@Pn` (Negative)
|
||||
- **Clobbers**: `clobbers (A, X, Y)` — list all hardware registers modified by the routine.
|
||||
- **Parameter names** are for documentation and type checking only. Use the registers in your assembly code.
|
||||
- **Inlining**: `inline asmsub` will paste the assembly code directly at the call site, avoiding `jsr`/`rts` overhead.
|
||||
|
||||
## External Subroutines (extsub)
|
||||
Used to call routines at fixed memory addresses (like ROM KERNAL routines or third-party drivers).
|
||||
- **Syntax**: `[private] extsub [@bank <value>] address = subname(params) [clobbers(regs)] [-> returns]`
|
||||
- **Address**: Can be a hex literal (`$C000`) or a constant expression.
|
||||
- **Bank (optional)**: `@bank <integer>` (constant bank) or `@bank <identifier>` (variable bank).
|
||||
- **No Body**: Unlike `asmsub`, `extsub` has no `{ }` body; it just maps a signature to an address.
|
||||
- **Example**:
|
||||
```prog8
|
||||
asmsub line(uword x1 @R0, ubyte y1 @A, uword x2 @R1, ubyte y2 @Y) clobbers (A, X, Y) {
|
||||
; CX16 KERNAL CHROUT
|
||||
extsub $FFD2 = chrout(ubyte char @A) clobbers(A, X, Y)
|
||||
|
||||
; Routine in a specific RAM bank
|
||||
extsub @bank 10 $C09F = audio_init() clobbers(A, X, Y) -> bool @Pc
|
||||
```
|
||||
|
||||
## Assembly Programming Details
|
||||
- **Symbol prefixes**: `p8v_` (variables), `p8s_` (subroutines), `p8b_` (blocks), `p8c_` (constants), `p8l_` (labels), `p8t_` (structs), `p8_` (other)
|
||||
- **Fully qualified names**: `p8b_blockname.p8v_varname`, `p8b_blockname.p8s_subname.p8v_localvar`
|
||||
- **Within a `.proc`**, short names often work. `%option no_symbol_prefixing` disables prefixes (used by `cbm`, `cx16`, `txt`)
|
||||
- **Split word arrays**: append `_lsb` / `_msb` to variable name (e.g., `p8v_myarray_lsb`)
|
||||
- **CX16 target**: use 65C02 instructions (STZ, PHX, etc). Others: 6502 only
|
||||
- **Assembly syntax**: 64tass. `.proc`/`.pend`, `_label` for locals, `.byte`/`.word`/`.dword` for data, `=` for equates
|
||||
- **Instructions** like `rol`, `ror`, `asl`, `lsr` require explicit operand: `rol a`
|
||||
- **Anonymous labels**: `+` (forward), `-` (backward), branch with `+`, `++`, `+++` or `-`, `--`, `---`
|
||||
- **Register Aliases**: It's common to define aliases at the start of an `asmsub`:
|
||||
```prog8
|
||||
asmsub my_routine(uword ptr @AX) {
|
||||
%asm {{
|
||||
x1 = cx16.r0
|
||||
x2 = cx16.r1
|
||||
lda x1
|
||||
ptr_lo = p8zp_scratch_w1
|
||||
ptr_hi = p8zp_scratch_w1+1
|
||||
sta ptr_lo
|
||||
stx ptr_hi
|
||||
}}
|
||||
}
|
||||
```
|
||||
- Symbol prefixes in assembly: `p8v_` (variables), `p8s_` (subroutines), `p8b_` (blocks), `p8c_` (constants), `p8l_` (labels), `p8t_` (structs), `p8_` (other)
|
||||
- Fully qualified: `p8b_blockname.p8v_varname`, `p8b_blockname.p8s_subname.p8v_localvar`
|
||||
- Within a `.proc`, short names often work. `%option no_symbol_prefixing` disables prefixes (used by `cbm`, `cx16`, `txt`)
|
||||
- Split word arrays: append `_lsb` / `_msb` to variable name (e.g., `p8v_myarray_lsb`)
|
||||
- CX16 target only: use 65C02 instructions (STZ etc). Others: 6502 only
|
||||
- Assembly syntax: 64tass assembler. `.proc`/`.pend`, `_label` for locals, `.byte`/`.word`/`.dword` for data, `=` for equates
|
||||
- Instructions like `rol`, `ror`, `asl`, `lsr` require explicit operand: `rol a`, not just `rol`
|
||||
- Anonymous labels: `+` (forward), `-` (backward), branch with `+`, `++`, `+++` or `-`, `--`, `---`
|
||||
|
||||
## Standard Library
|
||||
- Find routines, functions, variables, modules and signatures in the symbol dump file for the given compilation target.
|
||||
@@ -207,15 +256,11 @@ For kernel (ROM) routines or low-level assembly that gets arguments via register
|
||||
- **No `elif`**: use nested `else { if ... }`
|
||||
- Type casting: `expression as type` (e.g., `bytevar as word`). `as` has very low precedence (lower than arithmetic)
|
||||
- **No automatic type widening**: `byte*byte=byte` (overflow possible!). Cast explicitly
|
||||
- **No block scope**: `for`/`if` don't introduce scope. Only subroutines do. Variables in blocks are hoisted to subroutine level
|
||||
- **No bare `{ }` blocks** like C/Java
|
||||
- Indentation: 4 spaces for .p8 and .asm files (no tabs)
|
||||
- Character encoding: 6502 targets use PETSCII by default (call `txt.lowercase()` at start for lowercase). Virtual target uses ISO (`%encoding iso` + `txt.iso()`)
|
||||
- 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
|
||||
@@ -10,29 +10,30 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: build and install recent 64tass
|
||||
- name: install 64tass
|
||||
run: |
|
||||
sudo apt-get install -y make build-essential
|
||||
git clone --depth=1 https://github.com/irmen/64tass
|
||||
cd 64tass
|
||||
make -j4
|
||||
sudo make install
|
||||
sudo apt update
|
||||
sudo apt install 64tass
|
||||
64tass --version
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
- name: 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
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: prog8-compiler-jar-zipped
|
||||
path: |
|
||||
|
||||
+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
@@ -14,6 +14,6 @@
|
||||
<option name="additionalArguments" value="-jvm-default=no-compatibility" />
|
||||
</component>
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="2.3.20" />
|
||||
<option name="version" value="2.3.21" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+10
-10
@@ -1,23 +1,23 @@
|
||||
<component name="libraryTable">
|
||||
<library name="KotlinJavaRuntime" type="repository">
|
||||
<properties maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.3.20" />
|
||||
<properties maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.3.21" />
|
||||
<CLASSES>
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.20/kotlin-stdlib-jdk8-2.3.20.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.20/kotlin-stdlib-2.3.20.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.21/kotlin-stdlib-jdk8-2.3.21.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.21/kotlin-stdlib-2.3.21.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.3.20/kotlin-stdlib-jdk7-2.3.20.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.3.21/kotlin-stdlib-jdk7-2.3.21.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC>
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.20/kotlin-stdlib-jdk8-2.3.20-javadoc.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.20/kotlin-stdlib-2.3.20-javadoc.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.21/kotlin-stdlib-jdk8-2.3.21-javadoc.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.21/kotlin-stdlib-2.3.21-javadoc.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-javadoc.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.3.20/kotlin-stdlib-jdk7-2.3.20-javadoc.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.3.21/kotlin-stdlib-jdk7-2.3.21-javadoc.jar!/" />
|
||||
</JAVADOC>
|
||||
<SOURCES>
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.20/kotlin-stdlib-jdk8-2.3.20-sources.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.20/kotlin-stdlib-2.3.20-sources.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.21/kotlin-stdlib-jdk8-2.3.21-sources.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.21/kotlin-stdlib-2.3.21-sources.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-sources.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.3.20/kotlin-stdlib-jdk7-2.3.20-sources.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.3.21/kotlin-stdlib-jdk7-2.3.21-sources.jar!/" />
|
||||
</SOURCES>
|
||||
</library>
|
||||
</component>
|
||||
Generated
+4
-4
@@ -1,14 +1,14 @@
|
||||
<component name="libraryTable">
|
||||
<library name="github.irmen.ksim65" type="repository">
|
||||
<properties maven-id="com.github.irmen:ksim65:v2.0" />
|
||||
<properties maven-id="com.github.irmen:ksim65:v2.1" />
|
||||
<CLASSES>
|
||||
<root url="jar://$MAVEN_REPOSITORY$/com/github/irmen/ksim65/v2.0/ksim65-v2.0.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/com/github/irmen/ksim65/v2.1/ksim65-v2.1.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.20/kotlin-stdlib-jdk8-2.3.20.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.20/kotlin-stdlib-2.3.20.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.20/kotlin-stdlib-jdk8-2.3.20.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.3.20/kotlin-stdlib-jdk7-2.3.20.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
</component>
|
||||
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)
|
||||
|
||||
@@ -61,8 +61,7 @@ life {
|
||||
math.rndseed(12345,9999)
|
||||
uword offset = STRIDE+1
|
||||
ubyte x
|
||||
ubyte y
|
||||
for y in 0 to HEIGHT-1 {
|
||||
repeat HEIGHT {
|
||||
for x in 0 to WIDTH-1 {
|
||||
active_world[offset+x] = math.rnd() & 1
|
||||
}
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") version "2.3.20"
|
||||
kotlin("jvm") version "2.3.21"
|
||||
}
|
||||
|
||||
allprojects {
|
||||
@@ -53,7 +53,7 @@ subprojects {
|
||||
// This manages all Kotest module versions centrally
|
||||
dependencies {
|
||||
testImplementation(platform("io.kotest:kotest-bom:5.9.1"))
|
||||
// implementation("com.github.irmen:ksim65:v2.0")
|
||||
// implementation("com.github.irmen:ksim65:v2.1")
|
||||
}
|
||||
|
||||
// Common sourceSets configuration
|
||||
|
||||
@@ -173,6 +173,7 @@ val BuiltinFunctions: Map<String, FSignature> = mapOf(
|
||||
"rsave" to FSignature(false, emptyArray()),
|
||||
"rrestore" to FSignature(false, emptyArray()),
|
||||
"memory" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("name", BaseDataType.STR), FParam("size", BaseDataType.UWORD), FParam("alignment", BaseDataType.UWORD)),
|
||||
"memory__ref" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("name", BaseDataType.STR)),
|
||||
"callfar" to FSignature(false, arrayOf(BaseDataType.UWORD), FParam("bank", BaseDataType.UBYTE), FParam("address", BaseDataType.UWORD), FParam("arg", BaseDataType.UWORD)),
|
||||
"callfar2" to FSignature(false, arrayOf(BaseDataType.UWORD), FParam("bank", BaseDataType.UBYTE), FParam("address", BaseDataType.UWORD), FParam("argA", BaseDataType.UBYTE), FParam("argX", BaseDataType.UBYTE), FParam("argY", BaseDataType.UBYTE), FParam("argC", BaseDataType.BOOL)),
|
||||
"call" to FSignature(false, arrayOf(BaseDataType.UWORD), FParam("address", BaseDataType.UWORD)),
|
||||
|
||||
@@ -13,9 +13,10 @@ import kotlin.io.path.Path
|
||||
|
||||
object ImportFileSystem {
|
||||
|
||||
val userHome: Path get() = Path.of(System.getProperty("user.home"))
|
||||
|
||||
fun expandTilde(path: String): String = if (path.startsWith("~")) {
|
||||
val userHome = System.getProperty("user.home")
|
||||
userHome + path.drop(1)
|
||||
userHome.toString() + path.drop(1)
|
||||
} else {
|
||||
path
|
||||
}
|
||||
@@ -35,6 +36,11 @@ object ImportFileSystem {
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCaches() {
|
||||
cache.clear()
|
||||
lineSpanCache.clear()
|
||||
}
|
||||
|
||||
fun retrieveSourceLine(position: Position): String {
|
||||
if(SourceCode.isLibraryResource(position.file)) {
|
||||
val key = SourceCode.withoutPrefix(position.file)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -613,9 +613,12 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
lda $var1+3
|
||||
sbc $var2+3""")
|
||||
} else {
|
||||
// cmp() doesn't return a value and as such can't be used in an expression, so no need to save the temp registers' original values
|
||||
assignAsmGen.assignExpressionToRegister(arg2, RegisterOrPair.R14R15, true)
|
||||
if(!assignAsmGen.isRightTrivial(arg1))
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R14R15, 1)
|
||||
assignAsmGen.assignExpressionToRegister(arg1, RegisterOrPair.R12R13, true)
|
||||
if(!assignAsmGen.isRightTrivial(arg1))
|
||||
asmgen.popLongRegisters(RegisterOrPair.R14R15, 1)
|
||||
asmgen.out("""
|
||||
sec
|
||||
lda cx16.r12
|
||||
@@ -1846,7 +1849,7 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
if(tryOptimizedLoopCopyWithY("", "$address"))
|
||||
return emptyArray()
|
||||
|
||||
asmgen.assignVariableToRegister(address.toString(), RegisterOrPair.R14R15, null, fcall.position)
|
||||
asmgen.assignVariableToRegister(address.toString(), RegisterOrPair.R14R15, null, fcall.position, true)
|
||||
return arrayOf(RegisterOrPair.R14R15)
|
||||
}
|
||||
|
||||
@@ -1875,7 +1878,7 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
if(tryOptimizedLoopCopyWithY("lda #<$varname+$offset | sta P8ZP_SCRATCH_W1 | lda #>$varname+$offset | sta P8ZP_SCRATCH_W1+1", "(P8ZP_SCRATCH_W1)"))
|
||||
return emptyArray()
|
||||
|
||||
asmgen.assignVariableToRegister("$varname+$offset", RegisterOrPair.R14R15, null, fcall.position)
|
||||
asmgen.assignVariableToRegister("$varname+$offset", RegisterOrPair.R14R15, null, fcall.position, true)
|
||||
return arrayOf(RegisterOrPair.R14R15)
|
||||
} else if(result.second is PtIdentifier) {
|
||||
val offsetname = asmgen.asmVariableName(result.second as PtIdentifier)
|
||||
@@ -1996,7 +1999,11 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
}
|
||||
fcall.type.isLong -> {
|
||||
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.R12R13, true) // left
|
||||
if(!assignAsmGen.isRightTrivial(fcall.args[1]))
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
asmgen.assignExpressionToRegister(fcall.args[1], RegisterOrPair.R14R15, true) // right
|
||||
if(!assignAsmGen.isRightTrivial(fcall.args[1]))
|
||||
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
asmgen.out("""
|
||||
lda cx16.r12
|
||||
cmp cx16.r14
|
||||
@@ -2095,7 +2102,11 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
}
|
||||
fcall.type.isLong -> {
|
||||
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.R12R13, true) // left
|
||||
if(!assignAsmGen.isRightTrivial(fcall.args[1]))
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
asmgen.assignExpressionToRegister(fcall.args[1], RegisterOrPair.R14R15, true) // right
|
||||
if(!assignAsmGen.isRightTrivial(fcall.args[1]))
|
||||
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
asmgen.out("""
|
||||
lda cx16.r12
|
||||
cmp cx16.r14
|
||||
|
||||
@@ -42,12 +42,129 @@ internal class ForLoopsAsmGen(
|
||||
when {
|
||||
iterableDt.isByteArray -> forOverNonconstByteRange(stmt, iterableDt, range)
|
||||
iterableDt.isWordArray && !iterableDt.isSplitWordArray -> forOverNonconstWordRange(stmt, iterableDt, range)
|
||||
else -> throw AssemblyError("range expression can only be byte or word")
|
||||
iterableDt.isLongArray -> forOverNonconstLongRange(stmt, iterableDt, range)
|
||||
else -> throw AssemblyError("range expression can only be byte, word or long")
|
||||
}
|
||||
|
||||
asmgen.loopEndLabels.removeLast()
|
||||
}
|
||||
|
||||
private fun forOverNonconstLongRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
|
||||
val stepsize = range.step.asConstInteger()!!
|
||||
val loopLabel = asmgen.makeLabel("for_loop")
|
||||
val endLabel = asmgen.makeLabel("for_end")
|
||||
asmgen.loopEndLabels.add(endLabel)
|
||||
val varname = asmgen.asmVariableName(stmt.variable)
|
||||
|
||||
asmgen.assignExpressionToVariable(range.from, varname, iterableDt.elementType())
|
||||
|
||||
if (stepsize == 1 || stepsize == -1)
|
||||
forOverLongsRangeStepOne(range, varname, iterableDt, loopLabel, endLabel, stmt)
|
||||
else
|
||||
forOverLongsRangeStepGreaterOne(range, varname, iterableDt, loopLabel, endLabel, stmt)
|
||||
}
|
||||
|
||||
private fun forOverLongsRangeStepOne(range: PtRange, varname: String, iterableDt: DataType, loopLabel: String, endLabel: String, forloop: PtForLoop) {
|
||||
val stepsize = range.step.asConstInteger()!!
|
||||
|
||||
// Use temporary variable to store the limit
|
||||
val toValueVar = asmgen.createTempVarReused(iterableDt.elementType().base, false, range)
|
||||
asmgen.assignExpressionToVariable(range.to, toValueVar, iterableDt.elementType())
|
||||
|
||||
if (stepsize == 1) {
|
||||
// Increment toValueVar to make the loop inclusive (0 to N runs N+1 times)
|
||||
asmgen.out("""
|
||||
inc $toValueVar
|
||||
bne +
|
||||
inc $toValueVar+1
|
||||
bne +
|
||||
inc $toValueVar+2
|
||||
bne +
|
||||
inc $toValueVar+3
|
||||
+
|
||||
""")
|
||||
} else {
|
||||
// Decrement toValueVar to make the loop inclusive (N downto 0 runs N+1 times)
|
||||
val label1 = asmgen.makeLabel("dec_1")
|
||||
val label2 = asmgen.makeLabel("dec_2")
|
||||
val label3 = asmgen.makeLabel("dec_3")
|
||||
asmgen.out("""
|
||||
lda $toValueVar
|
||||
bne $label1
|
||||
lda $toValueVar+1
|
||||
bne $label2
|
||||
lda $toValueVar+2
|
||||
bne $label3
|
||||
dec $toValueVar+3
|
||||
$label3: dec $toValueVar+2
|
||||
$label2: dec $toValueVar+1
|
||||
$label1: dec $toValueVar
|
||||
""")
|
||||
}
|
||||
|
||||
asmgen.out(loopLabel)
|
||||
asmgen.translate(forloop.statements)
|
||||
|
||||
if (stepsize == 1) {
|
||||
asmgen.out("""
|
||||
inc $varname
|
||||
bne +
|
||||
inc $varname+1
|
||||
bne +
|
||||
inc $varname+2
|
||||
bne +
|
||||
inc $varname+3
|
||||
+ lda $varname
|
||||
cmp $toValueVar
|
||||
bne $loopLabel
|
||||
lda $varname+1
|
||||
cmp $toValueVar+1
|
||||
bne $loopLabel
|
||||
lda $varname+2
|
||||
cmp $toValueVar+2
|
||||
bne $loopLabel
|
||||
lda $varname+3
|
||||
cmp $toValueVar+3
|
||||
bne $loopLabel
|
||||
""")
|
||||
} else {
|
||||
asmgen.out("""
|
||||
lda $varname
|
||||
bne +++
|
||||
lda $varname+1
|
||||
bne ++
|
||||
lda $varname+2
|
||||
bne +
|
||||
dec $varname+3
|
||||
+ dec $varname+2
|
||||
+ dec $varname+1
|
||||
+ dec $varname
|
||||
lda $varname
|
||||
cmp $toValueVar
|
||||
bne $loopLabel
|
||||
lda $varname+1
|
||||
cmp $toValueVar+1
|
||||
bne $loopLabel
|
||||
lda $varname+2
|
||||
cmp $toValueVar+2
|
||||
bne $loopLabel
|
||||
lda $varname+3
|
||||
cmp $toValueVar+3
|
||||
bne $loopLabel
|
||||
""")
|
||||
}
|
||||
asmgen.out(endLabel)
|
||||
}
|
||||
|
||||
private fun forOverLongsRangeStepGreaterOne(range: PtRange, varname: String, iterableDt: DataType, loopLabel: String, endLabel: String, forloop: PtForLoop) {
|
||||
// TODO: implement 32-bit loop
|
||||
asmgen.romableError("for loops over long ranges with step != 1 not yet fully implemented", forloop.position)
|
||||
asmgen.out(loopLabel)
|
||||
asmgen.translate(forloop.statements)
|
||||
asmgen.jmp(loopLabel)
|
||||
asmgen.out(endLabel)
|
||||
}
|
||||
|
||||
private fun forOverNonconstByteRange(stmt: PtForLoop, iterableDt: DataType, range: PtRange) {
|
||||
val stepsize = range.step.asConstInteger()!!
|
||||
val loopLabel = asmgen.makeLabel("for_loop")
|
||||
|
||||
@@ -1580,7 +1580,13 @@ internal class IfElseAsmGen(private val program: PtProgram,
|
||||
lda $opL+3
|
||||
sbc $varR+3""")
|
||||
} else {
|
||||
if (varL == null && !assignmentAsmGen.isRightTrivial(r)) {
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
}
|
||||
asmgen.assignExpressionToRegister(r, RegisterOrPair.R14R15, true)
|
||||
if (varL == null && !assignmentAsmGen.isRightTrivial(r)) {
|
||||
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
}
|
||||
asmgen.out("""
|
||||
sec
|
||||
lda $opL
|
||||
|
||||
@@ -83,7 +83,7 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
|
||||
asmgen.out(endLabel)
|
||||
assignmentAsmGen.assignRegisterByte(target, CpuRegister.A, false, false)
|
||||
}
|
||||
expr.type.isWord || expr.type.isString -> {
|
||||
expr.type.isWord || expr.type.isString || expr.type.isPointer -> {
|
||||
asmgen.assignExpressionToRegister(expr.falsevalue, RegisterOrPair.AY)
|
||||
asmgen.jmp(endLabel)
|
||||
asmgen.out(trueLabel)
|
||||
@@ -656,7 +656,13 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
|
||||
lda $opL+3
|
||||
sbc $varR+3""")
|
||||
} else {
|
||||
if (varL == null && !assignmentAsmGen.isRightTrivial(r)) {
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
}
|
||||
asmgen.assignExpressionToRegister(r, RegisterOrPair.R14R15, true)
|
||||
if (varL == null && !assignmentAsmGen.isRightTrivial(r)) {
|
||||
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
}
|
||||
asmgen.out("""
|
||||
sec
|
||||
lda $opL
|
||||
|
||||
@@ -18,6 +18,9 @@ internal class AssignmentAsmGen(
|
||||
) {
|
||||
lateinit var augmentableAsmGen: AugmentableAssignmentAsmGen
|
||||
|
||||
internal fun isRightTrivial(expr: PtExpression): Boolean =
|
||||
expr is PtNumber || expr is PtBool || expr is PtIdentifier
|
||||
|
||||
fun translate(assignment: PtAssignment) {
|
||||
val target = AsmAssignTarget.fromAstAssignment(assignment.target, assignment.definingISub(), asmgen)
|
||||
val source = AsmAssignSource.fromAstSource(assignment.value, program, asmgen).adjustSignedUnsigned(target)
|
||||
@@ -867,12 +870,8 @@ internal class AssignmentAsmGen(
|
||||
|
||||
return if(translatedOk)
|
||||
true
|
||||
else {
|
||||
if(expr.type.isLong && expr.operator in "/%")
|
||||
TODO("long division - use floats for now ${expr.position} ")
|
||||
|
||||
else
|
||||
anyExprGen.assignAnyExpressionUsingStack(expr, assign)
|
||||
}
|
||||
}
|
||||
|
||||
private fun optimizedComparison(expr: PtBinaryExpression, assign: AsmAssignment): Boolean {
|
||||
@@ -1161,6 +1160,11 @@ internal class AssignmentAsmGen(
|
||||
// assignVariableWord(target, "cx16.r15", DataType.WORD)
|
||||
// return true
|
||||
}
|
||||
expr.type.isLong -> {
|
||||
asmgen.errors.err("remainder can only be used on unsigned integer operands on 6502 target for now", expr.right.position)
|
||||
return true
|
||||
// TODO implement the signed long remainder asm routine
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
@@ -1198,6 +1202,17 @@ internal class AssignmentAsmGen(
|
||||
assignRegisterpairWord(target, RegisterOrPair.AY)
|
||||
return true
|
||||
}
|
||||
expr.type.isLong -> {
|
||||
asmgen.assignExpressionToRegister(expr.left, RegisterOrPair.R12R13, true)
|
||||
if(!isRightTrivial(expr.right))
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
asmgen.assignExpressionToRegister(expr.right, RegisterOrPair.R14R15, true)
|
||||
if(!isRightTrivial(expr.right))
|
||||
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
asmgen.out(" jsr prog8_math.div_longs")
|
||||
assignRegisterLong(target, RegisterOrPair.R14R15)
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
@@ -1242,7 +1257,11 @@ internal class AssignmentAsmGen(
|
||||
}
|
||||
expr.type.isLong -> {
|
||||
asmgen.assignExpressionToRegister(expr.left, RegisterOrPair.R12R13, true)
|
||||
if(!isRightTrivial(expr.right))
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
asmgen.assignExpressionToRegister(expr.right, RegisterOrPair.R14R15, true)
|
||||
if(!isRightTrivial(expr.right))
|
||||
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
asmgen.out(" jsr prog8_math.multiply_longs")
|
||||
assignRegisterLong(target, RegisterOrPair.R14R15)
|
||||
return true
|
||||
@@ -2179,13 +2198,17 @@ internal class AssignmentAsmGen(
|
||||
else
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 2)
|
||||
assignExpressionToRegister(expr.left, RegisterOrPair.R14R15, expr.left.type.isSigned)
|
||||
// save left operand because right eval may clobber R14R15
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R14R15, 1)
|
||||
assignExpressionToRegister(expr.right, RegisterOrPair.R12R13, expr.right.type.isSigned)
|
||||
// restore left operand
|
||||
asmgen.popLongRegisters(RegisterOrPair.R14R15, 1)
|
||||
augmentableAsmGen.inplacemodificationLongWithVariable("cx16.r14", expr.operator, "cx16.r12")
|
||||
assignRegisterLong(target, RegisterOrPair.R14R15)
|
||||
if(targetreg==RegisterOrPair.R14R15)
|
||||
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
else
|
||||
asmgen.popLongRegisters(RegisterOrPair.R14R15, 2)
|
||||
asmgen.popLongRegisters(RegisterOrPair.R12R13, 2)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -2363,8 +2386,12 @@ internal class AssignmentAsmGen(
|
||||
else if(varname!=null)
|
||||
augmentableAsmGen.inplacemodificationLongWithVariable("cx16.r14", expr.operator, varname)
|
||||
else {
|
||||
// TODO: preserve R14:R15 on stack here? (the function does't do it for us)
|
||||
augmentableAsmGen.inplacemodificationLongWithExpression("cx16.r14", expr.operator, expr.right)
|
||||
if(!isRightTrivial(expr.right))
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R14R15, 1)
|
||||
assignExpressionToRegister(expr.right, RegisterOrPair.R12R13, expr.right.type.isSigned)
|
||||
if(!isRightTrivial(expr.right))
|
||||
asmgen.popLongRegisters(RegisterOrPair.R14R15, 1)
|
||||
augmentableAsmGen.inplacemodificationLongWithVariable("cx16.r14", expr.operator, "cx16.r12")
|
||||
}
|
||||
assignRegisterLong(target, RegisterOrPair.R14R15)
|
||||
if(targetreg!=RegisterOrPair.R14R15) {
|
||||
|
||||
+29
-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("""
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -26,7 +26,7 @@ class IRCodeGen(
|
||||
verifyNameScoping(program, symbolTable)
|
||||
changeGlobalVarInits(symbolTable)
|
||||
|
||||
val irSymbolTable = convertStToIRSt(symbolTable)
|
||||
val irSymbolTable = convertStToIRSt(symbolTable, options.romable)
|
||||
val irProg = IRProgram(program.name, irSymbolTable, options, program.encoding)
|
||||
|
||||
// collect global variables initializers
|
||||
@@ -39,6 +39,12 @@ class IRCodeGen(
|
||||
}
|
||||
}
|
||||
|
||||
// in romable mode, generate global init code for declaration-initialized variables
|
||||
// and clear their init value so they go into the BSS section
|
||||
if(options.romable) {
|
||||
generateRomableInits(irProg)
|
||||
}
|
||||
|
||||
irProg.addAsmSymbols(options.symbolDefs)
|
||||
|
||||
for (block in program.allBlocks())
|
||||
@@ -63,6 +69,11 @@ class IRCodeGen(
|
||||
fun registerTypes(): Map<RegisterNum, IRDataType> = registers.getTypes()
|
||||
|
||||
private fun changeGlobalVarInits(symbolTable: SymbolTable) {
|
||||
// In romable mode, don't pull initializers into the symbol table;
|
||||
// they stay as assignments in the AST and become global init code naturally.
|
||||
if(options.romable)
|
||||
return
|
||||
|
||||
// Normally, block level (global) variables that have a numeric initialization value
|
||||
// are initialized via an assignment statement.
|
||||
val initsToRemove = mutableListOf<Pair<PtBlock, PtAssignment>>()
|
||||
@@ -95,6 +106,39 @@ class IRCodeGen(
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateRomableInits(irProg: IRProgram) {
|
||||
// For romable mode, declaration-initialized variables marked inBss need
|
||||
// runtime init code in the globalInits chunk, and their initializationValue
|
||||
// must be cleared so they appear in the NOINIT (BSS) section of the IR file.
|
||||
val replacements = mutableListOf<IRStStaticVariable>()
|
||||
for(variable in irProg.st.allVariables()) {
|
||||
if(variable.inBss && variable.initializationValue != null) {
|
||||
when(val initValue = variable.initializationValue) {
|
||||
is IRVariableInitializer.Numeric -> {
|
||||
val dt = irType(variable.dt)
|
||||
val tempReg = registers.next(dt)
|
||||
val chunk = IRCodeChunk(null, null)
|
||||
chunk += IRInstruction(Opcode.LOAD, dt, reg1 = tempReg, immediate = initValue.value.toInt())
|
||||
chunk += IRInstruction(Opcode.STOREM, dt, reg1 = tempReg, labelSymbol = variable.name)
|
||||
irProg.addGlobalInits(chunk)
|
||||
// replace with uninitialized version so it goes to NOINIT section
|
||||
replacements += IRStStaticVariable(
|
||||
variable.name, variable.dt, null, variable.length,
|
||||
variable.zpwish, variable.align, variable.dirty,
|
||||
variable.inBss, variable.readonly
|
||||
)
|
||||
}
|
||||
is IRVariableInitializer.Str, is IRVariableInitializer.Array -> {
|
||||
// Strings and arrays: keep inline data but now marked readonly.
|
||||
// They can serve as ROM-based read-only data.
|
||||
}
|
||||
null -> {} // uninitialized, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
replacements.forEach { irProg.st.add(it) }
|
||||
}
|
||||
|
||||
private fun verifyNameScoping(program: PtProgram, symbolTable: SymbolTable) {
|
||||
fun verifyPtNode(node: PtNode) {
|
||||
when (node) {
|
||||
|
||||
@@ -3,14 +3,6 @@ package prog8.codegen.intermediate
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.intermediate.*
|
||||
|
||||
/**
|
||||
* Represents what value a register currently holds for LOADR forwarding optimization.
|
||||
*/
|
||||
private sealed class RegisterContent {
|
||||
data class Constant(val value: Any) : RegisterContent() // Int or Double
|
||||
data class Register(val originalReg: Int) : RegisterContent()
|
||||
}
|
||||
|
||||
class IRPeepholeOptimizer(private val irprog: IRProgram, private val retainSSA: Boolean) {
|
||||
fun optimize(optimizationsEnabled: Boolean, errors: IErrorReporter) {
|
||||
if(!optimizationsEnabled)
|
||||
@@ -65,6 +57,9 @@ class IRPeepholeOptimizer(private val irprog: IRProgram, private val retainSSA:
|
||||
|| removeNeedlessLoads(chunk1, indexedInstructions)
|
||||
|| removeDeadStores(chunk1, indexedInstructions)
|
||||
// || removeLoadrForwarding(chunk1, indexedInstructions) // DISABLED - needs debugging
|
||||
|| removeSelfIdentityOps(chunk1, indexedInstructions)
|
||||
|| simplifyShiftByZero(chunk1, indexedInstructions)
|
||||
|| cancelAdjacentOps(chunk1, indexedInstructions)
|
||||
|| removeNops(chunk1, indexedInstructions) // last time, in case one of the optimizers replaced something with a nop
|
||||
} while (changed)
|
||||
}
|
||||
@@ -544,6 +539,98 @@ jump p8_label_gen_2
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun removeSelfIdentityOps(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
|
||||
var changed = false
|
||||
indexedInstructions.reversed().forEach { (idx, ins) ->
|
||||
when(ins.opcode) {
|
||||
Opcode.LOADR -> {
|
||||
if((ins.reg1 != null && ins.reg1 == ins.reg2) || (ins.fpReg1 != null && ins.fpReg1 == ins.fpReg2)) {
|
||||
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
Opcode.ANDR, Opcode.ORR -> {
|
||||
if(ins.reg1 == ins.reg2) {
|
||||
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
Opcode.XORR -> {
|
||||
if(ins.reg1 == ins.reg2) {
|
||||
chunk.instructions[idx] = IRInstruction(Opcode.LOAD, ins.type, reg1 = ins.reg1, immediate = 0)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun simplifyShiftByZero(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
|
||||
var changed = false
|
||||
indexedInstructions.reversed().forEach { (idx, ins) ->
|
||||
if(idx>0 && ins.opcode in setOf(Opcode.LSLN, Opcode.LSRN, Opcode.ASRN) && ins.reg2 != null) {
|
||||
val prev = indexedInstructions[idx-1].value
|
||||
if(prev.opcode == Opcode.LOAD && prev.reg1 == ins.reg2 && prev.immediate == 0) {
|
||||
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
|
||||
chunk.instructions.removeAt(idx-1)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun cancelAdjacentOps(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
|
||||
var changed = false
|
||||
indexedInstructions.reversed().forEach { (idx, ins) ->
|
||||
if(idx >= chunk.instructions.size - 1)
|
||||
return@forEach
|
||||
val insAfter = chunk.instructions[idx+1]
|
||||
|
||||
when(ins.opcode) {
|
||||
Opcode.INV -> {
|
||||
if(insAfter.opcode == Opcode.INV && insAfter.reg1 == ins.reg1) {
|
||||
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
|
||||
chunk.instructions[idx+1] = IRInstruction(Opcode.NOP)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
Opcode.NEG -> {
|
||||
val sameReg = (ins.reg1 != null && ins.reg1 == insAfter.reg1) || (ins.fpReg1 != null && ins.fpReg1 == insAfter.fpReg1)
|
||||
if(insAfter.opcode == Opcode.NEG && sameReg) {
|
||||
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
|
||||
chunk.instructions[idx+1] = IRInstruction(Opcode.NOP)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
Opcode.EXT, Opcode.EXTS -> {
|
||||
if(insAfter.opcode == ins.opcode && insAfter.reg1 == ins.reg1 && insAfter.type == ins.type) {
|
||||
chunk.instructions[idx+1] = IRInstruction(Opcode.NOP)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
Opcode.INC -> {
|
||||
if(insAfter.opcode == Opcode.DEC && insAfter.reg1 == ins.reg1) {
|
||||
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
|
||||
chunk.instructions[idx+1] = IRInstruction(Opcode.NOP)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
Opcode.DEC -> {
|
||||
if(insAfter.opcode == Opcode.INC && insAfter.reg1 == ins.reg1) {
|
||||
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
|
||||
chunk.instructions[idx+1] = IRInstruction(Opcode.NOP)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun removeNops(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
|
||||
var changed = false
|
||||
indexedInstructions.reversed().forEach { (idx, ins) ->
|
||||
@@ -686,18 +773,6 @@ jump p8_label_gen_2
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun isRegisterRead(ins: IRInstruction, reg: Int): Boolean {
|
||||
// Check if the instruction reads from the given register
|
||||
val formats = instructionFormats.getValue(ins.opcode)
|
||||
val format = formats[ins.type] ?: formats[null]
|
||||
|
||||
val reg1Read = (format?.reg1 == OperandDirection.READ || format?.reg1 == OperandDirection.READWRITE) && (ins.reg1 == reg || ins.fpReg1?.value == reg)
|
||||
val reg2Read = (format?.reg2 == OperandDirection.READ || format?.reg2 == OperandDirection.READWRITE) && (ins.reg2 == reg || ins.fpReg2?.value == reg)
|
||||
val reg3Read = (format?.reg3 == OperandDirection.READ || format?.reg3 == OperandDirection.READWRITE) && (ins.reg3 == reg)
|
||||
|
||||
return reg1Read || reg2Read || reg3Read
|
||||
}
|
||||
|
||||
//private fun removeLoadrForwarding(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
|
||||
// Forward LOADR instructions to their original source.
|
||||
// Example:
|
||||
|
||||
@@ -8,7 +8,7 @@ import prog8.intermediate.*
|
||||
* Converter object for transforming SymbolTable nodes to IR symbol table nodes.
|
||||
* Groups all conversion functions together for better organization.
|
||||
*/
|
||||
private object StToIrConverter {
|
||||
private class StToIrConverter(val romable: Boolean) {
|
||||
fun convert(struct: StStruct): IRStStructDef =
|
||||
IRStStructDef(struct.scopedNameString, struct.fields, struct.size)
|
||||
|
||||
@@ -72,16 +72,21 @@ private object StToIrConverter {
|
||||
}
|
||||
|
||||
val name = if('.' in variable.name) variable.name else variable.scopedNameString
|
||||
return IRStStaticVariable(name, variable.dt, initValue, variable.length, variable.zpwish, variable.align, variable.dirty)
|
||||
val hasInit = initValue != null
|
||||
val inBss = romable && hasInit
|
||||
val readonly = romable && hasInit
|
||||
return IRStStaticVariable(name, variable.dt, initValue, variable.length, variable.zpwish, variable.align, variable.dirty, inBss, readonly)
|
||||
}
|
||||
|
||||
fun convert(variable: StMemVar): IRStMemVar {
|
||||
val readonly = romable && variable.address > 255u
|
||||
if('.' in variable.name) {
|
||||
return IRStMemVar(
|
||||
variable.name,
|
||||
variable.dt,
|
||||
variable.address,
|
||||
variable.length
|
||||
variable.length,
|
||||
readonly
|
||||
)
|
||||
} else {
|
||||
val scopedName = try {
|
||||
@@ -89,7 +94,7 @@ private object StToIrConverter {
|
||||
} catch (_: UninitializedPropertyAccessException) {
|
||||
variable.name
|
||||
}
|
||||
return IRStMemVar(scopedName, variable.dt, variable.address, variable.length)
|
||||
return IRStMemVar(scopedName, variable.dt, variable.address, variable.length, readonly)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,33 +139,34 @@ private object StToIrConverter {
|
||||
}
|
||||
}
|
||||
|
||||
fun convertStToIRSt(sourceSt: SymbolTable?): IRSymbolTable {
|
||||
fun convertStToIRSt(sourceSt: SymbolTable?, romable: Boolean = false): IRSymbolTable {
|
||||
val st = IRSymbolTable()
|
||||
val converter = StToIrConverter(romable)
|
||||
if (sourceSt != null) {
|
||||
sourceSt.flat.forEach {
|
||||
when(it.value.type) {
|
||||
StNodeType.STATICVAR -> st.add(StToIrConverter.convert(it.value as StStaticVariable))
|
||||
StNodeType.MEMVAR -> st.add(StToIrConverter.convert(it.value as StMemVar))
|
||||
StNodeType.STATICVAR -> st.add(converter.convert(it.value as StStaticVariable))
|
||||
StNodeType.MEMVAR -> st.add(converter.convert(it.value as StMemVar))
|
||||
StNodeType.CONSTANT -> {
|
||||
val constant = it.value as StConstant
|
||||
// If the constant has a memory() slab, add the slab to the IR symbol table first
|
||||
constant.memorySlab?.let { slab ->
|
||||
val converted = StToIrConverter.convert(slab)
|
||||
val converted = converter.convert(slab)
|
||||
st.add(converted)
|
||||
}
|
||||
st.add(StToIrConverter.convert(constant))
|
||||
st.add(converter.convert(constant))
|
||||
}
|
||||
StNodeType.MEMORYSLAB -> {
|
||||
val slab = it.value as StMemorySlab
|
||||
val converted = StToIrConverter.convert(slab)
|
||||
val converted = converter.convert(slab)
|
||||
st.add(converted)
|
||||
}
|
||||
StNodeType.STRUCTINSTANCE -> {
|
||||
val instance = it.value as StStructInstance
|
||||
val struct = sourceSt.lookup(instance.structName) as StStruct
|
||||
st.add(StToIrConverter.convert(instance, struct.fields))
|
||||
st.add(converter.convert(instance, struct.fields))
|
||||
}
|
||||
StNodeType.STRUCT -> st.add(StToIrConverter.convert(it.value as StStruct))
|
||||
StNodeType.STRUCT -> st.add(converter.convert(it.value as StStruct))
|
||||
else -> { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import prog8.code.core.*
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.OutputType
|
||||
import prog8.code.core.Position
|
||||
import prog8.code.core.ZeropageType
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8.codegen.intermediate.IRPeepholeOptimizer
|
||||
import prog8.intermediate.*
|
||||
@@ -289,4 +292,71 @@ class TestIRPeepholeOpt: FunSpec({
|
||||
modInstr[0].opcode shouldBe Opcode.MOD
|
||||
modInstr[0].immediate shouldBe 7
|
||||
}
|
||||
})
|
||||
|
||||
test("remove self identity operations") {
|
||||
// LOADR with same fp register is allowed (constructor only blocks integer reg1==reg2)
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.LOADR, IRDataType.FLOAT, fpReg1=RegisterNum(10), fpReg2=RegisterNum(10)),
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=99, immediate=42) // keep chunk non-empty
|
||||
))
|
||||
irProg.chunks().single().instructions.size shouldBe 2
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.size shouldBe 1
|
||||
instr[0].opcode shouldBe Opcode.LOAD
|
||||
instr[0].reg1 shouldBe 99
|
||||
instr[0].immediate shouldBe 42
|
||||
}
|
||||
|
||||
test("remove shift by zero") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=10, immediate=0),
|
||||
IRInstruction(Opcode.LSLN, IRDataType.BYTE, reg1=1, reg2=10),
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=11, immediate=0),
|
||||
IRInstruction(Opcode.LSRN, IRDataType.BYTE, reg1=2, reg2=11),
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=12, immediate=0),
|
||||
IRInstruction(Opcode.ASRN, IRDataType.BYTE, reg1=3, reg2=12),
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=99, immediate=42) // keep chunk non-empty
|
||||
))
|
||||
irProg.chunks().single().instructions.size shouldBe 7
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.size shouldBe 1
|
||||
instr[0].opcode shouldBe Opcode.LOAD
|
||||
instr[0].reg1 shouldBe 99
|
||||
instr[0].immediate shouldBe 42
|
||||
}
|
||||
|
||||
test("cancel adjacent ops") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.INV, IRDataType.BYTE, reg1=1),
|
||||
IRInstruction(Opcode.INV, IRDataType.BYTE, reg1=1),
|
||||
IRInstruction(Opcode.NEG, IRDataType.BYTE, reg1=2),
|
||||
IRInstruction(Opcode.NEG, IRDataType.BYTE, reg1=2),
|
||||
IRInstruction(Opcode.EXT, IRDataType.BYTE, reg1=3, reg2=33),
|
||||
IRInstruction(Opcode.EXT, IRDataType.BYTE, reg1=3, reg2=33),
|
||||
IRInstruction(Opcode.EXTS, IRDataType.WORD, reg1=4, reg2=44),
|
||||
IRInstruction(Opcode.EXTS, IRDataType.WORD, reg1=4, reg2=44),
|
||||
IRInstruction(Opcode.INC, IRDataType.BYTE, reg1=5),
|
||||
IRInstruction(Opcode.DEC, IRDataType.BYTE, reg1=5),
|
||||
IRInstruction(Opcode.DEC, IRDataType.BYTE, reg1=6),
|
||||
IRInstruction(Opcode.INC, IRDataType.BYTE, reg1=6),
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=99, immediate=42) // keep chunk non-empty
|
||||
))
|
||||
irProg.chunks().single().instructions.size shouldBe 13
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.size shouldBe 3
|
||||
instr[0].opcode shouldBe Opcode.EXT
|
||||
instr[0].reg1 shouldBe 3
|
||||
instr[0].reg2 shouldBe 33
|
||||
instr[1].opcode shouldBe Opcode.EXTS
|
||||
instr[1].reg1 shouldBe 4
|
||||
instr[1].reg2 shouldBe 44
|
||||
instr[2].opcode shouldBe Opcode.LOAD
|
||||
instr[2].immediate shouldBe 42
|
||||
}
|
||||
})
|
||||
@@ -32,6 +32,12 @@ class VarConstantValueTypeAdjuster(
|
||||
|
||||
private fun isConstantInitializer(expr: Expression): Boolean = getConstantInitializer(expr) != null
|
||||
|
||||
private fun isMemorySlabInit(expr: Expression): Boolean =
|
||||
expr is MemorySlabRef || (expr is IFunctionCall && expr.isMemoryCall)
|
||||
|
||||
private fun requiresZp(decl: VarDecl): Boolean =
|
||||
decl.zeropage == ZeropageWish.REQUIRE_ZEROPAGE
|
||||
|
||||
override fun after(decl: VarDecl, parent: Node): Iterable<AstModification> {
|
||||
|
||||
if(decl.parent is AnonymousScope)
|
||||
@@ -47,95 +53,95 @@ class VarConstantValueTypeAdjuster(
|
||||
}
|
||||
|
||||
// replace variables by constants, if possible
|
||||
if(options.optimize) {
|
||||
if (decl.sharedWithAsm || decl.type != VarDeclType.VAR || decl.origin != VarDeclOrigin.USERCODE
|
||||
|| !(decl.datatype.isNumericOrBool || decl.datatype.isPointer))
|
||||
return noModifications
|
||||
if (decl.value != null && !isConstantInitializer(decl.value!!))
|
||||
return noModifications
|
||||
val usages = callGraph.usages(decl)
|
||||
val (writes, reads) = usages
|
||||
.partition {
|
||||
it is InlineAssembly // can't really tell if it's written to or only read, assume the worst
|
||||
|| it.parent is AssignTarget
|
||||
|| it.parent is ForLoop
|
||||
|| (it.parent as? IFunctionCall)?.target?.nameInSource?.singleOrNull() in InplaceModifyingBuiltinFunctions
|
||||
}
|
||||
|
||||
// If the address of the variable is taken, we cannot replace it with a constant.
|
||||
if (reads.any { it.parent is AddressOf })
|
||||
return noModifications
|
||||
|
||||
var singleAssignment: Assignment? = null
|
||||
val singleWrite=writes.singleOrNull()
|
||||
if(singleWrite!=null) {
|
||||
singleAssignment = singleWrite.parent as? Assignment
|
||||
if (decl.sharedWithAsm || decl.type != VarDeclType.VAR || decl.origin != VarDeclOrigin.USERCODE
|
||||
|| !(decl.datatype.isNumericOrBool || decl.datatype.isPointer))
|
||||
return noModifications
|
||||
if (decl.value != null && !isConstantInitializer(decl.value!!))
|
||||
return noModifications
|
||||
if (decl.value != null && isMemorySlabInit(decl.value!!))
|
||||
return noModifications // a pointer to a memory slab is usually accessed often and/or in ways that benefit more from the pointer being an actual variable (preferrably in zeropage)
|
||||
val usages = callGraph.usages(decl)
|
||||
val (writes, reads) = usages
|
||||
.partition {
|
||||
it is InlineAssembly // can't really tell if it's written to or only read, assume the worst
|
||||
|| it.parent is AssignTarget
|
||||
|| it.parent is ForLoop
|
||||
|| (it.parent as? IFunctionCall)?.target?.nameInSource?.singleOrNull() in InplaceModifyingBuiltinFunctions
|
||||
}
|
||||
|
||||
// If the address of the variable is taken, we cannot replace it with a constant.
|
||||
if (reads.any { it.parent is AddressOf })
|
||||
return noModifications
|
||||
|
||||
var singleAssignment: Assignment? = null
|
||||
val singleWrite=writes.singleOrNull()
|
||||
if(singleWrite!=null) {
|
||||
singleAssignment = singleWrite.parent as? Assignment
|
||||
if(singleAssignment==null) {
|
||||
singleAssignment = singleWrite.parent.parent as? Assignment
|
||||
if(singleAssignment==null) {
|
||||
singleAssignment = singleWrite.parent.parent as? Assignment
|
||||
if(singleAssignment==null) {
|
||||
// we could be part of a multi-assign
|
||||
if(singleWrite.parent is AssignTarget && singleWrite.parent.parent is AssignTarget)
|
||||
singleAssignment = singleWrite.parent.parent.parent as? Assignment
|
||||
}
|
||||
// we could be part of a multi-assign
|
||||
if(singleWrite.parent is AssignTarget && singleWrite.parent.parent is AssignTarget)
|
||||
singleAssignment = singleWrite.parent.parent.parent as? Assignment
|
||||
}
|
||||
}
|
||||
|
||||
if (singleAssignment == null) {
|
||||
if (writes.isEmpty()) {
|
||||
if(reads.isEmpty()) {
|
||||
if(decl.names.size>1) {
|
||||
errors.info("unused variable '${decl.name}'", decl.position)
|
||||
} else {
|
||||
// variable is never used AT ALL so we just remove it altogether
|
||||
if ("ignore_unused" !in decl.definingBlock.options())
|
||||
errors.info("removing unused variable '${decl.name}'", decl.position)
|
||||
return listOf(AstRemove(decl, parent as IStatementContainer))
|
||||
}
|
||||
}
|
||||
val declValue = if (decl.value != null) getConstantInitializer(decl.value!!) else null
|
||||
if (declValue != null && canBeMadeConst(decl, usages)) {
|
||||
// variable is never written to, so it can be replaced with a constant, IF the value is a constant
|
||||
errors.info("variable '${decl.name}' is never written to and was made const", decl.position)
|
||||
val const = VarDecl.builder(decl.datatype, decl.position)
|
||||
.copyFrom(decl)
|
||||
.type(VarDeclType.CONST)
|
||||
.value(declValue)
|
||||
.build()
|
||||
decl.value = null
|
||||
return listOf(
|
||||
AstReplaceNode(decl, const, parent)
|
||||
)
|
||||
}
|
||||
|
||||
if (singleAssignment == null) {
|
||||
if (writes.isEmpty()) {
|
||||
if(reads.isEmpty()) {
|
||||
if(decl.names.size>1) {
|
||||
errors.info("unused variable '${decl.name}'", decl.position)
|
||||
} else {
|
||||
// variable is never used AT ALL so we just remove it altogether
|
||||
if ("ignore_unused" !in decl.definingBlock.options())
|
||||
errors.info("removing unused variable '${decl.name}'", decl.position)
|
||||
return listOf(AstRemove(decl, parent as IStatementContainer))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (singleAssignment.origin == AssignmentOrigin.VARINIT && isConstantInitializer(singleAssignment.value) && canBeMadeConst(decl, usages)) {
|
||||
if(reads.isEmpty()) {
|
||||
if(decl.names.size>1) {
|
||||
errors.info("unused variable '${decl.name}'", decl.position)
|
||||
} else {
|
||||
// variable is never used AT ALL so we just remove it altogether, including the single assignment
|
||||
if("ignore_unused" !in decl.definingBlock.options())
|
||||
errors.info("removing unused variable '${decl.name}'", decl.position)
|
||||
return listOf(
|
||||
AstRemove(decl, parent as IStatementContainer),
|
||||
AstRemove(singleAssignment, singleAssignment.parent as IStatementContainer)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// variable only has a single write, and it is the initialization value, so it can be replaced with a constant, but only IF the value is a constant
|
||||
val declValue = if (decl.value != null) getConstantInitializer(decl.value!!) else null
|
||||
if (declValue != null && !requiresZp(decl) && canBeMadeConst(decl, usages)) {
|
||||
// variable is never written to, so it can be replaced with a constant, IF the value is a constant
|
||||
errors.info("variable '${decl.name}' is never written to and was made const", decl.position)
|
||||
val const = VarDecl.builder(decl.datatype, decl.position)
|
||||
.copyFrom(decl)
|
||||
.type(VarDeclType.CONST)
|
||||
.value(singleAssignment.value)
|
||||
.value(declValue)
|
||||
.build()
|
||||
decl.value = null
|
||||
return listOf(
|
||||
AstReplaceNode(decl, const, parent),
|
||||
AstRemove(singleAssignment, singleAssignment.parent as IStatementContainer)
|
||||
AstReplaceNode(decl, const, parent)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (singleAssignment.origin == AssignmentOrigin.VARINIT && isConstantInitializer(singleAssignment.value) && !isMemorySlabInit(singleAssignment.value) && !requiresZp(decl) && canBeMadeConst(decl, usages)) {
|
||||
if(reads.isEmpty()) {
|
||||
if(decl.names.size>1) {
|
||||
errors.info("unused variable '${decl.name}'", decl.position)
|
||||
} else {
|
||||
// variable is never used AT ALL so we just remove it altogether, including the single assignment
|
||||
if("ignore_unused" !in decl.definingBlock.options())
|
||||
errors.info("removing unused variable '${decl.name}'", decl.position)
|
||||
return listOf(
|
||||
AstRemove(decl, parent as IStatementContainer),
|
||||
AstRemove(singleAssignment, singleAssignment.parent as IStatementContainer)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// variable only has a single write, and it is the initialization value, so it can be replaced with a constant, but only IF the value is a constant
|
||||
errors.info("variable '${decl.name}' is never written to and was made const", decl.position)
|
||||
val const = VarDecl.builder(decl.datatype, decl.position)
|
||||
.copyFrom(decl)
|
||||
.type(VarDeclType.CONST)
|
||||
.value(singleAssignment.value)
|
||||
.build()
|
||||
return listOf(
|
||||
AstReplaceNode(decl, const, parent),
|
||||
AstRemove(singleAssignment, singleAssignment.parent as IStatementContainer)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return noModifications
|
||||
@@ -148,10 +154,11 @@ class VarConstantValueTypeAdjuster(
|
||||
val p = it.parent
|
||||
val gp = p.parent
|
||||
it is PtrDereference || it is ArrayIndexedPtrDereference
|
||||
|| p is ArrayIndexedExpression || p is DirectMemoryRead || p is DirectMemoryWrite
|
||||
|| gp is DirectMemoryRead || gp is DirectMemoryWrite
|
||||
|| p is ArrayIndexedExpression || p is DirectMemoryRead || p is DirectMemoryWrite
|
||||
|| gp is DirectMemoryRead || gp is DirectMemoryWrite
|
||||
}
|
||||
if (derefCount > 2) return false
|
||||
if (derefCount > 0 && (decl.zeropage==ZeropageWish.PREFER_ZEROPAGE || decl.zeropage==ZeropageWish.REQUIRE_ZEROPAGE)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -340,6 +347,12 @@ internal class ConstantIdentifierReplacer(
|
||||
override fun before(addressOf: AddressOf, parent: Node): Iterable<AstModification> {
|
||||
val constValue = addressOf.constValue(program)
|
||||
if(constValue!=null) {
|
||||
// don't replace &constVar (without array index); leave it for AstChecker to reject as invalid address-of.
|
||||
// But still allow &constArray[idx] (those are valid address-of operations).
|
||||
val target = addressOf.identifier?.targetVarDecl()
|
||||
if(target?.type==VarDeclType.CONST && addressOf.arrayIndex==null) {
|
||||
return noModifications
|
||||
}
|
||||
return listOf(AstReplaceNode(addressOf, constValue, parent))
|
||||
}
|
||||
return noModifications
|
||||
@@ -348,7 +361,7 @@ internal class ConstantIdentifierReplacer(
|
||||
override fun after(identifier: IdentifierReference, parent: Node): Iterable<AstModification> {
|
||||
// replace identifiers that refer to const value, with the value itself
|
||||
// if it's a simple type and if it's not a left hand side variable
|
||||
if (identifier.parent is AssignTarget || identifier.parent is Alias)
|
||||
if (identifier.parent is AssignTarget || identifier.parent is Alias || identifier.parent is AddressOf)
|
||||
return noModifications
|
||||
var forloop = identifier.parent as? ForLoop
|
||||
if (forloop == null)
|
||||
|
||||
@@ -463,12 +463,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 +476,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 +489,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ private fun isEmptyReturn(stmt: Statement): Boolean = stmt is Return && stmt.va
|
||||
|
||||
class Inliner(private val program: Program, private val options: CompilationOptions): AstWalker() {
|
||||
|
||||
class DetermineInlineSubs(val program: Program): IAstVisitor {
|
||||
inner class DetermineInlineSubs(val program: Program): IAstVisitor {
|
||||
private val modifications = mutableListOf<AstModification>()
|
||||
|
||||
init {
|
||||
@@ -32,23 +32,26 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
return true
|
||||
|
||||
return stmt.values.all { value ->
|
||||
when (value) {
|
||||
is NumericLiteral -> true
|
||||
is IdentifierReference -> {
|
||||
makeFullyScoped(value)
|
||||
true
|
||||
}
|
||||
is FunctionCallExpression -> {
|
||||
if (value.args.size <= 1 && value.args.all {
|
||||
it is NumericLiteral || it is IdentifierReference
|
||||
}) {
|
||||
makeFullyScoped(value)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
if (isSimpleReturnExpression(value)) {
|
||||
val walker = object : AstWalker() {
|
||||
override fun after(identifier: IdentifierReference, parent: Node): Iterable<AstModification> {
|
||||
makeFullyScoped(identifier)
|
||||
return noModifications
|
||||
}
|
||||
}
|
||||
else -> false
|
||||
value.accept(walker, stmt)
|
||||
true
|
||||
} else if (value is FunctionCallExpression) {
|
||||
if (value.args.size <= 1 && value.args.all {
|
||||
it is NumericLiteral || it is IdentifierReference
|
||||
}) {
|
||||
makeFullyScoped(value)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,8 +109,10 @@ 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.
|
||||
val hasRegisterAliasedParams = subroutine.parameters.any { it.registerOrPair != null }
|
||||
if (!subroutine.isAsmSubroutine && (subroutine.parameters.isEmpty() || !hasRegisterAliasedParams)) {
|
||||
// NOTE: We allow subroutines with parameters to be considered for inlining,
|
||||
// unless they are assembly subroutines or have register-aliased parameters (which cannot be inlined as they depend on specific CPU registers to be set).
|
||||
// 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
|
||||
@@ -223,17 +228,13 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
private fun canInlineAtCallSiteWithReason(sub: Subroutine, fcall: IFunctionCall): Pair<Boolean, String> {
|
||||
if (!sub.inline)
|
||||
return false to "subroutine not marked as inlineable"
|
||||
|
||||
// Check parameter argument complexity
|
||||
|
||||
if (sub.parameters.isNotEmpty()) {
|
||||
// Only allow inlining for void statement calls, not expression context
|
||||
if (fcall !is FunctionCallStatement || !fcall.void)
|
||||
return false to "expression context calls with parameters not supported yet"
|
||||
|
||||
// Only inline if all arguments are simple (literals or identifiers)
|
||||
if (!fcall.args.all { it is NumericLiteral || it is IdentifierReference }) {
|
||||
val complexArgs = fcall.args.filterNot { it is NumericLiteral || it is IdentifierReference }
|
||||
return false to "complex arguments (${complexArgs.size}x): ${complexArgs.map { it::class.simpleName }.joinToString(", ")}"
|
||||
if (sub.isAsmSubroutine) {
|
||||
return false to "parameterized asmsub cannot be inlined"
|
||||
}
|
||||
if (sub.parameters.any { it.registerOrPair != null }) {
|
||||
return false to "parameterized subroutine with register aliased parameters cannot be inlined"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,13 +280,12 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
|
||||
fun inlineFunctionBody(toInline: Return): Iterable<AstModification> {
|
||||
// call site is an expression, so we have to have a Return here in the inlined sub to provide the values
|
||||
// note that we don't have to process any args, because we are currently only inlining parameterless subroutines.
|
||||
return if(toInline.values.size==1 && functionCallExpr!==toInline.values[0]) {
|
||||
return if (toInline.values.size == 1 && functionCallExpr !== toInline.values[0]) {
|
||||
// println(">>> INLINER: INLINED expression '${sub.name}' at ${functionCallExpr.position} (return value substituted)")
|
||||
sub.hasBeenInlined =true
|
||||
listOf(AstReplaceNode(functionCallExpr, toInline.values[0].copy(), parent))
|
||||
}
|
||||
else
|
||||
sub.hasBeenInlined = true
|
||||
val substitutedReturn = substituteParameters(sub, functionCallExpr, toInline.values[0]) as Expression
|
||||
listOf(AstReplaceNode(functionCallExpr, substitutedReturn, parent))
|
||||
} else
|
||||
noModifications
|
||||
}
|
||||
|
||||
@@ -303,7 +303,9 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
private fun possiblyInlineFunctioncallStmt(sub: Subroutine, origNode: Node, parent: Node): Iterable<AstModification> {
|
||||
|
||||
fun possiblyShortCircuitFunctionCall(toInline: Return): Iterable<AstModification> {
|
||||
val functionCalls = toInline.values.filterIsInstance<FunctionCallExpression>()
|
||||
// Substitute parameters in the return values first
|
||||
val substitutedReturn = substituteParameters(sub, origNode as IFunctionCall, toInline) as Return
|
||||
val functionCalls = substitutedReturn.values.filterIsInstance<FunctionCallExpression>()
|
||||
|
||||
if (functionCalls.isEmpty()) {
|
||||
// No function calls in the return values - the void call has no side effects and can be removed.
|
||||
@@ -331,22 +333,17 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
}
|
||||
|
||||
fun possiblyInlineFunctionBody(toInline: Statement): Iterable<AstModification> {
|
||||
// For void calls with parameters, we already verified in canInlineAtCallSiteWithReason()
|
||||
// that the parameter is unused in the body (so no substitution needed).
|
||||
// For expression context calls with parameters, those are rejected in canInlineAtCallSiteWithReason()
|
||||
// so we never reach here.
|
||||
// Just copy the statement - parameters are unused so no substitution needed.
|
||||
val inlinedStatement = toInline.copy()
|
||||
|
||||
// println(">>> INLINER: INLINED '${sub.name}' at ${functionCallStatement.position} (body inserted, ${sub.parameters.size} param(s))")
|
||||
return if(origNode !== toInline) {
|
||||
val inlinedStatement = substituteParameters(sub, origNode as IFunctionCall, toInline) as Statement
|
||||
|
||||
// println(">>> INLINER: INLINED '${sub.name}' at ${functionCallStatement.position} (body inserted)")
|
||||
return if (origNode !== toInline) {
|
||||
sub.hasBeenInlined = true
|
||||
listOf(AstReplaceNode(origNode, inlinedStatement, parent))
|
||||
} else
|
||||
noModifications
|
||||
}
|
||||
|
||||
if(sub.inline && sub.parameters.size >= 0) {
|
||||
if(sub.inline) {
|
||||
// Get the first non-parameter statement (skip VarDecl with SUBROUTINEPARAM origin)
|
||||
val bodyStmt = sub.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
|
||||
val hasOnlyBodyAndReturn = bodyStmt != null && (
|
||||
@@ -361,7 +358,6 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
sub.hasBeenInlined=true
|
||||
listOf(AstReplaceNode(origNode, bodyStmt.copy(), parent))
|
||||
} else {
|
||||
// note that we don't have to process any args, because we only inline parameterless subroutines.
|
||||
when (val toInline = bodyStmt) {
|
||||
is Return -> possiblyShortCircuitFunctionCall(toInline)
|
||||
else -> possiblyInlineFunctionBody(toInline)
|
||||
@@ -371,6 +367,19 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
return noModifications
|
||||
}
|
||||
|
||||
|
||||
private fun isSimpleReturnExpression(expr: Expression): Boolean {
|
||||
return when (expr) {
|
||||
is NumericLiteral -> true
|
||||
is IdentifierReference -> true
|
||||
is PrefixExpression -> isSimpleReturnExpression(expr.expression)
|
||||
is BinaryExpression ->
|
||||
(expr.left is NumericLiteral || expr.left is IdentifierReference) &&
|
||||
(expr.right is NumericLiteral || expr.right is IdentifierReference)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitutes parameter references in a statement with argument values.
|
||||
*/
|
||||
@@ -382,22 +391,25 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
val sub = fcall.target.targetStatement(program.builtinFunctions) as? Subroutine
|
||||
?: return noModifications
|
||||
|
||||
// Only handle parameterless subroutines with simple returns
|
||||
if (!sub.inline || sub.parameters.isNotEmpty() || !canInlineAtCallSite(sub, fcall))
|
||||
// Only handle inlining if sub is marked as such and call site is okay
|
||||
if (!sub.inline || !canInlineAtCallSite(sub, fcall))
|
||||
return noModifications
|
||||
|
||||
val toInline = sub.statements.firstOrNull() as? Return ?: return noModifications
|
||||
val toInline = sub.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM } as? Return ?: return noModifications
|
||||
|
||||
// Check return count matches target count
|
||||
if (toInline.values.size != multiTargets.size)
|
||||
return noModifications
|
||||
|
||||
// Only allow simple return values (literals or identifiers, no function calls)
|
||||
if (!toInline.values.all { it is NumericLiteral || it is IdentifierReference })
|
||||
// Only allow simple return values to prevent code bloat on 6502
|
||||
if (!toInline.values.all { isSimpleReturnExpression(it) })
|
||||
return noModifications
|
||||
|
||||
// Substitute parameters in all return values
|
||||
val substitutedReturn = substituteParameters(sub, fcall, toInline) as Return
|
||||
|
||||
// Create multiple single assignments, skipping void targets (they discard the value)
|
||||
val newAssignments = multiTargets.zip(toInline.values)
|
||||
val newAssignments = multiTargets.zip(substitutedReturn.values)
|
||||
.filter { (target, _) -> !target.void }
|
||||
.map { (target, value) ->
|
||||
Assignment(
|
||||
@@ -420,4 +432,59 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
val scope = AnonymousScope(newAssignments.toMutableList(), assignment.position)
|
||||
return listOf(AstReplaceNode(assignment, scope, parent))
|
||||
}
|
||||
|
||||
private fun substituteParameters(sub: Subroutine, fcall: IFunctionCall, node: Node): Node {
|
||||
val paramVarDecls = sub.statements.filterIsInstance<VarDecl>().filter { it.origin == VarDeclOrigin.SUBROUTINEPARAM }
|
||||
val paramMap: Map<VarDecl, Expression> = paramVarDecls.zip(fcall.args).associate { it.first to it.second }
|
||||
|
||||
fun substitute(n: Node): Node {
|
||||
if (n is IdentifierReference) {
|
||||
val target = n.targetStatement(program.builtinFunctions)
|
||||
val arg = if (target is VarDecl) paramMap[target] else null
|
||||
if (arg != null) return arg.copy()
|
||||
}
|
||||
|
||||
return when (n) {
|
||||
is BinaryExpression -> BinaryExpression(substitute(n.left) as Expression, n.operator, substitute(n.right) as Expression, n.position)
|
||||
is PrefixExpression -> PrefixExpression(n.operator, substitute(n.expression) as Expression, n.position)
|
||||
is Return -> Return(n.values.map { substitute(it) as Expression }.toTypedArray(), n.position)
|
||||
is Assignment -> {
|
||||
val newTarget = n.target.copy()
|
||||
if (n.target.identifier != null) {
|
||||
val substituted = substitute(n.target.identifier!!)
|
||||
if (substituted is IdentifierReference) {
|
||||
newTarget.identifier = substituted
|
||||
}
|
||||
// if substituted is not an identifier, we leave the original (copied) identifier
|
||||
// in the target. This is not ideal but avoids the ClassCastException.
|
||||
}
|
||||
Assignment(
|
||||
newTarget,
|
||||
substitute(n.value) as Expression,
|
||||
n.origin,
|
||||
n.position
|
||||
)
|
||||
}
|
||||
is FunctionCallExpression -> FunctionCallExpression(
|
||||
substitute(n.target) as IdentifierReference,
|
||||
n.args.map { substitute(it) as Expression }.toMutableList(),
|
||||
n.position
|
||||
)
|
||||
is FunctionCallStatement -> FunctionCallStatement(
|
||||
substitute(n.target) as IdentifierReference,
|
||||
n.args.map { substitute(it) as Expression }.toMutableList(),
|
||||
n.void,
|
||||
n.position
|
||||
)
|
||||
is AnonymousScope -> AnonymousScope(
|
||||
n.statements.map { substitute(it) as Statement }.toMutableList(),
|
||||
n.position
|
||||
)
|
||||
is Jump -> Jump(substitute(n.target) as Expression, n.position)
|
||||
else -> n.copy()
|
||||
}
|
||||
}
|
||||
|
||||
return substitute(node)
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ dependencies {
|
||||
testImplementation(project(":intermediate"))
|
||||
testImplementation("io.kotest:kotest-runner-junit5")
|
||||
testImplementation("io.kotest:kotest-framework-datatest")
|
||||
testImplementation("com.github.irmen:ksim65:v2.0")
|
||||
testImplementation("com.github.irmen:ksim65:v2.1")
|
||||
}
|
||||
|
||||
// Exclude transitive antlr4 dependency (we only need it in parser module)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
; serial routines for the serial/wifi card of the X16.
|
||||
; assumes the wifi part is supported via zimodem.
|
||||
|
||||
%import strings
|
||||
|
||||
serial {
|
||||
|
||||
%option ignore_unused
|
||||
|
||||
const ubyte REG_TXRX_BUFFER = 0
|
||||
const ubyte REG_INTERRUPT_ENABLE = 1
|
||||
const ubyte REG_INTERRUPT_IDENT = 2
|
||||
const ubyte REG_FIFO_CONTROL = 2
|
||||
const ubyte REG_LINE_CONTROL = 3
|
||||
const ubyte REG_MODEM_CONTROL = 4
|
||||
const ubyte REG_LINE_STATUS = 5
|
||||
const ubyte REG_MODEM_STATUS = 6
|
||||
const ubyte REG_SCRATCH = 7
|
||||
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
|
||||
OE = %00000010, ; Overrun Error
|
||||
PE = %00000100, ; Parity Error
|
||||
FE = %00001000, ; Framing Error
|
||||
BI = %00010000, ; Break Interrupt
|
||||
THRE = %00100000, ; Transmit Holding Register Empty
|
||||
TEMT = %01000000, ; Transmitter Empty
|
||||
RCVR = %10000000, ; Error in RCVR FIFO
|
||||
}
|
||||
|
||||
; generic UART routines (not ZiModem specific):
|
||||
|
||||
sub detect_uarts() -> uword, uword {
|
||||
; Detects the presence of UART chips (up to 2) on the system.
|
||||
; Returns the discovered UART chip addresses, or 0 if none found.
|
||||
|
||||
const uword FIRST_UART_ADDRESS = $9F60
|
||||
const uword LAST_UART_ADDRESS = $9FF8
|
||||
|
||||
alias uart1 = cx16.r1
|
||||
alias uart2 = cx16.r2
|
||||
uart1 = uart2 = 0
|
||||
|
||||
for cx16.r0 in FIRST_UART_ADDRESS to LAST_UART_ADDRESS step 8 {
|
||||
if probe(cx16.r0) {
|
||||
if uart1 == 0
|
||||
uart1 = cx16.r0
|
||||
else if uart2 == 0 {
|
||||
uart2 = cx16.r0
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return uart1, uart2
|
||||
|
||||
private sub probe(uword address) -> bool {
|
||||
sys.set_irqd()
|
||||
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
|
||||
found = address[REG_MODEM_CONTROL] == $3f ; Bits 7 and 6 of MCR are always clear
|
||||
address[REG_MODEM_CONTROL] = 0
|
||||
}
|
||||
sys.clear_irqd()
|
||||
return found
|
||||
}
|
||||
}
|
||||
|
||||
sub initialize_uart(uword uart_addr) {
|
||||
; 921600 baud, 8,N,1, AutoFlow Control, FIFOS, no interrupts
|
||||
uart_addr[REG_LINE_CONTROL] = $80 ; Set DLAB
|
||||
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) {
|
||||
; Writes the string data to the uart.
|
||||
alias data_ptr = cx16.r0
|
||||
data_ptr = data
|
||||
while @(data_ptr) != 0 {
|
||||
while uart_addr[REG_LINE_STATUS] & LSR::THRE == 0 {
|
||||
; wait until data can be written
|
||||
}
|
||||
@(uart_addr) = @(data_ptr)
|
||||
data_ptr++
|
||||
}
|
||||
}
|
||||
|
||||
sub read_until(uword @zp uart_addr, str match, ^^ubyte buffer, uword max_size) -> uword {
|
||||
; 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.
|
||||
|
||||
alias buffer_ptr = cx16.r0
|
||||
alias match_ptr = cx16.r1
|
||||
alias data = cx16.r2L
|
||||
alias read_size = cx16.r3
|
||||
alias max_read_size = cx16.r4
|
||||
|
||||
match_ptr = match
|
||||
buffer_ptr = buffer
|
||||
max_read_size = max_size
|
||||
read_size = 0
|
||||
|
||||
while read_size < max_read_size {
|
||||
while uart_addr[REG_LINE_STATUS] & LSR::DR == 0 {
|
||||
; wait until data is present
|
||||
}
|
||||
@(buffer_ptr) = data = @(uart_addr)
|
||||
buffer_ptr++
|
||||
read_size++
|
||||
if data == @(match_ptr) {
|
||||
match_ptr++
|
||||
if @(match_ptr)==0
|
||||
break
|
||||
}
|
||||
else
|
||||
match_ptr = match
|
||||
}
|
||||
|
||||
return read_size
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
repeat {
|
||||
while uart_addr[REG_LINE_STATUS] & LSR::DR == 0 {
|
||||
; wait until data is present
|
||||
}
|
||||
if @(uart_addr) == @(match_ptr) {
|
||||
match_ptr++
|
||||
if @(match_ptr)==0
|
||||
break
|
||||
}
|
||||
else
|
||||
match_ptr = match
|
||||
}
|
||||
}
|
||||
|
||||
; 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
|
||||
}
|
||||
return @(uart_addr)
|
||||
}
|
||||
|
||||
private sub write_eol(uword uart_addr) {
|
||||
write(uart_addr, "\x0d\x0a")
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
%import math
|
||||
|
||||
lineclip {
|
||||
|
||||
; Based on Mark Moxon's Elite deep dive: https://elite.bbcelite.com/deep_dives/line-clipping.html
|
||||
|
||||
private word cx1, cy1, cx2, cy2
|
||||
|
||||
; Set the clipping rectangle coordinates (all inclusive)
|
||||
sub set_cliprect(word x1, word y1, word x2, word y2) {
|
||||
cx1 = x1
|
||||
cy1 = y1
|
||||
cx2 = x2
|
||||
cy2 = y2
|
||||
}
|
||||
|
||||
; returns true if (x,y) is inside the clipping rectangle set by set_cliprect()
|
||||
sub inside(word x, word y) -> bool {
|
||||
return x >= cx1 and x <= cx2 and y >= cy1 and y <= cy2
|
||||
}
|
||||
|
||||
; Clip line segment (x1,y1)-(x2,y2) against the current clipping rectangle, set by set_cliprect().
|
||||
; Returns (visible, clipped_x1, clipped_y1, clipped_x2, clipped_y2).
|
||||
sub clip(word x1, word y1, word x2, word y2) -> bool, word, word, word, word {
|
||||
bool a_inside = inside(x1, y1)
|
||||
bool b_inside = inside(x2, y2)
|
||||
|
||||
if a_inside and b_inside
|
||||
return true, x1, y1, x2, y2
|
||||
|
||||
; Quick reject: both off-screen on same side
|
||||
ubyte a_code = 0
|
||||
ubyte b_code = 0
|
||||
if x1 < cx1 a_code |= 1
|
||||
if x1 > cx2 a_code |= 2
|
||||
if y1 < cy1 a_code |= 4
|
||||
if y1 > cy2 a_code |= 8
|
||||
if x2 < cx1 b_code |= 1
|
||||
if x2 > cx2 b_code |= 2
|
||||
if y2 < cy1 b_code |= 4
|
||||
if y2 > cy2 b_code |= 8
|
||||
|
||||
if (a_code & b_code) != 0
|
||||
return false, 0, 0, 0, 0
|
||||
|
||||
word ddx = x2 - x1
|
||||
word ddy = y2 - y1
|
||||
if not a_inside
|
||||
x1, y1 = clip_one(x1, y1, ddx, ddy)
|
||||
if not b_inside
|
||||
x2, y2 = clip_one(x2, y2, -ddx, -ddy)
|
||||
|
||||
if not (inside(x1, y1) and inside(x2, y2))
|
||||
return false, 0, 0, 0, 0
|
||||
|
||||
return true, x1, y1, x2, y2
|
||||
|
||||
|
||||
sub clip_one(word ax, word ay, word odx, word ody) -> word, word {
|
||||
|
||||
word x, y
|
||||
bool did_clip
|
||||
|
||||
repeat 2 {
|
||||
x = ax
|
||||
y = ay
|
||||
did_clip = false
|
||||
|
||||
if x < cx1 and odx > 0 {
|
||||
y += frac(cx1 - x, ody, odx)
|
||||
x = cx1
|
||||
did_clip = true
|
||||
} else if x > cx2 and odx < 0 {
|
||||
y += frac(cx2 - x, ody, odx)
|
||||
x = cx2
|
||||
did_clip = true
|
||||
}
|
||||
|
||||
if y < cy1 and ody > 0 {
|
||||
x += frac(cy1 - y, odx, ody)
|
||||
y = cy1
|
||||
did_clip = true
|
||||
} else if y > cy2 and ody < 0 {
|
||||
x += frac(cy2 - y, odx, ody)
|
||||
y = cy2
|
||||
did_clip = true
|
||||
}
|
||||
|
||||
ax = x
|
||||
ay = y
|
||||
if not did_clip
|
||||
break
|
||||
}
|
||||
return ax, ay
|
||||
|
||||
; (int16_t)((int32_t)dist * num / den)
|
||||
sub frac(word dist, word num, word den) -> word {
|
||||
; 16×16→32 multiply, R starts as upper word of product
|
||||
alias Q_lo = cx16.r0
|
||||
alias Q_hi = cx16.r1
|
||||
alias R = cx16.r2
|
||||
alias aden = cx16.r3
|
||||
|
||||
; No long division available on 6502, so we use restoring division 32/16->16
|
||||
aden = abs(den) as uword
|
||||
Q_hi = abs(dist) * abs(num)
|
||||
R = math.mul16_last_upper()
|
||||
Q_lo = 0
|
||||
repeat 16 {
|
||||
R = R << 1
|
||||
if (Q_hi & $8000) != 0
|
||||
R |= 1
|
||||
Q_hi <<= 1
|
||||
if (Q_lo & $8000) != 0
|
||||
Q_hi |= 1
|
||||
Q_lo <<= 1
|
||||
if R >= aden {
|
||||
R -= aden
|
||||
Q_lo |= 1
|
||||
}
|
||||
}
|
||||
|
||||
if (dist < 0) xor (num < 0) xor (den < 0)
|
||||
return -(Q_lo as word)
|
||||
return Q_lo as word
|
||||
|
||||
/*
|
||||
here is the implementation using a 32/32 long division (shorter but less efficient than 32/16):
|
||||
|
||||
uword adist = abs(dist) as uword
|
||||
uword anum = abs(num) as uword
|
||||
uword aden = abs(den) as uword
|
||||
|
||||
; do 16*16->32 bit multiplication instead of slower full 32*32
|
||||
cx16.r2 = adist * anum
|
||||
long product32 = mklong2(math.mul16_last_upper(), cx16.r2)
|
||||
|
||||
; unfortunately the long division is still needed in prog8
|
||||
uword result_u = (product32 / (aden as long)) as uword
|
||||
word result = result_u as word
|
||||
if (dist < 0) xor (num < 0) xor (den < 0)
|
||||
result = -result
|
||||
return result
|
||||
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -439,7 +439,7 @@ divmod_uw_asm .proc
|
||||
|
||||
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
|
||||
@@ -1216,3 +1216,173 @@ _nosqadd:
|
||||
rts
|
||||
|
||||
.pend
|
||||
|
||||
|
||||
|
||||
|
||||
div_longs .proc
|
||||
; 32-bit SIGNED division.
|
||||
; long1 in cx16.r12/r13, long2 in cx16.r14/r15, result in cx16.r14/r15
|
||||
; clobbers A,X, cx16.r0, cx16.r1, cx16.r2, P8ZP_SCRATCH_W1, P8ZP_SCRATCH_W2
|
||||
|
||||
dividend_lo = cx16.r12 ; low word of dividend (also receives quotient bits)
|
||||
dividend_hi = cx16.r13 ; high word of dividend
|
||||
divisor_lo = cx16.r14 ; low word of divisor (also output quotient)
|
||||
divisor_hi = cx16.r15 ; high word of divisor
|
||||
saved_div_lo = cx16.r0 ; absolute divisor saved copy (low word)
|
||||
saved_div_hi = cx16.r1 ; absolute divisor saved copy (high word)
|
||||
remainder_lo = P8ZP_SCRATCH_W1 ; remainder low word (zeropage)
|
||||
remainder_hi = P8ZP_SCRATCH_W2 ; remainder high word (zeropage)
|
||||
sign = P8ZP_SCRATCH_B1 ; result sign
|
||||
temp = P8ZP_SCRATCH_REG ; temporary byte storage
|
||||
|
||||
; Check for division by zero
|
||||
lda divisor_lo
|
||||
ora divisor_lo+1
|
||||
ora divisor_hi
|
||||
ora divisor_hi+1
|
||||
bne _non_zero
|
||||
lda #0
|
||||
sta divisor_lo
|
||||
sta divisor_lo+1
|
||||
sta divisor_hi
|
||||
sta divisor_hi+1
|
||||
rts
|
||||
|
||||
_non_zero
|
||||
; Determine result sign: XOR MSBs of dividend and divisor
|
||||
lda dividend_hi+1
|
||||
eor divisor_hi+1
|
||||
sta sign
|
||||
|
||||
; Take absolute value of dividend if negative
|
||||
lda dividend_hi+1
|
||||
bpl _abs_divisor
|
||||
jsr _neg_dividend
|
||||
|
||||
_abs_divisor
|
||||
; Take absolute value of divisor if negative
|
||||
lda divisor_hi+1
|
||||
bpl _save_div
|
||||
jsr _neg_divisor
|
||||
|
||||
_save_div
|
||||
; Save absolute divisor into saved_div
|
||||
lda divisor_lo
|
||||
sta saved_div_lo
|
||||
lda divisor_lo+1
|
||||
sta saved_div_lo+1
|
||||
lda divisor_hi
|
||||
sta saved_div_hi
|
||||
lda divisor_hi+1
|
||||
sta saved_div_hi+1
|
||||
|
||||
; Clear remainder
|
||||
lda #0
|
||||
sta remainder_lo
|
||||
sta remainder_lo+1
|
||||
sta remainder_hi
|
||||
sta remainder_hi+1
|
||||
|
||||
; 32-bit restoring division loop (32 iterations)
|
||||
ldx #32
|
||||
|
||||
_loop
|
||||
; Shift (remainder : dividend) left 1 bit
|
||||
asl dividend_lo
|
||||
rol dividend_lo+1
|
||||
rol dividend_hi
|
||||
rol dividend_hi+1
|
||||
rol remainder_lo
|
||||
rol remainder_lo+1
|
||||
rol remainder_hi
|
||||
rol remainder_hi+1
|
||||
|
||||
; Subtract saved_div from remainder, check for borrow
|
||||
sec
|
||||
lda remainder_lo
|
||||
sbc saved_div_lo
|
||||
sta cx16.r2L ; byte 0 result -> r2L (NOT tax! X is loop counter)
|
||||
|
||||
lda remainder_lo+1
|
||||
sbc saved_div_lo+1
|
||||
sta cx16.r2H ; byte 1 result -> r2H
|
||||
|
||||
lda remainder_hi
|
||||
sbc saved_div_hi
|
||||
sta temp ; byte 2 result -> temp
|
||||
|
||||
lda remainder_hi+1
|
||||
sbc saved_div_hi+1
|
||||
bcc _next ; borrow means divisor didn't fit
|
||||
|
||||
; Divisor fits - store subtraction result as new remainder
|
||||
sta remainder_hi+1 ; byte 3 result from A
|
||||
lda temp
|
||||
sta remainder_hi ; byte 2
|
||||
lda cx16.r2H
|
||||
sta remainder_lo+1 ; byte 1
|
||||
lda cx16.r2L
|
||||
sta remainder_lo ; byte 0
|
||||
inc dividend_lo ; set quotient bit
|
||||
|
||||
_next
|
||||
dex
|
||||
bne _loop
|
||||
|
||||
; Copy quotient from dividend (r12/r13) to divisor (r14/r15)
|
||||
lda dividend_lo
|
||||
sta divisor_lo
|
||||
lda dividend_lo+1
|
||||
sta divisor_lo+1
|
||||
lda dividend_hi
|
||||
sta divisor_hi
|
||||
lda dividend_hi+1
|
||||
sta divisor_hi+1
|
||||
|
||||
; Apply sign if needed
|
||||
lda sign
|
||||
bpl _done
|
||||
jsr _neg_result
|
||||
|
||||
_done
|
||||
rts
|
||||
|
||||
; Helper: negate dividend (R12R13) in place
|
||||
_neg_dividend
|
||||
sec
|
||||
lda #0
|
||||
sbc dividend_lo
|
||||
sta dividend_lo
|
||||
lda #0
|
||||
sbc dividend_lo+1
|
||||
sta dividend_lo+1
|
||||
lda #0
|
||||
sbc dividend_hi
|
||||
sta dividend_hi
|
||||
lda #0
|
||||
sbc dividend_hi+1
|
||||
sta dividend_hi+1
|
||||
rts
|
||||
|
||||
; Helper: negate divisor (R14R15) in place
|
||||
_neg_divisor
|
||||
sec
|
||||
lda #0
|
||||
sbc divisor_lo
|
||||
sta divisor_lo
|
||||
lda #0
|
||||
sbc divisor_lo+1
|
||||
sta divisor_lo+1
|
||||
lda #0
|
||||
sbc divisor_hi
|
||||
sta divisor_hi
|
||||
lda #0
|
||||
sbc divisor_hi+1
|
||||
sta divisor_hi+1
|
||||
rts
|
||||
|
||||
; Helper: negate result (R14R15) — same as negating divisor
|
||||
_neg_result = _neg_divisor
|
||||
|
||||
.pend
|
||||
@@ -212,7 +212,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
|
||||
@@ -385,7 +385,7 @@ _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.
|
||||
|
||||
|
||||
@@ -90,9 +90,9 @@ io_error:
|
||||
if filenames_buf_size<=20
|
||||
return 0
|
||||
|
||||
uword filenames_buffer_start = filenames_buffer
|
||||
ubyte files_found = 0
|
||||
uword buf_ptr = filenames_buffer
|
||||
@(buf_ptr) = 0
|
||||
@(filenames_buffer) = 0
|
||||
cbm.SETNAM(1, "$")
|
||||
cbm.SETLFS(READ_IO_CHANNEL, drivenumber, 0)
|
||||
ubyte status = 1
|
||||
@@ -138,8 +138,12 @@ io_error:
|
||||
@(name_ptr) = 0
|
||||
|
||||
if pattern_ptr==0 or strings.pattern_match(list_filename, pattern_ptr) {
|
||||
buf_ptr += strings.copy(list_filename, buf_ptr) + 1
|
||||
filenames_buffer += strings.copy(list_filename, filenames_buffer) as uword + 1
|
||||
files_found++
|
||||
if filenames_buffer - filenames_buffer_start > filenames_buf_size-20 {
|
||||
@(filenames_buffer)=0
|
||||
goto end_listing_more
|
||||
}
|
||||
}
|
||||
|
||||
while cbm.CHRIN()!=0 {
|
||||
@@ -155,8 +159,13 @@ io_error:
|
||||
}
|
||||
|
||||
end_listing:
|
||||
@(buf_ptr) = 0
|
||||
@(filenames_buffer) = 0
|
||||
status = cbm.READST()
|
||||
sys.clear_carry()
|
||||
goto io_error
|
||||
|
||||
end_listing_more:
|
||||
sys.set_carry()
|
||||
|
||||
io_error:
|
||||
cbm.CLRCHN() ; restore default i/o devices
|
||||
@@ -407,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,19 +2,23 @@ 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
|
||||
import prog8.code.target.Cx16Target
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8.code.target.getCompilationTargetByName
|
||||
import prog8.compiler.CompilationResult
|
||||
import prog8.compiler.CompilerArguments
|
||||
import prog8.compiler.ErrorReporter
|
||||
import prog8.compiler.compileProgram
|
||||
import prog8.compiler.*
|
||||
import prog8.intermediate.IRFileReader
|
||||
import prog8.intermediate.Opcode
|
||||
import java.io.File
|
||||
import java.io.*
|
||||
import java.net.ConnectException
|
||||
import java.net.StandardProtocolFamily
|
||||
import java.net.URI
|
||||
import java.net.UnixDomainSocketAddress
|
||||
import java.nio.channels.Channels
|
||||
import java.nio.channels.SocketChannel
|
||||
import java.nio.file.*
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.io.path.*
|
||||
@@ -22,6 +26,11 @@ import kotlin.system.exitProcess
|
||||
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
if (args.contains("--daemon-server")) {
|
||||
val socketPath = CompilerDaemon.getDefaultSocketPath()
|
||||
CompilerDaemon(socketPath).run()
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE: if your compiler/IDE complains here about "Unresolved reference: buildversion",
|
||||
// it means that you have to run the gradle task once to generate this file.
|
||||
@@ -77,6 +86,7 @@ private fun compileMain(args: Array<String>): Boolean {
|
||||
val watchMode by cli.option(ArgType.Boolean, fullName = "watch", description = "continuous compilation mode (watch for file changes)")
|
||||
val version by cli.option(ArgType.Boolean, fullName = "version", description = "print compiler version and exit")
|
||||
val compareIR by cli.option(ArgType.String, fullName = "compareir", description = "compare generated .p8ir file with existing .p8ir file")
|
||||
val daemonMode by cli.option(ArgType.Boolean, fullName = "daemon", description = "use the prog8c compilation daemon (auto-starts it if not running)")
|
||||
val moduleFiles by cli.argument(ArgType.String, fullName = "modules", description = "main module file(s) to compile").optional().multiple(999)
|
||||
|
||||
try {
|
||||
@@ -188,6 +198,7 @@ private fun compileMain(args: Array<String>): Boolean {
|
||||
val results = mutableListOf<CompilationResult>()
|
||||
while(true) {
|
||||
println("Continuous watch mode active. Modules: $moduleFiles")
|
||||
ImportFileSystem.clearCaches()
|
||||
results.clear()
|
||||
for(filepathRaw in moduleFiles) {
|
||||
val filepath = pathFrom(filepathRaw).normalize()
|
||||
@@ -223,9 +234,6 @@ private fun compileMain(args: Array<String>): Boolean {
|
||||
errors = ErrorReporter(txtcolors)
|
||||
)
|
||||
val compilationResult = compileProgram(compilerArgs)
|
||||
// Note: ImportFileSystem.clearCaches() removed - it caused race conditions with parallel tests
|
||||
// where one test's cache clearing would break another test's source line lookups.
|
||||
// The cache is bounded by unique source files and doesn't need manual clearing.
|
||||
|
||||
if(checkSource==true)
|
||||
println("No output produced.")
|
||||
@@ -272,6 +280,71 @@ private fun compileMain(args: Array<String>): Boolean {
|
||||
println("\u001b[H\u001b[2J") // clear the screen
|
||||
}
|
||||
|
||||
} 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().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,
|
||||
if(checkSource==true) false else dontWriteAssembly != true,
|
||||
warnSymbolShadowing == true,
|
||||
warnImplicitTypeCasts == true,
|
||||
quietAll == true,
|
||||
quietAll == true || quietAssembler == true,
|
||||
showTimings == true,
|
||||
asmListfile == true,
|
||||
dontIncludeSourcelines != true,
|
||||
experimentalCodegen == true,
|
||||
dumpVariables == true,
|
||||
dumpSymbols == true,
|
||||
varsHighBank,
|
||||
varsGolden == true,
|
||||
slabsHighBank,
|
||||
slabsGolden == true,
|
||||
compilationTarget!!,
|
||||
breakpointCpuInstruction,
|
||||
printAst1 == true,
|
||||
printAst2 == true,
|
||||
ignoreFootguns == true,
|
||||
profilingInstrumentation == true,
|
||||
nostdlib == true,
|
||||
processedSymbols,
|
||||
absoluteSrcDirs,
|
||||
outputPath,
|
||||
cwd = Path.of(System.getProperty("user.dir")),
|
||||
errors = ErrorReporter(txtcolors)
|
||||
)
|
||||
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 {
|
||||
if((startEmulator1==true || startEmulator2==true) && moduleFiles.size>1) {
|
||||
System.err.println("can't start emulator when multiple module files are specified")
|
||||
@@ -333,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!!))
|
||||
@@ -341,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,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")
|
||||
@@ -558,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) {
|
||||
@@ -585,13 +661,208 @@ 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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---- daemon client ----
|
||||
|
||||
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 null
|
||||
}
|
||||
channel = connectToDaemon(socketPath, 4_000)
|
||||
if (channel == null) {
|
||||
System.err.println("prog8c daemon did not start in time")
|
||||
return null
|
||||
}
|
||||
println("prog8c daemon started.")
|
||||
} else {
|
||||
println("Using existing prog8c daemon at $socketPath")
|
||||
}
|
||||
|
||||
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 {
|
||||
return try {
|
||||
val javaHome = System.getProperty("java.home")
|
||||
val javaCmd = "$javaHome/bin/java"
|
||||
val classpath = System.getProperty("java.class.path")
|
||||
|
||||
// Shadow JAR (fat JAR with Main-Class manifest) takes priority
|
||||
val shadowJar = classpath.split(File.pathSeparatorChar)
|
||||
.firstOrNull { it.contains("prog8c") && it.endsWith("-all.jar") }
|
||||
|
||||
val cmd = mutableListOf<String>()
|
||||
cmd.add(javaCmd)
|
||||
if (shadowJar != null) {
|
||||
cmd.addAll(listOf("-jar", shadowJar))
|
||||
} else {
|
||||
cmd.addAll(listOf("-cp", classpath, "prog8.CompilerMainKt"))
|
||||
}
|
||||
cmd.add("--daemon-server")
|
||||
|
||||
val pb = ProcessBuilder(cmd)
|
||||
pb.directory(ImportFileSystem.userHome.toFile())
|
||||
pb.inheritIO()
|
||||
pb.start()
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
System.err.println("Cannot start daemon: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectToDaemon(socketPath: Path): SocketChannel? {
|
||||
return try {
|
||||
val address = UnixDomainSocketAddress.of(socketPath)
|
||||
val channel = SocketChannel.open(StandardProtocolFamily.UNIX)
|
||||
channel.connect(address)
|
||||
channel
|
||||
} catch (_: ConnectException) {
|
||||
null
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectToDaemon(socketPath: Path, timeoutMs: Long): SocketChannel? {
|
||||
val deadline = System.currentTimeMillis() + timeoutMs
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
connectToDaemon(socketPath)?.let { return it }
|
||||
Thread.sleep(100)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
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.BUILD_UNIX_TIME.toString(),
|
||||
filepath = compilerArgs.filepath.toString(),
|
||||
optimize = compilerArgs.optimize,
|
||||
writeAssembly = compilerArgs.writeAssembly,
|
||||
warnSymbolShadowing = compilerArgs.warnSymbolShadowing,
|
||||
warnImplicitTypeCasts = compilerArgs.warnImplicitTypeCasts,
|
||||
quietAll = compilerArgs.quietAll,
|
||||
quietAssembler = compilerArgs.quietAssembler,
|
||||
showTimings = compilerArgs.showTimings,
|
||||
asmListfile = compilerArgs.asmListfile,
|
||||
includeSourcelines = compilerArgs.includeSourcelines,
|
||||
experimentalCodegen = compilerArgs.experimentalCodegen,
|
||||
dumpVariables = compilerArgs.dumpVariables,
|
||||
dumpSymbols = compilerArgs.dumpSymbols,
|
||||
varsHighBank = compilerArgs.varsHighBank,
|
||||
varsGolden = compilerArgs.varsGolden,
|
||||
slabsHighBank = compilerArgs.slabsHighBank,
|
||||
slabsGolden = compilerArgs.slabsGolden,
|
||||
compilationTarget = compilerArgs.compilationTarget,
|
||||
breakpointCpuInstruction = compilerArgs.breakpointCpuInstruction,
|
||||
printAst1 = compilerArgs.printAst1,
|
||||
printAst2 = compilerArgs.printAst2,
|
||||
ignoreFootguns = compilerArgs.ignoreFootguns,
|
||||
profilingInstrumentation = compilerArgs.profilingInstrumentation,
|
||||
nostdlib = compilerArgs.nostdlib,
|
||||
symbolDefs = compilerArgs.symbolDefs,
|
||||
sourceDirs = compilerArgs.sourceDirs,
|
||||
outputDir = compilerArgs.outputDir.toString(),
|
||||
cwd = compilerArgs.cwd.toString()
|
||||
)
|
||||
|
||||
val requestJson = DaemonProtocol.encodeRequest(request)
|
||||
writer.write(requestJson)
|
||||
writer.newLine()
|
||||
writer.flush()
|
||||
|
||||
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 null
|
||||
}
|
||||
|
||||
val response = DaemonProtocol.decodeResponse(responseJson)
|
||||
|
||||
if (response.versionError != null) {
|
||||
System.err.println(response.versionError)
|
||||
return response
|
||||
}
|
||||
|
||||
val txtcolors = if(plainText) ErrorReporter.PlainText else ErrorReporter.AnsiColors
|
||||
val reporter = ErrorReporter(txtcolors)
|
||||
for (error in response.errors) {
|
||||
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
|
||||
} 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()
|
||||
@@ -539,6 +543,8 @@ private fun processAst(program: Program, errors: IErrorReporter, compilerOptions
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
program.checkPrivateAccess(errors)
|
||||
errors.report()
|
||||
program.checkIdentifiers(errors, compilerOptions)
|
||||
errors.report()
|
||||
program.charLiteralsToUByteLiterals(compilerOptions.compTarget, errors)
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
package prog8.compiler
|
||||
|
||||
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
|
||||
import java.nio.channels.*
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.measureTime
|
||||
|
||||
|
||||
internal class CompilerDaemon(private val socketPath: Path) {
|
||||
companion object {
|
||||
private const val IDLE_TIMEOUT_MS = 5 * 60_000L
|
||||
|
||||
fun getDefaultSocketPath(): Path {
|
||||
val baseDir = System.getenv("XDG_RUNTIME_DIR")
|
||||
?.let { Path.of(it) }
|
||||
?: ImportFileSystem.userHome.resolve(".cache")
|
||||
return baseDir.resolve("prog8c-daemon.sock")
|
||||
}
|
||||
|
||||
private fun myPid(): String? = try {
|
||||
ProcessHandle.current().pid().toString()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private var shutdownRequested = false
|
||||
|
||||
fun run() {
|
||||
socketPath.deleteIfExists()
|
||||
socketPath.parent?.let { Files.createDirectories(it) }
|
||||
|
||||
val address = UnixDomainSocketAddress.of(socketPath)
|
||||
val server = ServerSocketChannel.open(StandardProtocolFamily.UNIX).bind(address)
|
||||
server.configureBlocking(false)
|
||||
val selector = Selector.open()
|
||||
server.register(selector, SelectionKey.OP_ACCEPT)
|
||||
|
||||
val pid = myPid()
|
||||
System.err.println(buildString {
|
||||
append("prog8c daemon")
|
||||
if (pid != null) append(" (pid $pid)")
|
||||
append(" running at ${socketPath}")
|
||||
})
|
||||
|
||||
var lastRequestTime = System.currentTimeMillis()
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
val idleTime = System.currentTimeMillis() - lastRequestTime
|
||||
if (idleTime > IDLE_TIMEOUT_MS && lastRequestTime > 0) {
|
||||
// daemon idle timeout reached, shutting down
|
||||
break
|
||||
}
|
||||
val timeout = maxOf(1L, IDLE_TIMEOUT_MS - idleTime)
|
||||
if (selector.select(timeout) == 0)
|
||||
continue
|
||||
|
||||
val selected = selector.selectedKeys()
|
||||
val it = selected.iterator()
|
||||
if (it.hasNext()) {
|
||||
it.next()
|
||||
it.remove()
|
||||
val client = server.accept()
|
||||
if (client != null) {
|
||||
handleClient(client)
|
||||
client.close()
|
||||
lastRequestTime = System.currentTimeMillis()
|
||||
if (shutdownRequested) break
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
selector.close()
|
||||
server.close()
|
||||
socketPath.deleteIfExists()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleClient(clientChannel: SocketChannel) {
|
||||
try {
|
||||
val reader = BufferedReader(InputStreamReader(Channels.newInputStream(clientChannel), Charsets.UTF_8))
|
||||
val writer = BufferedWriter(OutputStreamWriter(Channels.newOutputStream(clientChannel), Charsets.UTF_8))
|
||||
|
||||
val reqJson = reader.readLine() ?: return
|
||||
val request: DaemonRequest = try {
|
||||
DaemonProtocol.decodeRequest(reqJson)
|
||||
} catch (e: Exception) {
|
||||
System.err.println("daemon: failed to decode request: ${e.message}")
|
||||
return
|
||||
}
|
||||
|
||||
// Version check (build timestamp must match)
|
||||
if (request.version != BUILD_UNIX_TIME.toString()) {
|
||||
val resp = DaemonResponse(
|
||||
ok = false,
|
||||
versionError = "daemon version mismatch: build time differs",
|
||||
errors = emptyList(),
|
||||
stdout = "",
|
||||
stderr = "",
|
||||
t_ms = 0,
|
||||
outputFiles = emptyList(),
|
||||
importedFiles = emptyList()
|
||||
)
|
||||
writer.write(DaemonProtocol.encodeResponse(resp))
|
||||
writer.newLine()
|
||||
writer.flush()
|
||||
System.err.println("daemon: version mismatch, shutting down")
|
||||
shutdownRequested = true
|
||||
return
|
||||
}
|
||||
|
||||
// Capture stdout/stderr
|
||||
val combinedBytes = ByteArrayOutputStream()
|
||||
val combinedStream = PrintStream(combinedBytes, true, Charsets.UTF_8.name())
|
||||
val oldOut = System.out
|
||||
val oldErr = System.err
|
||||
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)
|
||||
result = compileProgram(compilerArgs)
|
||||
}.toLong(DurationUnit.MILLISECONDS)
|
||||
} catch (e: Exception) {
|
||||
System.err.println("daemon: compilation crash: ${e.message}")
|
||||
daemonErr.err("internal daemon error: ${e.message}", Position.DUMMY)
|
||||
0L
|
||||
} finally {
|
||||
System.out.flush()
|
||||
System.err.flush()
|
||||
System.setOut(oldOut)
|
||||
System.setErr(oldErr)
|
||||
}
|
||||
|
||||
val outputFiles = if (result != null)
|
||||
listOfNotNull(
|
||||
request.outputDir.let { d ->
|
||||
val name = Path.of(request.filepath).fileName.toString().substringBeforeLast('.')
|
||||
val dir = if (d == ".") "" else "$d/"
|
||||
listOf(
|
||||
"${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()
|
||||
else
|
||||
emptyList()
|
||||
|
||||
val importedFiles = result?.importedFiles?.map { it.toString() } ?: emptyList()
|
||||
|
||||
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 = 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,
|
||||
startCol = msg.position?.startCol ?: 1,
|
||||
endCol = msg.position?.endCol ?: 1
|
||||
)
|
||||
},
|
||||
stdout = stdout,
|
||||
stderr = stderr,
|
||||
t_ms = t_ms,
|
||||
outputFiles = outputFiles,
|
||||
importedFiles = importedFiles
|
||||
)
|
||||
|
||||
val respJson = DaemonProtocol.encodeResponse(resp)
|
||||
writer.write(respJson)
|
||||
writer.newLine()
|
||||
writer.flush()
|
||||
} catch (e: IOException) {
|
||||
System.err.println("daemon: client I/O error: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun DaemonRequest.toCompilerArguments(daemonErr: DaemonErrorReporter): CompilerArguments {
|
||||
return CompilerArguments(
|
||||
filepath = Path.of(filepath),
|
||||
optimize = optimize,
|
||||
writeAssembly = writeAssembly,
|
||||
warnSymbolShadowing = warnSymbolShadowing,
|
||||
warnImplicitTypeCasts = warnImplicitTypeCasts,
|
||||
quietAll = quietAll,
|
||||
quietAssembler = quietAssembler,
|
||||
showTimings = showTimings,
|
||||
asmListfile = asmListfile,
|
||||
includeSourcelines = includeSourcelines,
|
||||
experimentalCodegen = experimentalCodegen,
|
||||
dumpVariables = dumpVariables,
|
||||
dumpSymbols = dumpSymbols,
|
||||
varsHighBank = varsHighBank,
|
||||
varsGolden = varsGolden,
|
||||
slabsHighBank = slabsHighBank,
|
||||
slabsGolden = slabsGolden,
|
||||
compilationTarget = compilationTarget,
|
||||
breakpointCpuInstruction = breakpointCpuInstruction,
|
||||
printAst1 = printAst1,
|
||||
printAst2 = printAst2,
|
||||
ignoreFootguns = ignoreFootguns,
|
||||
profilingInstrumentation = profilingInstrumentation,
|
||||
nostdlib = nostdlib,
|
||||
symbolDefs = symbolDefs,
|
||||
sourceDirs = sourceDirs,
|
||||
outputDir = Path.of(outputDir),
|
||||
cwd = Path.of(cwd),
|
||||
errors = daemonErr
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package prog8.compiler
|
||||
|
||||
import prog8.code.core.ErrorsReportedException
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.code.core.Position
|
||||
|
||||
|
||||
internal class DaemonErrorReporter : IErrorReporter {
|
||||
data class CompilerMessage(val severity: String, val message: String, val position: Position?)
|
||||
|
||||
private val messages = mutableListOf<CompilerMessage>()
|
||||
|
||||
override fun err(msg: String, position: Position) {
|
||||
messages.add(CompilerMessage("ERROR", msg, position))
|
||||
}
|
||||
|
||||
override fun warn(msg: String, position: Position) {
|
||||
messages.add(CompilerMessage("WARNING", msg, position))
|
||||
}
|
||||
|
||||
override fun info(msg: String, position: Position) {
|
||||
messages.add(CompilerMessage("INFO", msg, position))
|
||||
}
|
||||
|
||||
override fun undefined(symbol: List<String>, suggestImport: Boolean, position: Position) {
|
||||
if (suggestImport && symbol.size > 1)
|
||||
err("undefined symbol: ${symbol.joinToString(".")} (maybe you forgot to import a module that defines ${symbol.first()}?)", position)
|
||||
else
|
||||
err("undefined symbol: ${symbol.joinToString(".")}", position)
|
||||
}
|
||||
|
||||
override fun noErrors() = messages.none { it.severity == "ERROR" }
|
||||
|
||||
override fun report() {
|
||||
// Don't print anything, but throw so compileProgram() stops and returns null
|
||||
if (!noErrors())
|
||||
throw ErrorsReportedException("")
|
||||
}
|
||||
|
||||
override fun noErrorForLine(position: Position) =
|
||||
!messages.any { it.position?.line == position.line && it.severity != "INFO" }
|
||||
|
||||
override fun printSingleError(errormessage: String) {
|
||||
messages.add(CompilerMessage("ERROR", errormessage, null))
|
||||
}
|
||||
|
||||
fun getMessages() = messages.toList()
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
package prog8.compiler
|
||||
|
||||
|
||||
internal data class DaemonRequest(
|
||||
val version: String,
|
||||
val filepath: String,
|
||||
val optimize: Boolean,
|
||||
val writeAssembly: Boolean,
|
||||
val warnSymbolShadowing: Boolean,
|
||||
val warnImplicitTypeCasts: Boolean,
|
||||
val quietAll: Boolean,
|
||||
val quietAssembler: Boolean,
|
||||
val showTimings: Boolean,
|
||||
val asmListfile: Boolean,
|
||||
val includeSourcelines: Boolean,
|
||||
val experimentalCodegen: Boolean,
|
||||
val dumpVariables: Boolean,
|
||||
val dumpSymbols: Boolean,
|
||||
val varsHighBank: Int?,
|
||||
val varsGolden: Boolean,
|
||||
val slabsHighBank: Int?,
|
||||
val slabsGolden: Boolean,
|
||||
val compilationTarget: String,
|
||||
val breakpointCpuInstruction: String?,
|
||||
val printAst1: Boolean,
|
||||
val printAst2: Boolean,
|
||||
val ignoreFootguns: Boolean,
|
||||
val profilingInstrumentation: Boolean,
|
||||
val nostdlib: Boolean,
|
||||
val symbolDefs: Map<String, String>,
|
||||
val sourceDirs: List<String>,
|
||||
val outputDir: String,
|
||||
val cwd: String
|
||||
)
|
||||
|
||||
internal data class DaemonResponse(
|
||||
val ok: Boolean,
|
||||
val versionError: String?,
|
||||
val errors: List<DaemonError>,
|
||||
val stdout: String,
|
||||
val stderr: String,
|
||||
val t_ms: Long,
|
||||
val outputFiles: List<String>,
|
||||
val importedFiles: List<String>
|
||||
)
|
||||
|
||||
internal data class DaemonError(
|
||||
val severity: String,
|
||||
val message: String,
|
||||
val file: String?,
|
||||
val line: Int,
|
||||
val startCol: Int,
|
||||
val endCol: Int
|
||||
)
|
||||
|
||||
|
||||
// Minimal JSON encoder/decoder for the daemon protocol
|
||||
internal object DaemonProtocol {
|
||||
|
||||
fun encodeRequest(req: DaemonRequest): String = buildString {
|
||||
append('{')
|
||||
append(prop("version", req.version))
|
||||
append(prop("filepath", req.filepath))
|
||||
append(prop("optimize", req.optimize))
|
||||
append(prop("writeAssembly", req.writeAssembly))
|
||||
append(prop("warnSymbolShadowing", req.warnSymbolShadowing))
|
||||
append(prop("warnImplicitTypeCasts", req.warnImplicitTypeCasts))
|
||||
append(prop("quietAll", req.quietAll))
|
||||
append(prop("quietAssembler", req.quietAssembler))
|
||||
append(prop("showTimings", req.showTimings))
|
||||
append(prop("asmListfile", req.asmListfile))
|
||||
append(prop("includeSourcelines", req.includeSourcelines))
|
||||
append(prop("experimentalCodegen", req.experimentalCodegen))
|
||||
append(prop("dumpVariables", req.dumpVariables))
|
||||
append(prop("dumpSymbols", req.dumpSymbols))
|
||||
append(prop("varsHighBank", req.varsHighBank))
|
||||
append(prop("varsGolden", req.varsGolden))
|
||||
append(prop("slabsHighBank", req.slabsHighBank))
|
||||
append(prop("slabsGolden", req.slabsGolden))
|
||||
append(prop("compilationTarget", req.compilationTarget))
|
||||
append(propOpt("breakpointCpuInstruction", req.breakpointCpuInstruction))
|
||||
append(prop("printAst1", req.printAst1))
|
||||
append(prop("printAst2", req.printAst2))
|
||||
append(prop("ignoreFootguns", req.ignoreFootguns))
|
||||
append(prop("profilingInstrumentation", req.profilingInstrumentation))
|
||||
append(prop("nostdlib", req.nostdlib))
|
||||
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('}')
|
||||
}
|
||||
|
||||
fun encodeResponse(resp: DaemonResponse): String = buildString {
|
||||
append('{')
|
||||
append(prop("ok", resp.ok))
|
||||
append(propOpt("versionError", resp.versionError))
|
||||
append("\"errors\":[")
|
||||
for ((i, e) in resp.errors.withIndex()) {
|
||||
if (i > 0) append(',')
|
||||
append('{')
|
||||
append(prop("severity", e.severity))
|
||||
append(prop("message", e.message))
|
||||
append(propOpt("file", e.file))
|
||||
append(prop("line", e.line))
|
||||
append(prop("startCol", e.startCol))
|
||||
append(prop("endCol", e.endCol))
|
||||
append("null")
|
||||
setLength(length - 4)
|
||||
append('}')
|
||||
}
|
||||
append("],")
|
||||
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))
|
||||
append("null")
|
||||
setLength(length - 5)
|
||||
append('}')
|
||||
}
|
||||
|
||||
fun decodeResponse(json: String): DaemonResponse {
|
||||
val map = parseJsonObject(json)
|
||||
val errorsJson = map["errors"] as? List<*> ?: emptyList<Any>()
|
||||
val errors = errorsJson.map { e ->
|
||||
val em = e as Map<*, *>
|
||||
DaemonError(
|
||||
severity = em["severity"] as String,
|
||||
message = em["message"] as String,
|
||||
file = em["file"] as? String,
|
||||
line = (em["line"] 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()
|
||||
val importedFiles = (map["importedFiles"] as? List<*>)?.map { it as String } ?: emptyList()
|
||||
return DaemonResponse(
|
||||
ok = map["ok"] as Boolean,
|
||||
versionError = map["versionError"] as? String,
|
||||
errors = errors,
|
||||
stdout = map["stdout"] as String,
|
||||
stderr = map["stderr"] as String,
|
||||
t_ms = (map["t_ms"] as Number).toLong(),
|
||||
outputFiles = outputFiles,
|
||||
importedFiles = importedFiles
|
||||
)
|
||||
}
|
||||
|
||||
fun decodeRequest(json: String): DaemonRequest {
|
||||
val map = parseJsonObject(json)
|
||||
return DaemonRequest(
|
||||
version = map["version"] as String,
|
||||
filepath = map["filepath"] as String,
|
||||
optimize = map["optimize"] as Boolean,
|
||||
writeAssembly = map["writeAssembly"] as Boolean,
|
||||
warnSymbolShadowing = map["warnSymbolShadowing"] as Boolean,
|
||||
warnImplicitTypeCasts = map["warnImplicitTypeCasts"] as Boolean,
|
||||
quietAll = map["quietAll"] as Boolean,
|
||||
quietAssembler = map["quietAssembler"] as Boolean,
|
||||
showTimings = map["showTimings"] as Boolean,
|
||||
asmListfile = map["asmListfile"] as Boolean,
|
||||
includeSourcelines = map["includeSourcelines"] as Boolean,
|
||||
experimentalCodegen = map["experimentalCodegen"] as Boolean,
|
||||
dumpVariables = map["dumpVariables"] as Boolean,
|
||||
dumpSymbols = map["dumpSymbols"] as Boolean,
|
||||
varsHighBank = map["varsHighBank"] as? Int,
|
||||
varsGolden = map["varsGolden"] as Boolean,
|
||||
slabsHighBank = map["slabsHighBank"] as? Int,
|
||||
slabsGolden = map["slabsGolden"] as Boolean,
|
||||
compilationTarget = map["compilationTarget"] as String,
|
||||
breakpointCpuInstruction = map["breakpointCpuInstruction"] as? String,
|
||||
printAst1 = map["printAst1"] as Boolean,
|
||||
printAst2 = map["printAst2"] as Boolean,
|
||||
ignoreFootguns = map["ignoreFootguns"] as Boolean,
|
||||
profilingInstrumentation = map["profilingInstrumentation"] as Boolean,
|
||||
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,
|
||||
cwd = map["cwd"] as String
|
||||
)
|
||||
}
|
||||
|
||||
private fun encodeJsonString(s: String): String {
|
||||
val sb = StringBuilder(s.length + 2)
|
||||
sb.append('"')
|
||||
for (c in s) {
|
||||
when (c) {
|
||||
'"' -> sb.append("\\\"")
|
||||
'\\' -> sb.append("\\\\")
|
||||
'\n' -> sb.append("\\n")
|
||||
'\r' -> sb.append("\\r")
|
||||
'\t' -> sb.append("\\t")
|
||||
else -> sb.append(c)
|
||||
}
|
||||
}
|
||||
sb.append('"')
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun prop(name: String, value: String): String = "\"$name\":${encodeJsonString(value)},"
|
||||
private fun prop(name: String, value: Boolean): String = "\"$name\":$value,"
|
||||
private fun prop(name: String, value: Int): String = "\"$name\":$value,"
|
||||
private fun prop(name: String, value: Long): String = "\"$name\":$value,"
|
||||
private fun prop(name: String, value: Int?): String = value?.let { "\"$name\":$it," } ?: "\"$name\":null,"
|
||||
private fun prop(name: String, value: Map<String, String>): String {
|
||||
val entries = value.entries.joinToString(",") { "${encodeJsonString(it.key)}:${encodeJsonString(it.value)}" }
|
||||
return "\"$name\":{$entries},"
|
||||
}
|
||||
private fun prop(name: String, value: List<String>): String {
|
||||
val items = value.joinToString(",") { encodeJsonString(it) }
|
||||
return "\"$name\":[$items],"
|
||||
}
|
||||
private fun propOpt(name: String, value: String?): String = value?.let { "\"$name\":${encodeJsonString(it)}," } ?: ""
|
||||
|
||||
private fun parseJsonObject(json: String): Map<String, Any?> {
|
||||
val trimmed = json.trim()
|
||||
if (!trimmed.startsWith('{') || !trimmed.endsWith('}'))
|
||||
throw IllegalArgumentException("not a JSON object: $json")
|
||||
val inner = trimmed.substring(1, trimmed.length - 1)
|
||||
return parsePairs(inner)
|
||||
}
|
||||
|
||||
private fun parsePairs(s: String): Map<String, Any?> {
|
||||
val result = LinkedHashMap<String, Any?>()
|
||||
var pos = 0
|
||||
while (pos < s.length) {
|
||||
skipWhitespace(s, pos).let { pos = it }
|
||||
if (pos >= s.length) break
|
||||
// key
|
||||
check(s[pos] == '"') { "expected '\"' at pos $pos in: $s" }
|
||||
pos++
|
||||
val keyEnd = s.indexOf('"', pos)
|
||||
check(keyEnd > 0) { "unterminated string at pos $pos" }
|
||||
val key = unescapeJson(s.substring(pos, keyEnd))
|
||||
pos = keyEnd + 1
|
||||
skipWhitespace(s, pos).let { pos = it }
|
||||
check(s[pos] == ':') { "expected ':' at pos $pos" }
|
||||
pos++
|
||||
skipWhitespace(s, pos).let { pos = it }
|
||||
// value
|
||||
val (value, newPos) = parseValue(s, pos)
|
||||
result[key] = value
|
||||
pos = newPos
|
||||
skipWhitespace(s, pos).let { pos = it }
|
||||
if (pos < s.length && s[pos] == ',') pos++
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun parseValue(s: String, start: Int): Pair<Any?, Int> {
|
||||
var pos = start
|
||||
skipWhitespace(s, pos).let { pos = it }
|
||||
return when {
|
||||
pos >= s.length -> null to pos
|
||||
s[pos] == '"' -> parseStringValue(s, pos)
|
||||
s[pos] == '{' -> parseMapValue(s, pos)
|
||||
s[pos] == '[' -> parseListValue(s, pos)
|
||||
s.startsWith("null", pos) -> null to (pos + 4)
|
||||
s.startsWith("true", pos) -> true to (pos + 4)
|
||||
s.startsWith("false", pos) -> false to (pos + 5)
|
||||
else -> parseNumberValue(s, pos)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseStringValue(s: String, start: Int): Pair<String, Int> {
|
||||
check(s[start] == '"')
|
||||
pos@ for (pos in (start + 1) until s.length) {
|
||||
when (s[pos]) {
|
||||
'\\' -> continue@pos // skip next char
|
||||
'"' -> return unescapeJson(s.substring(start + 1, pos)) to (pos + 1)
|
||||
}
|
||||
}
|
||||
error("unterminated string in JSON")
|
||||
}
|
||||
|
||||
private fun parseMapValue(s: String, start: Int): Pair<Map<String, Any?>, Int> {
|
||||
check(s[start] == '{')
|
||||
var depth = 1
|
||||
var idx = start + 1
|
||||
while (idx < s.length) {
|
||||
when (s[idx]) {
|
||||
'{' -> depth++
|
||||
'}' -> {
|
||||
depth--
|
||||
if (depth == 0)
|
||||
return parsePairs(s.substring(start + 1, idx)) to (idx + 1)
|
||||
}
|
||||
'"' -> {
|
||||
// skip string content
|
||||
var p = idx + 1
|
||||
while (p < s.length) {
|
||||
when (s[p]) {
|
||||
'\\' -> p += 2
|
||||
'"' -> { idx = p; break }
|
||||
else -> p++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
idx++
|
||||
}
|
||||
error("unterminated JSON object")
|
||||
}
|
||||
|
||||
private fun parseListValue(s: String, start: Int): Pair<List<Any?>, Int> {
|
||||
check(s[start] == '[')
|
||||
val items = mutableListOf<Any?>()
|
||||
var pos = start + 1
|
||||
while (pos < s.length) {
|
||||
skipWhitespace(s, pos).let { pos = it }
|
||||
if (s[pos] == ']') return items to (pos + 1)
|
||||
val (value, newPos) = parseValue(s, pos)
|
||||
items.add(value)
|
||||
pos = newPos
|
||||
skipWhitespace(s, pos).let { pos = it }
|
||||
if (pos < s.length && s[pos] == ',') pos++
|
||||
}
|
||||
error("unterminated JSON array")
|
||||
}
|
||||
|
||||
private fun parseNumberValue(s: String, start: Int): Pair<Number, Int> {
|
||||
var end = start
|
||||
while (end < s.length && (s[end].isDigit() || s[end] == '-' || s[end] == '+' || s[end] == '.' || s[end] == 'e' || s[end] == 'E'))
|
||||
end++
|
||||
val num = s.substring(start, end)
|
||||
return if (num.contains('.') || num.contains('e') || num.contains('E'))
|
||||
num.toDouble() to end
|
||||
else
|
||||
num.toLong() to end
|
||||
}
|
||||
|
||||
private fun skipWhitespace(s: String, pos: Int): Int {
|
||||
var p = pos
|
||||
while (p < s.length && s[p].isWhitespace()) p++
|
||||
return p
|
||||
}
|
||||
|
||||
private fun unescapeJson(s: String): String {
|
||||
if ('\\' !in s) return s
|
||||
val sb = StringBuilder(s.length)
|
||||
var i = 0
|
||||
while (i < s.length) {
|
||||
if (s[i] == '\\' && i + 1 < s.length) {
|
||||
when (s[i + 1]) {
|
||||
'"' -> { sb.append('"'); i += 2 }
|
||||
'\\' -> { sb.append('\\'); i += 2 }
|
||||
'/' -> { sb.append('/'); i += 2 }
|
||||
'n' -> { sb.append('\n'); i += 2 }
|
||||
'r' -> { sb.append('\r'); i += 2 }
|
||||
't' -> { sb.append('\t'); i += 2 }
|
||||
'u' -> {
|
||||
val hex = s.substring(i + 2, i + 6)
|
||||
sb.append(hex.toInt(16).toChar())
|
||||
i += 6
|
||||
}
|
||||
else -> { sb.append(s[i]); i++ }
|
||||
}
|
||||
} else {
|
||||
sb.append(s[i])
|
||||
i++
|
||||
}
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun check(condition: Boolean, lazyMessage: () -> String) {
|
||||
if (!condition) throw IllegalArgumentException(lazyMessage())
|
||||
}
|
||||
|
||||
private fun error(message: String): Nothing = throw IllegalArgumentException(message)
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -38,6 +38,8 @@ internal class AstChecker(private val program: Program,
|
||||
} else {
|
||||
if (startSub.parameters.isNotEmpty() || startSub.returntypes.isNotEmpty())
|
||||
errors.err("program entrypoint subroutine can't have parameters and/or return values", startSub.position)
|
||||
if (startSub.isPrivate)
|
||||
errors.err("program entrypoint subroutine 'start' cannot be private", startSub.position)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,26 +102,6 @@ internal class AstChecker(private val program: Program,
|
||||
errors.undefined(identifier.nameInSource, identifier.firstTarget(program.builtinFunctions)==null, identifier.position)
|
||||
}
|
||||
else {
|
||||
// Check for private symbol access
|
||||
val privateError = when (stmt) {
|
||||
is VarDecl -> {
|
||||
if (stmt.isPrivate && !isAccessWithinSameBlock(identifier, stmt.definingBlock))
|
||||
"private variable '${stmt.scopedName.joinToString(".")}'"
|
||||
else
|
||||
null
|
||||
}
|
||||
is Subroutine -> {
|
||||
if (stmt.isPrivate && !isAccessWithinSameBlock(identifier, stmt.definingBlock))
|
||||
"private subroutine '${stmt.scopedName.joinToString(".")}'"
|
||||
else
|
||||
null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
if (privateError != null) {
|
||||
errors.err("cannot access $privateError from outside its block", identifier.position)
|
||||
}
|
||||
|
||||
val target = stmt as? VarDecl
|
||||
if (target != null && target.origin == VarDeclOrigin.SUBROUTINEPARAM) {
|
||||
if (target.definingSubroutine!!.isAsmSubroutine) {
|
||||
@@ -137,15 +119,13 @@ internal class AstChecker(private val program: Program,
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAccessWithinSameBlock(identifier: IdentifierReference, targetBlock: Block): Boolean {
|
||||
var current: Node? = identifier.parent
|
||||
while (current != null) {
|
||||
if (current is Block) {
|
||||
return current === targetBlock
|
||||
}
|
||||
current = current.parent
|
||||
private fun checkPrivateTypeAccess(type: DataType, position: Position, currentBlock: Block) {
|
||||
val struct = type.subType
|
||||
if(struct is StructDecl && struct.isPrivate) {
|
||||
val structBlock = struct.definingBlock
|
||||
if(currentBlock !== structBlock)
|
||||
errors.err("cannot access private struct '${struct.scopedName.joinToString(".")}' from outside its block", position)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun visit(unrollLoop: UnrollLoop) {
|
||||
@@ -294,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 -> {
|
||||
@@ -507,6 +486,16 @@ internal class AstChecker(private val program: Program,
|
||||
if(address != null && address !is NumericLiteral)
|
||||
err("address must be a constant")
|
||||
|
||||
val currentBlock = subroutine.definingBlock
|
||||
subroutine.parameters.forEach { param ->
|
||||
if(param.type.isPointer || param.type.isStructInstance)
|
||||
checkPrivateTypeAccess(param.type, subroutine.position, currentBlock)
|
||||
}
|
||||
subroutine.returntypes.forEach { rt ->
|
||||
if(rt.isPointer || rt.isStructInstance)
|
||||
checkPrivateTypeAccess(rt, subroutine.position, currentBlock)
|
||||
}
|
||||
|
||||
super.visit(subroutine)
|
||||
|
||||
// subroutine must contain at least one 'return' or 'goto'
|
||||
@@ -708,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
|
||||
@@ -917,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) {
|
||||
@@ -959,9 +953,13 @@ internal class AstChecker(private val program: Program,
|
||||
}
|
||||
|
||||
override fun visit(decl: VarDecl) {
|
||||
|
||||
if(decl.names.size>1)
|
||||
throw InternalCompilerException("vardecls with multiple names should have been converted into individual vardecls")
|
||||
|
||||
if(decl.datatype.isPointer || decl.datatype.isStructInstance)
|
||||
checkPrivateTypeAccess(decl.datatype, decl.position, decl.definingBlock)
|
||||
|
||||
if(decl.type!=VarDeclType.CONST && "::" in decl.name)
|
||||
errors.err("only enum members can be accessed with '::' syntax", decl.position)
|
||||
|
||||
@@ -981,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")
|
||||
}
|
||||
|
||||
@@ -1759,6 +1759,12 @@ internal class AstChecker(private val program: Program,
|
||||
if(!typecast.expression.inferType(program).isKnown)
|
||||
errors.err("this expression doesn't return a value", typecast.expression.position)
|
||||
|
||||
if(typecast.type.isPointer || typecast.type.isStructInstance) {
|
||||
val currentBlock = findParentNode<Block>(typecast)
|
||||
if(currentBlock!=null)
|
||||
checkPrivateTypeAccess(typecast.type, typecast.position, currentBlock)
|
||||
}
|
||||
|
||||
val cv = typecast.expression.constValue(program)
|
||||
if(cv != null) {
|
||||
if(typecast.type.isBasic) {
|
||||
@@ -1862,7 +1868,7 @@ internal class AstChecker(private val program: Program,
|
||||
checkPointer(binexpr.left as IdentifierReference)
|
||||
}
|
||||
|
||||
if(builtinFunctionName=="memory") {
|
||||
if(builtinFunctionName=="memory" || builtinFunctionName=="memory__ref") {
|
||||
errors.err("memory() function call should have been desugared into dedicated AST nodes", functionCallExpr.position)
|
||||
}
|
||||
|
||||
@@ -1870,7 +1876,7 @@ internal class AstChecker(private val program: Program,
|
||||
}
|
||||
|
||||
override fun visit(functionCallStatement: FunctionCallStatement) {
|
||||
if(functionCallStatement.isMemoryCall) {
|
||||
if(functionCallStatement.isMemoryCall || functionCallStatement.isMemoryRefCall) {
|
||||
errors.err("memory() function call should have been desugared into dedicated AST nodes", functionCallStatement.position)
|
||||
}
|
||||
|
||||
|
||||
@@ -123,6 +123,12 @@ internal fun Program.verifyFunctionArgTypes(errors: IErrorReporter, options: Com
|
||||
fixer.visit(this)
|
||||
}
|
||||
|
||||
internal fun Program.checkPrivateAccess(errors: IErrorReporter) {
|
||||
val checker = PrivateAccessChecker(this, errors)
|
||||
checker.visit(this)
|
||||
}
|
||||
|
||||
|
||||
internal fun Program.preprocessAst(errors: IErrorReporter, options: CompilationOptions) {
|
||||
val mergeBlocks = BlockMerger(errors)
|
||||
mergeBlocks.visit(this)
|
||||
|
||||
@@ -179,6 +179,13 @@ internal class AstIdentifiersChecker(private val errors: IErrorReporter,
|
||||
super.visit(subroutine)
|
||||
}
|
||||
|
||||
override fun visit(alias: Alias) {
|
||||
val existing = alias.definingScope.lookup(listOf(alias.alias))
|
||||
if(existing != null && existing !== alias)
|
||||
nameError(alias.alias, alias.position, existing)
|
||||
super.visit(alias)
|
||||
}
|
||||
|
||||
override fun visit(label: Label) {
|
||||
if(label.name in BuiltinFunctions) {
|
||||
errors.err("builtin function cannot be redefined", label.position)
|
||||
|
||||
@@ -356,6 +356,10 @@ class AstPreprocessor(val program: Program,
|
||||
|
||||
checkStringParam(functionCallExpr as IFunctionCall, stmtOfExpression)
|
||||
|
||||
if (functionCallExpr.isMemoryCall && functionCallExpr.args.size == 1) {
|
||||
functionCallExpr.target = IdentifierReference(listOf("memory__ref"), functionCallExpr.target.position).apply { linkParents(functionCallExpr) }
|
||||
}
|
||||
|
||||
if(functionCallExpr.target.nameInSource==listOf("sizeof")) {
|
||||
val arg = functionCallExpr.args.firstOrNull()
|
||||
if (arg is PtrDereference) {
|
||||
@@ -373,6 +377,11 @@ class AstPreprocessor(val program: Program,
|
||||
|
||||
override fun after(functionCallStatement: FunctionCallStatement, parent: Node): Iterable<AstModification> {
|
||||
checkStringParam(functionCallStatement as IFunctionCall, functionCallStatement)
|
||||
|
||||
if (functionCallStatement.isMemoryCall && functionCallStatement.args.size == 1) {
|
||||
functionCallStatement.target = IdentifierReference(listOf("memory__ref"), functionCallStatement.target.position).apply { linkParents(functionCallStatement) }
|
||||
}
|
||||
|
||||
return noModifications
|
||||
}
|
||||
|
||||
@@ -400,15 +409,15 @@ class AstPreprocessor(val program: Program,
|
||||
val replacement = if(tgt2!=null) {
|
||||
if(tgt2 is BuiltinFunctionPlaceholder) {
|
||||
val unscopedTarget = IdentifierReference(listOf(tgt2.name), alias.position)
|
||||
Alias(alias.alias, unscopedTarget, alias.position)
|
||||
Alias(alias.alias, unscopedTarget, alias.isPrivate, alias.position)
|
||||
} else if(tgt2.scopedName != chainedTargetName.nameInSource) {
|
||||
val scopedTarget = IdentifierReference(tgt2.scopedName, alias.position)
|
||||
Alias(alias.alias, scopedTarget, alias.position)
|
||||
Alias(alias.alias, scopedTarget, alias.isPrivate, alias.position)
|
||||
} else {
|
||||
Alias(alias.alias, chainedTargetName, alias.position)
|
||||
Alias(alias.alias, chainedTargetName, alias.isPrivate, alias.position)
|
||||
}
|
||||
} else {
|
||||
Alias(alias.alias, chainedTargetName, alias.position)
|
||||
Alias(alias.alias, chainedTargetName, alias.isPrivate, alias.position)
|
||||
}
|
||||
return listOf(AstReplaceNode(alias, replacement, parent))
|
||||
}
|
||||
@@ -469,6 +478,10 @@ class AstPreprocessor(val program: Program,
|
||||
}
|
||||
|
||||
override fun after(enum: Enumeration, parent: Node): Iterable<AstModification> {
|
||||
if (enum.members.isEmpty()) {
|
||||
errors.err("enumeration must contain at least one member", enum.position)
|
||||
}
|
||||
|
||||
// first check that there is no name conflict
|
||||
(parent as? IStatementContainer)?.let {
|
||||
val allNamed = it.statements.asSequence()
|
||||
@@ -510,6 +523,7 @@ class AstPreprocessor(val program: Program,
|
||||
.names(membername)
|
||||
.type(VarDeclType.CONST)
|
||||
.value(membervalue)
|
||||
.isPrivate(enum.isPrivate)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -26,18 +26,18 @@ internal class CodeDesugarer(val program: Program, private val target: ICompilat
|
||||
// - replace ptr^^ by @(ptr) if ptr is just an uword.
|
||||
// - replace p1^^ = p2^^ by memcopy.
|
||||
|
||||
private val globalReservedSlabs = mutableSetOf<String>()
|
||||
private val globalReservedSlabs = mutableMapOf<String, MemorySlabReservation>()
|
||||
|
||||
override fun before(program: Program): Iterable<AstModification> {
|
||||
globalReservedSlabs.clear()
|
||||
// Pre-collect existing reservations from the entire program to avoid duplicates in multiple passes
|
||||
val collector = object : IAstVisitor {
|
||||
override fun visit(reservation: MemorySlabReservation) {
|
||||
globalReservedSlabs.add(reservation.slabName)
|
||||
globalReservedSlabs[reservation.slabName] = reservation
|
||||
}
|
||||
}
|
||||
program.modules.forEach { module ->
|
||||
module.statements.forEach { it.accept(collector) }
|
||||
module.accept(collector)
|
||||
}
|
||||
return super.before(program)
|
||||
}
|
||||
@@ -262,6 +262,20 @@ _after:
|
||||
}
|
||||
|
||||
override fun after(functionCallExpr: FunctionCallExpression, parent: Node): Iterable<AstModification> {
|
||||
if (functionCallExpr.isMemoryRefCall) {
|
||||
val str = functionCallExpr.args[0] as? StringLiteral
|
||||
if (str == null) {
|
||||
errors.err("memory name argument must be a string literal", functionCallExpr.args[0].position)
|
||||
return noModifications
|
||||
} else if (str.value.isEmpty()) {
|
||||
errors.err("memory name argument cannot be empty string", functionCallExpr.args[0].position)
|
||||
return noModifications
|
||||
}
|
||||
val slabName = sanitizeSlabName(str.value)
|
||||
val ref = MemorySlabRef(slabName, functionCallExpr.position)
|
||||
return listOf(AstReplaceNode(functionCallExpr, ref, parent))
|
||||
}
|
||||
|
||||
if (functionCallExpr.isMemoryCall) {
|
||||
val str = functionCallExpr.args[0] as? StringLiteral
|
||||
if (str == null) {
|
||||
@@ -297,9 +311,10 @@ _after:
|
||||
val container = containingStmt.parent as IStatementContainer
|
||||
|
||||
val mods = mutableListOf<AstModification>(AstReplaceNode(functionCallExpr, ref, parent))
|
||||
if (slabName !in globalReservedSlabs) {
|
||||
val existing = globalReservedSlabs[slabName]
|
||||
if (existing == null || existing.size != size || existing.align != align) {
|
||||
mods.add(AstInsert.before(containingStmt, reservation, container))
|
||||
globalReservedSlabs.add(slabName)
|
||||
globalReservedSlabs[slabName] = reservation
|
||||
}
|
||||
return mods
|
||||
}
|
||||
@@ -307,6 +322,11 @@ _after:
|
||||
}
|
||||
|
||||
override fun after(functionCallStatement: FunctionCallStatement, parent: Node): Iterable<AstModification> {
|
||||
if (functionCallStatement.isMemoryRefCall) {
|
||||
val container = parent as IStatementContainer
|
||||
return listOf(AstRemove(functionCallStatement, container))
|
||||
}
|
||||
|
||||
if (functionCallStatement.isMemoryCall) {
|
||||
val str = functionCallStatement.args[0] as? StringLiteral
|
||||
if (str == null) {
|
||||
@@ -336,11 +356,12 @@ _after:
|
||||
val align = alignNum.toUInt()
|
||||
|
||||
val container = parent as IStatementContainer
|
||||
return if (slabName in globalReservedSlabs) {
|
||||
val existing = globalReservedSlabs[slabName]
|
||||
return if (existing != null && existing.size == size && existing.align == align) {
|
||||
listOf(AstRemove(functionCallStatement, container))
|
||||
} else {
|
||||
globalReservedSlabs.add(slabName)
|
||||
val reservation = MemorySlabReservation(slabName, size, align, functionCallStatement.position)
|
||||
globalReservedSlabs[slabName] = reservation
|
||||
listOf(AstReplaceNode(functionCallStatement, reservation, parent))
|
||||
}
|
||||
}
|
||||
|
||||
+27
-2
@@ -20,12 +20,13 @@ internal class LiteralsToAutoVarsAndRecombineIdentifiers(private val program: Pr
|
||||
val parentFunc = (string.parent as? IFunctionCall)?.target
|
||||
if(parentFunc!=null) {
|
||||
val parentCall = string.parent as? IFunctionCall
|
||||
if(parentCall!=null && parentCall.isMemoryCall) {
|
||||
if(parentCall!=null && (parentCall.isMemoryCall || parentCall.isMemoryRefCall)) {
|
||||
// memory() builtin function just uses the string as a label name
|
||||
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))
|
||||
}
|
||||
@@ -149,6 +150,14 @@ internal class LiteralsToAutoVarsAndRecombineIdentifiers(private val program: Pr
|
||||
// First component might be an alias
|
||||
val tgt2 = identifier.definingScope.lookup(identifier.nameInSource[0]) as? Alias
|
||||
if(tgt2!=null && parent !is Alias) {
|
||||
if(tgt2.isPrivate) {
|
||||
val referencingBlock = findParentNode<Block>(identifier)
|
||||
val aliasBlock = findParentNode<Block>(tgt2)
|
||||
if(referencingBlock!=null && aliasBlock!=null && referencingBlock!==aliasBlock) {
|
||||
errors.err("cannot access private alias '${tgt2.alias}' from outside its block", identifier.position)
|
||||
return noModifications
|
||||
}
|
||||
}
|
||||
val aliasTarget = resolveAliasTarget(tgt2)
|
||||
if(aliasTarget != null) {
|
||||
val actual = when(aliasTarget) {
|
||||
@@ -161,6 +170,14 @@ internal class LiteralsToAutoVarsAndRecombineIdentifiers(private val program: Pr
|
||||
}
|
||||
|
||||
if(target is Alias && parent !is Alias) {
|
||||
if(target.isPrivate) {
|
||||
val referencingBlock = findParentNode<Block>(identifier)
|
||||
val aliasBlock = findParentNode<Block>(target)
|
||||
if(referencingBlock!=null && aliasBlock!=null && referencingBlock!==aliasBlock) {
|
||||
errors.err("cannot access private alias '${target.alias}' from outside its block", identifier.position)
|
||||
return noModifications
|
||||
}
|
||||
}
|
||||
val targetStatement = resolveAliasTarget(target)
|
||||
if(targetStatement == null) {
|
||||
return noModifications // alias loop or unresolved
|
||||
@@ -220,6 +237,14 @@ internal class LiteralsToAutoVarsAndRecombineIdentifiers(private val program: Pr
|
||||
if(deref.chain.isEmpty()) return emptyList()
|
||||
val tgt2 = deref.definingScope.lookup(deref.chain[0]) as? Alias
|
||||
if(tgt2!=null && parent !is Alias) {
|
||||
if(tgt2.isPrivate) {
|
||||
val referencingBlock = findParentNode<Block>(deref)
|
||||
val aliasBlock = findParentNode<Block>(tgt2)
|
||||
if(referencingBlock!=null && aliasBlock!=null && referencingBlock!==aliasBlock) {
|
||||
errors.err("cannot access private alias '${tgt2.alias}' from outside its block", deref.position)
|
||||
return noModifications
|
||||
}
|
||||
}
|
||||
val aliasTarget = resolveAliasTarget(tgt2)
|
||||
if(aliasTarget != null) {
|
||||
val unaliased = when(aliasTarget) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package prog8.compiler.astprocessing
|
||||
|
||||
import prog8.ast.Node
|
||||
import prog8.ast.ParentSentinel
|
||||
import prog8.ast.Program
|
||||
import prog8.ast.expressions.BinaryExpression
|
||||
import prog8.ast.expressions.IdentifierReference
|
||||
import prog8.ast.statements.*
|
||||
import prog8.ast.walk.IAstVisitor
|
||||
import prog8.code.core.IErrorReporter
|
||||
|
||||
internal class PrivateAccessChecker(
|
||||
private val program: Program,
|
||||
private val errors: IErrorReporter
|
||||
) : IAstVisitor {
|
||||
|
||||
override fun visit(identifier: IdentifierReference) {
|
||||
val parentExpr = identifier.parent as? BinaryExpression
|
||||
if (parentExpr?.operator == ".") {
|
||||
return // identifiers will be checked over at the BinaryExpression itself
|
||||
}
|
||||
|
||||
val stmt = identifier.targetStatement(program.builtinFunctions)
|
||||
if (stmt != null) {
|
||||
val privateError = when (stmt) {
|
||||
is VarDecl -> {
|
||||
if (stmt.isPrivate && !isAccessWithinSameBlock(identifier, stmt.definingBlock))
|
||||
"private variable '${stmt.scopedName.joinToString(".")}'"
|
||||
else null
|
||||
}
|
||||
is Subroutine -> {
|
||||
if (stmt.isPrivate && !isAccessWithinSameBlock(identifier, stmt.definingBlock))
|
||||
"private subroutine '${stmt.scopedName.joinToString(".")}'"
|
||||
else null
|
||||
}
|
||||
is StructDecl -> {
|
||||
if (stmt.isPrivate && !isAccessWithinSameBlock(identifier, stmt.definingBlock))
|
||||
"private struct '${stmt.scopedName.joinToString(".")}'"
|
||||
else null
|
||||
}
|
||||
is Enumeration -> {
|
||||
if (stmt.isPrivate && !isAccessWithinSameBlock(identifier, stmt.definingBlock))
|
||||
"private enum '${stmt.scopedName.joinToString(".")}'"
|
||||
else null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
if (privateError != null) {
|
||||
errors.err("cannot access $privateError from outside its block", identifier.position)
|
||||
}
|
||||
}
|
||||
|
||||
super.visit(identifier)
|
||||
}
|
||||
|
||||
private fun isAccessWithinSameBlock(identifier: IdentifierReference, definingBlock: Block): Boolean {
|
||||
var node: Node = identifier
|
||||
while (node !is Block) {
|
||||
node = node.parent
|
||||
if (node is ParentSentinel)
|
||||
return false
|
||||
}
|
||||
return node === definingBlock
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -21,6 +21,14 @@ internal class VerifyFunctionArgTypes(val program: Program, val options: Compila
|
||||
}
|
||||
}
|
||||
|
||||
// check for missing reservations
|
||||
val slabNames = memorySlabs.map { it.name }.toSet()
|
||||
for (ref in memorySlabRefs) {
|
||||
if (ref.slabName !in slabNames) {
|
||||
errors.err("missing memory reservation with name '${ref.slabName}'", ref.position)
|
||||
}
|
||||
}
|
||||
|
||||
// remove unused strings from interned strings block
|
||||
val internedBlock = program.allBlocks.singleOrNull { it.name == INTERNED_STRINGS_MODULENAME }
|
||||
internedBlock?.statements?.withIndex()?.reversed()?.forEach { (index, st) ->
|
||||
@@ -40,12 +48,14 @@ internal class VerifyFunctionArgTypes(val program: Program, val options: Compila
|
||||
|
||||
private class Slab(val name: String, val size: Int, val align: Int, val position: Position)
|
||||
private val memorySlabs = mutableListOf<Slab>()
|
||||
private val memorySlabRefs = mutableListOf<MemorySlabRef>()
|
||||
|
||||
override fun visit(reservation: MemorySlabReservation) {
|
||||
memorySlabs.add(Slab(reservation.slabName, reservation.size.toInt(), reservation.align.toInt(), reservation.position))
|
||||
}
|
||||
|
||||
override fun visit(ref: MemorySlabRef) {
|
||||
memorySlabRefs.add(ref)
|
||||
}
|
||||
|
||||
override fun visit(functionCallExpr: FunctionCallExpression) {
|
||||
|
||||
@@ -65,7 +65,7 @@ class TestCompilerOnCharLit: FunSpec({
|
||||
main {
|
||||
extsub $FFD2 = chrout(ubyte ch @ A)
|
||||
sub start() {
|
||||
ubyte ch = '\n'
|
||||
ubyte @shared ch = '\n'
|
||||
chrout(ch)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,24 +145,22 @@ 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("banking", c64SizeOptimized=680, c64SizeUnoptimized=1451),
|
||||
ExampleSizes("bdmusic-irq", c64SizeOptimized=1001, c64SizeUnoptimized=1019),
|
||||
ExampleSizes("bdmusic", c64SizeOptimized=944, c64SizeUnoptimized=1011),
|
||||
ExampleSizes("charset", c64SizeOptimized=841, c64SizeUnoptimized=860),
|
||||
ExampleSizes("bdmusic", c64SizeOptimized=938, c64SizeUnoptimized=1011),
|
||||
ExampleSizes("charset", c64SizeOptimized=835, c64SizeUnoptimized=860),
|
||||
ExampleSizes("cube3d-sprites", c64SizeOptimized=2712, c64SizeUnoptimized=2764),
|
||||
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=1696, c64SizeUnoptimized=1731),
|
||||
ExampleSizes("plasma", c64SizeOptimized=1690, c64SizeUnoptimized=1731),
|
||||
ExampleSizes("rasterbars", c64SizeOptimized=845, c64SizeUnoptimized=892),
|
||||
ExampleSizes("simplemultiplexer", c64SizeOptimized=843, c64SizeUnoptimized=914),
|
||||
ExampleSizes("sprites", c64SizeOptimized=963, c64SizeUnoptimized=1052),
|
||||
@@ -176,17 +173,17 @@ class TestCompilerOnExamplesC64: FunSpec({
|
||||
)
|
||||
|
||||
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,16 +191,14 @@ 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-wav", cx16SizeOptimized=5636, cx16SizeUnoptimized=7878),
|
||||
ExampleSizes("pcmaudio/stream-simple-aflow", cx16SizeOptimized=1850, cx16SizeUnoptimized=3753),
|
||||
ExampleSizes("pcmaudio/stream-simple-poll", cx16SizeOptimized=1564, cx16SizeUnoptimized=3462),
|
||||
ExampleSizes("pcmaudio/vumeter", cx16SizeOptimized=3888, cx16SizeUnoptimized=6381),
|
||||
@@ -223,53 +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("bdmusic", cx16SizeOptimized=2427, cx16SizeUnoptimized=2792),
|
||||
ExampleSizes("bobs", cx16SizeOptimized=2800, cx16SizeUnoptimized=3414),
|
||||
ExampleSizes("bubbleuniverse", cx16SizeOptimized=1449, cx16SizeUnoptimized=1958),
|
||||
ExampleSizes("bubbleuniverse", cx16SizeOptimized=1450, cx16SizeUnoptimized=1958),
|
||||
ExampleSizes("charfade", cx16SizeOptimized=1054, cx16SizeUnoptimized=1362),
|
||||
ExampleSizes("charsets", cx16SizeOptimized=1125, cx16SizeUnoptimized=1417),
|
||||
ExampleSizes("circles", cx16SizeOptimized=2392, cx16SizeUnoptimized=4547),
|
||||
ExampleSizes("circles", cx16SizeOptimized=2385, cx16SizeUnoptimized=4547),
|
||||
ExampleSizes("cobramk3-gfx", cx16SizeOptimized=7705, cx16SizeUnoptimized=10612),
|
||||
ExampleSizes("colorbars", cx16SizeOptimized=1606, cx16SizeUnoptimized=1904),
|
||||
ExampleSizes("cxlogo", cx16SizeOptimized=599, cx16SizeUnoptimized=895),
|
||||
ExampleSizes("diskspeed", cx16SizeOptimized=6280, cx16SizeUnoptimized=8283),
|
||||
ExampleSizes("fileseek", cx16SizeOptimized=2474, cx16SizeUnoptimized=4496),
|
||||
ExampleSizes("fileseek", cx16SizeOptimized=2468, cx16SizeUnoptimized=4496),
|
||||
ExampleSizes("floatparse", cx16SizeOptimized=8303, cx16SizeUnoptimized=9190),
|
||||
ExampleSizes("interpolation", cx16SizeOptimized=3791, cx16SizeUnoptimized=4768),
|
||||
ExampleSizes("kefrenbars", cx16SizeOptimized=2392, cx16SizeUnoptimized=4654),
|
||||
ExampleSizes("kefrenbars", cx16SizeOptimized=2386, cx16SizeUnoptimized=4654),
|
||||
ExampleSizes("keyboardhandler", cx16SizeOptimized=412, cx16SizeUnoptimized=717),
|
||||
ExampleSizes("landscape", cx16SizeOptimized=1699, cx16SizeUnoptimized=1997),
|
||||
ExampleSizes("life", cx16SizeOptimized=1304, cx16SizeUnoptimized=1751),
|
||||
ExampleSizes("landscape", cx16SizeOptimized=1673, cx16SizeUnoptimized=1997),
|
||||
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("rasterbars", cx16SizeOptimized=1268, cx16SizeUnoptimized=1685),
|
||||
ExampleSizes("serialdownload", cx16SizeOptimized=2516, cx16SizeUnoptimized=3945),
|
||||
ExampleSizes("showbmx", cx16SizeOptimized=2745, cx16SizeUnoptimized=5269),
|
||||
ExampleSizes("snow", cx16SizeOptimized=3127, cx16SizeUnoptimized=5295),
|
||||
ExampleSizes("snow", cx16SizeOptimized=3120, cx16SizeUnoptimized=5295),
|
||||
ExampleSizes("sortingbench", cx16SizeOptimized=2928, cx16SizeUnoptimized=3847),
|
||||
ExampleSizes("spotlight", cx16SizeOptimized=908, cx16SizeUnoptimized=1364),
|
||||
ExampleSizes("starszoom", cx16SizeOptimized=2406, cx16SizeUnoptimized=4863),
|
||||
ExampleSizes("starszoom", cx16SizeOptimized=2399, cx16SizeUnoptimized=4863),
|
||||
ExampleSizes("test_gfx_hires", cx16SizeOptimized=4835, cx16SizeUnoptimized=7514),
|
||||
ExampleSizes("test_gfx_lores", cx16SizeOptimized=5863, cx16SizeUnoptimized=7188),
|
||||
ExampleSizes("testmonogfx", cx16SizeOptimized=10494, cx16SizeUnoptimized=11982),
|
||||
ExampleSizes("test_gfx_lores", cx16SizeOptimized=5841, cx16SizeUnoptimized=7188),
|
||||
ExampleSizes("testmonogfx", cx16SizeOptimized=10463, cx16SizeUnoptimized=11982),
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -277,22 +273,20 @@ 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=1484, c64SizeUnoptimized=1544, cx16SizeOptimized=1486, cx16SizeUnoptimized=1962),
|
||||
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("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("maze", c64SizeOptimized=3100, c64SizeUnoptimized=3189, cx16SizeOptimized=2873, cx16SizeUnoptimized=3251),
|
||||
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("multitasking", c64SizeOptimized=1956, c64SizeUnoptimized=2029, cx16SizeOptimized=1826, cx16SizeUnoptimized=2159),
|
||||
@@ -302,77 +296,74 @@ class TestCompilerOnExamplesBothC64andCx16: FunSpec({
|
||||
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=4339, c64SizeUnoptimized=4504, cx16SizeOptimized=6211, cx16SizeUnoptimized=6742),
|
||||
ExampleSizes("textelite", c64SizeOptimized=11440, c64SizeUnoptimized=12423, cx16SizeOptimized=10778, cx16SizeUnoptimized=13051),
|
||||
ExampleSizes("tehtriz", c64SizeOptimized=4333, c64SizeUnoptimized=4504, cx16SizeOptimized=6252, cx16SizeUnoptimized=6773),
|
||||
ExampleSizes("textelite", c64SizeOptimized=11440, c64SizeUnoptimized=12423, cx16SizeOptimized=10778, cx16SizeUnoptimized=13046),
|
||||
ExampleSizes("pointers/animalgame", c64SizeOptimized=1570, c64SizeUnoptimized=2126, cx16SizeOptimized=1605, cx16SizeUnoptimized=2412),
|
||||
ExampleSizes("pointers/binarytree", c64SizeOptimized=2552, c64SizeUnoptimized=2640, cx16SizeOptimized=2085, cx16SizeUnoptimized=2389),
|
||||
ExampleSizes("pointers/hashtable", c64SizeOptimized=3288, c64SizeUnoptimized=4140, cx16SizeOptimized=2832, cx16SizeUnoptimized=3786),
|
||||
ExampleSizes("pointers/sortedlist", c64SizeOptimized=1268, c64SizeUnoptimized=1319, cx16SizeOptimized=1223, cx16SizeUnoptimized=1532),
|
||||
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/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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -380,10 +371,8 @@ 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(
|
||||
@@ -394,17 +383,17 @@ class TestCompilerOnExamplesPET32: FunSpec({
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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") {
|
||||
|
||||
@@ -28,10 +28,8 @@ class TestLookupSemantics: FunSpec({
|
||||
|
||||
fun compileAndGetSymbolTable(source: String): SymbolTable {
|
||||
val errors = ErrorReporterForTests()
|
||||
val result = compileText(C64Target(), false, source, outputDir, errors = errors, writeAssembly = true)
|
||||
if(result == null) {
|
||||
throw AssertionError("Compilation failed: ${errors.errors.joinToString("; ")}")
|
||||
}
|
||||
val result = compileText(C64Target(), false, source, outputDir, errors = errors)
|
||||
?: throw AssertionError("Compilation failed: ${errors.errors.joinToString("; ")}")
|
||||
return result.codegenSymboltable
|
||||
?: throw AssertionError("No symbol table available")
|
||||
}
|
||||
@@ -53,9 +51,9 @@ class TestLookupSemantics: FunSpec({
|
||||
test("qualified lookup always starts from root") {
|
||||
val src = """
|
||||
main {
|
||||
uword value = 10
|
||||
uword @shared value = 10
|
||||
sub start() {
|
||||
uword result = main.value
|
||||
uword @shared result = main.value
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -66,9 +64,9 @@ class TestLookupSemantics: FunSpec({
|
||||
test("unqualified lookup walks parent chain") {
|
||||
val src = """
|
||||
main {
|
||||
uword value = 10
|
||||
uword @shared value = 10
|
||||
sub start() {
|
||||
uword result = value
|
||||
uword @shared result = value
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -82,9 +80,9 @@ class TestLookupSemantics: FunSpec({
|
||||
test("qualified lookup with multiple path segments") {
|
||||
val src = """
|
||||
main {
|
||||
uword deep = 100
|
||||
uword @shared deep = 100
|
||||
sub start() {
|
||||
uword result = main.deep
|
||||
uword @shared result = main.deep
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -99,10 +97,10 @@ class TestLookupSemantics: FunSpec({
|
||||
test("local variable shadows parent scope unqualified lookup") {
|
||||
val src = """
|
||||
main {
|
||||
uword value = 10
|
||||
uword @shared value = 10
|
||||
sub start() {
|
||||
uword value = 5
|
||||
uword result = value
|
||||
uword @shared value = 5
|
||||
uword @shared result = value
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -114,10 +112,10 @@ class TestLookupSemantics: FunSpec({
|
||||
test("qualified lookup unaffected by shadowing") {
|
||||
val src = """
|
||||
main {
|
||||
uword value = 10
|
||||
uword @shared value = 10
|
||||
sub start() {
|
||||
uword value = 5
|
||||
uword result = main.value
|
||||
uword @shared value = 5
|
||||
uword @shared result = main.value
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -129,11 +127,11 @@ class TestLookupSemantics: FunSpec({
|
||||
test("multiple levels of shadowing") {
|
||||
val src = """
|
||||
main {
|
||||
uword value = 1
|
||||
uword @shared value = 1
|
||||
sub start() {
|
||||
uword value = 2
|
||||
uword v1 = value
|
||||
uword v2 = main.value
|
||||
uword @shared value = 2
|
||||
uword @shared v1 = value
|
||||
uword @shared v2 = main.value
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -149,11 +147,11 @@ class TestLookupSemantics: FunSpec({
|
||||
test("qualified lookup across modules") {
|
||||
val src = """
|
||||
module1 {
|
||||
uword m1_var = 111
|
||||
uword @shared m1_var = 111
|
||||
}
|
||||
main {
|
||||
sub start() {
|
||||
uword result = module1.m1_var
|
||||
uword @shared result = module1.m1_var
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -178,9 +176,9 @@ class TestLookupSemantics: FunSpec({
|
||||
test("nested block can access outer module symbols") {
|
||||
val src = """
|
||||
main {
|
||||
uword outer = 100
|
||||
uword @shared outer = 100
|
||||
sub start() {
|
||||
uword result = outer
|
||||
uword @shared result = outer
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -197,7 +195,7 @@ class TestLookupSemantics: FunSpec({
|
||||
val src = """
|
||||
main {
|
||||
sub start() {
|
||||
uword result = abs(-5)
|
||||
uword @shared result = abs(-5)
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -209,8 +207,8 @@ class TestLookupSemantics: FunSpec({
|
||||
val src = """
|
||||
main {
|
||||
sub start() {
|
||||
uword myabs = 10
|
||||
uword result = myabs
|
||||
uword @shared myabs = 10
|
||||
uword @shared result = myabs
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -232,7 +230,7 @@ class TestLookupSemantics: FunSpec({
|
||||
}
|
||||
sub start() {
|
||||
^^Point ptr
|
||||
word result = ptr.x
|
||||
word @shared result = ptr.x
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -210,9 +210,9 @@ class TestNumbers: FunSpec({
|
||||
val src="""
|
||||
main {
|
||||
sub start() {
|
||||
uword uw1 = -32768
|
||||
uword uw = -1
|
||||
ubyte ub = -1
|
||||
uword @shared uw1 = -32768
|
||||
uword @shared uw = -1
|
||||
ubyte @shared ub = -1
|
||||
uw = -2 as uword
|
||||
ub = -2 as ubyte
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -76,6 +78,26 @@ main {
|
||||
compileText(C64Target(), true, src, outputDir) shouldNotBe null
|
||||
}
|
||||
|
||||
test("do not inline asmsub with parameters") {
|
||||
// Parameterized asmsubs cannot be inlined by the AST Inliner because it doesn't understand
|
||||
// the low-level register mapping (like @A) and cannot generate the necessary code to load
|
||||
// the arguments into those registers.
|
||||
val src="""
|
||||
main {
|
||||
inline asmsub foo(ubyte a @A) {
|
||||
%asm {{
|
||||
inx
|
||||
}}
|
||||
}
|
||||
sub start() {
|
||||
foo(10)
|
||||
}
|
||||
}"""
|
||||
val result = compileText(C64Target(), true, src, outputDir)!!
|
||||
val startSub = result.compilerAst.entrypoint
|
||||
startSub.statements.any { it is FunctionCallStatement && it.target.nameInSource.last() == "foo" } shouldBe true
|
||||
}
|
||||
|
||||
test("don't remove empty subroutine if it's referenced") {
|
||||
val sourcecode = """
|
||||
main {
|
||||
@@ -1211,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") {
|
||||
@@ -1693,8 +1723,7 @@ main {
|
||||
hasVoidCall shouldBe false
|
||||
}
|
||||
|
||||
xtest("inline call with one return value and one parameter") {
|
||||
// DISABLED: Requires parameter substitution in expression context (not yet implemented)
|
||||
test("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 = """
|
||||
@@ -1724,8 +1753,7 @@ main {
|
||||
binExpr.right shouldBe instanceOf<NumericLiteral>()
|
||||
}
|
||||
|
||||
xtest("inline call with two return values and two parameters") {
|
||||
// DISABLED: Requires parameter substitution for multi-return calls (not yet implemented)
|
||||
test("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 = """
|
||||
@@ -1768,6 +1796,36 @@ main {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
test("inline void call with six parameters") {
|
||||
// Tests that void calls with six parameters are inlined when args are simple
|
||||
val src = """
|
||||
@@ -1974,4 +2032,57 @@ main {
|
||||
(y3.left as PtPrefix).operator shouldBe "-"
|
||||
(y3.right as PtNumber).number shouldBe 3.0
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
@@ -1213,9 +1213,9 @@ main {
|
||||
byte bb
|
||||
}
|
||||
|
||||
^^List l1 = 2000
|
||||
^^List l2 = 3000
|
||||
^^Foo f1 = 4000
|
||||
^^List @shared l1 = 2000
|
||||
^^List @shared l2 = 3000
|
||||
^^Foo @shared f1 = 4000
|
||||
|
||||
l1^^ = l2^^ ; memcpy1
|
||||
l1[3] = l2^^ ; memcpy2
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
package prog8tests.compiler
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.engine.spec.tempdir
|
||||
import io.kotest.matchers.shouldBe
|
||||
import prog8.ast.statements.VarDecl
|
||||
import prog8.ast.statements.VarDeclType
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8tests.helpers.compileText
|
||||
|
||||
class TestPtrConstPromotion : FunSpec({
|
||||
val outputDir = tempdir().toPath()
|
||||
val vmTarget = VMTarget()
|
||||
|
||||
test("pointer variable with 2 dereferences is promoted to const") {
|
||||
val src = """
|
||||
%import textio
|
||||
main {
|
||||
sub start() {
|
||||
^^ubyte ptr = memory("slab", 10, 1)
|
||||
ubyte a = ptr^^
|
||||
ubyte b = ptr^^
|
||||
txt.print_ub(a)
|
||||
txt.print_ub(b)
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = compileText(vmTarget, true, src, outputDir)
|
||||
val mainBlock = result!!.compilerAst.allBlocks.first { it.name == "main" }
|
||||
val startSub = mainBlock.subScope("start")!!
|
||||
val ptrVar = startSub.lookup(listOf("ptr"))!!
|
||||
(ptrVar as VarDecl).type shouldBe VarDeclType.CONST
|
||||
}
|
||||
|
||||
test("pointer variable with 3 dereferences is NOT promoted to const") {
|
||||
val src = """
|
||||
%import textio
|
||||
main {
|
||||
sub start() {
|
||||
^^ubyte ptr = memory("slab", 10, 1)
|
||||
ubyte a = ptr^^
|
||||
ubyte b = ptr^^
|
||||
ubyte c = ptr^^
|
||||
txt.print_ub(a)
|
||||
txt.print_ub(b)
|
||||
txt.print_ub(c)
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = compileText(vmTarget, true, src, outputDir)
|
||||
val mainBlock = result!!.compilerAst.allBlocks.first { it.name == "main" }
|
||||
val startSub = mainBlock.subScope("start")!!
|
||||
val ptrVar = startSub.lookup(listOf("ptr"))!!
|
||||
(ptrVar as VarDecl).type shouldBe VarDeclType.VAR
|
||||
}
|
||||
|
||||
test("pointer variable with indexing dereferences is handled") {
|
||||
val src = """
|
||||
%import textio
|
||||
main {
|
||||
sub start() {
|
||||
^^ubyte ptr = memory("slab", 10, 1)
|
||||
ubyte a = ptr[0]
|
||||
ubyte b = ptr[1]
|
||||
ubyte c = ptr[2]
|
||||
txt.print_ub(a)
|
||||
txt.print_ub(b)
|
||||
txt.print_ub(c)
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = compileText(vmTarget, true, src, outputDir)
|
||||
val mainBlock = result!!.compilerAst.allBlocks.first { it.name == "main" }
|
||||
val startSub = mainBlock.subScope("start")!!
|
||||
val ptrVar = startSub.lookup(listOf("ptr"))!!
|
||||
(ptrVar as VarDecl).type shouldBe VarDeclType.VAR
|
||||
}
|
||||
})
|
||||
@@ -317,7 +317,7 @@ class TestScoping: FunSpec({
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
byte wrongaddress = 100
|
||||
byte @shared wrongaddress = 100
|
||||
|
||||
goto wrongaddress ; must be uword
|
||||
goto main.routine ; can't take args
|
||||
|
||||
@@ -361,8 +361,8 @@ class TestStringEncodings: FunSpec({
|
||||
str string1 = "default"
|
||||
str string2 = petscii:"petscii"
|
||||
|
||||
ubyte char1 = 'd'
|
||||
ubyte char2 = petscii:'p'
|
||||
ubyte @shared char1 = 'd'
|
||||
ubyte @shared char2 = petscii:'p'
|
||||
|
||||
sub start() {
|
||||
}
|
||||
@@ -389,8 +389,8 @@ class TestStringEncodings: FunSpec({
|
||||
str string1 = "default"
|
||||
str string2 = petscii:"petscii"
|
||||
|
||||
ubyte char1 = 'd'
|
||||
ubyte char2 = petscii:'p'
|
||||
ubyte @shared char1 = 'd'
|
||||
ubyte @shared char2 = petscii:'p'
|
||||
|
||||
sub start() {
|
||||
}
|
||||
|
||||
@@ -570,4 +570,32 @@ main {
|
||||
}"""
|
||||
compileText(C64Target(), optimize=false, src, outputDir) shouldNotBe null
|
||||
}
|
||||
|
||||
test("empty enum declaration should give error") {
|
||||
val src="""
|
||||
main {
|
||||
enum Color {
|
||||
}
|
||||
sub start() { }
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests()
|
||||
compileText(VMTarget(), false, src, outputDir, errors, false)
|
||||
errors.errors.size shouldBe 1
|
||||
errors.errors[0] shouldContain "enumeration must contain at least one member"
|
||||
}
|
||||
|
||||
test("empty struct declaration should give error") {
|
||||
val src="""
|
||||
main {
|
||||
struct Node {
|
||||
}
|
||||
sub start() { }
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests()
|
||||
compileText(VMTarget(), false, src, outputDir, errors, false)
|
||||
errors.errors.size shouldBe 1
|
||||
errors.errors[0] shouldContain "struct must contain at least one field"
|
||||
}
|
||||
})
|
||||
@@ -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 {
|
||||
|
||||
@@ -13,37 +13,72 @@ import prog8tests.helpers.compileText
|
||||
class TestPointerConstPromotion : FunSpec({
|
||||
val outputDir = tempdir().toPath()
|
||||
|
||||
test("pointer constant promotion scenarios") {
|
||||
test("zp pointer variable with a dereference is NOT promoted to const") {
|
||||
val src = """
|
||||
%zeropage basicsafe
|
||||
%option no_sysinit
|
||||
main {
|
||||
sub start() {
|
||||
^^uword ptr1 = 4444 ; size 2 > 1, no promotion
|
||||
^^ubyte ptr2 = 5555 ; size 1, promotion
|
||||
uword mem1 = memory("mem1", 10, 0) ; not a pointer, promotion
|
||||
^^uword mem1b = memory("mem2", 10, 0) ; size 2 > 1, no promotion
|
||||
^^ubyte mem3 = memory("mem3", 10, 0) ; size 1, promotion
|
||||
|
||||
cx16.r0 = ptr1
|
||||
cx16.r1 = ptr2 as uword
|
||||
cx16.r2 = mem1
|
||||
cx16.r3 = mem1b
|
||||
cx16.r4 = mem3 as uword
|
||||
^^ubyte @zp ptr = 9999
|
||||
ubyte @shared a = ptr^^
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = compileText(VMTarget(), true, src, outputDir)!!
|
||||
val mainSub = result.compilerAst.entrypoint
|
||||
val decls = mainSub.statements.filterIsInstance<VarDecl>().associateBy { it.name }
|
||||
|
||||
decls["ptr1"]?.type shouldBe VarDeclType.VAR
|
||||
decls["ptr2"]?.type shouldBe VarDeclType.CONST
|
||||
decls["mem1"]?.type shouldBe VarDeclType.CONST
|
||||
decls["mem1b"]?.type shouldBe VarDeclType.VAR
|
||||
decls["mem3"]?.type shouldBe VarDeclType.CONST
|
||||
val result = compileText(VMTarget(), true, src, outputDir)
|
||||
val mainBlock = result!!.compilerAst.allBlocks.first { it.name == "main" }
|
||||
val startSub = mainBlock.subScope("start")!!
|
||||
val ptrVar = startSub.lookup(listOf("ptr"))!!
|
||||
(ptrVar as VarDecl).type shouldBe VarDeclType.VAR
|
||||
}
|
||||
|
||||
test("nonzp pointer variable with a dereference is promoted to const") {
|
||||
val src = """
|
||||
main {
|
||||
sub start() {
|
||||
^^ubyte @nozp ptr = 9999
|
||||
ubyte @shared a = ptr^^
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = compileText(VMTarget(), true, src, outputDir)
|
||||
val mainBlock = result!!.compilerAst.allBlocks.first { it.name == "main" }
|
||||
val startSub = mainBlock.subScope("start")!!
|
||||
val ptrVar = startSub.lookup(listOf("ptr"))!!
|
||||
(ptrVar as VarDecl).type shouldBe VarDeclType.CONST
|
||||
}
|
||||
|
||||
test("pointer variable with memory is not promoted to const") {
|
||||
val src = """
|
||||
main {
|
||||
sub start() {
|
||||
^^ubyte ptr = memory("slab", 10, 1)
|
||||
ubyte @shared a = ptr^^
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = compileText(VMTarget(), true, src, outputDir)
|
||||
val mainBlock = result!!.compilerAst.allBlocks.first { it.name == "main" }
|
||||
val startSub = mainBlock.subScope("start")!!
|
||||
val ptrVar = startSub.lookup(listOf("ptr"))!!
|
||||
(ptrVar as VarDecl).type shouldBe VarDeclType.VAR
|
||||
}
|
||||
|
||||
test("pointer variable with 3 dereferences is NOT promoted to const") {
|
||||
val src = """
|
||||
main {
|
||||
sub start() {
|
||||
^^ubyte ptr = 9999
|
||||
ubyte @shared a = ptr^^
|
||||
ubyte @shared b = ptr^^
|
||||
ubyte @shared c = ptr^^
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = compileText(VMTarget(), true, src, outputDir)
|
||||
val mainBlock = result!!.compilerAst.allBlocks.first { it.name == "main" }
|
||||
val startSub = mainBlock.subScope("start")!!
|
||||
val ptrVar = startSub.lookup(listOf("ptr"))!!
|
||||
(ptrVar as VarDecl).type shouldBe VarDeclType.VAR
|
||||
}
|
||||
|
||||
test("explicit const pointer to >1 byte type gives error") {
|
||||
val src = """
|
||||
main {
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package prog8tests.ast
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.engine.spec.tempdir
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.string.shouldContain
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8tests.helpers.ErrorReporterForTests
|
||||
import prog8tests.helpers.compileText
|
||||
|
||||
class TestPrivateAccess : FunSpec({
|
||||
val outputDir = tempdir().toPath()
|
||||
|
||||
test("cannot access private variable from outside its block") {
|
||||
val text = """
|
||||
M {
|
||||
private ubyte x = 10
|
||||
}
|
||||
main {
|
||||
sub start() {
|
||||
ubyte v = M.x
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
||||
compileText(VMTarget(), true, text, outputDir, errors = errors) shouldBe null
|
||||
errors.errors.size shouldBe 1
|
||||
errors.errors[0] shouldContain "cannot access private variable 'M.x' from outside its block"
|
||||
}
|
||||
|
||||
test("cannot access private subroutine from outside its block") {
|
||||
val text = """
|
||||
M {
|
||||
private sub foo() {}
|
||||
}
|
||||
main {
|
||||
sub start() {
|
||||
M.foo()
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
||||
compileText(VMTarget(), true, text, outputDir, errors = errors) shouldBe null
|
||||
errors.errors.size shouldBe 1
|
||||
errors.errors[0] shouldContain "cannot access private subroutine 'M.foo' from outside its block"
|
||||
}
|
||||
|
||||
test("cannot access private struct from outside its block") {
|
||||
val text = """
|
||||
M {
|
||||
private struct S { ubyte f }
|
||||
}
|
||||
main {
|
||||
sub start() {
|
||||
ubyte v = sizeof(M.S)
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
||||
compileText(VMTarget(), true, text, outputDir, errors = errors) shouldBe null
|
||||
errors.errors.any { it.contains("cannot access private struct 'M.S' from outside its block") } shouldBe true
|
||||
}
|
||||
|
||||
test("cannot access private enum from outside its block") {
|
||||
val text = """
|
||||
M {
|
||||
private enum E { A, B }
|
||||
}
|
||||
main {
|
||||
sub start() {
|
||||
ubyte v = M.E::A
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
||||
compileText(VMTarget(), true, text, outputDir, errors = errors) shouldBe null
|
||||
// Enum members are reported as private variable 'M.E::A' because they're converted to constant variables
|
||||
// that inherit the private status of the enum.
|
||||
errors.errors.any { it.contains("cannot access private variable 'M.E::A' from outside its block") } shouldBe true
|
||||
}
|
||||
|
||||
test("cannot access private asmsub from outside its block") {
|
||||
val text = """
|
||||
M {
|
||||
private asmsub foo() {
|
||||
%asm {{
|
||||
rts
|
||||
}}
|
||||
}
|
||||
}
|
||||
main {
|
||||
sub start() {
|
||||
M.foo()
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
||||
compileText(VMTarget(), true, text, outputDir, errors = errors) shouldBe null
|
||||
errors.errors.size shouldBe 1
|
||||
errors.errors[0] shouldContain "cannot access private subroutine 'M.foo' from outside its block"
|
||||
}
|
||||
|
||||
test("cannot access private extsub from outside its block") {
|
||||
val text = $$"""
|
||||
M {
|
||||
private extsub $c000 = foo()
|
||||
}
|
||||
main {
|
||||
sub start() {
|
||||
M.foo()
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
||||
compileText(VMTarget(), true, text, outputDir, errors = errors) shouldBe null
|
||||
errors.errors.size shouldBe 1
|
||||
errors.errors[0] shouldContain "cannot access private subroutine 'M.foo' from outside its block"
|
||||
}
|
||||
|
||||
test("can access private elements from within the same block") {
|
||||
val text = """
|
||||
other {
|
||||
private ubyte x = 10
|
||||
private sub foo() {}
|
||||
private asmsub bar_asm() {
|
||||
rts
|
||||
}
|
||||
private struct S { ubyte f }
|
||||
|
||||
sub bar() {
|
||||
x = 20
|
||||
foo()
|
||||
bar_asm()
|
||||
S* myS = 0
|
||||
}
|
||||
}
|
||||
main {
|
||||
sub start() {
|
||||
other.bar()
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
||||
compileText(VMTarget(), true, text, outputDir, errors = errors, writeAssembly = false)
|
||||
errors.errors.size shouldBe 0
|
||||
}
|
||||
})
|
||||
@@ -918,9 +918,9 @@ class TestProg8Parser: FunSpec( {
|
||||
main {
|
||||
sub start() {
|
||||
str string = "\x00\xff\u0041"
|
||||
ubyte zero = '\x00'
|
||||
ubyte ff = '\xff'
|
||||
ubyte letter = '\u0041'
|
||||
ubyte @shared zero = '\x00'
|
||||
ubyte @shared ff = '\xff'
|
||||
ubyte @shared letter = '\u0041'
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -997,10 +997,10 @@ class TestProg8Parser: FunSpec( {
|
||||
sub start() {
|
||||
ubyte @zp @shared @requirezp var1
|
||||
ubyte @shared @zp var2
|
||||
ubyte @zp var3
|
||||
ubyte @zp @shared var3
|
||||
ubyte @shared var4
|
||||
ubyte @requirezp var5
|
||||
ubyte var6
|
||||
ubyte @requirezp @shared var5
|
||||
ubyte @shared var6
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -1014,16 +1014,16 @@ class TestProg8Parser: FunSpec( {
|
||||
var2.sharedWithAsm shouldBe true
|
||||
var2.zeropage shouldBe ZeropageWish.PREFER_ZEROPAGE
|
||||
val var3 = stmt[4] as VarDecl
|
||||
var3.sharedWithAsm shouldBe false
|
||||
var3.sharedWithAsm shouldBe true
|
||||
var3.zeropage shouldBe ZeropageWish.PREFER_ZEROPAGE
|
||||
val var4 = stmt[6] as VarDecl
|
||||
var4.sharedWithAsm shouldBe true
|
||||
var4.zeropage shouldBe ZeropageWish.DONTCARE
|
||||
val var5 = stmt[8] as VarDecl
|
||||
var5.sharedWithAsm shouldBe false
|
||||
var5.sharedWithAsm shouldBe true
|
||||
var5.zeropage shouldBe ZeropageWish.REQUIRE_ZEROPAGE
|
||||
val var6 = stmt[10] as VarDecl
|
||||
var6.sharedWithAsm shouldBe false
|
||||
var6.sharedWithAsm shouldBe true
|
||||
var6.zeropage shouldBe ZeropageWish.DONTCARE
|
||||
}
|
||||
|
||||
@@ -1089,10 +1089,10 @@ main {
|
||||
%option enable_floats
|
||||
main {
|
||||
sub start() {
|
||||
uword w1 = 000_1234_5__
|
||||
uword w2 = ${'$'}ff_ee
|
||||
uword w3 = %11_0000_111111__0000
|
||||
float fl = 3_000_001.141_592_654
|
||||
uword @shared w1 = 000_1234_5__
|
||||
uword @shared w2 = ${'$'}ff_ee
|
||||
uword @shared w3 = %11_0000_111111__0000
|
||||
float @shared fl = 3_000_001.141_592_654
|
||||
}
|
||||
}"""
|
||||
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = false)!!
|
||||
|
||||
@@ -14,6 +14,7 @@ import prog8.code.core.DataType
|
||||
import prog8.code.source.SourceCode
|
||||
import prog8.code.target.C64Target
|
||||
import prog8.code.target.Cx16Target
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8.parser.Prog8Parser.parseModule
|
||||
import prog8tests.helpers.ErrorReporterForTests
|
||||
import prog8tests.helpers.compileText
|
||||
@@ -279,4 +280,24 @@ main {
|
||||
errors.errors.size shouldBe 1
|
||||
errors.errors[0] shouldContain "doesn't end with a return"
|
||||
}
|
||||
|
||||
test("asmsub body restriction") {
|
||||
val src="""
|
||||
main {
|
||||
asmsub foo() {
|
||||
ubyte x = 1
|
||||
}
|
||||
asmsub ok1() {
|
||||
%asm {{ nop }}
|
||||
}
|
||||
asmsub ok2() {
|
||||
%asm {{ nop }}
|
||||
%asm {{ nop }}
|
||||
}
|
||||
}"""
|
||||
val errors = ErrorReporterForTests()
|
||||
compileText(VMTarget(), false, src, outputDir, errors, false)
|
||||
errors.errors.size shouldBe 1
|
||||
errors.errors[0] shouldContain "asmsub can only contain inline assembly (%asm)"
|
||||
}
|
||||
})
|
||||
@@ -458,11 +458,11 @@ main {
|
||||
val src="""
|
||||
main {
|
||||
sub start() {
|
||||
ubyte w
|
||||
ubyte @shared w
|
||||
|
||||
for w in 0 to 20 {
|
||||
ubyte @zp x,y,z=13
|
||||
ubyte q,r,s
|
||||
ubyte @shared @zp x,y,z=13
|
||||
ubyte @shared q,r,s
|
||||
x++
|
||||
y++
|
||||
z++
|
||||
@@ -471,28 +471,6 @@ main {
|
||||
}"""
|
||||
val result = compileText(VMTarget(), optimize = false, src, outputDir, writeAssembly = false)!!
|
||||
val st = result.compilerAst.entrypoint.statements
|
||||
/*
|
||||
sub start () {
|
||||
ubyte s
|
||||
s = 0
|
||||
ubyte r
|
||||
r = 0
|
||||
ubyte q
|
||||
q = 0
|
||||
ubyte @zp z
|
||||
ubyte @zp y
|
||||
ubyte @zp x
|
||||
ubyte w
|
||||
for w in 0 to 20 step 1 {
|
||||
z = 13
|
||||
y = 13
|
||||
x = 13
|
||||
x++
|
||||
y++
|
||||
z++
|
||||
}
|
||||
}
|
||||
*/
|
||||
val vars = st.filterIsInstance<VarDecl>()
|
||||
vars.size shouldBe 7
|
||||
vars.all { it.names.size<=1 } shouldBe true
|
||||
@@ -724,8 +702,8 @@ main {
|
||||
val src="""
|
||||
main {
|
||||
sub start() {
|
||||
ubyte shift = 10
|
||||
uword value = 1<<shift
|
||||
ubyte @shared shift = 10
|
||||
uword @shared value = 1<<shift
|
||||
value++
|
||||
value = 1<<shift
|
||||
value++
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package prog8tests.codegeneration
|
||||
|
||||
import io.kotest.assertions.fail
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.engine.spec.tempdir
|
||||
import io.kotest.matchers.shouldBe
|
||||
@@ -24,7 +25,7 @@ class Test2DArrays: FunSpec({
|
||||
ubyte[3][4] m
|
||||
m[0][0] = 1
|
||||
m[2][3] = 12
|
||||
ubyte v = m[0][0]
|
||||
ubyte @shared v = m[0][0]
|
||||
}
|
||||
}""",
|
||||
// Variable indexing
|
||||
@@ -32,10 +33,10 @@ class Test2DArrays: FunSpec({
|
||||
main {
|
||||
sub start() {
|
||||
ubyte[3][4] m
|
||||
ubyte r = 1
|
||||
ubyte c = 2
|
||||
ubyte @shared r = 1
|
||||
ubyte @shared c = 2
|
||||
m[r][c] = 42
|
||||
ubyte v = m[r][c]
|
||||
ubyte @shared v = m[r][c]
|
||||
}
|
||||
}""",
|
||||
// Used in expressions
|
||||
@@ -45,7 +46,7 @@ class Test2DArrays: FunSpec({
|
||||
ubyte[2][3] m
|
||||
m[0][0] = 10
|
||||
m[1][1] = 15
|
||||
ubyte s = m[0][0] + m[1][1]
|
||||
ubyte @shared s = m[0][0] + m[1][1]
|
||||
}
|
||||
}""",
|
||||
// As function argument
|
||||
@@ -73,8 +74,8 @@ class Test2DArrays: FunSpec({
|
||||
main {
|
||||
ubyte[4][4] m
|
||||
sub start() {
|
||||
ubyte r = 0
|
||||
ubyte c = 0
|
||||
ubyte @shared r = 0
|
||||
ubyte @shared c = 0
|
||||
for r in 0 to 3 {
|
||||
for c in 0 to 3 {
|
||||
m[r][c] = r * c
|
||||
@@ -87,13 +88,14 @@ class Test2DArrays: FunSpec({
|
||||
main {
|
||||
sub start() {
|
||||
ubyte[3][4] m
|
||||
uword addr = &m
|
||||
uword @shared addr = &m
|
||||
}
|
||||
}"""
|
||||
)
|
||||
for(src in sources) {
|
||||
compileText(VMTarget(), false, src, outputDir) shouldNotBe null
|
||||
compileText(C64Target(), false, src, outputDir) shouldNotBe null
|
||||
val result = compileText(VMTarget(), false, src, outputDir) ?: fail("Compilation failed for source:\n$src")
|
||||
val resultc64 =
|
||||
compileText(C64Target(), false, src, outputDir) ?: fail("Compilation failed for source:\n$src")
|
||||
}
|
||||
// Also verify optimized compilation
|
||||
compileText(VMTarget(), true, sources[0], outputDir) shouldNotBe null
|
||||
|
||||
@@ -14,8 +14,8 @@ class TestComparisonIssues : FunSpec({
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
long a = 1000
|
||||
long b = 2000
|
||||
long @shared a = 1000
|
||||
long @shared b = 2000
|
||||
a += b
|
||||
}
|
||||
}
|
||||
@@ -23,15 +23,19 @@ 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") {
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
long a = 2000
|
||||
long b = 1000
|
||||
long @shared a = 2000
|
||||
long @shared b = 1000
|
||||
a -= b
|
||||
}
|
||||
}
|
||||
@@ -39,14 +43,35 @@ 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)") {
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
word w = -1
|
||||
word @shared w = -1
|
||||
if w <= 0 {
|
||||
cx16.r0 = 1
|
||||
}
|
||||
@@ -64,7 +89,7 @@ class TestComparisonIssues : FunSpec({
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
word w = 1
|
||||
word @shared w = 1
|
||||
if w > 0 {
|
||||
cx16.r0 = 1
|
||||
}
|
||||
@@ -82,7 +107,7 @@ class TestComparisonIssues : FunSpec({
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
uword uw = 0
|
||||
uword @shared uw = 0
|
||||
if uw <= 0 {
|
||||
cx16.r0 = 1
|
||||
}
|
||||
@@ -100,7 +125,7 @@ class TestComparisonIssues : FunSpec({
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
word w = 1
|
||||
word @shared w = 1
|
||||
if w <= 0 {
|
||||
cx16.r0 = 1
|
||||
} else {
|
||||
|
||||
@@ -115,10 +115,63 @@ class TestExecution6502 : FunSpec({
|
||||
}
|
||||
""".trimIndent()
|
||||
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
||||
val machine = compileResult!!.simulate()
|
||||
val machine = compileResult!!.simulate(1000)
|
||||
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
|
||||
%launcher none
|
||||
%address $1000
|
||||
main {
|
||||
sub start() {
|
||||
; do nothing just RTS
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
||||
val machine = compileResult!!.simulate(1000)
|
||||
machine.cpu.totalCycles shouldBeLessThan 250
|
||||
}
|
||||
|
||||
test("reset register") {
|
||||
val src = $$"""
|
||||
%option no_sysinit
|
||||
@@ -287,4 +340,42 @@ class TestExecution6502 : FunSpec({
|
||||
compileResult!!.simulate(maxCycles = 100)
|
||||
}
|
||||
}
|
||||
|
||||
test("div_longs positive 1234567890 / 88888 = 13889") {
|
||||
val src = $$"""
|
||||
%option no_sysinit
|
||||
%launcher none
|
||||
%address $1000
|
||||
|
||||
main {
|
||||
&ubyte poweroff = $f203
|
||||
sub start() {
|
||||
long @shared v = 1234567890
|
||||
v /= 88888
|
||||
%asm {{
|
||||
lda p8v_v
|
||||
sta $2000
|
||||
lda p8v_v+1
|
||||
sta $2001
|
||||
lda p8v_v+2
|
||||
sta $2002
|
||||
lda p8v_v+3
|
||||
sta $2003
|
||||
}}
|
||||
poweroff = 1
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
||||
val machine = compileResult!!.simulate(maxCycles = 30000)
|
||||
val b0 = machine.ram[0x2000].toInt() and 0xff
|
||||
val b1 = machine.ram[0x2001].toInt() and 0xff
|
||||
val b2 = machine.ram[0x2002].toInt() and 0xff
|
||||
val b3 = machine.ram[0x2003].toInt() and 0xff
|
||||
println("div_longs result bytes: $b0 $b1 $b2 $b3 (hex: ${b0.toString(16)} ${b1.toString(16)} ${b2.toString(16)} ${b3.toString(16)})")
|
||||
machine.assertMemory(0x2000, 0x41)
|
||||
machine.assertMemory(0x2001, 0x36)
|
||||
machine.assertMemory(0x2002, 0x00)
|
||||
machine.assertMemory(0x2003, 0x00)
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -210,8 +210,8 @@ test("if-expression bitwise AND optimization (BIT instruction)") {
|
||||
main {
|
||||
word[10] @shared words
|
||||
sub start() {
|
||||
ubyte i = 5
|
||||
ubyte result = if words[i] < 0 then 1 else 0
|
||||
ubyte @shared i = 5
|
||||
ubyte @shared result = if words[i] < 0 then 1 else 0
|
||||
cx16.r1L = result
|
||||
}
|
||||
}
|
||||
@@ -228,8 +228,8 @@ test("if-expression bitwise AND optimization (BIT instruction)") {
|
||||
main {
|
||||
long[10] @shared longs
|
||||
sub start() {
|
||||
ubyte i = 2
|
||||
ubyte result = if longs[i] > 0 then 1 else 0
|
||||
ubyte @shared i = 2
|
||||
ubyte @shared result = if longs[i] > 0 then 1 else 0
|
||||
cx16.r1L = result
|
||||
}
|
||||
}
|
||||
@@ -279,8 +279,8 @@ test("if-expression bitwise AND optimization (BIT instruction)") {
|
||||
main {
|
||||
uword[10] @shared words
|
||||
sub start() {
|
||||
ubyte i = 5
|
||||
ubyte result = if words[i] == 0 then 1 else 0
|
||||
ubyte @shared i = 5
|
||||
ubyte @shared result = if words[i] == 0 then 1 else 0
|
||||
cx16.r1L = result
|
||||
}
|
||||
}
|
||||
@@ -295,8 +295,8 @@ test("if-expression bitwise AND optimization (BIT instruction)") {
|
||||
main {
|
||||
word[10] @shared words
|
||||
sub start() {
|
||||
ubyte i = 2
|
||||
ubyte result = if words[i] != 0 then 1 else 0
|
||||
ubyte @shared i = 2
|
||||
ubyte @shared result = if words[i] != 0 then 1 else 0
|
||||
cx16.r1L = result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
@@ -97,9 +98,9 @@ main {
|
||||
main {
|
||||
sub start() {
|
||||
uword[3] arr
|
||||
ubyte i = 42
|
||||
uword ww = peekw(arr[i])
|
||||
ubyte xx = peek(arr[i])
|
||||
ubyte @shared i = 42
|
||||
uword @shared ww = peekw(arr[i])
|
||||
ubyte @shared xx = peek(arr[i])
|
||||
xx = @(arr[i])
|
||||
|
||||
@(arr[i]) = 42
|
||||
@@ -114,7 +115,7 @@ main {
|
||||
val text="""
|
||||
main {
|
||||
sub start() {
|
||||
uword factor1
|
||||
uword @shared factor1
|
||||
ubyte[3] bytearray
|
||||
uword[3] wordarray
|
||||
@(factor1) = bytearray[0]
|
||||
@@ -308,10 +309,10 @@ main
|
||||
{
|
||||
sub start()
|
||||
{
|
||||
uword uw = 54321
|
||||
ubyte ub = 123
|
||||
word sw = -12345
|
||||
byte sb = -123
|
||||
uword @shared uw = 54321
|
||||
ubyte @shared ub = 123
|
||||
word @shared sw = -12345
|
||||
byte @shared sb = -123
|
||||
|
||||
func_uw(~ub as uword)
|
||||
func_ub(~uw as ubyte)
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -11,7 +11,7 @@ fun String.shouldContainInOrder(vararg substrings: String) {
|
||||
for (substring in substrings) {
|
||||
val index = this.indexOf(substring, currentSearchIndex)
|
||||
if (index == -1) {
|
||||
fail("Could not find '$substring' after index $currentSearchIndex in string:\n$this")
|
||||
fail("Could not find '$substring' after index $currentSearchIndex")
|
||||
}
|
||||
currentSearchIndex = index + substring.length
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
Loaded 100 of 171 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user