Compare commits

...
64 Commits
Author SHA1 Message Date
Irmen de Jong f2d1dffbe0 preparing for 12.2 release 2026-06-08 20:15:19 +02:00
Irmen de Jong 4008280a62 fix 64tass output order in daemon 2026-06-07 02:45:52 +02:00
Irmen de Jong 2e7235ec02 fix daemon mode error reporting paths 2026-06-07 02:31:06 +02:00
Irmen de Jong a4330f5048 fix -emu program name problem with -daemon 2026-06-07 00:34:35 +02:00
Irmen de Jong f65aeff231 -emu works again together with -daemon 2026-06-07 00:08:07 +02:00
Irmen de Jong 505243aff7 fix a optimizer crash 2026-06-06 23:29:16 +02:00
Irmen de Jong d0f2fc13ba add dummy rambank() and rombank() support for virtual target 2026-06-06 23:26:47 +02:00
Irmen de Jong 53d51dcd38 improve -libsearch by adding fuzzy search fallback 2026-06-06 19:28:53 +02:00
Irmen de Jong c1aa0e49fe fix a possible vera registers clobbering issue when using psg2 from irq handler 2026-06-06 17:30:08 +02:00
Irmen de Jong e1e72d4317 fix crash when using const ubyte pointer as string argument 2026-06-06 17:12:15 +02:00
Irmen de Jong feec343cbc made -daemon more robust 2026-06-06 12:41:15 +02:00
Irmen de Jong 5cdc4233f3 ideas & update prog8-coder skill 2026-06-04 01:16:41 +02:00
Irmen de Jong 1ae89d18cc prevent string deduplication for struct initializers 2026-06-03 01:43:50 +02:00
Irmen de Jong eb19d00a1e prevent faulty inlining of subs with register-aliased parameters 2026-06-02 23:14:55 +02:00
Irmen de Jong e01ee10ef0 fix output binary path displayed to the user 2026-06-02 21:05:03 +02:00
Irmen de Jong f4b3cb12e1 history 2026-06-01 23:19:31 +02:00
Irmen de Jong 77ced60ad6 fix psg2 release timing to make it linear just as attack.
clarify envelope timing details in docs.
2026-06-01 22:58:42 +02:00
Irmen de Jong 6fad90af6e long for loop warning 2026-05-31 23:42:58 +02:00
Irmen de Jong 3076318d7e implement missing for loops on long rages for 6502 2026-05-31 23:38:35 +02:00
Irmen de Jong 9c99110738 doc 2026-05-31 23:07:16 +02:00
Irmen de Jong 1457d36997 beta3 2026-05-31 17:47:37 +02:00
Irmen de Jong 43fb9296dd clarify serial module docs 2026-05-31 17:46:33 +02:00
Irmen de Jong 8bf744cc4a fix some bitwise expression optimization bugs 2026-05-31 07:35:32 +02:00
Irmen de Jong f1f5f070cf fix invalid asm for some in-place long operations on zp vars 2026-05-31 05:14:33 +02:00
Irmen de Jong 191a240d0d adjust extsub jump table offsets to more closely match reality 2026-05-30 23:42:52 +02:00
Irmen de Jong cafb2e355b implement long loop counter support 2026-05-30 23:36:08 +02:00
Irmen de Jong c99c5751f8 tweak serial 2026-05-30 21:11:19 +02:00
Irmen de Jong 435b7e56fa IR VM refactor to reduce class size 2026-05-29 23:14:44 +02:00
Irmen de Jong 14f13fac53 IR tweaks 2026-05-29 22:17:56 +02:00
Irmen de Jong f8b5037924 spelling corrections 2026-05-29 20:58:06 +02:00
Irmen de Jong d2870cb02e implement hex mode file transfer in serial module
as an attempt to have a working file transfer because the binary mode is still failing right now
2026-05-29 20:04:40 +02:00
Irmen de Jong 8e43fc426c IR: handle extsub with banked addresses properly 2026-05-28 03:25:04 +02:00
Irmen de Jong ba1b6e127a IR: properly consume registers for void targets 2026-05-28 00:57:00 +02:00
Irmen de Jong 4f36a6958c fix a few if-expression typecast issues 2026-05-28 00:55:15 +02:00
Irmen de Jong df8f08b4ab more serial tweaks to attempt to fix binary downloads 2026-05-27 23:42:55 +02:00
Irmen de Jong fd5746d3fc tweak the serial module a bit for more robustness 2026-05-27 02:22:35 +02:00
Irmen de Jong 6ab74fd51d don't erroneously allow taking the address of const values 2026-05-27 00:58:01 +02:00
Irmen de Jong 245082ca1d plain console output in github action builds 2026-05-24 12:40:43 +02:00
Irmen de Jong 845e58ea10 64tass install step 2026-05-24 12:33:47 +02:00
Irmen de Jong 2a5dbc1ef7 update project urls that have been moved from github to codeberg 2026-05-24 12:29:09 +02:00
Irmen de Jong 02a1deb1e0 update github build actions 2026-05-23 00:13:14 +02:00
Irmen de Jong 3ba8e5de10 Add serial download example for CX16 and fix serial module import 2026-05-23 00:05:40 +02:00
Irmen de Jong 819d1b4af7 Add serial library for CX16 with UART and ZiModem support
version 12.2-BETA2
2026-05-22 23:52:58 +02:00
Irmen de Jong cdd3d37376 update asmsub inlining behavior and tests: improve support for parameterized calls 2026-05-22 04:17:43 +02:00
Irmen de Jong d0bc01d618 improve inliner: support slightly more complex return expressions 2026-05-22 03:15:57 +02:00
Irmen de Jong ffab73334a enforce errors for empty enum and struct declarations, add corresponding tests 2026-05-22 02:39:09 +02:00
Irmen de Jong 83aeb8846c add support for private keyword for structs, enums, aliases, variables, and subroutines 2026-05-22 02:27:38 +02:00
Irmen de Jong 3a2087310b background daemon compiler mode via -daemon option 2026-05-21 19:08:29 +02:00
Irmen de Jong 61b616701e fix -watch mode to not use stale cached files on subsequent runs 2026-05-20 22:54:52 +02:00
Irmen de Jong bddf872906 better romable awareness in IR 2026-05-20 19:54:12 +02:00
Irmen de Jong 48214a6fdb more IR peephole optimizations, added some implementation plans to the todo 2026-05-19 00:26:35 +02:00
Irmen de Jong b7340856df fix long expression evaluation bug clobbering R14R15 intermediary results 2026-05-18 20:07:23 +02:00
Irmen de Jong 2685854113 memory("name") with only 1 arg now allowed to refer to the actual memory reservation call elsewhere (with all three name, size, align args). 2026-05-16 23:24:41 +02:00
Irmen de Jong ad92f39469 fix error message for conflicting memory() calls 2026-05-16 23:01:49 +02:00
Irmen de Jong d3fe041e01 update kotlin and ksim65 2026-05-16 15:33:03 +02:00
Irmen de Jong f32bac5004 implement 6502 long division routine 2026-05-16 15:04:09 +02:00
Irmen de Jong 9a8e4ac105 lineclip.inside() at top level to check a single point 2026-05-15 21:53:13 +02:00
Irmen de Jong 78857e5c22 pet32: diskio.list_filenames() now checks buffer size 2026-05-15 19:38:45 +02:00
Irmen de Jong 1bb828875d improved vm waitvsync 2026-05-15 18:05:01 +02:00
Irmen de Jong 82aa67ffda lineclip module added 2026-05-15 17:45:13 +02:00
Irmen de Jong 7c44835772 const folding is always done even when -noopt is used 2026-05-15 12:22:52 +02:00
Irmen de Jong 8b9c63fe90 lineclip 2026-05-15 00:31:27 +02:00
Irmen de Jong e77fcc8c16 fix an unsigned long problem 2026-05-10 21:29:42 +02:00
Irmen de Jong 08c12131d4 tweak the rules for turning pointers into consts 2026-05-10 19:51:06 +02:00
171 changed files with 6135 additions and 2857 deletions

No files matched your search

+9
View File
@@ -9,6 +9,9 @@ compatibility: opencode
You are writing **6502/65C02 assembly** using **64tass syntax**, in separate `*.asm` files or embedded in a Prog8 program (inside `%asm {{ }}` blocks or `asmsub` routines). Follow all rules below.
## Git Operations
- When moving, renaming, or deleting git-tracked files, **always use `git mv` or `git rm`** instead of plain `mv`/`rm`. This preserves history and properly stages the change. Plain `mv`/`rm` causes git to see them as delete+add (losing history).
## Debugging Generated Assembly
- The prog8 compiler outputs `*.asm` (assembly source) and (with -asmlist option) also `*.list` (full listing with address/symbols) when compiling. Inspect these to debug generated code, verify optimizations, and trace instruction sequences.
@@ -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
+75 -30
View File
@@ -12,6 +12,7 @@ You are working with **Prog8** source code (`.p8` files) or its Intermediate Rep
Follow ALL the rules below carefully.
## General & Setup
- **Git operations**: When moving, renaming, or deleting git-tracked files, **always use `git mv` or `git rm`** instead of plain `mv`/`rm`. This preserves history and properly stages the change. Plain `mv`/`rm` causes git to see them as delete+add (losing history).
- A program = a `main` block containing a `start` subroutine entry point, plus optional other subroutines/blocks
- Add `%zeropage basicsafe` at the top of your program to allow clean return on exit (instead of resetting the machine/emulator)
- Module imports: `%import modulename` — no `as` aliasing. Use the module's defined prefix (e.g., `%import textio` → `txt.xxx`)
@@ -20,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
+11 -10
View File
@@ -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
View File
@@ -5,6 +5,7 @@
.idea/shelf/
.junie/
.junie/memory/
.serena
build/
dist/
output/
@@ -38,7 +39,6 @@ parsetab.py
.pytest_cache/
.attach_pid*
compiler/lib/
.gradle
sd*.img
*.d64
+1 -1
View File
@@ -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>
+10 -10
View File
@@ -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>
+4 -4
View File
@@ -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
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>
+2 -2
View File
@@ -12,9 +12,9 @@ endif
all:
$(GRADLE) installdist installshadowdist
@echo "compiler launch script can be found here: compiler/build/install/compiler-shadow/bin/prog8c"
@echo "compiler launch script can be found here: compiler/build/install/prog8c-shadow/bin/prog8c"
test:
$(GRADLE) build
@echo "compiler launch script can be found here: compiler/build/install/compiler-shadow/bin/prog8c"
@echo "compiler launch script can be found here: compiler/build/install/prog8c-shadow/bin/prog8c"
+4 -1
View File
@@ -15,10 +15,12 @@ which aims to provide many conveniences over raw assembly code (even when using
**Want to buy me a coffee or a pizza perhaps?**
This project was created over the last couple of years by dedicating thousands of hours of my free time to it, to make it the best I possibly can.
This project was created over the last couple of years by dedicating a huge amoung of my free time to it, to make it the best I possibly can.
If you like Prog8, and think it's worth a nice cup of hot coffee or a delicious pizza,
you can help me out a little bit over at [ko-fi.com/irmen](https://ko-fi.com/irmen) or [PayPal](https://paypal.me/irmendejong)
[![JetBrains logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSource)
I am using JetBrains products to develop the Prog8 compiler.
Documentation
-------------
@@ -63,6 +65,7 @@ What does Prog8 provide?
- programs run very fast because it's compiled to native machine code
- compiled code is very small; much smaller than equivalent C code compiled with CC65, and usually runs faster as well
- modularity, symbol scoping, subroutines. No need for forward declarations.
- Can make vars and routines private, as a simple way to restrict access and hide implementation details
- various data types other than just bytes (16-bit words, long integers, floats, strings)
- Structs and typed pointers
- 2D arrays (`matrix[row][col]` syntax)
+1 -2
View File
@@ -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
View File
@@ -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) {
@@ -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 -> { }
}
}
+72 -2
View File
@@ -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
}
}
+119 -52
View File
@@ -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)
}
}
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -14,7 +14,7 @@ txt {
const ubyte DEFAULT_WIDTH = 40
const ubyte DEFAULT_HEIGHT = 25
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly ofcourse. Note: takes a PETSCII encoded character.
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly of course. Note: takes a PETSCII encoded character.
sub bell() {
+1 -1
View File
@@ -13,7 +13,7 @@ txt {
const ubyte DEFAULT_WIDTH = 40
const ubyte DEFAULT_HEIGHT = 25
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly ofcourse. Note: takes a PETSCII encoded character.
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly of course. Note: takes a PETSCII encoded character.
sub bell() {
+1 -1
View File
@@ -872,7 +872,7 @@ exit:
}
sub get_loadaddress(str filename) -> uword {
; get the load adress from a PRG file (usually $0801 but it can be different)
; get the load address from a PRG file (usually $0801 but it can be different)
cbm.SETNAM(strings.length(filename), filename)
cbm.SETLFS(READ_IO_CHANNEL, drivenumber, READ_IO_CHANNEL)
+1 -1
View File
@@ -185,7 +185,7 @@ gfx_hires {
sub vertical_line(uword xx, uword yy, uword lheight, ubyte color) {
; highres 4c
; use TWO vera adress pointers simultaneously one for reading, one for writing, so auto-increment is possible
; use TWO vera address pointers simultaneously one for reading, one for writing, so auto-increment is possible
if lheight==0
return
position2(xx,yy,true)
+2 -2
View File
@@ -1,5 +1,5 @@
; optimized graphics routines for just the single screen mode: lores 320*240, 256c (8bpp)
; bitmap image needs to start at VRAM addres $00000.
; bitmap image needs to start at VRAM address $00000.
; This is compatible with the CX16's screen mode 128.
@@ -33,7 +33,7 @@ gfx_lores {
sub clear_screen(ubyte color) {
if verafx.available() {
; use verafx cache writes to quicly clear the screen
; use verafx cache writes to quickly clear the screen
const ubyte vbank = 0
const uword vaddr = 0
cx16.VERA_CTRL = 0
+94 -17
View File
@@ -4,6 +4,23 @@
; and then calling the update() routine periodically to apply the changes.
; You can read and write all parameters in the Voice structure as desired.
; For efficiency reasons, the volume envelope state is not kept in there though.
;
; Envelope timing (ASR - Attack, Sustain, Release):
; The envelope advances by one tick each time update() is called.
; The actual real-time duration depends on how often you call update().
; If you call it every vsync (60Hz), 1 tick = ~16.7ms.
; Attack and Release produce linear volume ramps.
; Higher speed = faster ramp (shorter duration).
; Timing formula: duration_ticks = ceil(256 / speed)
; speed=1 -> 256 ticks (at 60Hz: ~4.3s)
; speed=4 -> 64 ticks (at 60Hz: ~1.07s)
; speed=64 -> 4 ticks (at 60Hz: ~67ms)
; speed=255 -> 1 tick (instant envelope change on next update() call)
; Even speed=255 applies on the *next* call to ``update()`` — there is always at least ~1 frame of delay if you depend on the normal update cycle.
; For truly immediate changes (0ms), set the voice struct fields and call ``update()`` directly.
; Sustain holds for speed ticks (0-255) then transitions to release.
; If speed=0 the envelope stalls at that phase (no progression - useful for infinite sustain).
; The envelope volume is tracked internally as 8.8 fixed point.
; Vera PSG registers:
; $1F9C0 - $1F9FF 16 blocks of 4 PSG registers (16 voices)
@@ -43,21 +60,25 @@ psg2 {
ubyte volume ; 0-63, use setvolume() to adjust this if you are also using ADSR envelope
ubyte waveform ; PULSE/SQUARE,SAWTOOTH,TRIANGLE,NOISE
ubyte pulsewidth ; 0-63
uword frequency
uword frequency ; direct VERA PSG register value (no scaling): freqword = HERZ(Hz) / 0.3725290298461914
}
; envelope parameters are kept in arrays for maximum efficiency
ubyte[16] envelope_states
ubyte[16] envelope_attacks
ubyte[16] envelope_sustains
ubyte[16] envelope_releases
ubyte[16] envelope_maxvolumes
uword[16] envelope_volumes ; 8.8 fixed point (scaled by 256)
private ubyte[16] envelope_states
private ubyte[16] envelope_attacks
private ubyte[16] envelope_sustains
private ubyte[16] envelope_releases
private ubyte[16] envelope_maxvolumes
private uword[16] envelope_volumes ; 8.8 fixed point (scaled by 256)
; private VERA context save buffer, so we don't clobber the global _vera_storage in syslib
; (which is shared with the cx16 module's save_vera_context and would cause a race
; condition if update() is interrupted by a VSYNC IRQ handler that also saves VERA context)
private ubyte[8] @shared vera_storage
^^Voice voices = memory("psg2voices", 16*sizeof(Voice), 0) ; can't use array of structs so use mem block
^^Voice vptr
private ^^Voice vptr
sub init() {
; -- required before using this module; initializes all registers to default (off) values
@@ -133,7 +154,7 @@ psg2 {
}
sub update() -> bool {
; -- update adsr envelopes, then write all 16 voices to Vera PSG
; -- update adsr envelopes, then write all 16 voices to Vera PSG. This call is IRQ-safe!
; This has to be called every time you want to apply changed psg voice parameters.
; If you want to use real-time volume envelopes (Attack-Sustain-Release),
; you have to call this routine every 1/60th second, for example from your vsync irq handler.
@@ -155,11 +176,16 @@ psg2 {
when envelope_states[voice] {
EnvelopeState::ATTACK -> {
; while current volume is less than max volume, increase volume by maxvolume * (attackspeed /256.0)
; special case: speed=255 jumps directly to max volume (instant)
maxvolume = mkword(envelope_maxvolumes[voice], 0)
currentvolume = envelope_volumes[voice]
if currentvolume < maxvolume {
newvolume = currentvolume + (maxvolume >> 8)*envelope_attacks[voice]
newvolume = min(newvolume, maxvolume)
if envelope_attacks[voice] == 255
newvolume = maxvolume
else {
newvolume = currentvolume + (maxvolume >> 8)*envelope_attacks[voice]
newvolume = min(newvolume, maxvolume)
}
envelope_volumes[voice] = newvolume
vptr.volume = msb(newvolume)
} else
@@ -173,13 +199,18 @@ psg2 {
}
EnvelopeState::RELEASE -> {
; while current volume is not zero, decrease volume by maxvolume * (releasespeed /256.0)
; special case: speed=255 jumps directly to zero (instant)
currentvolume = envelope_volumes[voice]
if currentvolume > 0 {
uword subtraction = (currentvolume>>8)*envelope_releases[voice]
if subtraction > currentvolume
if envelope_releases[voice] == 255
newvolume = 0
else
newvolume = currentvolume - subtraction
else {
uword subtraction = (envelope_maxvolumes[voice] as uword) * envelope_releases[voice]
if subtraction > currentvolume
newvolume = 0
else
newvolume = currentvolume - subtraction
}
envelope_volumes[voice] = newvolume
vptr.volume = msb(newvolume)
} else
@@ -190,7 +221,7 @@ psg2 {
vptr++
}
cx16.save_vera_context()
psg2_save_vera_context()
cx16.VERA_CTRL = 0
cx16.VERA_ADDR = lsw(cx16.VERA_PSG_BASE)
cx16.VERA_ADDR_H = %00010000 | msw(cx16.VERA_PSG_BASE)
@@ -207,7 +238,7 @@ psg2 {
}
vptr++
}
cx16.restore_vera_context()
psg2_restore_vera_context()
cx16.r3 = popw()
cx16.r2 = popw()
cx16.r1 = popw()
@@ -215,4 +246,50 @@ psg2 {
return true
}
private asmsub psg2_save_vera_context() clobbers(A) {
%asm {{
; save VERA registers to our own private storage (not clobbering syslib's global _vera_storage)
lda cx16.VERA_ADDR_L
sta p8b_psg2.p8v_vera_storage
lda cx16.VERA_ADDR_M
sta p8b_psg2.p8v_vera_storage+1
lda cx16.VERA_ADDR_H
sta p8b_psg2.p8v_vera_storage+2
lda cx16.VERA_CTRL
sta p8b_psg2.p8v_vera_storage+3
eor #1
sta p8b_psg2.p8v_vera_storage+7
sta cx16.VERA_CTRL
lda cx16.VERA_ADDR_L
sta p8b_psg2.p8v_vera_storage+4
lda cx16.VERA_ADDR_M
sta p8b_psg2.p8v_vera_storage+5
lda cx16.VERA_ADDR_H
sta p8b_psg2.p8v_vera_storage+6
rts
}}
}
private asmsub psg2_restore_vera_context() clobbers(A) {
%asm {{
lda p8b_psg2.p8v_vera_storage+7
sta cx16.VERA_CTRL
lda p8b_psg2.p8v_vera_storage+6
sta cx16.VERA_ADDR_H
lda p8b_psg2.p8v_vera_storage+5
sta cx16.VERA_ADDR_M
lda p8b_psg2.p8v_vera_storage+4
sta cx16.VERA_ADDR_L
lda p8b_psg2.p8v_vera_storage+3
sta cx16.VERA_CTRL
lda p8b_psg2.p8v_vera_storage+2
sta cx16.VERA_ADDR_H
lda p8b_psg2.p8v_vera_storage+1
sta cx16.VERA_ADDR_M
lda p8b_psg2.p8v_vera_storage+0
sta cx16.VERA_ADDR_L
rts
}}
}
}
+342
View File
@@ -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")
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ const ubyte DEFAULT_WIDTH = 80
const ubyte DEFAULT_HEIGHT = 60
const long VERA_TEXTMATRIX = $1b000
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly ofcourse. Note: takes a PETSCII encoded character.
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly of course. Note: takes a PETSCII encoded character.
sub bell() {
+149
View File
@@ -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
*/
}
}
}
}
+171 -1
View File
@@ -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
+2 -2
View File
@@ -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.
+14 -5
View File
@@ -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)
+1 -1
View File
@@ -12,7 +12,7 @@ txt {
const ubyte DEFAULT_WIDTH = 40
const ubyte DEFAULT_HEIGHT = 25
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly ofcourse. Note: takes a PETSCII encoded character.
extsub $FFD2 = chrout(ubyte character @ A) ; for consistency. You can also use cbm.CHROUT directly of course. Note: takes a PETSCII encoded character.
sub bell() {
+1 -1
View File
@@ -482,7 +482,7 @@ from_scratchW1
.pend
func_peekl .proc
; -- read the ;pmg value on the address in AY, into R14:R15
; -- read the long value on the address in AY, into R14:R15
sta P8ZP_SCRATCH_W1
sty P8ZP_SCRATCH_W1+1
from_scratchW1
+1 -1
View File
@@ -716,7 +716,7 @@ exit:
}
sub get_loadaddress(str filename) -> uword {
; get the load adress from a PRG file (usually $0801 but it can be different)
; get the load address from a PRG file (usually $0801 but it can be different)
cbm.SETNAM(strings.length(filename), filename)
cbm.SETLFS(READ_IO_CHANNEL, drivenumber, READ_IO_CHANNEL)
@@ -293,7 +293,7 @@ sub interpolate(float v, float inputMin, float inputMax, float outputMin, float
}
asmsub internal_long_AY_to_FAC() {
; Used by the compiler to cast a long to a a float in FAC
; Used by the compiler to cast a long to a float in FAC
; convert the long pointed to by AY into a float and store it in FAC
%asm {{
sta cx16.r1L
@@ -308,7 +308,7 @@ sub interpolate(float v, float inputMin, float inputMax, float outputMin, float
}
private sub internal_long_R1_to_float_AY() {
; Used by the compiler to cast a long to a a float variable:
; Used by the compiler to cast a long to a float variable:
; make the float pointed to by AY equal to the long pointed to by R1 (as float)
%asm {{
sta cx16.r0L
+4 -4
View File
@@ -44,7 +44,7 @@ _done
}
sub gnomesort_by_ub(^^ubyte @requirezp ub_keys, ^^uword wordvalues, ubyte num_elements) {
; sorts the 'wordvalues' array (no-split array of words) according to the 'ub_keys' array (which also gets sorted ofcourse).
; sorts the 'wordvalues' array (no-split array of words) according to the 'ub_keys' array (which also gets sorted of course).
ubyte @zp pos=1
while pos != num_elements {
if ub_keys[pos]>=ub_keys[pos-1]
@@ -88,7 +88,7 @@ _done
}
sub gnomesort_by_uw(uword @requirezp uw_keys, uword wordvalues, ubyte num_elements) {
; Sorts the 'wordvalues' array according to the 'uw_keys' array (which also gets sorted ofcourse).
; Sorts the 'wordvalues' array according to the 'uw_keys' array (which also gets sorted of course).
; both arrays should be no-split array of words. uw_keys are unsigned.
; Max number of elements is 128 to keep indexing code size small and fast. Clobbers R0 and R1.
ubyte @zp pos=2
@@ -161,7 +161,7 @@ _done
}
sub shellsort_by_ub(^^ubyte @requirezp ub_keys, ^^uword @requirezp wordvalues, ubyte num_elements) {
; sorts the 'wordvalues' array (no-split array of words) according to the 'ub_keys' array (which also gets sorted ofcourse).
; sorts the 'wordvalues' array (no-split array of words) according to the 'ub_keys' array (which also gets sorted of course).
num_elements--
ubyte @zp gap
for gap in [132, 57, 23, 10, 4, 1] {
@@ -187,7 +187,7 @@ _done
}
sub shellsort_by_uw(^^uword @requirezp uw_keys, ^^uword @requirezp wordvalues, ubyte num_elements) {
; sorts the 'wordvalues' array according to the 'uw_keys' array (which also gets sorted ofcourse).
; sorts the 'wordvalues' array according to the 'uw_keys' array (which also gets sorted of course).
; both arrays should be no-split array of words. uw_keys are unsigned.
num_elements--
ubyte gap
+13 -13
View File
@@ -7,7 +7,7 @@ strings {
asmsub length(str string @AY) clobbers(A) -> ubyte @Y {
; Returns the number of bytes in the string.
; This value is determined during runtime and counts upto the first terminating 0 byte in the string,
; This value is determined during runtime and counts up to the first terminating 0 byte in the string,
; regardless of the size of the string during compilation time. Don’t confuse this with len and sizeof!
; uses P8ZP_SCRATCH_W1 to store the string address, do not change this, other routines here may depend on it (as optimization implementation detail)
@@ -29,7 +29,7 @@ strings {
; Also, you have to make sure yourself that length is smaller or equal to the length of the source string.
; Modifies in-place, doesn’t return a value (so can’t be used in an expression).
%asm {{
; need to copy the the cx16 virtual registers to zeropage to be compatible with C64...
; need to copy the cx16 virtual registers to zeropage to be compatible with C64...
sta P8ZP_SCRATCH_W1
stx P8ZP_SCRATCH_W1+1
lda cx16.r1
@@ -56,7 +56,7 @@ _loop dey
; Also, you have to make sure yourself that length is smaller or equal to the length of the source string.
; Modifies in-place, doesn’t return a value (so can’t be used in an expression).
%asm {{
; need to copy the the cx16 virtual registers to zeropage to be compatible with C64...
; need to copy the cx16 virtual registers to zeropage to be compatible with C64...
stx P8ZP_SCRATCH_B1
sta cx16.r0
sty cx16.r0+1
@@ -96,7 +96,7 @@ _loop dey
; Also, you have to make sure yourself that start and length are within bounds of the strings.
; Modifies in-place, doesn’t return a value (so can’t be used in an expression).
%asm {{
; need to copy the the cx16 virtual registers to zeropage to be compatible with C64...
; need to copy the cx16 virtual registers to zeropage to be compatible with C64...
; substr(source, target, start, length)
sta P8ZP_SCRATCH_B1
lda cx16.r0
@@ -131,7 +131,7 @@ _startloop dey
; Locates the first position of the given character in the string,
; returns Carry set if found + index in A, or Carry clear if not found (and A will be 255, an invalid index).
%asm {{
; need to copy the the cx16 virtual registers to zeropage to make this run on C64...
; need to copy the cx16 virtual registers to zeropage to make this run on C64...
sta P8ZP_SCRATCH_W1
sty P8ZP_SCRATCH_W1+1
stx P8ZP_SCRATCH_B1
@@ -157,7 +157,7 @@ _found tya
; This is a convenience function that looks for both a CR or LF (byte 13 or byte 10) as being a possible Line Ending.
; returns Carry set if found + index in A, or Carry clear if not found (and A will be 255, an invalid index).
%asm {{
; need to copy the the cx16 virtual registers to zeropage to make this run on C64...
; need to copy the cx16 virtual registers to zeropage to make this run on C64...
sta P8ZP_SCRATCH_W1
sty P8ZP_SCRATCH_W1+1
ldy #0
@@ -297,7 +297,7 @@ _max_too_small
asmsub compare(str string1 @R0, str string2 @AY) clobbers(Y) -> byte @A {
; Compares two strings for sorting, case-sensitively.
; Returns -1 (255), 0 or 1, meaning: string1 sorts before, equal or after string2.
; Note that you can also directly compare strings and string values with eachother using
; Note that you can also directly compare strings and string values with each other using
; comparison operators ==, < etcetera (this will use strcmp automatically).
%asm {{
sta P8ZP_SCRATCH_W2
@@ -311,7 +311,7 @@ _max_too_small
asmsub compare_nocase(str string1 @R0, str string2 @AY) clobbers(Y) -> byte @A {
; Compares two strings for sorting, case-insensitively for PETSCII strings.
; Returns -1 (255), 0 or 1, meaning: string1 sorts before, equal or after string2.
; Note that you can also directly compare strings and string values with eachother using
; Note that you can also directly compare strings and string values with each other using
; comparison operators ==, < etcetera (this will use strcmp automatically).
%asm {{
sta P8ZP_SCRATCH_W2
@@ -350,7 +350,7 @@ _c1_zero
asmsub compare_nocase_iso(str string1 @R0, str string2 @AY) clobbers(Y) -> byte @A {
; Compares two strings for sorting, case-insensitively for ISO strings.
; Returns -1 (255), 0 or 1, meaning: string1 sorts before, equal or after string2.
; Note that you can also directly compare strings and string values with eachother using
; Note that you can also directly compare strings and string values with each other using
; comparison operators ==, < etcetera (this will use strcmp automatically).
%asm {{
sta P8ZP_SCRATCH_W2
@@ -525,7 +525,7 @@ _done rts
; ? matches any one character. For example, F?? matches FOO but not FU, and ?? matches all two-character strings.
; * matches any string, including the empty string.
; For example, F* matches all strings starting with F. *O*O* matches all strings with at least two Os. Finally, ?* matches all non-empty strings.
; Both the pattern and the string must be NUL-terminated (that it, followed with a 00 byte) and at most 255 characters long (excluding the NUL).
; Both the pattern and the string must be NUL-terminated (that is, followed with a 00 byte) and at most 255 characters long (excluding the NUL).
; Code taken from http://6502.org/source/strings/patmatch.htm
;
; Input: cx16.r0: A NUL-terminated, <255-length pattern
@@ -562,7 +562,7 @@ next lda $ffff,x ; look at next pattern character MODIFIED
cmp #'*' ; is it a star?
beq star ; yes, do the complicated stuff
iny ; no, let's look at the string
cmp #'?' ; is the pattern caracter a ques?
cmp #'?' ; is the pattern character a ques?
bne reg ; no, it's a regular character
lda (strptr),y ; yes, so it will match anything
beq fail ; except the end of string
@@ -602,7 +602,7 @@ fail clc ; yes, no match found, return with c=0
; ? matches any one character. For example, F?? matches FOO but not FU, and ?? matches all two-character strings.
; * matches any string, including the empty string.
; For example, F* matches all strings starting with F. *O*O* matches all strings with at least two Os. Finally, ?* matches all non-empty strings.
; Both the pattern and the string must be NUL-terminated (that it, followed with a 00 byte) and at most 255 characters long (excluding the NUL).
; Both the pattern and the string must be NUL-terminated (that is, followed with a 00 byte) and at most 255 characters long (excluding the NUL).
; Code taken from http://6502.org/source/strings/patmatch.htm
;
; Input: cx16.r0: A NUL-terminated, <255-length pattern
@@ -651,7 +651,7 @@ next lda $ffff,x ; look at next pattern character MODIFIED
cmp #'*' ; is it a star?
beq star ; yes, do the complicated stuff
iny ; no, let's look at the string
cmp #'?' ; is the pattern caracter a ques?
cmp #'?' ; is the pattern character a ques?
bne reg ; no, it's a regular character
lda (strptr),y ; yes, so it will match anything
beq fail ; except the end of string
+1 -1
View File
@@ -300,7 +300,7 @@ diskio {
}
sub get_loadaddress(str filename) -> uword {
; get the load adress from a PRG file (usually $0801 but it can be different)
; get the load address from a PRG file (usually $0801 but it can be different)
if f_open(filename) {
uword address
void f_read(&address, 2)
+1 -1
View File
@@ -277,7 +277,7 @@ math {
sub atan2(ubyte x1, ubyte y1, ubyte x2, ubyte y2) -> ubyte {
;; Calculate the angle, in a 256-degree circle, between two points into A.
;; The points (x1, y1) and (x2, y2) have to use *unsigned coordinates only* from the positive quadrant in the carthesian plane!
;; The points (x1, y1) and (x2, y2) have to use *unsigned coordinates only* from the positive quadrant in the cartesian plane!
%ir {{
loadm.b r99100,math.atan2.x1
loadm.b r99101,math.atan2.y1
+2 -2
View File
@@ -50,7 +50,7 @@ sorting {
sub shellsort_by_ub(^^ubyte @requirezp ub_keys, ^^uword @requirezp wordvalues, ubyte num_elements) {
; sorts the 'wordvalues' array (no-split array of words) according to the 'ub_keys' array (which also gets sorted ofcourse).
; sorts the 'wordvalues' array (no-split array of words) according to the 'ub_keys' array (which also gets sorted of course).
num_elements--
ubyte @zp gap
for gap in [132, 57, 23, 10, 4, 1] {
@@ -76,7 +76,7 @@ sorting {
}
sub shellsort_by_uw(^^uword @requirezp uw_keys, ^^uword @requirezp wordvalues, ubyte num_elements) {
; sorts the 'wordvalues' array according to the 'uw_keys' array (which also gets sorted ofcourse).
; sorts the 'wordvalues' array according to the 'uw_keys' array (which also gets sorted of course).
; both arrays should be no-split array of words. uw_keys are unsigned.
num_elements--
ubyte gap
+2 -2
View File
@@ -147,7 +147,7 @@ strings {
sub compare(str st1, str st2) -> byte {
; Compares two strings for sorting (case sensitive).
; Returns -1 (255), 0 or 1, meaning: string1 sorts before, equal or after string2.
; Note that you can also directly compare strings and string values with eachother using
; Note that you can also directly compare strings and string values with each other using
; comparison operators ==, < etcetera (this will use strcmp automatically).
%ir {{
loadm.w r99000,strings.compare.st1
@@ -160,7 +160,7 @@ strings {
sub compare_nocase(str st1, str st2) -> byte {
; Compares two strings for sorting (case insensitive).
; Returns -1 (255), 0 or 1, meaning: string1 sorts before, equal or after string2.
; Note that you can also directly compare strings and string values with eachother using
; Note that you can also directly compare strings and string values with each other using
; comparison operators ==, < etcetera (this will use strcmp automatically).
%ir {{
loadm.w r99000,strings.compare_nocase.st1
+36
View File
@@ -440,4 +440,40 @@ cx16 {
r14 = cx16.save_virtual_registers.storage[14]
r15 = cx16.save_virtual_registers.storage[15]
}
private sub print_error (str message) {
%ir {{
loadm.w r99000,cx16.print_error.message
syscall 3 (r99000.w)
}}
}
sub rombank(ubyte bank) {
if bank==0
return
print_error("\nerror: rombank() only accepts 0 - aborting")
sys.exit(1)
}
sub rambank(ubyte bank) {
if bank==0
return
print_error("\nerror: rambank() only accepts 0 - aborting")
sys.exit(1)
}
inline sub getrombank() -> ubyte {
return 0
}
inline sub getrambank() -> ubyte {
return 0
}
sub numbanks() -> uword {
return 1
}
}
+292 -21
View File
@@ -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) {}
}
}
+7 -1
View File
@@ -26,6 +26,7 @@ import prog8.parser.MultipleParseErrors
import prog8.parser.ParseError
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.absolute
import kotlin.io.path.isRegularFile
import kotlin.io.path.nameWithoutExtension
import kotlin.system.exitProcess
@@ -73,6 +74,7 @@ class CompilerArguments(val filepath: Path,
val symbolDefs: Map<String, String>,
val sourceDirs: List<String> = emptyList(),
val outputDir: Path = Path(""),
val cwd: Path = Path("").absolute(),
val errors: IErrorReporter = ErrorReporter(ErrorReporter.AnsiColors))
@@ -111,6 +113,7 @@ fun compileProgram(args: CompilerArguments): CompilationResult? {
compTarget,
args.sourceDirs,
libraryDirs,
args.cwd,
args.quietAll,
args.nostdlib
)
@@ -408,13 +411,14 @@ fun parseMainModule(filepath: Path,
compTarget: ICompilationTarget,
sourceDirs: List<String>,
libraryDirs: List<String>,
cwd: Path,
quiet: Boolean,
nostdlib: Boolean): Triple<Program, CompilationOptions, List<Path>> {
val bf = BuiltinFunctionsFacade(BuiltinFunctions)
val program = Program(filepath.nameWithoutExtension, bf, compTarget, compTarget)
bf.program = program
val importer = ModuleImporter(program, compTarget.name, errors, sourceDirs, libraryDirs, quiet, nostdlib)
val importer = ModuleImporter(program, compTarget.name, errors, sourceDirs, libraryDirs, cwd, quiet, nostdlib)
val importedModuleResult = importer.importMainModule(filepath)
importedModuleResult.onErr { throw it }
errors.report()
@@ -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)
}
+16 -6
View File
@@ -15,7 +15,6 @@ import prog8.parser.Prog8Parser
import java.io.File
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.absolute
import kotlin.io.path.exists
@@ -24,6 +23,7 @@ class ModuleImporter(private val program: Program,
val errors: IErrorReporter,
sourceDirs: List<String>,
libraryDirs: List<String>,
val cwd: Path,
val quiet: Boolean,
val nostdlib: Boolean = false) {
@@ -31,15 +31,18 @@ class ModuleImporter(private val program: Program,
private val libraryPaths: List<Path> = libraryDirs.map { Path(it).sanitize() }.toSortedSet().toList()
fun importMainModule(filePath: Path): Result<Module, NoSuchFileException> {
val searchIn = (listOf(Path("").absolute()) + sourcePaths).toSortedSet()
val searchIn = sourcePaths.toSortedSet()
val normalizedFilePath = filePath.normalize()
if (normalizedFilePath.exists()) {
printCompileInfo(normalizedFilePath.toAbsolutePath())
return Ok(importModule(ImportFileSystem.getFile(normalizedFilePath)))
}
for(path in searchIn) {
val programPath = path.resolve(normalizedFilePath)
if(programPath.exists()) {
if(!quiet) {
println("Compiling program ${Path("").absolute().relativize(programPath)}")
println("Compiler target: $compilationTargetName")
}
printCompileInfo(programPath)
val source = ImportFileSystem.getFile(programPath)
return Ok(importModule(source))
}
@@ -49,6 +52,13 @@ class ModuleImporter(private val program: Program,
reason = "Searched in $searchIn"))
}
private fun printCompileInfo(programPath: Path) {
if(!quiet) {
println("Compiling program ${cwd.toAbsolutePath().relativize(programPath)}")
println("Compiler target: $compilationTargetName")
}
}
fun importImplicitLibraryModule(name: String): Module? {
val import = Directive("%import", listOf(
DirectiveArg(name, 42u, position = Position("~implicit-import~", 0, 0, 0))
@@ -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))
}
}
@@ -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) {
+1 -1
View File
@@ -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)
}
}
+102 -113
View File
@@ -2,7 +2,6 @@ package prog8tests.compiler
import io.kotest.common.ExperimentalKotest
import io.kotest.core.spec.style.FunSpec
import io.kotest.datatest.withData
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
@@ -146,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") {
+26 -28
View File
@@ -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
}
}
"""
+1 -1
View File
@@ -29,7 +29,7 @@ class TestModuleImporter: FunSpec({
}
fun makeImporter(errors: IErrorReporter? = null, searchIn: Iterable<String>) =
ModuleImporter(program, "blah", errors ?: ErrorReporterForTests(false), searchIn.toList(), emptyList(), false)
ModuleImporter(program, "blah", errors ?: ErrorReporterForTests(false), searchIn.toList(), emptyList(), Path(".").absolute(), false)
fun makeImporter(errors: IErrorReporter?, vararg searchIn: String): ModuleImporter {
return makeImporter(errors, searchIn.asList())
+3 -3
View File
@@ -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
}
+127 -16
View File
@@ -1,5 +1,6 @@
package prog8tests.compiler
import io.kotest.assertions.fail
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
@@ -14,6 +15,7 @@ import prog8.ast.ParentSentinel
import prog8.ast.Program
import prog8.ast.expressions.*
import prog8.ast.statements.*
import prog8.ast.walk.IAstVisitor
import prog8.code.ast.*
import prog8.code.core.BaseDataType
import prog8.code.core.DataType
@@ -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
}
})
+3 -3
View File
@@ -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
-78
View File
@@ -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
}
})
+1 -1
View File
@@ -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
+4 -4
View File
@@ -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() {
}
+28
View File
@@ -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"
}
})
+15
View File
@@ -498,6 +498,21 @@ main {
compileText(Cx16Target(), true, src, outputDir, writeAssembly = false) shouldNotBe null
}
test("const string to pointer type compiles") {
val src = """
main {
sub start() {
const ^^ubyte name = "irmen"
thing(name)
}
sub thing(str x) {
cx16.r0L++
}
}"""
compileText(VMTarget(), true, src, outputDir, writeAssembly = false) shouldNotBe null
}
test("const long with large unsigned long values should be converted to signed longs") {
val src = $$"""
main {
+58 -23
View File
@@ -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 {
+147
View File
@@ -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
}
})
+13 -13
View File
@@ -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)!!
+21
View File
@@ -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)"
}
})
+5 -27
View File
@@ -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++
+12 -10
View File
@@ -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
}
})
+1 -1
View File
@@ -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
}
+7 -4
View File
@@ -18,7 +18,8 @@ internal fun compileFile(
errors: IErrorReporter? = null,
writeAssembly: Boolean = true,
varshigh: Int? = null,
slabshigh: Int? = null
slabshigh: Int? = null,
experimentalCodegen: Boolean = false
) : CompilationResult? {
val filepath = fileDir.resolve(fileName)
assumeReadableFile(filepath)
@@ -33,7 +34,7 @@ internal fun compileFile(
showTimings = false,
asmListfile = false,
includeSourcelines = false,
experimentalCodegen = false,
experimentalCodegen = experimentalCodegen,
dumpVariables = false,
dumpSymbols = false,
varsHighBank = varshigh,
@@ -67,11 +68,13 @@ internal fun compileText(
errors: IErrorReporter? = null,
writeAssembly: Boolean = true,
varshigh: Int? = null,
slabshigh: Int? = null
slabshigh: Int? = null,
experimentalCodegen: Boolean = false
) : CompilationResult? {
val filePath = outputDir.resolve("on_the_fly_test_${sourceText.hashCode().toUInt().toString(16)}.p8")
// we don't assumeNotExists(filePath) - should be ok to just overwrite it
filePath.toFile().writeText(sourceText)
return compileFile(platform, optimize, filePath.parent, filePath.name, outputDir,
errors=errors, writeAssembly=writeAssembly, varshigh=varshigh, slabshigh=slabshigh)
errors=errors, writeAssembly=writeAssembly, varshigh=varshigh, slabshigh=slabshigh,
experimentalCodegen = experimentalCodegen)
}
@@ -0,0 +1,49 @@
package prog8tests.optimizer
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.shouldBe
import prog8.ast.expressions.BinaryExpression
import prog8.ast.expressions.IdentifierReference
import prog8.ast.expressions.NumericLiteral
import prog8.ast.statements.Assignment
import prog8.code.target.Cx16Target
import prog8tests.helpers.compileText
class TestExpressionSimplifier : FunSpec({
val outputDir = tempdir().toPath()
test("long & -1 is correctly simplified") {
val text = """
main {
sub start() {
long @shared x = 1000
long @shared y = x & -1
}
}
""".trimIndent()
val result = compileText(Cx16Target(), true, text, outputDir, writeAssembly = false)!!
val s = result.compilerAst.entrypoint.statements
val expr = (s[3] as Assignment).value as BinaryExpression
expr.operator shouldBe "&"
(expr.left as IdentifierReference).nameInSource shouldBe listOf("x")
(expr.right as NumericLiteral).number shouldBe -1.0
}
test("long & 255 is correctly handled") {
val text = """
main {
sub start() {
long @shared x = 1000
long @shared y = x & 255
}
}
""".trimIndent()
val result = compileText(Cx16Target(), true, text, outputDir, writeAssembly = false)!!
val s = result.compilerAst.entrypoint.statements
val expr = (s[3] as Assignment).value as BinaryExpression
expr.operator shouldBe "&"
(expr.left as IdentifierReference).nameInSource shouldBe listOf("x")
(expr.right as NumericLiteral).number shouldBe 255.0
}
})
Loaded 100 of 171 files, more files were not shown because too many files have changed in this diff. Show more