From 215d8d92b433483d66a651e653a0153167aad094 Mon Sep 17 00:00:00 2001 From: Karol Stasiak Date: Thu, 12 Jul 2018 18:30:35 +0200 Subject: [PATCH] Preprocessor. Z80 improvements. Library improvements. --- CHANGELOG.md | 7 + docs/api/command-line.md | 1 + docs/api/custom-platform.md | 4 + docs/index.md | 13 + docs/lang/assembly.md | 6 +- docs/lang/assemblyz80.md | 127 +++++++- docs/lang/interfacing.md | 11 +- docs/lang/operators.md | 1 + docs/lang/preprocessor.md | 96 ++++++ docs/stdlib/c64.md | 23 ++ docs/stdlib/nes.md | 40 +++ docs/stdlib/other.md | 53 ++++ docs/stdlib/stdlib.md | 33 ++ include/a8.ini | 9 +- include/a8_kernel.mfk | 5 + include/apple2.ini | 10 +- include/apple2_kernel.mfk | 5 + include/bbc_hardware.mfk | 5 + include/bbc_kernal.mfk | 4 + include/bbcmicro.ini | 10 +- include/c128.ini | 12 +- include/c128_hardware.mfk | 5 + include/c128_kernal.mfk | 4 + include/c1531.mfk | 13 +- include/c16.ini | 10 +- include/c264_hardware.mfk | 4 + include/c264_kernal.mfk | 3 + include/c264_ted.mfk | 3 + include/c64.ini | 8 + include/c64_basic.mfk | 4 + include/c64_cia.mfk | 6 +- include/c64_hardware.mfk | 6 +- include/c64_kernal.mfk | 2 +- include/c64_panic.mfk | 2 +- include/c64_scpu.ini | 9 + include/c64_scpu16.ini | 9 + include/c64_sid.mfk | 4 + include/c64_vic.mfk | 5 + include/cpu6510.mfk | 3 + include/loader_0401.mfk | 4 + include/loader_0801.mfk | 4 + include/loader_0801_16bit.mfk | 4 + include/loader_1001.mfk | 4 + include/loader_1201.mfk | 4 + include/loader_1c01.mfk | 4 + include/lunix.ini | 10 +- include/lunix.mfk | 4 + include/mouse.mfk | 10 +- include/nes_hardware.mfk | 4 + include/nes_mmc4.ini | 9 +- include/nes_mmc4.mfk | 4 + include/nes_routines.mfk | 3 + include/nes_small.ini | 9 +- include/pc88.ini | 10 +- include/pet.ini | 8 + include/pet_kernal.mfk | 4 + include/plus4.ini | 10 +- include/stdio.mfk | 19 +- include/stdio_zxspectrum.mfk | 31 ++ include/stdlib.mfk | 51 +-- include/stdlib_6502.mfk | 50 +++ include/stdlib_z80.mfk | 45 +++ include/string.mfk | 10 + include/vcs.ini | 9 +- include/vcs_hardware.mfk | 5 + include/vic20.ini | 10 +- include/vic20_3k.ini | 10 +- include/vic20_8k.ini | 10 +- include/vic20_kernal.mfk | 4 + include/x_coord.mfk | 6 + include/zp_reg.mfk | 4 + include/zxspectrum.ini | 10 +- include/zxspectrum.mfk | 19 ++ .../scala/millfork/CompilationOptions.scala | 2 +- src/main/scala/millfork/Main.scala | 18 +- src/main/scala/millfork/Platform.scala | 40 ++- .../z80/Z80BulkMemoryOperations.scala | 2 +- .../compiler/z80/Z80StatementCompiler.scala | 25 +- src/main/scala/millfork/node/Node.scala | 2 +- .../scala/millfork/output/Z80Assembler.scala | 17 + .../parser/AbstractSourceLoadingQueue.scala | 8 +- src/main/scala/millfork/parser/MfParser.scala | 33 +- .../scala/millfork/parser/MosParser.scala | 4 +- .../parser/MosSourceLoadingQueue.scala | 3 +- .../scala/millfork/parser/Preprocessor.scala | 243 ++++++++++++++ .../scala/millfork/parser/Z80Parser.scala | 300 ++++++++++++++---- .../millfork/parser/ZSourceLoadingQueue.scala | 3 +- .../scala/millfork/test/BasicSymonTest.scala | 41 +++ .../scala/millfork/test/emu/EmuPlatform.scala | 1 + src/test/scala/millfork/test/emu/EmuRun.scala | 5 +- .../scala/millfork/test/emu/EmuZ80Run.scala | 5 +- 91 files changed, 1560 insertions(+), 169 deletions(-) create mode 100644 docs/lang/preprocessor.md create mode 100644 docs/stdlib/c64.md create mode 100644 docs/stdlib/nes.md create mode 100644 docs/stdlib/other.md create mode 100644 docs/stdlib/stdlib.md create mode 100644 include/stdio_zxspectrum.mfk create mode 100644 include/stdlib_6502.mfk create mode 100644 include/stdlib_z80.mfk create mode 100644 include/string.mfk create mode 100644 include/x_coord.mfk create mode 100644 src/main/scala/millfork/parser/Preprocessor.scala diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c12c234..ba6b400a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,18 +10,25 @@ * Added aliases. +* Added preprocessor + * Automatic selection of text encoding based on target platform. * **Potentially breaking change!** `scr` now refers to the default screencodes as defined for the platform. Code that uses both a custom platform definition and the `scr` encoding needs attention (either change `scr` to `petscr` or add `screen_encoding=petscr` in the platform definition file). +* **Potentially breaking change!** Platform definitions now need appropriate feature definitions. +Code that uses a custom platform definitions will cause extra warnings until fixed. + * Optimizations for stack variables. * Fixed emitting constant decimal expressions. * Parser performance improvement. +* Standard libraries improvements. + * Other improvements. ## 0.3.0 diff --git a/docs/api/command-line.md b/docs/api/command-line.md index 3a77db53..2b1650cf 100644 --- a/docs/api/command-line.md +++ b/docs/api/command-line.md @@ -34,6 +34,7 @@ no extension for BBC micro program file, * `-r ` – Run given program after successful compilation. Useful for automatically launching emulators without any external scripting. +* `-D =` – Defines a feature value for the preprocessor. ## Verbosity options diff --git a/docs/api/custom-platform.md b/docs/api/custom-platform.md index 3b8c0c9d..ffede18b 100644 --- a/docs/api/custom-platform.md +++ b/docs/api/custom-platform.md @@ -64,6 +64,10 @@ Default: the same as `encoding`. * `lenient_encoding` - allow for automatic substitution of invalid characters in string literals using the default encodings, default is `false`. +#### `[define]` section + +This section defines values of features of the target. +See the [preprocessor documentation](../lang/preprocessor.md) for more info. #### `[allocation]` section diff --git a/docs/index.md b/docs/index.md index bd6089b3..0ff81f09 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,6 +15,8 @@ ## Language reference +* [Preprocessor](lang/preprocessor.md) + * [Syntax](lang/syntax.md) * [Types](lang/types.md) @@ -25,8 +27,19 @@ * [Inline 6502 assembly syntax](lang/assembly.md) +* [Inline Z80 assembly syntax](lang/assemblyz80.md) + * [Important guidelines regarding reentrancy](lang/reentrancy.md) +## Library reference + +* [`stdlib` module](stdlib/stdlib.md) + +* [Other cross-platform modules](stdlib/other.md) + +* [C64-only modules](stdlib/c64.md) + +* [NES-only modules](stdlib/nes.md) ## Implementation details diff --git a/docs/lang/assembly.md b/docs/lang/assembly.md index 2c7027c7..9a7c3dbe 100644 --- a/docs/lang/assembly.md +++ b/docs/lang/assembly.md @@ -16,7 +16,7 @@ Indexing syntax is also the same. Only instructions available on the current CPU **Work in progress**: Currently, `RMBx`/`SMBx`/`BBRx`/`BBSx` and some extra 65CE02/HuC6280/65816 instructions are not supported yet. -Undocumented instructions are supported using various opcodes +Undocumented instructions are supported using various opcodes. Labels have to be followed by a colon and they can optionally be on a separate line. Indentation is not important: @@ -29,7 +29,7 @@ Indentation is not important: Label names have to start with a letter and can contain digits, underscores and letters. This means than they cannot start with a period like in many other assemblers. -Similarly, anonymous labels designated with `+` or `-` are also not supported +Similarly, anonymous labels designated with `+` or `-` are also not supported. Assembly can refer to variables and constants defined in Millfork, but you need to be careful with using absolute vs immediate addressing: @@ -106,7 +106,7 @@ Macro assembly functions can have the following parameter types: For example, if you have: - inline asm void increase(byte ref v, byte const inc) { + macro asm void increase(byte ref v, byte const inc) { LDA v CLC ADC #inc diff --git a/docs/lang/assemblyz80.md b/docs/lang/assemblyz80.md index 1b0faaee..03908a7f 100644 --- a/docs/lang/assemblyz80.md +++ b/docs/lang/assemblyz80.md @@ -2,7 +2,132 @@ # Using Z80 assembly within Millfork programs -The compiler does not yet support Z80 assembly. This will be remedied in the future. +The compiler supports Z80 assembly only partially. This will be remedied in the future. + +There are two ways to include raw assembly code in your Millfork programs: + +* inline assembly code blocks + +* whole assembly functions + +## Assembly syntax + +Millfork inline assembly uses the same three-letter opcodes as most other 6502 assemblers. +Indexing syntax is also the same. Only instructions available on the current CPU architecture are available. + +**Work in progress**: +Currently, `RES/SET/BIT` and some few more instructions are not supported yet. + +Undocumented instructions are not supported, except for `SLL`. + +Labels have to be followed by a colon and they can optionally be on a separate line. +Indentation is not important: + + first: INC a + second: + INC b + INC c + + +Label names have to start with a letter and can contain digits, underscores and letters. +This means than they cannot start with a period like in many other assemblers. +Similarly, anonymous labels designated with `+` or `-` are also not supported. + +Assembly can refer to variables and constants defined in Millfork, +but you need to be careful with using absolute vs immediate addressing: + + const byte fiveConstant = 5 + byte fiveVariable = 5 + + byte ten() { + byte result + asm { + LD A, (fiveVariable) + ADD A,fiveConstant + LD (result), A + } + return result + } + +Any assembly opcode can be prefixed with `?`, which allows the optimizer change it or elide it if needed. +Opcodes without that prefix will be always compiled as written. + +You can insert macros into assembly, by prefixing them with `+` and using the same syntax as in Millfork: + + macro void run(byte x) { + output = x + } + + byte output @$c000 + + void main () { + byte a + a = 7 + asm { + + run(a) + } + } + +You can insert raw bytes into your assembly using the array syntax: + + [ $00, $00 ] + "this is a string to print" bbc + ["this is a string to print but this time it's zero-terminated so it will actually work" bbc, 0] + [for x,0,until,8 [x]] + +## Assembly functions + +Assembly functions can be declared as `macro` or not. + +A macro assembly function is inserted into the calling function like an inline assembly block, +and therefore usually it shouldn't end with `RET`, `RETI` or `RETN`. + +A non-macro assembly function should end with `RET`, `JP`, `RETI` or `RETN` as appropriate, +or it should be an external function. + +For both macro and non-macro assembly functions, +the return type can be any valid return type, like for Millfork functions. +If the size of the return type is one byte, +then the result is passed via the A register. +If the size of the return type is two bytes, +then the result is passed via the HL register pair. + +### Assembly function parameters + +An assembly function can have parameters. +They differ from what is used by Millfork functions. + +Macro assembly functions can have the following parameter types: + +* reference parameters: `byte ref paramname`: every occurrence of the parameter will be replaced with the variable given as an argument + +* constant parameters: `byte const paramname`: every occurrence of the parameter will be replaced with the constant value given as an argument + +For example, if you have: + + macro asm void increase(byte ref v, byte const inc) { + LD A,(v) + ADD A,inc + LDA (v),A + } + +and call `increase(score, 10)`, the entire call will compile into: + + LD A,(score) + ADD A,10 + LD (score),A + +Non-macro functions can only have their parameters passed via registers: + +* `byte a`, `byte b`, etc.: a single byte passed via the given CPU register + +* `word hl`, `word bc`, `word de`: a 2-byte word byte passed via given 16-bit register + +**Work in progress**: +Currently, only 3 parameter signatures are supported for non-macro assembly functions: +`()`, `(byte a)` and `(word hl)`. More parameters or parameters passed via other registers do not work yet. + +Macro assembly functions cannot have any parameter passed via registers. ## Safe assembly diff --git a/docs/lang/interfacing.md b/docs/lang/interfacing.md index 505282f0..98848a47 100644 --- a/docs/lang/interfacing.md +++ b/docs/lang/interfacing.md @@ -17,20 +17,23 @@ the function itself is located in ROM at $FFD2. A call like this: putchar(13) ``` -will be compiled to something like this: +will be compiled to something like this on 6502: ``` LDA #13 JSR $FFD2 ``` -For more details about how to pass parameters to `asm` functions, -see [Using assembly within Millfork programs#Assembly functions](./assembly.md#assembly-functions). +For more details about how to pass parameters to `asm` functions, see: + +* for 6502: [Using 6502 assembly within Millfork programs#Assembly functions](./assembly.md#assembly-functions). + +* for Z80: [Using Z80 assembly within Millfork programs#Assembly functions](./assembly.md#assembly-functions). ## Calling external functions at a dynamic address To call a function that has its address calculated dynamically, -you just need to do the same as what you would do in assembly: +you just need to do the same as what you would do in assembly; 6502 example: ``` asm void call_function(byte a) { diff --git a/docs/lang/operators.md b/docs/lang/operators.md index 7589865f..728d0cae 100644 --- a/docs/lang/operators.md +++ b/docs/lang/operators.md @@ -12,6 +12,7 @@ Certain expressions require the commandline flag `-fzp-register` (`.ini` equival They will be marked with (zpreg) next to them. The flag is enabled by default, but you can disable it if you need to. +**Work in progress**: Certain expressions may not work on non-6502 targets yet. This should improve in the future. ## Precedence diff --git a/docs/lang/preprocessor.md b/docs/lang/preprocessor.md new file mode 100644 index 00000000..48b3de26 --- /dev/null +++ b/docs/lang/preprocessor.md @@ -0,0 +1,96 @@ +[< back to index](../index.md) + +# Preprocessor + +The Millfork preprocessor does 2 things: + +* filters lines in the input file according to current target's features + +* injects the target's feature values as constants into the current file + +Despite its similarity to the C preprocessor, it's much more restricted in its power: + +* no file inclusion + +* no macros + +* separate namespaces for the preprocessor and the language (you need to use `#use` to use a preprocessor constant in the code) + +### Defining feature values + +Feature values are defined in the `[define]` section of the platform definition file. +Each value is a signed 64-bit integer number. + +Example: + + [define] + WIDESCREEN=1 + +You can also define feature values using the `-D` command line option. + +### Built-in features + +* `ARCH_6502` – 1 if compiling for 6502, 0 otherwise + +* `ARCH_Z80` – 1 if compiling for Z80, 0 otherwise + +### Commonly used features + +* `WIDESCREEN` – 1 if the horizontal screen resolution, ignoring borders, is greater than 256, 0 otherwise + +* `CBM` – 1 if the target is an 8-bit Commodore computer, 0 otherwise + +* `CBM_64` – 1 if the target is an 8-bit Commodore computer compatible with Commodore 64, 0 otherwise + +* `KEYBOARD` – 1 if the target has a keyboard, 0 otherwise + +* `JOYSTICKS` – the maximum number of joysticks using standard hardware configurations, may be 0 + +* `HAS_BITMAP_MODE` – 1 if the target has a display mode with every pixel addressable, 0 otherwise + +### Built-in preprocessor functions and operators + +The `defined` function returns 1 if the feature is defined, 0 otherwise. +All the other functions and operators treat undefined features as if they were defined as 0. + +TODO +`not`, `lo`, `hi`, `+`, `-`, `*`, `|`, `&`, `^`, `||`, `&&`, `<<`, `>>`,`==`, `!=`, `>`, `>=`, `<`, `<=` + +The following Millfork operators and functions are not available in the preprocessor: +`+'`, `-'`, `*'`, `<<'`, `>>'`, `:`, `>>>>`, `nonet`, all the assignment operators + +### `#if/#elseif/#else/#endif` + + #if + #elseif + #else + #endif + +TODO + +### `#fatal/#error/#warn/#info` + + #error fatal error message + #error error message + #warn warning message + #info informational message + +Emits a diagnostic message. + +`#fatal` interrupts the compilation immediately. +`#error` causes an error, but the compilation will continue. +`#warn` emits a warning. It may be treated as an error depending on compilation options. +`#info` emits a benign diagnostic message. + + +### `#use` + +Exports a feature value under its name to the parser. +The parser will substitute every use of that name as a variable or constant +with the numeric value of the feature. +The substitution will happen only within the current file. +To use such value in other files, consider this: + + #use WIDESCREEN + const byte is_widescreen = WIDESCREEN + diff --git a/docs/stdlib/c64.md b/docs/stdlib/c64.md new file mode 100644 index 00000000..d159ae04 --- /dev/null +++ b/docs/stdlib/c64.md @@ -0,0 +1,23 @@ +[< back to index](../index.md) + +# Commodore 64-oriented modules + +## `c64_kernal` module + +The `c64_kernal` module is imported automatically on the C64 target. + +TODO + +## `c64_basic` module + +TODO + +## `c1531_mouse` module + +The `c1531_mouse` module implements a Commodore 1531 proportional mouse driver compatible with the `mouse` module. + +#### `void c1531_mouse ()` + +Updates the state of the mouse. + + diff --git a/docs/stdlib/nes.md b/docs/stdlib/nes.md new file mode 100644 index 00000000..45fc60ab --- /dev/null +++ b/docs/stdlib/nes.md @@ -0,0 +1,40 @@ +[< back to index](../index.md) + +# NES/Famicom-oriented modules + +## `nes_hardware` module + +The `nes_hardware` module is imported automatically on NES targets. + +TODO + +## `nes_mmc4` module + +The `nes_mmc4` module is imported automatically on the NES MMC4 target. +and contains routines related to MMC4 bankswitching. + +#### `void set_prg_bank(byte a)` + +Changes the $8000-$BFFF PRG bank. + +#### `void set_chr_bank0(byte a)` + +Changes the CHR bank 0 ($0000-$0fff in the PPU memory space). + +The high nibble (0 or 1) selects between `chrrom0` and `chrrom1` segments. +The low nibble L (0-$F) selects a 4K-aligned address in the segment ($L000). + +#### `void set_chr_bank1(byte a)` + +Changes the CHR bank 1 ($1000-$1fff in the PPU memory space). + +The high nibble (0 or 1) selects between `chrrom0` and `chrrom1` segments. +The low nibble L (0-$F) selects a 4K-aligned address in the segment ($L000). + +#### `void set_vertical_mirroring()` + +Switches nametable mirroring to vertical. + +#### `void set_horizontal_mirroring()` + +Switches nametable mirroring to horizontal. \ No newline at end of file diff --git a/docs/stdlib/other.md b/docs/stdlib/other.md new file mode 100644 index 00000000..666c73a4 --- /dev/null +++ b/docs/stdlib/other.md @@ -0,0 +1,53 @@ +[< back to index](../index.md) + +## `stdio` module + +The `stdio` module automatically imports the `string` module. +It requires an implementation of `void putchar(byte a)` and therefore works only on targets with console output. + +#### `void putstr(pointer str, byte len)` + +Prints a string of length `len` located at address `str`. + +#### `void putstrz(pointer str)` + +Prints a null-terminated string located at address `str`. +If the string is longer than 255 bytes, then the behaviour is undefined (might even crash). + +## `string` module + +#### `byte strzlen(pointer str)` + +Calculates the length of a null-terminated string. +If the string is longer than 255 bytes, then the behaviour is undefined (might even crash). + +## `mouse` module + +The `mouse` module automatically imports the `x_coord` module. + +The module contains global variables representing the state of the mouse. +If the program is not using any mouse driver, the state of these variables is undefined. + +#### `x_coord mouse_x` + +Mouse X position. + +#### `byte mouse_y` + +Mouse Y position. + +#### `byte mouse_lbm` + +1 if the left mouse button is being pressed, 0 otherwise + +#### `byte mouse_rbm` + +1 if the right mouse button is being pressed, 0 otherwise + +## `x_coord` module + +#### `alias x_coord` + +The type for representing horizontal screen coordinates. +It's `byte` if the screen is 256 pixels wide or less, +or `word` if the screen is more that 256 pixels wide. \ No newline at end of file diff --git a/docs/stdlib/stdlib.md b/docs/stdlib/stdlib.md new file mode 100644 index 00000000..9453a7e0 --- /dev/null +++ b/docs/stdlib/stdlib.md @@ -0,0 +1,33 @@ +[< back to index](../index.md) + +## `stdlib` module + +The `stdlib` module is automatically imported on most targets. + +#### `macro asm void poke(word const addr, byte a)` + +Stores a byte at given constant address. Will not be optimized away by the optimizer. + +#### `macro asm byte peek(word const addr)` + +Reads a byte from given constant address. Will not be optimized away by the optimizer. + +#### `macro asm void disable_irq()` + +Disables interrupts. + +#### `macro asm void enable_irq()` + +Enables interrupts. + +#### `byte hi_nibble_to_hex(byte a)` + +Returns an ASCII representation of the upper nibble of the given byte. + +#### `byte lo_nibble_to_hex(byte a)` + +Returns an ASCII representation of the lower nibble of the given byte. + +#### `macro asm void panic()` + +Crashes the program. \ No newline at end of file diff --git a/include/a8.ini b/include/a8.ini index 54c842f4..e0737f40 100644 --- a/include/a8.ini +++ b/include/a8.ini @@ -1,6 +1,6 @@ [compilation] arch=strict -modules=a8_kernel,default_panic +modules=a8_kernel,default_panic,stdlib encoding=atascii [allocation] @@ -10,6 +10,13 @@ segment_default_start=$2000 ; TODO segment_default_end=$3fff +[define] +ATARI_8=1 +WIDESCREEN=1 +KEYBOARD=1 +JOYSTICKS=2 +HAS_BITMAP_MODE=1 + [output] ;TODO style=single diff --git a/include/a8_kernel.mfk b/include/a8_kernel.mfk index 76afe512..98b7c8c9 100644 --- a/include/a8_kernel.mfk +++ b/include/a8_kernel.mfk @@ -1,3 +1,8 @@ + +#if not(ATARI_8) +#warn a8_kernel module should be used only on Atari computer-compatible targets +#endif + asm void putchar(byte a) { tax lda $347 diff --git a/include/apple2.ini b/include/apple2.ini index f4e53db1..6279ccf5 100644 --- a/include/apple2.ini +++ b/include/apple2.ini @@ -1,7 +1,7 @@ [compilation] arch=strict encoding=apple2 -modules=apple2_kernel,default_panic +modules=apple2_kernel,default_panic,stdlib [allocation] @@ -10,6 +10,14 @@ zp_pointers=6,8,$EB,$ED,$FA,$FC segment_default_start=$0C00 segment_default_end=$95FF +[define] +APPLE_2=1 +WIDESCREEN=0 +KEYBOARD=1 +; TODO: ? +JOYSTICKS=0 +HAS_BITMAP_MODE=1 + [output] ;TODO style=single diff --git a/include/apple2_kernel.mfk b/include/apple2_kernel.mfk index fb161caf..d8e1396f 100644 --- a/include/apple2_kernel.mfk +++ b/include/apple2_kernel.mfk @@ -1,3 +1,8 @@ + +#if not(APPLE_2) +#warn apple2_kernel module should be used only on Apple II-compatible targets +#endif + array hires_page_1 [$2000] @$2000 array hires_page_2 [$2000] @$4000 diff --git a/include/bbc_hardware.mfk b/include/bbc_hardware.mfk index 8b137891..3bdf15fd 100644 --- a/include/bbc_hardware.mfk +++ b/include/bbc_hardware.mfk @@ -1 +1,6 @@ +#if not(BBC_MICRO) +#warn bbc_hardware module should be used only on BBC Micro-compatible targets +#endif + + diff --git a/include/bbc_kernal.mfk b/include/bbc_kernal.mfk index 879e7564..232bd596 100644 --- a/include/bbc_kernal.mfk +++ b/include/bbc_kernal.mfk @@ -1,4 +1,8 @@ +#if not(BBC_MICRO) +#warn bbc_kernal module should be used only on BBC Micro-compatible targets +#endif + // OSASCI. Write a character (to screen) from A plus LF if (A)=&0D // Input: A = Byte to write. asm void putchar(byte a) @$FFE3 extern diff --git a/include/bbcmicro.ini b/include/bbcmicro.ini index 95b4d3ec..0e65afb9 100644 --- a/include/bbcmicro.ini +++ b/include/bbcmicro.ini @@ -2,7 +2,7 @@ ; "strict" guarantees compatibility with Rockwell CPU's in some later Model B's arch=strict encoding=bbc -modules=bbc_kernal,bbc_hardware,default_panic +modules=bbc_kernal,bbc_hardware,default_panic,stdlib [allocation] @@ -13,6 +13,14 @@ segment_default_start=$0E00 ; The following is for Model B; for Model A, consider changing it to $31FF segment_default_end=$71FF +[define] +BBC_MICRO=1 +WIDESCREEN=1 +KEYBOARD=1 +; TODO: ? +JOYSTICKS=1 +HAS_BITMAP_MODE=1 + [output] style=single format=allocated diff --git a/include/c128.ini b/include/c128.ini index d4117389..0b18dd34 100644 --- a/include/c128.ini +++ b/include/c128.ini @@ -2,7 +2,7 @@ arch=nmos encoding=petscii screen_encoding=petscr -modules=c128_hardware,loader_1c01,c128_kernal,default_panic +modules=c128_hardware,loader_1c01,c128_kernal,default_panic,stdlib [allocation] @@ -11,6 +11,16 @@ zp_pointers=$FB,$FD,$43,$45,$47,$4B,$F7,$F9,$9E,$9B,$3D segment_default_start=$1C0D segment_default_end=$FEFF +[define] +CBM=1 +CBM_128=1 +CBM_64=1 +MOS_6510=1 +WIDESCREEN=1 +KEYBOARD=1 +JOYSTICKS=2 +HAS_BITMAP_MODE=1 + [output] style=single format=startaddr,allocated diff --git a/include/c128_hardware.mfk b/include/c128_hardware.mfk index ba0b81dd..3cd34be3 100644 --- a/include/c128_hardware.mfk +++ b/include/c128_hardware.mfk @@ -1,3 +1,8 @@ + +#if not(CBM_128) +#warn c128_hardware module should be only used on C128-compatible targets +#endif + import c64_vic import c64_sid import c64_cia diff --git a/include/c128_kernal.mfk b/include/c128_kernal.mfk index ac73145d..f30fbcd0 100644 --- a/include/c128_kernal.mfk +++ b/include/c128_kernal.mfk @@ -1,5 +1,9 @@ // Routines from Commodore 128 KERNAL ROM +#if not(CBM_128) +#warn c128_kernal module should be only used on C128-compatible targets +#endif + // CHROUT. Write byte to default output. (If not screen, must call OPEN and CHKOUT beforehands.) // Input: A = Byte to write. asm void putchar(byte a) @$FFD2 extern \ No newline at end of file diff --git a/include/c1531.mfk b/include/c1531.mfk index f42e8eba..0bfe83bc 100644 --- a/include/c1531.mfk +++ b/include/c1531.mfk @@ -1,7 +1,12 @@ // mouse driver for Commodore 1531 mouse on Commodore 64 +#if not(CBM_64) || not(WIDESCREEN) +#warn c1531 module should be only used on C64-compatible targets +#endif + import mouse import c64_hardware +import stdlib sbyte _c1531_calculate_delta (byte old, byte new) { byte mouse_delta @@ -53,8 +58,12 @@ byte _c1531_handle_y() { } void c1531_mouse () { - cia1_pra = ($3f & cia1_pra) | $40 _c1531_handle_x() _c1531_handle_y() -} \ No newline at end of file + byte value + poke($dc03, 0) + value = peek($dc01) + mouse_rbm = (value & 1) ^ 1 + if value & 16 == 0 { mouse_lbm = 1 } else { mouse_lbm = 0} +} diff --git a/include/c16.ini b/include/c16.ini index e0385222..c8198cdb 100644 --- a/include/c16.ini +++ b/include/c16.ini @@ -2,7 +2,7 @@ arch=nmos encoding=petscii screen_encoding=petscr -modules=loader_1001,c264_kernal,c264_hardware,default_panic +modules=loader_1001,c264_kernal,c264_hardware,default_panic,stdlib [allocation] @@ -11,6 +11,14 @@ zp_pointers=$C1,$C3,$FB,$FD,$39,$3B,$3D,$43,$4B segment_default_start=$100D segment_default_end=$3FFF +[define] +CBM=1 +CBM_264=1 +WIDESCREEN=1 +KEYBOARD=1 +JOYSTICKS=2 +HAS_BITMAP_MODE=1 + [output] style=single format=startaddr,allocated diff --git a/include/c264_hardware.mfk b/include/c264_hardware.mfk index e66fc411..c5aa3c93 100644 --- a/include/c264_hardware.mfk +++ b/include/c264_hardware.mfk @@ -1 +1,5 @@ +#if not(CBM_264) +#warn c264_hardware module should be only used on C264-compatible targets +#endif + import c264_ted \ No newline at end of file diff --git a/include/c264_kernal.mfk b/include/c264_kernal.mfk index 2f1ae990..12ef5bcc 100644 --- a/include/c264_kernal.mfk +++ b/include/c264_kernal.mfk @@ -1,4 +1,7 @@ // Routines from C16 and Plus/4 KERNAL ROM +#if not(CBM_264) +#warn c264_kernal module should be only used on C264-compatible targets +#endif // CHROUT. Write byte to default output. (If not screen, must call OPEN and CHKOUT beforehands.) // Input: A = Byte to write. diff --git a/include/c264_ted.mfk b/include/c264_ted.mfk index c41e7692..0fd6a750 100644 --- a/include/c264_ted.mfk +++ b/include/c264_ted.mfk @@ -1,3 +1,6 @@ +#if not(CBM_264) +#warn c264_ted module should be only used on C264-compatible targets +#endif const byte black = 0 const byte white = $71 diff --git a/include/c64.ini b/include/c64.ini index 56b8cdf2..be961cda 100644 --- a/include/c64.ini +++ b/include/c64.ini @@ -21,6 +21,14 @@ segment_default_codeend=$9fff segment_default_datastart=after_code segment_default_end=$cfff +[define] +CBM=1 +CBM_64=1 +MOS_6510=1 +WIDESCREEN=1 +KEYBOARD=1 +JOYSTICKS=2 +HAS_BITMAP_MODE=1 [output] ; how the banks are laid out in the output files; so far, there is no bank support in the compiler yet diff --git a/include/c64_basic.mfk b/include/c64_basic.mfk index 700d7a72..bef806dd 100644 --- a/include/c64_basic.mfk +++ b/include/c64_basic.mfk @@ -1,5 +1,9 @@ // Routines from C64 BASIC ROM +#if not(CBM_64) +#warn c64_basic module should be only used on C64-compatible targets +#endif + import c64_kernal // print a 16-bit number on the standard output diff --git a/include/c64_cia.mfk b/include/c64_cia.mfk index 85c4db6f..f93b112b 100644 --- a/include/c64_cia.mfk +++ b/include/c64_cia.mfk @@ -1,5 +1,9 @@ // Hardware addresses for C64 +#if not(CBM_64) +#warn c64_cia module should be only used on C64-compatible targets +#endif + // CIA1 byte cia1_pra @$DC00 byte cia1_prb @$DC01 @@ -37,4 +41,4 @@ macro void vic_bank_8000() { macro void vic_bank_C000() { cia2_ddra = 3 cia2_pra = 0 -} \ No newline at end of file +} diff --git a/include/c64_hardware.mfk b/include/c64_hardware.mfk index 50b493bb..957df4ac 100644 --- a/include/c64_hardware.mfk +++ b/include/c64_hardware.mfk @@ -1,3 +1,7 @@ +#if not(CBM_64) +#warn c64_hardware module should be only used on C64-compatible targets +#endif + import c64_vic import c64_sid import c64_cia @@ -38,4 +42,4 @@ macro void c64_ram_charset_kernal() { macro void c64_ram_charset_basic() { cpu6510_ddr = 7 cpu6510_port = 3 -} \ No newline at end of file +} diff --git a/include/c64_kernal.mfk b/include/c64_kernal.mfk index ed69eb0b..5efc3796 100644 --- a/include/c64_kernal.mfk +++ b/include/c64_kernal.mfk @@ -29,4 +29,4 @@ asm clear_carry load(byte a, word yx) @$FFD5 extern // Output: Carry: 0 = No errors, 1 = Error; A = KERNAL error code (if Carry = 1). asm clear_carry save(byte a, word yx) @$FFD5 extern -word irq_pointer @$314 \ No newline at end of file +word irq_pointer @$314 diff --git a/include/c64_panic.mfk b/include/c64_panic.mfk index daecdcd2..769cbf93 100644 --- a/include/c64_panic.mfk +++ b/include/c64_panic.mfk @@ -23,4 +23,4 @@ void _panic() { STA $D020 } while(true){} -} \ No newline at end of file +} diff --git a/include/c64_scpu.ini b/include/c64_scpu.ini index a96e35af..f1b57614 100644 --- a/include/c64_scpu.ini +++ b/include/c64_scpu.ini @@ -14,6 +14,15 @@ segment_default_start=$80D segment_default_codeend=$9fff segment_default_end=$cfff +[define] +CBM=1 +CBM_64=1 +MOS_6510=1 +WIDESCREEN=1 +KEYBOARD=1 +JOYSTICKS=2 +HAS_BITMAP_MODE=1 + [output] style=single format=startaddr,allocated diff --git a/include/c64_scpu16.ini b/include/c64_scpu16.ini index 98a0b366..02501682 100644 --- a/include/c64_scpu16.ini +++ b/include/c64_scpu16.ini @@ -16,6 +16,15 @@ segment_default_start=$80D segment_default_codeend=$9fff segment_default_end=$cfff +[define] +CBM=1 +CBM_64=1 +MOS_6510=1 +WIDESCREEN=1 +KEYBOARD=1 +JOYSTICKS=2 +HAS_BITMAP_MODE=1 + [output] style=single format=startaddr,allocated diff --git a/include/c64_sid.mfk b/include/c64_sid.mfk index 79cd772b..898e92cf 100644 --- a/include/c64_sid.mfk +++ b/include/c64_sid.mfk @@ -1,5 +1,9 @@ // Hardware addresses for C64 +#if not(CBM_64) +#warn c64_sid module should be only used on C64-compatible targets +#endif + // SID word sid_v1_freq @$D400 diff --git a/include/c64_vic.mfk b/include/c64_vic.mfk index 0e123bf0..30583a7a 100644 --- a/include/c64_vic.mfk +++ b/include/c64_vic.mfk @@ -1,5 +1,10 @@ // Hardware addresses for C64 +#if not(CBM_64) +#warn c64_vic module should be only used on C64-compatible targets +#endif + + // VIC-II byte vic_spr0_x @$D000 byte vic_spr0_y @$D001 diff --git a/include/cpu6510.mfk b/include/cpu6510.mfk index 90a674ca..6349b624 100644 --- a/include/cpu6510.mfk +++ b/include/cpu6510.mfk @@ -1,3 +1,6 @@ +#if not(MOS_6510) && not(CBM_64) +#warn cpu6510 module should be only used on 6510-compatible targets +#endif byte cpu6510_ddr @0 byte cpu6510_port @1 diff --git a/include/loader_0401.mfk b/include/loader_0401.mfk index 0a748c51..9e1db8ad 100644 --- a/include/loader_0401.mfk +++ b/include/loader_0401.mfk @@ -1,3 +1,7 @@ +#if not(CBM) +#warn loader_0401 module should be only used on Commodore targets +#endif + array _basic_loader @$401 = [ $0b, 4, diff --git a/include/loader_0801.mfk b/include/loader_0801.mfk index 796b8b2f..8d369804 100644 --- a/include/loader_0801.mfk +++ b/include/loader_0801.mfk @@ -1,3 +1,7 @@ +#if not(CBM) +#warn loader_0801 module should be only used on Commodore targets +#endif + array _basic_loader @$801 = [ $0b, $08, diff --git a/include/loader_0801_16bit.mfk b/include/loader_0801_16bit.mfk index a38d8296..cc306a78 100644 --- a/include/loader_0801_16bit.mfk +++ b/include/loader_0801_16bit.mfk @@ -1,3 +1,7 @@ +#if not(CBM) +#warn loader_0801_16bit module should be only used on Commodore targets +#endif + array _basic_loader @$801 = [ $0b, $08, diff --git a/include/loader_1001.mfk b/include/loader_1001.mfk index b16d858c..b1bdac12 100644 --- a/include/loader_1001.mfk +++ b/include/loader_1001.mfk @@ -1,3 +1,7 @@ +#if not(CBM) +#warn loader_1001 module should be only used on Commodore targets +#endif + array _basic_loader @$1001 = [ $0b, $10, diff --git a/include/loader_1201.mfk b/include/loader_1201.mfk index 144a3e90..e3fdece0 100644 --- a/include/loader_1201.mfk +++ b/include/loader_1201.mfk @@ -1,3 +1,7 @@ +#if not(CBM) +#warn loader_1201 module should be only used on Commodore targets +#endif + array _basic_loader @$1201 = [ $0b, $12, diff --git a/include/loader_1c01.mfk b/include/loader_1c01.mfk index 73962381..a405c45e 100644 --- a/include/loader_1c01.mfk +++ b/include/loader_1c01.mfk @@ -1,3 +1,7 @@ +#if not(CBM) +#warn loader_1c01 module should be only used on Commodore targets +#endif + array _basic_loader @$1C01 = [ $0b, $1C, diff --git a/include/lunix.ini b/include/lunix.ini index cf38e3cf..8627bde1 100644 --- a/include/lunix.ini +++ b/include/lunix.ini @@ -4,7 +4,7 @@ arch=nmos encoding=petscii screen_encoding=petscr -modules=lunix +modules=lunix,stdlib lunix=true @@ -17,6 +17,14 @@ segment_default_codeend=$8fff segment_default_datastart=after_code segment_default_end=$8fff +[define] +LUNIX=1 +CBM_64=1 +WIDESCREEN=1 +KEYBOARD=1 +; TODO: ? +JOYSTICKS=2 +HAS_BITMAP_MODE=1 [output] style=lunix diff --git a/include/lunix.mfk b/include/lunix.mfk index d6765d11..8c355a4f 100644 --- a/include/lunix.mfk +++ b/include/lunix.mfk @@ -1,3 +1,7 @@ +#if not(LUNIX) +#warn lunix module should be only used on Lunix targets +#endif + const word lkf_jumptab = $200 byte relocation_offset @$1001 diff --git a/include/mouse.mfk b/include/mouse.mfk index f4a65210..0d266d80 100644 --- a/include/mouse.mfk +++ b/include/mouse.mfk @@ -1,8 +1,16 @@ // Generic module for mouse support // Resolutions up to 512x256 are supported +import x_coord // Mouse X coordinate -word mouse_x +x_coord mouse_x + // Mouse Y coordinate byte mouse_y + +// Left mouse button pressed +byte mouse_lbm + +// Right mouse button pressed +byte mouse_rbm diff --git a/include/nes_hardware.mfk b/include/nes_hardware.mfk index 7ccdb850..d06f6c8f 100644 --- a/include/nes_hardware.mfk +++ b/include/nes_hardware.mfk @@ -1,3 +1,7 @@ +#if not(NES) +#warn nes_hardware module should be only used on NES/Famicom targets +#endif + byte ppu_ctrl @$2000 byte ppu_mask @$2001 byte ppu_status @$2002 diff --git a/include/nes_mmc4.ini b/include/nes_mmc4.ini index 1d2a52a8..70c6cc99 100644 --- a/include/nes_mmc4.ini +++ b/include/nes_mmc4.ini @@ -10,7 +10,7 @@ [compilation] arch=ricoh -modules=nes_hardware,nes_routines,default_panic,nes_mmc4 +modules=nes_hardware,nes_routines,default_panic,nes_mmc4,stdlib ro_arrays=true [allocation] @@ -55,6 +55,13 @@ segment_chrrom0_end=$ffff segment_chrrom1_start=$0000 segment_chrrom1_end=$ffff +[define] +NES=1 +WIDESCREEN=0 +KEYBOARD=0 +JOYSTICKS=2 +HAS_BITMAP_MODE=0 + [output] style=single format=$4E,$45,$53,$1A, 8,16,$A0,8, 0,0,$07,0, 2,0,0,0, prgrom0:$8000:$bfff,prgrom1:$8000:$bfff,prgrom2:$8000:$bfff,prgrom3:$8000:$bfff,prgrom4:$8000:$bfff,prgrom5:$8000:$bfff,prgrom6:$8000:$bfff,prgrom7:$c000:$ffff,chrrom0:$0000:$ffff,chrrom1:$0000:$ffff diff --git a/include/nes_mmc4.mfk b/include/nes_mmc4.mfk index 8df04216..6b0a26a1 100644 --- a/include/nes_mmc4.mfk +++ b/include/nes_mmc4.mfk @@ -1,3 +1,7 @@ +#if not(NES) +#warn nes_mmc4 module should be only used on NES/Famicom targets +#endif + asm inline void set_prg_bank(byte a) { STA $A000 ? RTS diff --git a/include/nes_routines.mfk b/include/nes_routines.mfk index 30e358bc..a9c760f8 100644 --- a/include/nes_routines.mfk +++ b/include/nes_routines.mfk @@ -1,3 +1,6 @@ +#if not(NES) +#warn nes_routines module should be only used on NES/Famicom targets +#endif asm void on_reset() { diff --git a/include/nes_small.ini b/include/nes_small.ini index f5331bc5..c53ad35f 100644 --- a/include/nes_small.ini +++ b/include/nes_small.ini @@ -6,7 +6,7 @@ [compilation] arch=ricoh -modules=nes_hardware,nes_routines,default_panic +modules=nes_hardware,nes_routines,default_panic,stdlib ro_arrays=true [allocation] @@ -24,6 +24,13 @@ segment_prgrom_end=$ffff segment_chrrom_start=$0000 segment_chrrom_end=$1fff +[define] +NES=1 +WIDESCREEN=0 +KEYBOARD=0 +JOYSTICKS=2 +HAS_BITMAP_MODE=0 + [output] style=single format=$4E,$45,$53,$1A, 2,1,0,0, 0,0,0,0, 0,0,0,0, prgrom:$8000:$ffff, chrrom:$0000:$1fff diff --git a/include/pc88.ini b/include/pc88.ini index bca7f6f6..c1cbf08b 100644 --- a/include/pc88.ini +++ b/include/pc88.ini @@ -3,7 +3,7 @@ [compilation] arch=z80 encoding=jisx -modules=default_panic +modules=default_panic,stdlib [allocation] ; TODO: find a more optimal start address @@ -12,6 +12,14 @@ segment_default_codeend=$bfff segment_default_datastart=after_code segment_default_end=$efff +[define] +NEC_PC_88=1 +WIDESCREEN=1 +KEYBOARD=1 +; TODO: +JOYSTICKS=1 +HAS_BITMAP_MODE=1 + [output] style=single format=d88 diff --git a/include/pet.ini b/include/pet.ini index 8bdf03d9..fb5175fe 100644 --- a/include/pet.ini +++ b/include/pet.ini @@ -9,6 +9,14 @@ zp_pointers=$C1,$C3,$FB,$FD,$39,$3B,$3D,$43,$4B segment_default_start=$40D segment_default_end=$FFF +[define] +CBM=1 +CBM_PET=1 +WIDESCREEN=0 +KEYBOARD=1 +JOYSTICKS=0 +HAS_BITMAP_MODE=0 + [output] style=single format=startaddr,allocated diff --git a/include/pet_kernal.mfk b/include/pet_kernal.mfk index 31d7daf8..40ceb4e3 100644 --- a/include/pet_kernal.mfk +++ b/include/pet_kernal.mfk @@ -1,5 +1,9 @@ // Routines from Commodore PET KERNAL ROM +#if not(CBM_PET) +#warn pet_kernal module should be only used on PET targets +#endif + // CHROUT. Write byte to default output. (If not screen, must call OPEN and CHKOUT beforehands.) // Input: A = Byte to write. asm void putchar(byte a) @$FFD2 extern \ No newline at end of file diff --git a/include/plus4.ini b/include/plus4.ini index 86514327..c6cb167b 100644 --- a/include/plus4.ini +++ b/include/plus4.ini @@ -2,7 +2,7 @@ arch=nmos encoding=petscii screen_encoding=petscr -modules=loader_1001,c264_kernal,c264_hardware,default_panic +modules=loader_1001,c264_kernal,c264_hardware,default_panic,stdlib [allocation] @@ -11,6 +11,14 @@ zp_pointers=$C1,$C3,$FB,$FD,$39,$3B,$3D,$43,$4B segment_default_start=$100D segment_default_end=$3FFF +[define] +CBM=1 +CBM_264=1 +WIDESCREEN=1 +KEYBOARD=1 +JOYSTICKS=2 +HAS_BITMAP_MODE=1 + [output] style=per_bank format=startaddr,allocated diff --git a/include/stdio.mfk b/include/stdio.mfk index 81eeff51..d3958453 100644 --- a/include/stdio.mfk +++ b/include/stdio.mfk @@ -1,5 +1,13 @@ // target-independent standard I/O routines +import string +#if ZX_SPECTRUM +import stdio_zxspectrum +#endif + + +#if not(ZX_SPECTRUM) + void putstr(pointer str, byte len) { byte index index = 0 @@ -8,7 +16,6 @@ void putstr(pointer str, byte len) { index += 1 } } - void putstrz(pointer str) { byte index index = 0 @@ -18,11 +25,5 @@ void putstrz(pointer str) { } } -byte strzlen(pointer str) { - byte index - index = 0 - while str[index] != 0 { - index += 1 - } - return index -} +#endif + diff --git a/include/stdio_zxspectrum.mfk b/include/stdio_zxspectrum.mfk new file mode 100644 index 00000000..07accb56 --- /dev/null +++ b/include/stdio_zxspectrum.mfk @@ -0,0 +1,31 @@ + +#if not(ZX_SPECTRUM) +#warn stdio_zxspectrum module should be only used on ZX Spectrum-compatible targets +#endif + +import stdio + +void putstr(pointer str, byte len) { + asm { + ? LD HL,(str) + ? LD D, H + ? LD E, L + ? LD A,(len) + ? LD B, 0 + ? LD C, A + CALL 8252 + } +} + +void putstrz(pointer str) { + word length = strzlen(str) + asm { + ? LD HL,(str) + ? LD D, H + ? LD E, L + ? LD HL,(length) + ? LD B, H + ? LD C, L + CALL 8252 + } +} \ No newline at end of file diff --git a/include/stdlib.mfk b/include/stdlib.mfk index 46634d8f..834426cb 100644 --- a/include/stdlib.mfk +++ b/include/stdlib.mfk @@ -1,46 +1,9 @@ // target-independent things -word nmi_routine_addr @$FFFA -word reset_routine_addr @$FFFC -word irq_routine_addr @$FFFE - -macro asm void poke(word const addr, byte a) { - STA addr -} - -macro asm byte peek(word const addr) { - ?LDA addr -} - -macro asm void disable_irq() { - SEI -} - -macro asm void enable_irq() { - CLI -} - -asm byte hi_nibble_to_hex(byte a) { - LSR - LSR - LSR - LSR - JMP lo_nibble_to_hex -} - -asm byte lo_nibble_to_hex(byte a) { - AND #$F - CLC - ADC #$30 - CMP #$3A - BCC _lo_nibble_to_hex_lbl - ADC #$6 // carry is set -_lo_nibble_to_hex_lbl: - RTS -} - -macro asm void panic() { - JSR _panic -} - -array __constant8 = [8] +#if ARCH_6502 +import stdlib_6502 +#elseif ARCH_Z80 +import stdlib_z80 +#else +#warn Unsupported architecture +#endif diff --git a/include/stdlib_6502.mfk b/include/stdlib_6502.mfk new file mode 100644 index 00000000..3e26dee7 --- /dev/null +++ b/include/stdlib_6502.mfk @@ -0,0 +1,50 @@ +// target-independent things + +#if not(ARCH_6502) +#warn stdlib_6502 module should be only used on 6502-compatible targets +#endif + +word nmi_routine_addr @$FFFA +word reset_routine_addr @$FFFC +word irq_routine_addr @$FFFE + +macro asm void poke(word const addr, byte a) { + STA addr +} + +macro asm byte peek(word const addr) { + LDA addr +} + +macro asm void disable_irq() { + SEI +} + +macro asm void enable_irq() { + CLI +} + +asm byte hi_nibble_to_hex(byte a) { + LSR + LSR + LSR + LSR + JMP lo_nibble_to_hex +} + +asm byte lo_nibble_to_hex(byte a) { + AND #$F + CLC + ADC #$30 + CMP #$3A + BCC _lo_nibble_to_hex_lbl + ADC #$6 // carry is set +_lo_nibble_to_hex_lbl: + RTS +} + +macro asm void panic() { + JSR _panic +} + +array __constant8 = [8] diff --git a/include/stdlib_z80.mfk b/include/stdlib_z80.mfk new file mode 100644 index 00000000..02e1dc20 --- /dev/null +++ b/include/stdlib_z80.mfk @@ -0,0 +1,45 @@ +// target-independent things + +#if not(ARCH_Z80) +#warn stdlib_z80 module should be only used on 6502-compatible targets +#endif + +macro asm void poke(word const addr, byte a) { + LD (addr), A +} + +macro asm byte peek(word const addr) { + LD A, (addr) +} + +macro asm void disable_irq() { + DI +} + +macro asm void enable_irq() { + EI +} + +asm byte hi_nibble_to_hex(byte a) { + SRL A + SRL A + SRL A + SRL A + JP lo_nibble_to_hex +} + +asm byte lo_nibble_to_hex(byte a) { + AND $F + ADD A,$30 + CP $3A + JR C,_lo_nibble_to_hex_lbl + ADD A,$6 // carry is set +_lo_nibble_to_hex_lbl: + RET +} + +macro asm void panic() { + CALL _panic +} + +array __constant8 = [8] diff --git a/include/string.mfk b/include/string.mfk new file mode 100644 index 00000000..8d9bba27 --- /dev/null +++ b/include/string.mfk @@ -0,0 +1,10 @@ + + +byte strzlen(pointer str) { + byte index + index = 0 + while str[index] != 0 { + index += 1 + } + return index +} \ No newline at end of file diff --git a/include/vcs.ini b/include/vcs.ini index 882b64eb..18d1d1d5 100644 --- a/include/vcs.ini +++ b/include/vcs.ini @@ -3,7 +3,7 @@ [compilation] arch=nmos -modules=vcs_hardware,default_panic +modules=vcs_hardware,default_panic,stdlib ro_arrays=true ; use -fzp-register to override this: zeropage_register=false @@ -21,6 +21,13 @@ segment_default_end=$ef segment_prgrom_start=$f000 segment_prgrom_end=$ffff +[define] +ATARI_2600=1 +WIDESCREEN=0 +KEYBOARD=0 +JOYSTICKS=2 +HAS_BITMAP_MODE=0 + [output] style=single format=prgrom:$f000:$ffff diff --git a/include/vcs_hardware.mfk b/include/vcs_hardware.mfk index d16f6949..5af1ef4d 100644 --- a/include/vcs_hardware.mfk +++ b/include/vcs_hardware.mfk @@ -1,3 +1,8 @@ + +#if not(ATARI_2600) +#warn vcs_hardware module should be only used on Atari 2600-compatible targets +#endif + byte VSYNC @$00 byte VBLANK @$01 byte WSYNC @$02 diff --git a/include/vic20.ini b/include/vic20.ini index be3a3e4b..12799477 100644 --- a/include/vic20.ini +++ b/include/vic20.ini @@ -2,7 +2,7 @@ arch=nmos encoding=petscii screen_encoding=petscr -modules=loader_1001,vic20_kernal,default_panic +modules=loader_1001,vic20_kernal,default_panic,stdlib [allocation] @@ -11,6 +11,14 @@ zp_pointers=$C1,$C3,$FB,$FD,$39,$3B,$3D,$43,$4B segment_default_start=$100D segment_default_end=$1CFF +[define] +CBM=1 +CBM_VIC=1 +WIDESCREEN=0 +KEYBOARD=1 +JOYSTICKS=1 +HAS_BITMAP_MODE=1 + [output] style=single format=startaddr,allocated diff --git a/include/vic20_3k.ini b/include/vic20_3k.ini index 2d78a439..ea574329 100644 --- a/include/vic20_3k.ini +++ b/include/vic20_3k.ini @@ -2,7 +2,7 @@ arch=nmos encoding=petscii screen_encoding=petscr -modules=loader_0401,vic20_kernal,default_panic +modules=loader_0401,vic20_kernal,default_panic,stdlib [allocation] @@ -11,6 +11,14 @@ zp_pointers=$C1,$C3,$FB,$FD,$39,$3B,$3D,$43,$4B segment_default_start=$40D segment_default_end=$1CFF +[define] +CBM=1 +CBM_VIC=1 +WIDESCREEN=0 +KEYBOARD=1 +JOYSTICKS=1 +HAS_BITMAP_MODE=1 + [output] style=single format=startaddr,allocated diff --git a/include/vic20_8k.ini b/include/vic20_8k.ini index bfdf0492..22c8e4aa 100644 --- a/include/vic20_8k.ini +++ b/include/vic20_8k.ini @@ -2,7 +2,7 @@ arch=nmos encoding=petscii screen_encoding=petscr -modules=loader_1201,vic20_kernal,default_panic +modules=loader_1201,vic20_kernal,default_panic,stdlib [allocation] @@ -11,6 +11,14 @@ zp_pointers=$C1,$C3,$FB,$FD,$39,$3B,$3D,$43,$4B segment_default_start=$120D segment_default_end=$1FFF +[define] +CBM=1 +CBM_VIC=1 +WIDESCREEN=0 +KEYBOARD=1 +JOYSTICKS=1 +HAS_BITMAP_MODE=1 + [output] style=single format=startaddr,allocated diff --git a/include/vic20_kernal.mfk b/include/vic20_kernal.mfk index 2f1ae990..53ba2f01 100644 --- a/include/vic20_kernal.mfk +++ b/include/vic20_kernal.mfk @@ -1,5 +1,9 @@ // Routines from C16 and Plus/4 KERNAL ROM +#if not(CBM_VIC) +#warn vic20_kernal module should be only used on VIC-20 targets +#endif + // CHROUT. Write byte to default output. (If not screen, must call OPEN and CHKOUT beforehands.) // Input: A = Byte to write. asm void putchar(byte a) @$FFD2 extern \ No newline at end of file diff --git a/include/x_coord.mfk b/include/x_coord.mfk new file mode 100644 index 00000000..95caa9e4 --- /dev/null +++ b/include/x_coord.mfk @@ -0,0 +1,6 @@ + +#if WIDESCREEN +alias x_coord = word +#else +alias x_coord = byte +#endif \ No newline at end of file diff --git a/include/zp_reg.mfk b/include/zp_reg.mfk index c0906359..83ff1b30 100644 --- a/include/zp_reg.mfk +++ b/include/zp_reg.mfk @@ -1,4 +1,8 @@ +#if not(ARCH_6502) +#warn zp_reg module should be used only on 6502-compatible targets +#endif + inline asm byte __mul_u8u8u8() { ? LDA #0 ? JMP __mul_u8u8u8_start diff --git a/include/zxspectrum.ini b/include/zxspectrum.ini index 9812e78f..918d17ea 100644 --- a/include/zxspectrum.ini +++ b/include/zxspectrum.ini @@ -3,7 +3,7 @@ [compilation] arch=z80 encoding=bbc -modules=default_panic,zxspectrum +modules=default_panic,zxspectrum,stdlib [allocation] segments=default,slowram @@ -13,6 +13,14 @@ segment_default_end=$ffff segment_slowram_start=$5ccb segment_slowram_end=$7fff +[define] +ZX_SPECTRUM=1 +WIDESCREEN=0 +KEYBOARD=1 +; TODO: ? +JOYSTICKS=1 +HAS_BITMAP_MODE=1 + [output] style=single format=tap diff --git a/include/zxspectrum.mfk b/include/zxspectrum.mfk index 2b35c9ed..c9f2bf1c 100644 --- a/include/zxspectrum.mfk +++ b/include/zxspectrum.mfk @@ -1,4 +1,23 @@ + +#if not(ZX_SPECTRUM) +#warn zxspectrum module should be only used on ZX Spectrum-compatible targets +#endif + inline asm void putchar(byte a) { rst $10 ? ret } + +inline asm void set_border(byte a) { + out (254),a + ? ret +} + +const byte black = 0 +const byte blue = 1 +const byte red = 2 +const byte purple = 3 +const byte green = 4 +const byte cyan = 5 +const byte yellow = 6 +const byte white = 7 diff --git a/src/main/scala/millfork/CompilationOptions.scala b/src/main/scala/millfork/CompilationOptions.scala index 452dc603..7a88ab77 100644 --- a/src/main/scala/millfork/CompilationOptions.scala +++ b/src/main/scala/millfork/CompilationOptions.scala @@ -116,7 +116,7 @@ case class CompilationOptions(platform: Platform, commandLineFlags: Map[Compilat } object CpuFamily extends Enumeration { - val M6502, I80, M6809, I8086, M65K, ARM = Value + val M6502, I80, M6809, I86, M68K, ARM = Value def forType(cpu: Cpu.Value): CpuFamily.Value = { import Cpu._ diff --git a/src/main/scala/millfork/Main.scala b/src/main/scala/millfork/Main.scala index 926511ec..eec00f07 100644 --- a/src/main/scala/millfork/Main.scala +++ b/src/main/scala/millfork/Main.scala @@ -30,6 +30,7 @@ case class Context(inputFileNames: List[String], outputLabels: Boolean = false, includePath: List[String] = Nil, flags: Map[CompilationFlag.Value, Boolean] = Map(), + features: Map[String, Long] = Map(), verbosity: Option[Int] = None) { def changeFlag(f: CompilationFlag.Value, b: Boolean): Context = { if (flags.contains(f)) { @@ -73,7 +74,7 @@ object Main { val platform = Platform.lookupPlatformFile(c.includePath, c.platform.getOrElse { ErrorReporting.info("No platform selected, defaulting to `c64`") "c64" - }) + }, c.features) val options = CompilationOptions(platform, c.flags, c.outputFileName, c.zpRegisterSize.getOrElse(platform.zpRegisterSize)) ErrorReporting.debug("Effective flags: ") options.flags.toSeq.sortBy(_._1).foreach{ @@ -258,6 +259,21 @@ object Main { c.copy(runFileName = Some(p)) }.description("Program to run after successful compilation.") + parameter("-D", "--define").placeholder("=").action { (p, c) => + val tokens = p.split('=') + if (tokens.length == 2) { + assertNone(c.features.get(tokens(0)), "Feature already defined") + try { + c.copy(features = c.features + (tokens(0) -> tokens(1).toLong)) + } catch { + case _:java.lang.NumberFormatException => + ErrorReporting.fatal("Invalid syntax for -D option") + } + } else { + ErrorReporting.fatal("Invalid syntax for -D option") + } + }.description("Define a feature value for the preprocessor.") + endOfFlags("--").description("Marks the end of options.") fluff("", "Verbosity options:", "") diff --git a/src/main/scala/millfork/Platform.scala b/src/main/scala/millfork/Platform.scala index c1ee36a0..2e2a87fa 100644 --- a/src/main/scala/millfork/Platform.scala +++ b/src/main/scala/millfork/Platform.scala @@ -23,6 +23,7 @@ class Platform( val startingModules: List[String], val defaultCodec: TextCodec, val screenCodec: TextCodec, + val features: Map[String, Long], val outputPackager: OutputPackager, val codeAllocators: Map[String, UpwardByteAllocator], val variableAllocators: Map[String, VariableAllocator], @@ -41,18 +42,18 @@ class Platform( object Platform { - def lookupPlatformFile(includePath: List[String], platformName: String): Platform = { + def lookupPlatformFile(includePath: List[String], platformName: String, featureOverrides: Map[String, Long]): Platform = { includePath.foreach { dir => val file = Paths.get(dir, platformName + ".ini").toFile ErrorReporting.debug("Checking " + file) if (file.exists()) { - return load(file) + return load(file, featureOverrides) } } ErrorReporting.fatal(s"Platfom definition `$platformName` not found", None) } - def load(file: File): Platform = { + def load(file: File, featureOverrides: Map[String, Long]): Platform = { val conf = new INIConfiguration() val bytes = Files.readAllBytes(file.toPath) conf.read(new StringReader(new String(bytes, StandardCharsets.UTF_8))) @@ -193,12 +194,26 @@ object Platform { case x => ErrorReporting.fatal(s"Invalid output style: `$x`") } + val builtInFeatures = builtInCpuFeatures(cpu) + + import scala.collection.JavaConverters._ + val ds = conf.getSection("define") + val definedFeatures = ds.getKeys().asScala.toList.map { k => + val value = ds.get(classOf[String], k).trim() match { + case "true" | "on" | "yes" => 1L + case "false" | "off" | "no" | "" => 0L + case x => x.toLong + } + k -> value + }.toMap + new Platform( cpu, flagOverrides, startingModules, codec, srcCodec, + builtInFeatures ++ definedFeatures ++ featureOverrides, outputPackager, codeAllocators.toMap, variableAllocators.toMap, @@ -211,6 +226,25 @@ object Platform { outputStyle) } + @inline + private def toLong(b: Boolean): Long = if (b) 1L else 0L + + def builtInCpuFeatures(cpu: Cpu.Value): Map[String, Long] = { + Map[String, Long]( + "ARCH_6502" -> toLong(CpuFamily.forType(cpu) == CpuFamily.M6502), + "ARCH_Z80" -> toLong(CpuFamily.forType(cpu) == CpuFamily.I80), + "ARCH_X86" -> toLong(CpuFamily.forType(cpu) == CpuFamily.I86), + "ARCH_6509" -> toLong(CpuFamily.forType(cpu) == CpuFamily.M6809), + "ARCH_ARM" -> toLong(CpuFamily.forType(cpu) == CpuFamily.ARM), + "ARCH_68K" -> toLong(CpuFamily.forType(cpu) == CpuFamily.M68K), + "HAS_HARDWARE_MULTIPLY" -> (CpuFamily.forType(cpu) match { + case CpuFamily.M6502 | CpuFamily.I80 | CpuFamily.M6809 => 0L + case CpuFamily.I86 | CpuFamily.ARM | CpuFamily.M68K => 1L + }) + // TODO + ) + } + def parseNumberOrRange(s:String): Seq[Int] = { if (s.contains("-")) { val segments = s.split("-") diff --git a/src/main/scala/millfork/compiler/z80/Z80BulkMemoryOperations.scala b/src/main/scala/millfork/compiler/z80/Z80BulkMemoryOperations.scala index b55bf040..b1c4098d 100644 --- a/src/main/scala/millfork/compiler/z80/Z80BulkMemoryOperations.scala +++ b/src/main/scala/millfork/compiler/z80/Z80BulkMemoryOperations.scala @@ -378,7 +378,7 @@ object Z80BulkMemoryOperations { } Z80StatementCompiler.compile(ctx, IfStatement( FunctionCallExpression(operator, List(f.start, f.end)), - List(Z80AssemblyStatement(ZOpcode.NOP, NoRegisters, LiteralExpression(0, 1), elidable = false)), + List(Z80AssemblyStatement(ZOpcode.NOP, NoRegisters, None, LiteralExpression(0, 1), elidable = false)), Nil)) } diff --git a/src/main/scala/millfork/compiler/z80/Z80StatementCompiler.scala b/src/main/scala/millfork/compiler/z80/Z80StatementCompiler.scala index 53323371..30348aaf 100644 --- a/src/main/scala/millfork/compiler/z80/Z80StatementCompiler.scala +++ b/src/main/scala/millfork/compiler/z80/Z80StatementCompiler.scala @@ -141,14 +141,35 @@ object Z80StatementCompiler extends AbstractStatementCompiler[ZLine] { } case ExpressionStatement(e) => Z80ExpressionCompiler.compile(ctx, e, ZExpressionTarget.NOTHING) - case Z80AssemblyStatement(op, reg, expression, elidable) => + case Z80AssemblyStatement(op, reg, offset, expression, elidable) => val param = ctx.env.evalForAsm(expression) match { case Some(v) => v case None => ErrorReporting.error("Inlining failed due to non-constant things", expression.position) Constant.Zero } - List(ZLine(op, reg, param, elidable)) + val registers = (reg, offset) match { + case (OneRegister(r), Some(o)) => ctx.env.evalForAsm(expression) match { + case Some(NumericConstant(v, _)) => OneRegisterOffset(r, v.toInt) + case Some(_) => + ErrorReporting.error("Non-numeric constant", o.position) + reg + case None => + ErrorReporting.error("Inlining failed due to non-constant things", o.position) + reg + } + case (TwoRegisters(t, s), Some(o)) => ctx.env.evalForAsm(expression) match { + case Some(NumericConstant(v, _)) => TwoRegistersOffset(t, s, v.toInt) + case Some(_) => + ErrorReporting.error("Non-numeric constant", o.position) + reg + case None => + ErrorReporting.error("Inlining failed due to non-constant things", o.position) + reg + } + case _ => reg + } + List(ZLine(op, registers, param, elidable)) } } diff --git a/src/main/scala/millfork/node/Node.scala b/src/main/scala/millfork/node/Node.scala index 1a5e5935..f920c68a 100644 --- a/src/main/scala/millfork/node/Node.scala +++ b/src/main/scala/millfork/node/Node.scala @@ -298,7 +298,7 @@ case class MosAssemblyStatement(opcode: Opcode.Value, addrMode: AddrMode.Value, override def getAllExpressions: List[Expression] = List(expression) } -case class Z80AssemblyStatement(opcode: ZOpcode.Value, registers: ZRegisters, expression: Expression, elidable: Boolean) extends ExecutableStatement { +case class Z80AssemblyStatement(opcode: ZOpcode.Value, registers: ZRegisters, offsetExpression: Option[Expression], expression: Expression, elidable: Boolean) extends ExecutableStatement { override def getAllExpressions: List[Expression] = List(expression) } diff --git a/src/main/scala/millfork/output/Z80Assembler.scala b/src/main/scala/millfork/output/Z80Assembler.scala index af7d5d6b..1963b558 100644 --- a/src/main/scala/millfork/output/Z80Assembler.scala +++ b/src/main/scala/millfork/output/Z80Assembler.scala @@ -183,6 +183,23 @@ class Z80Assembler(program: Program, writeByte(bank, index, 0x40 + internalRegisterIndex(source) + internalRegisterIndex(target) * 8) index + 1 } + case ZLine(IN_IMM, OneRegister(ZRegister.A), param, _) => + writeByte(bank, index, 0xdb) + writeByte(bank, index + 1, param) + index + 2 + case ZLine(IN_C, OneRegister(reg), param, _) => + writeByte(bank, index, 0xed) + writeByte(bank, index + 1, 0x40 + 8 * internalRegisterIndex(reg)) + index + 2 + case ZLine(OUT_IMM, OneRegister(ZRegister.A), param, _) => + writeByte(bank, index, 0xd3) + writeByte(bank, index + 1, param) + index + 2 + case ZLine(OUT_C, OneRegister(reg), param, _) => + writeByte(bank, index, 0xed) + writeByte(bank, index + 1, 0x41 + 8 * internalRegisterIndex(reg)) + index + 2 + case ZLine(CALL, NoRegisters, param, _) => writeByte(bank, index, 0xcd) writeWord(bank, index + 1, param) diff --git a/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala b/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala index f0a043f3..4905375f 100644 --- a/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala +++ b/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala @@ -1,5 +1,6 @@ package millfork.parser +import java.nio.charset.StandardCharsets import java.nio.file.{Files, Paths} import fastparse.core.Parsed.{Failure, Success} @@ -8,6 +9,7 @@ import millfork.error.ErrorReporting import millfork.node.{ImportStatement, Position, Program} import scala.collection.mutable +import scala.collection.convert.ImplicitConversionsToScala._ abstract class AbstractSourceLoadingQueue[T](val initialFilenames: List[String], val includePath: List[String], @@ -49,15 +51,15 @@ abstract class AbstractSourceLoadingQueue[T](val initialFilenames: List[String], ErrorReporting.fatal(s"Module `$moduleName` not found", position) } - def createParser(filename: String, src: String, parentDir: String) : MfParser[T] + def createParser(filename: String, src: String, parentDir: String, featureConstants: Map[String, Long]) : MfParser[T] def parseModule(moduleName: String, includePath: List[String], why: Either[Option[Position], String]): Unit = { val filename: String = why.fold(p => lookupModuleFile(includePath, moduleName, p), s => s) ErrorReporting.debug(s"Parsing $filename") val path = Paths.get(filename) val parentDir = path.toFile.getAbsoluteFile.getParent - val src = new String(Files.readAllBytes(path)) - val parser = createParser(filename, src, parentDir) + val (src, featureConstants) = Preprocessor(options, Files.readAllLines(path, StandardCharsets.UTF_8).toIndexedSeq) + val parser = createParser(filename, src, parentDir, featureConstants) parser.toAst match { case Success(prog, _) => parsedModules.synchronized { diff --git a/src/main/scala/millfork/parser/MfParser.scala b/src/main/scala/millfork/parser/MfParser.scala index 2d7f0b9b..5029b701 100644 --- a/src/main/scala/millfork/parser/MfParser.scala +++ b/src/main/scala/millfork/parser/MfParser.scala @@ -1,5 +1,6 @@ package millfork.parser +import java.lang.Long.parseLong import java.nio.file.{Files, Paths} import java.util @@ -12,7 +13,7 @@ import millfork.{CompilationFlag, CompilationOptions, SeparatedList} /** * @author Karol Stasiak */ -abstract class MfParser[T](filename: String, input: String, currentDirectory: String, options: CompilationOptions) { +abstract class MfParser[T](filename: String, input: String, currentDirectory: String, options: CompilationOptions, featureConstants: Map[String, Long]) { import MfParser._ @@ -62,6 +63,14 @@ abstract class MfParser[T](filename: String, input: String, currentDirectory: St } } + //noinspection NameBooleanParameters + val variableAtom: P[Expression] = identifier.map{ i => + featureConstants.get(i) match { + case Some(value) => LiteralExpression(value, size(value, false ,false, false)) + case None => VariableExpression(i) + } + } + val literalAtom: P[LiteralExpression] = charAtom | binaryAtom | hexAtom | octalAtom | quaternaryAtom | decimalAtom val atom: P[Expression] = P(position() ~ (literalAtom | variableAtom)).map{case (p,a) => a.pos(p)} @@ -108,6 +117,12 @@ abstract class MfParser[T](filename: String, input: String, currentDirectory: St asmExpression.map(_ -> false) )).map { case (p, e) => e._1.pos(p) -> e._2 } + def asmExpressionWithParensOrApostrophe: P[(Expression, Boolean)] = (position() ~ NoCut( + ("(" ~ HWS ~ asmExpression ~ HWS ~ ")").map(_ -> true) | + (asmExpression ~ "'").map(_ -> true) | + asmExpression.map(_ -> false) + )).map { case (p, e) => e._1.pos(p) -> e._2 } + def asmParamDefinition: P[ParameterDeclaration] def arrayListElement: P[ArrayContents] = arrayStringContents | arrayLoopContents | arrayFileContents | mfExpression(nonStatementLevel).map(e => LiteralContents(List(e))) @@ -400,14 +415,14 @@ object MfParser { val doubleQuotedString: P[List[Char]] = P("\"" ~/ CharsWhile(c => c != '\"' && c != '\n' && c != '\r').! ~ "\"").map(_.toList) - def size(value: Int, wordLiteral: Boolean, farwordLiteral: Boolean, longLiteral: Boolean): Int = { + def size(value: Long, wordLiteral: Boolean, farwordLiteral: Boolean, longLiteral: Boolean): Int = { val w = value > 255 || value < -0x80 || wordLiteral val f = value > 0xffff || value < -0x8000 || farwordLiteral val l = value > 0xffffff || value < -0x800000 || longLiteral if (l) 4 else if (f) 3 else if (w) 2 else 1 } - def sign(abs: Int, minus: Boolean): Int = if (minus) -abs else abs + def sign(abs: Long, minus: Boolean): Long = if (minus) -abs else abs val invalidCharLiteralTypes: Set[Int] = Set[Int]( Character.LINE_SEPARATOR, @@ -422,7 +437,7 @@ object MfParser { minus <- "-".!.? s <- CharsWhileIn("1234567890", min = 1).!.opaque("") ~ !("x" | "b") } yield { - val abs = Integer.parseInt(s, 10) + val abs = parseLong(s, 10) val value = sign(abs, minus.isDefined) LiteralExpression(value, size(value, s.length > 3, s.length > 5, s.length > 7)) } @@ -433,7 +448,7 @@ object MfParser { _ <- P("0b" | "%") ~/ Pass s <- CharsWhileIn("01", min = 1).!.opaque("") } yield { - val abs = Integer.parseInt(s, 2) + val abs = parseLong(s, 2) val value = sign(abs, minus.isDefined) LiteralExpression(value, size(value, s.length > 8, s.length > 16, s.length > 24)) } @@ -444,7 +459,7 @@ object MfParser { _ <- P("0x" | "0X" | "$") ~/ Pass s <- CharsWhileIn("1234567890abcdefABCDEF", min = 1).!.opaque("") } yield { - val abs = Integer.parseInt(s, 16) + val abs = parseLong(s, 16) val value = sign(abs, minus.isDefined) LiteralExpression(value, size(value, s.length > 2, s.length > 4, s.length > 6)) } @@ -455,7 +470,7 @@ object MfParser { _ <- P("0o" | "0O") ~/ Pass s <- CharsWhileIn("01234567", min = 1).!.opaque("") } yield { - val abs = Integer.parseInt(s, 8) + val abs = parseLong(s, 8) val value = sign(abs, minus.isDefined) LiteralExpression(value, size(value, s.length > 3, s.length > 6, s.length > 9)) } @@ -466,13 +481,11 @@ object MfParser { _ <- P("0q" | "0Q") ~/ Pass s <- CharsWhileIn("0123", min = 1).!.opaque("") } yield { - val abs = Integer.parseInt(s, 4) + val abs = parseLong(s, 4) val value = sign(abs, minus.isDefined) LiteralExpression(value, size(value, s.length > 4, s.length > 8, s.length > 12)) } - val variableAtom: P[VariableExpression] = identifier.map(VariableExpression) - val mfOperators = List( List("+=", "-=", "+'=", "-'=", "^=", "&=", "|=", "*=", "*'=", "<<=", ">>=", "<<'=", ">>'="), List("||", "^^"), diff --git a/src/main/scala/millfork/parser/MosParser.scala b/src/main/scala/millfork/parser/MosParser.scala index 4b0f567a..8d5dd710 100644 --- a/src/main/scala/millfork/parser/MosParser.scala +++ b/src/main/scala/millfork/parser/MosParser.scala @@ -10,7 +10,7 @@ import millfork.CompilationOptions /** * @author Karol Stasiak */ -case class MosParser(filename: String, input: String, currentDirectory: String, options: CompilationOptions) extends MfParser[AssemblyLine](filename, input, currentDirectory, options) { +case class MosParser(filename: String, input: String, currentDirectory: String, options: CompilationOptions, featureConstants: Map[String, Long]) extends MfParser[AssemblyLine](filename, input, currentDirectory, options, featureConstants) { import MfParser._ @@ -67,7 +67,7 @@ case class MosParser(filename: String, input: String, currentDirectory: String, val asmStatement: P[ExecutableStatement] = (position("assembly statement") ~ P(asmLabel | asmMacro | arrayContentsForAsm | asmInstruction)).map { case (p, s) => s.pos(p) } // TODO: macros - val appcSimple: P[ParamPassingConvention] = P("xy" | "yx" | "ax" | "ay" | "xa" | "ya" | "stack" | "a" | "x" | "y").!.map { + val appcSimple: P[ParamPassingConvention] = P(("xy" | "yx" | "ax" | "ay" | "xa" | "ya" | "stack" | "a" | "x" | "y") ~ !letterOrDigit).!.map { case "xy" => ByMosRegister(MosRegister.XY) case "yx" => ByMosRegister(MosRegister.YX) case "ax" => ByMosRegister(MosRegister.AX) diff --git a/src/main/scala/millfork/parser/MosSourceLoadingQueue.scala b/src/main/scala/millfork/parser/MosSourceLoadingQueue.scala index 3ea0868e..1e0112b6 100644 --- a/src/main/scala/millfork/parser/MosSourceLoadingQueue.scala +++ b/src/main/scala/millfork/parser/MosSourceLoadingQueue.scala @@ -10,7 +10,8 @@ class MosSourceLoadingQueue(initialFilenames: List[String], includePath: List[String], options: CompilationOptions) extends AbstractSourceLoadingQueue[AssemblyLine](initialFilenames, includePath, options) { - override def createParser(filename: String, src: String, parentDir: String): MfParser[AssemblyLine] = MosParser(filename, src, parentDir, options) + override def createParser(filename: String, src: String, parentDir: String, featureConstants: Map[String, Long]): MfParser[AssemblyLine] = + MosParser(filename, src, parentDir, options, featureConstants) def enqueueStandardModules(): Unit = { if (options.zpRegisterSize > 0) { diff --git a/src/main/scala/millfork/parser/Preprocessor.scala b/src/main/scala/millfork/parser/Preprocessor.scala new file mode 100644 index 00000000..62f8c405 --- /dev/null +++ b/src/main/scala/millfork/parser/Preprocessor.scala @@ -0,0 +1,243 @@ +package millfork.parser + +import fastparse.core.Parsed.{Failure, Success} +import millfork.{CompilationOptions, Platform, SeparatedList} +import millfork.error.ErrorReporting +import millfork.node.Position + +import scala.collection.mutable + +/** + * @author Karol Stasiak + */ +object Preprocessor { + + private val Regex = raw"\A\s*#\s*([a-z]+)\s*(.*?)\s*\z".r + + def preprocessForTest(options: CompilationOptions, code: String): (String, Map[String, Long]) = { + apply(options, code.lines.toSeq) + } + + case class IfContext(hadEnabled: Boolean, hadElse: Boolean, enabledBefore: Boolean) + + def apply(options: CompilationOptions, lines: Seq[String]): (String, Map[String, Long]) = { + val platform = options.platform +// if (ErrorReporting.traceEnabled) { +// platform.features.foreach{ +// case (k, v) => ErrorReporting.trace(f"#define $k%15s $v%d") +// } +// } + val result = mutable.ListBuffer[String]() + val featureConstants = mutable.Map[String, Long]() + var enabled = true + val ifStack = mutable.Stack[IfContext]() + var lineNo = 0 + + def evalParam(param: String, pos: Some[Position]): Long = { + PreprocessorParser.expression.parse(param) match { + case Success(q, _) => + val value = q.apply(platform.features).getOrElse(0L) +// ErrorReporting.trace(param + " ===> " + value) + value + case Failure(_, _, _) => + ErrorReporting.error("Failed to parse expression", pos) + 0L + } + } + + for (line <- lines) { + lineNo += 1 + var resulting = "" + line match { + case Regex(keyword, param) => + val pos = Some(Position("", lineNo, 0, 0)) + keyword match { + case "use" => if (enabled) { + if (param == "") ErrorReporting.error("#use should have a parameter", pos) + featureConstants += param -> platform.features.getOrElse(param, { + ErrorReporting.warn(s"Undefined parameter $param, assuming 0", options, pos) + 0L + }) + } + case "fatal" => if (enabled) ErrorReporting.fatal(param, pos) + case "error" => if (enabled) ErrorReporting.error(param, pos) + case "warn" => if (enabled) ErrorReporting.warn(param, options, pos) + case "info" => if (enabled) ErrorReporting.info(param, pos) + case "if" => + if (enabled) { + val value = evalParam(param, pos) + enabled = value != 0 + ifStack.push(IfContext(enabled, false, true)) + } else { + ifStack.push(IfContext(false, false, false)) + } + case "endif" => + if (param != "") ErrorReporting.error("#endif shouldn't have a parameter", pos) + if (ifStack.isEmpty) ErrorReporting.error("Unmatched #endif", pos) + else { + enabled = ifStack.pop().enabledBefore + } + case "elseif" => + val skippingDepth = ifStack.top + if (ifStack.isEmpty) ErrorReporting.error("Unmatched #elseif", pos) + else { + val i = ifStack.top + if (i.hadElse) ErrorReporting.error("#elseif after #else", pos) + if (i.hadEnabled) { + enabled = false + } else { + val value = evalParam(param, pos) + enabled = value != 0 + ifStack.push(ifStack.pop().copy(hadEnabled = enabled)) + } + } + case "else" => + if (param != "") ErrorReporting.error("#else shouldn't have a parameter", pos) + if (ifStack.isEmpty) ErrorReporting.error("Unmatched #else", pos) + else { + val i = ifStack.top + if (i.hadElse) ErrorReporting.error("Duplicate #else", pos) + if (i.hadEnabled) { + enabled = false + } else { + enabled = !enabled + } + ifStack.push(ifStack.pop().copy(hadEnabled = true, hadElse = true)) + } + case _ => + ErrorReporting.error("Invalid preprocessor directive: #" + keyword, pos) + + } + case _ => if (enabled) resulting = line + } + result += resulting + } + if (ifStack.nonEmpty) { + ErrorReporting.error("Unclosed #if") + } +// if (ErrorReporting.traceEnabled) { +// result.zipWithIndex.foreach { +// case (line, i) => ErrorReporting.trace(f"${i + 1}%-4d $line%s") +// } +// } + (result.mkString("\n"), featureConstants.toMap) + } + + +} + +object PreprocessorParser { + + import fastparse.all._ + import MfParser.{HWS, identifier, nonStatementLevel, mfOperators} + + type M = Map[String, Long] + type Q = M => Option[Long] + val alwaysNone: M => Option[Long] = (_: M) => None + + val literalAtom: P[Q] = (MfParser.binaryAtom | MfParser.hexAtom | MfParser.octalAtom | MfParser.quaternaryAtom | MfParser.decimalAtom).map(l => _ => Some(l.value)) + + val variableAtom: P[Q] = identifier.map(k => _.get(k)) + + val atom: P[Q] = P(literalAtom | variableAtom) + + def mfParenExpr: P[Q] = P("(" ~/ HWS ~/ mfExpression(nonStatementLevel) ~ HWS ~/ ")") + + def functionCall: P[Q] = for { + name <- identifier + params <- HWS ~ "(" ~/ HWS ~/ mfExpression(nonStatementLevel).rep(min = 0, sep = HWS ~ "," ~/ HWS) ~ HWS ~/ ")" ~/ "" + } yield (name, params.toList) match { + case ("defined", List(p)) => {m:M => Some(if (p(m).isDefined) 1 else 0)} + case ("not", List(p)) => {m:M => Some(if (p(m).getOrElse(0L) == 0) 1 else 0)} + case ("lo", List(p)) => {m:M => Some(p(m).getOrElse(0L) & 0xff)} + case ("hi", List(p)) => {m:M => Some(p(m).getOrElse(0L).>>(8).&(0xff))} + case ("defined" | "lo" | "hi" | "not", ps) => + ErrorReporting.error(s"Invalid number of parameters to $name: ${ps.length}") + alwaysNone + case _ => + ErrorReporting.error("Invalid preprocessor function " + name) + alwaysNone + } + + + def tightMfExpression: P[Q] = P(mfParenExpr | functionCall | atom) // TODO + + def tightMfExpressionButNotCall: P[Q] = P(mfParenExpr | atom) // TODO + + def expression: P[Q] = Start ~ HWS ~/ mfExpression(nonStatementLevel) ~ HWS ~ End + + def mfExpression(level: Int): P[Q] = { + val allowedOperators = MfParser.mfOperatorsDropFlatten(level) + + def inner: P[SeparatedList[Q, String]] = { + for { + head <- tightMfExpression ~/ HWS + maybeOperator <- StringIn(allowedOperators: _*).!.? + maybeTail <- maybeOperator.fold[P[Option[List[(String, Q)]]]](Pass.map(_ => None))(o => (HWS ~/ inner ~/ HWS).map(x2 => Some((o -> x2.head) :: x2.tail))) + } yield { + maybeTail.fold[SeparatedList[Q, String]](SeparatedList.of(head))(t => SeparatedList(head, t)) + } + } + + def arithOp(xs:SeparatedList[SeparatedList[Q, String], String], level: Int, f: (Long, Long) => Long): Q = { + m:M => Some(xs.items.map{value => p(value, level + 1)(m).getOrElse(0L)}.reduce(f)) + } + + def boolOp(xs:SeparatedList[SeparatedList[Q, String], String], level: Int, f: (Boolean, Boolean) => Boolean): Q = { + m:M => { + val b = xs.items.map { value => p(value, level + 1)(m).getOrElse(0L) }.map(_ != 0).reduce(f) + Some(if (b) 1L else 0L) + } + } + + def compOp(xs:SeparatedList[SeparatedList[Q, String], String], level: Int, f: (Long, Long) => Boolean): Q = { + m:M => { + val values = xs.items.map { value => p(value, level + 1)(m).getOrElse(0L) } + Some(if (values.init.zip(values.tail).forall(f.tupled)) 1L else 0L) + } + } + + def p(list: SeparatedList[Q, String], level: Int): Q = + if (level == mfOperators.length) list.head + else { + val xs = list.split(mfOperators(level).toSet(_)) + xs.separators.distinct match { + case Nil => + if (xs.tail.nonEmpty) + ErrorReporting.error("Too many different operators") + p(xs.head, level + 1) + case List("+") | List("-") | List("+", "-") | List("-", "+") => + val tuples = xs.toPairList("+") + m:M => Some(tuples.map { + case ("+", value) => p(value, level + 1)(m).getOrElse(0L) + case ("-", value) => -p(value, level + 1)(m).getOrElse(0L) + case _ => 0L + }.sum) + case List("+'") | List("-'") | List("+'", "-'") | List("-'", "+'") => ??? + case List(":") => ??? + case List("&&") => boolOp(xs, level, _ && _) + case List("||") => boolOp(xs, level, _ || _) + case List("&") => arithOp(xs, level, _ & _) + case List("|") => arithOp(xs, level, _ | _) + case List("^") => arithOp(xs, level, _ ^ _) + case List("*") => arithOp(xs, level, _ * _) + case List("<<") => arithOp(xs, level, _ << _) + case List(">>") => arithOp(xs, level, _ >> _) + case List("==") => compOp(xs, level, _ == _) + case List("!=") => compOp(xs, level, _ != _) + case List("<") => compOp(xs, level, _ < _) + case List(">") => compOp(xs, level, _ > _) + case List("<=") => compOp(xs, level, _ <= _) + case List(">=") => compOp(xs, level, _ >= _) + case List(x) => + ErrorReporting.error("Unsupported operator " + x) + alwaysNone + case _ => + ErrorReporting.error("Too many different operators") + alwaysNone + } + } + + inner.map(x => p(x, 0)) + } +} \ No newline at end of file diff --git a/src/main/scala/millfork/parser/Z80Parser.scala b/src/main/scala/millfork/parser/Z80Parser.scala index 162a3136..f94f5e65 100644 --- a/src/main/scala/millfork/parser/Z80Parser.scala +++ b/src/main/scala/millfork/parser/Z80Parser.scala @@ -2,7 +2,7 @@ package millfork.parser import java.util.Locale -import fastparse.all._ +import fastparse.all.{parserApi, _} import fastparse.core import millfork.CompilationOptions import millfork.assembly.z80.{ZOpcode, _} @@ -13,18 +13,20 @@ import millfork.node._ /** * @author Karol Stasiak */ -case class Z80Parser(filename: String, input: String, currentDirectory: String, options: CompilationOptions) extends MfParser[ZLine](filename, input, currentDirectory, options) { +case class Z80Parser(filename: String, input: String, currentDirectory: String, options: CompilationOptions, featureConstants: Map[String, Long]) extends MfParser[ZLine](filename, input, currentDirectory, options, featureConstants) { import MfParser._ private val zero = LiteralExpression(0, 1) - val appcSimple: P[ParamPassingConvention] = P("a" | "b" | "c" | "d" | "e" | "hl" | "bc" | "de").!.map { + val appcSimple: P[ParamPassingConvention] = (P("hl" | "bc" | "de" | "a" | "b" | "c" | "d" | "e" | "h" | "l").! ~ !letterOrDigit).map { case "a" => ByZRegister(ZRegister.A) case "b" => ByZRegister(ZRegister.B) case "c" => ByZRegister(ZRegister.C) case "d" => ByZRegister(ZRegister.D) case "e" => ByZRegister(ZRegister.E) + case "h" => ByZRegister(ZRegister.H) + case "l" => ByZRegister(ZRegister.L) case "hl" => ByZRegister(ZRegister.HL) case "bc" => ByZRegister(ZRegister.BC) case "de" => ByZRegister(ZRegister.DE) @@ -38,33 +40,60 @@ case class Z80Parser(filename: String, input: String, currentDirectory: String, } yield ParameterDeclaration(typ, appc).pos(p) // TODO: label and instruction in one line - val asmLabel: P[ExecutableStatement] = (identifier ~ HWS ~ ":" ~/ HWS).map(l => Z80AssemblyStatement(ZOpcode.LABEL, NoRegisters, VariableExpression(l), elidable = true)) + val asmLabel: P[ExecutableStatement] = (identifier ~ HWS ~ ":" ~/ HWS).map(l => Z80AssemblyStatement(ZOpcode.LABEL, NoRegisters, None, VariableExpression(l), elidable = true)) val asmMacro: P[ExecutableStatement] = ("+" ~/ HWS ~/ functionCall).map(ExpressionStatement) - private def normalOp8(op: ZOpcode.Value): P[(ZOpcode.Value, ZRegisters, Expression)] = asmExpressionWithParens.map { - case (VariableExpression("A" | "a"), false) => (op, OneRegister(ZRegister.A), zero) - case (VariableExpression("B" | "b"), false) => (op, OneRegister(ZRegister.A), zero) - case (VariableExpression("C" | "c"), false) => (op, OneRegister(ZRegister.A), zero) - case (VariableExpression("D" | "d"), false) => (op, OneRegister(ZRegister.A), zero) - case (VariableExpression("E" | "e"), false) => (op, OneRegister(ZRegister.A), zero) - case (VariableExpression("HL" | "hl"), true) => (op, OneRegister(ZRegister.MEM_HL), zero) - // TODO: IX/IY - case (e, false) => (op, OneRegister(ZRegister.IMM_8), e) + private val toRegister: Map[String, ZRegister.Value] = Map( + "A" -> ZRegister.A, "a" -> ZRegister.A, + "B" -> ZRegister.B, "b" -> ZRegister.B, + "C" -> ZRegister.C, "c" -> ZRegister.C, + "D" -> ZRegister.D, "d" -> ZRegister.D, + "E" -> ZRegister.E, "e" -> ZRegister.E, + "H" -> ZRegister.H, "h" -> ZRegister.H, + "L" -> ZRegister.L, "l" -> ZRegister.L, + "HL" -> ZRegister.HL, "hl" -> ZRegister.HL, + "AF" -> ZRegister.AF, "af" -> ZRegister.AF, + "BC" -> ZRegister.BC, "bc" -> ZRegister.BC, + "DE" -> ZRegister.DE, "de" -> ZRegister.DE, + "IX" -> ZRegister.IX, "ix" -> ZRegister.IX, + "IY" -> ZRegister.IY, "iy" -> ZRegister.IY, + "SP" -> ZRegister.SP, "sp" -> ZRegister.SP, + ) + + private def param(allowAbsolute: Boolean): P[(ZRegister.Value, Option[Expression])] = asmExpressionWithParens.map { + case (VariableExpression(r), false) if toRegister.contains(r)=> (toRegister(r), None) + case (VariableExpression("HL" | "hl"), true) => (ZRegister.MEM_HL, None) + case (VariableExpression("BC" | "bc"), true) => (ZRegister.MEM_BC, None) + case (VariableExpression("DE" | "de"), true) => (ZRegister.MEM_DE, None) + case (FunctionCallExpression("IX" | "ix", List(o)), true) => (ZRegister.MEM_IX_D, Some(o)) + case (FunctionCallExpression("IY" | "iy", List(o)), true) => (ZRegister.MEM_IY_D, Some(o)) + case (e, true) if allowAbsolute => (ZRegister.MEM_ABS_8, Some(e)) + case (e, _) => (ZRegister.IMM_8, Some(e)) } - private def modifyOp8(op: ZOpcode.Value): P[(ZOpcode.Value, ZRegisters, Expression)] = asmExpressionWithParens.map { - case (VariableExpression("A" | "a"), false) => (op, OneRegister(ZRegister.A), zero) - case (VariableExpression("B" | "b"), false) => (op, OneRegister(ZRegister.A), zero) - case (VariableExpression("C" | "c"), false) => (op, OneRegister(ZRegister.A), zero) - case (VariableExpression("D" | "d"), false) => (op, OneRegister(ZRegister.A), zero) - case (VariableExpression("E" | "e"), false) => (op, OneRegister(ZRegister.A), zero) - case (VariableExpression("HL" | "hl"), true) => (op, OneRegister(ZRegister.MEM_HL), zero) - // TODO: IX/IY - case (e, _) => ErrorReporting.fatal("Invalid parameter for " + op, e.position) + def mapOne8Register(op: ZOpcode.Value)(param: (ZRegister.Value, Option[Expression])): (ZOpcode.Value, OneRegister, Option[Expression], Expression) = param match { + case (reg@(ZRegister.MEM_IX_D | ZRegister.MEM_IY_D), Some(e)) => (op, OneRegister(reg), Some(e), zero) + case (reg, addr) => (op, OneRegister(reg), None, addr.getOrElse(zero)) } - private val jumpCondition: P[ZRegisters] = (HWS ~ identifier ~ HWS).map { + def one8Register(op: ZOpcode.Value): P[(ZOpcode.Value, OneRegister, Option[Expression], Expression)] = param(allowAbsolute = false).map{ + case (reg@(ZRegister.MEM_IX_D | ZRegister.MEM_IY_D), Some(e)) => (op, OneRegister(reg), Some(e), zero) + case (reg, addr) => (op, OneRegister(reg), None, addr.getOrElse(zero)) + } + + def one16Register(op: ZOpcode.Value): P[(ZOpcode.Value, OneRegister, Option[Expression], Expression)] = param(allowAbsolute = false).map{ + case (reg@(ZRegister.MEM_IX_D | ZRegister.MEM_IY_D), Some(e)) => (op, OneRegister(reg), Some(e), zero) + case (ZRegister.MEM_ABS_8, addr) => (op, OneRegister(ZRegister.MEM_ABS_16), None, addr.getOrElse(zero)) + case (ZRegister.IMM_8, addr) => (op, OneRegister(ZRegister.IMM_16), None, addr.getOrElse(zero)) + case (reg, addr) => (op, OneRegister(reg), None, addr.getOrElse(zero)) + } + + private val jumpCondition: P[ZRegisters] = (HWS ~ ( + "NZ" | "nz" | "nc" | "NC" | + "PO" | "po" | "PE" | "pe" | + "m" | "M" | "p" | "P" | + "c" | "C" | "Z" | "z").! ~ HWS).map { case "Z" | "z" => IfFlagSet(ZFlag.Z) case "PE" | "pe" => IfFlagSet(ZFlag.P) case "C" | "c" => IfFlagSet(ZFlag.C) @@ -73,43 +102,202 @@ case class Z80Parser(filename: String, input: String, currentDirectory: String, case "PO" | "po" => IfFlagClear(ZFlag.P) case "NC" | "nc" => IfFlagClear(ZFlag.C) case "P" | "p" => IfFlagClear(ZFlag.S) - case _ => ErrorReporting.fatal("Invalid condition flag") + case _ => NoRegisters // shouldn't happen + } + + private def is16Bit(r: ZRegister.Value): Boolean = r match { + case ZRegister.HL => true + case ZRegister.BC => true + case ZRegister.DE => true + case ZRegister.IX => true + case ZRegister.IY => true + case ZRegister.SP => true + case ZRegister.AF => true + case ZRegister.MEM_ABS_16 => true + case ZRegister.IMM_16 => true + case _ => false + } + + private def merge(op8: ZOpcode.Value, op16: ZOpcode.Value, skipTargetA: Boolean) + (params: (ZRegister.Value, Option[Expression], ZRegister.Value, Option[Expression])) + : (ZOpcode.Value, ZRegisters, Option[Expression], Expression) = params match { + case (ZRegister.A, _, s, e) if skipTargetA => mapOne8Register(op8)(s -> e) + + case (tr@(ZRegister.MEM_IX_D | ZRegister.MEM_IY_D), Some(o), sr@(ZRegister.MEM_ABS_8 | ZRegister.IMM_8), Some(v)) => + (op8, TwoRegisters(tr, sr), Some(o), v) + case (tr@(ZRegister.MEM_ABS_8 | ZRegister.IMM_8), Some(v), sr@(ZRegister.MEM_IX_D | ZRegister.MEM_IY_D), Some(o)) => + (op8, TwoRegisters(tr, sr), Some(o), v) + + case (tr@(ZRegister.MEM_IX_D | ZRegister.MEM_IY_D), Some(o), sr, _) => + (op8, TwoRegisters(tr, sr), Some(o), zero) + case (tr, _, sr@(ZRegister.MEM_IX_D | ZRegister.MEM_IY_D), Some(o)) => + (op8, TwoRegisters(tr, sr), Some(o), zero) + + case (tr, _, ZRegister.MEM_ABS_8, Some(v)) if is16Bit(tr) => + (op16, TwoRegisters(tr, ZRegister.MEM_ABS_16), None, v) + case (tr, _, ZRegister.IMM_8, Some(v)) if is16Bit(tr) => + (op16, TwoRegisters(tr, ZRegister.IMM_16), None, v) + case (ZRegister.MEM_ABS_8, Some(v), sr, _) if is16Bit(sr) => + (op16, TwoRegisters(ZRegister.MEM_ABS_16, sr), None, v) + case (ZRegister.IMM_8, Some(v), sr, _) if is16Bit(sr) => + (op16, TwoRegisters(ZRegister.IMM_16, sr), None, v) + + case (t, Some(v), s, None) => + (op8, TwoRegisters(t, s), None, v) + case (t, None, s, Some(v)) => + (op8, TwoRegisters(t, s), None, v) + case (t, None, s, None) => + (op8, TwoRegisters(t, s), None, zero) + case _ => ??? } private val jumpConditionWithComma: P[ZRegisters] = (jumpCondition ~ "," ~/ HWS).?.map (_.getOrElse(NoRegisters)) + private val jumpConditionWithoutComma: P[ZRegisters] = (jumpCondition ~/ HWS).?.map (_.getOrElse(NoRegisters)) val asmInstruction: P[ExecutableStatement] = { import ZOpcode._ for { el <- elidable + pos <- position() opcode: String <- identifier ~/ HWS - (actualOpcode, registers, param) <- opcode.toUpperCase(Locale.ROOT) match { - case "RST" => asmExpression.map((RST, NoRegisters, _)) - case "RET" => P("").map(imm(RET)) // TODO: conditionals - case "CALL" => (jumpCondition~asmExpression).map{case (reg, param) => (CALL, reg, param)} - case "JP" => (jumpCondition~asmExpression).map{case (reg, param) => (JP, reg, param)} - case "JR" => (jumpCondition~asmExpression).map{case (reg, param) => (JR, reg, param)} - case "DJNZ" => asmExpression.map((DJNZ, NoRegisters, _)) - case "CP" => normalOp8(CP) - case "AND" => normalOp8(AND) - case "OR" => normalOp8(OR) - case "XOR" => normalOp8(XOR) - case "RL" => modifyOp8(RL) - case "RR" => modifyOp8(RR) - case "RLC" => modifyOp8(RLC) - case "RRC" => modifyOp8(RRC) - case "SLA" => modifyOp8(SLA) - case "SLL" => modifyOp8(SLL) - case "SRA" => modifyOp8(SRA) - case "SRL" => modifyOp8(SRL) - case _ => ErrorReporting.fatal("Unsupported opcode " + opcode) + tuple4/*: (ZOpcode.Value, ZRegisters, Option[Expression], Expression)*/ <- opcode.toUpperCase(Locale.ROOT) match { + case "RST" => asmExpression.map((RST, NoRegisters, None, _)) + case "EI" => imm(EI) + case "DI" => imm(DI) + case "HALT" => imm(HALT) + + case "RETN" => imm(RETN) + case "RETI" => imm(RETI) + case "RET" => P(jumpConditionWithoutComma).map((RET, _, None, zero)) + case "CALL" => (jumpConditionWithComma~asmExpression).map{case (reg, param) => (CALL, reg, None, param)} + case "JP" => (jumpConditionWithComma~asmExpression).map{case (reg, param) => (JP, reg, None, param)} + case "JR" => (jumpConditionWithComma~asmExpression).map{case (reg, param) => (JR, reg, None, param)} + case "DJNZ" => asmExpression.map((DJNZ, NoRegisters, None, _)) + + case "CP" => one8Register(CP) + case "AND" => one8Register(AND) + case "OR" => one8Register(OR) + case "XOR" => one8Register(XOR) + case "SUB" => one8Register(SUB) + case "DEC" => one8Register(DEC) + case "INC" => one8Register(INC) + + case "RLA" => regA(RL) + case "RRA" => regA(RR) + case "RLCA" => regA(RLC) + case "RRCA" => regA(RRC) + case "RL" => one8Register(RL) + case "RR" => one8Register(RR) + case "RLC" => one8Register(RLC) + case "RRC" => one8Register(RRC) + case "SLA" => one8Register(SLA) + case "SLL" => one8Register(SLL) + case "SRA" => one8Register(SRA) + case "SRL" => one8Register(SRL) + + case "SCF" => imm(SCF) + case "CCF" => imm(CCF) + case "CPL" => imm(CPL) + case "DAA" => imm(DAA) + case "EXX" => imm(EXX) + case "NOP" => imm(NOP) + + case "LDI" => imm(LDI) + case "LDD" => imm(LDD) + case "LDIR" => imm(LDIR) + case "LDDR" => imm(LDDR) + case "CPI" => imm(CPI) + case "CPD" => imm(CPD) + case "CPIR" => imm(CPIR) + case "CPDR" => imm(CPDR) + case "INI" => imm(INI) + case "IND" => imm(IND) + case "INIR" => imm(INIR) + case "INDR" => imm(INDR) + case "OUTI" => imm(OUTI) + case "OUTD" => imm(OUTD) + case "OUTIR" => imm(OUTIR) + case "OUTDR" => imm(OUTDR) + case "OTIR" => imm(OUTIR) + case "OTDR" => imm(OUTDR) + + case "PUSH" => one16Register(PUSH) + case "POP" => one16Register(POP) + + case "IN" => (asmExpressionWithParens ~ HWS ~ position("comma").map(_ => ()) ~ "," ~/ HWS ~ asmExpressionWithParens).map { p: (Expression, Boolean, (Expression, Boolean)) => + p match { + case (VariableExpression(r), false, (VariableExpression("C" | "c"), true)) + if toRegister.contains(r) => + (IN_C, OneRegister(toRegister(r)), None, zero) + case (VariableExpression(r), false, (port, true)) + if toRegister.contains(r) => + (IN_IMM, OneRegister(toRegister(r)), None, port) + case _ => + ErrorReporting.error("Invalid parameters for IN", Some(pos)) + (NOP, NoRegisters, None, zero) + } + } + case "OUT" => (asmExpressionWithParens ~ HWS ~ position("comma").map(_ => ()) ~ "," ~/ HWS ~ asmExpressionWithParens).map { p: (Expression, Boolean, (Expression, Boolean)) => + p match { + case (VariableExpression("C" | "c"), true, (VariableExpression(r), false)) + if toRegister.contains(r) => + (OUT_C, OneRegister(toRegister(r)), None, zero) + case (port, true, (VariableExpression(r), false)) + if toRegister.contains(r) => + (OUT_IMM, OneRegister(toRegister(r)), None, port) + case _ => + ErrorReporting.error("Invalid parameters for OUT", Some(pos)) + (NOP, NoRegisters, None, zero) + } + } + case "EX" => (asmExpressionWithParens ~ HWS ~ position("comma").map(_ => ()) ~ "," ~/ HWS ~ asmExpressionWithParens).map { p: (Expression, Boolean, (Expression, Boolean)) => + p match { + case (VariableExpression("AF" | "af"), false, (VariableExpression("AF" | "af"), true)) => + (EX_AF_AF, NoRegisters, None, zero) + case (VariableExpression("DE" | "de"), false, (VariableExpression("HL" | "hl"), false)) => + (EX_DE_HL, NoRegisters, None, zero) + case (VariableExpression("HL" | "hl"), false, (VariableExpression("DE" | "de"), false)) => + (EX_DE_HL, NoRegisters, None, zero) + case (VariableExpression("SP" | "sp"), true, (VariableExpression(r), false)) + if toRegister.contains(r) => + (EX_SP, OneRegister(toRegister(r)), None, zero) + case (VariableExpression(r), false, (VariableExpression("SP" | "sp"), true)) + if toRegister.contains(r) => + (EX_SP, OneRegister(toRegister(r)), None, zero) + case _ => + ErrorReporting.error("Invalid parameters for EX", Some(pos)) + (NOP, NoRegisters, None, zero) + } + } + + case "LD" => (param(true) ~ HWS ~ position("comma").map(_ => ()) ~ "," ~/ HWS ~ param(true)).map { + case (r1, e1, (r2, e2)) => merge(LD, LD_16, skipTargetA = false)((r1, e1, r2, e2)) + } + case "ADD" => (param(false) ~ HWS ~ position("comma").map(_ => ()) ~ "," ~/ HWS ~ param(false)).map { + case (r1, e1, (r2, e2)) => merge(ADD, ADD_16, skipTargetA = true)((r1, e1, r2, e2)) + } + case "ADC" => (param(false) ~ HWS ~ position("comma").map(_ => ()) ~ "," ~/ HWS ~ param(false)).map { + case (r1, e1, (r2, e2)) => merge(ADC, ADC_16, skipTargetA = true)((r1, e1, r2, e2)) + } + case "SBC" => (param(false) ~ HWS ~ position("comma").map(_ => ()) ~ "," ~/ HWS ~ param(false)).map { + case (r1, e1, (r2, e2)) => merge(SBC, SBC_16, skipTargetA = true)((r1, e1, r2, e2)) + } + + case _ => + ErrorReporting.error("Unsupported opcode " + opcode, Some(pos)) + imm(NOP) } - } yield Z80AssemblyStatement(actualOpcode, registers, param, el) + } yield { + val (actualOpcode, registers, offset, param) = tuple4 + Z80AssemblyStatement(actualOpcode, registers, offset, param, el) + } } - private def imm(opcode: ZOpcode.Value): Any => (ZOpcode.Value, ZRegisters, Expression) = (_: Any) => { - (opcode, NoRegisters, zero) - } + private def imm(opcode: ZOpcode.Value): P[(ZOpcode.Value, ZRegisters, Option[Expression], Expression)] = + P("").map(_=>(opcode, NoRegisters, None, zero)) + + private def regA(opcode: ZOpcode.Value): P[(ZOpcode.Value, ZRegisters, Option[Expression], Expression)] = + P("").map(_=>(opcode, OneRegister(ZRegister.A), None, zero)) override val asmStatement: P[ExecutableStatement] = (position("assembly statement") ~ P(asmLabel | asmMacro | arrayContentsForAsm | asmInstruction)).map { case (p, s) => s.pos(p) } // TODO: macros @@ -119,23 +307,23 @@ case class Z80Parser(filename: String, input: String, currentDirectory: String, case Some(xs) => if (flags("interrupt")) { if (xs.exists { - case Z80AssemblyStatement(ZOpcode.RET, _, _, _) => true + case Z80AssemblyStatement(ZOpcode.RET, _, _, _, _) => true case _ => false }) ErrorReporting.warn("Assembly interrupt function `$name` contains RET, did you mean RETI/RETN?", options, Some(p)) } else { if (xs.exists { - case Z80AssemblyStatement(ZOpcode.RETI, _, _, _) => true - case Z80AssemblyStatement(ZOpcode.RETN, _, _, _) => true + case Z80AssemblyStatement(ZOpcode.RETI, _, _, _, _) => true + case Z80AssemblyStatement(ZOpcode.RETN, _, _, _, _) => true case _ => false }) ErrorReporting.warn("Assembly non-interrupt function `$name` contains RETI or RETN, did you mean RET?", options, Some(p)) } if (!name.startsWith("__") && !flags("macro")) { xs.last match { - case Z80AssemblyStatement(ZOpcode.RET, NoRegisters, _, _) => () // OK - case Z80AssemblyStatement(ZOpcode.RETN, NoRegisters, _, _) => () // OK - case Z80AssemblyStatement(ZOpcode.RETI, NoRegisters, _, _) => () // OK - case Z80AssemblyStatement(ZOpcode.JP, NoRegisters, _, _) => () // OK - case Z80AssemblyStatement(ZOpcode.JR, NoRegisters, _, _) => () // OK + case Z80AssemblyStatement(ZOpcode.RET, NoRegisters, _, _, _) => () // OK + case Z80AssemblyStatement(ZOpcode.RETN, NoRegisters, _, _, _) => () // OK + case Z80AssemblyStatement(ZOpcode.RETI, NoRegisters, _, _, _) => () // OK + case Z80AssemblyStatement(ZOpcode.JP, NoRegisters, _, _, _) => () // OK + case Z80AssemblyStatement(ZOpcode.JR, NoRegisters, _, _, _) => () // OK case _ => val validReturn = if (flags("interrupt")) "RETI/RETN" else "RET" ErrorReporting.warn(s"Non-macro assembly function `$name` should end in " + validReturn, options, Some(p)) diff --git a/src/main/scala/millfork/parser/ZSourceLoadingQueue.scala b/src/main/scala/millfork/parser/ZSourceLoadingQueue.scala index 054caa23..bbccc1f6 100644 --- a/src/main/scala/millfork/parser/ZSourceLoadingQueue.scala +++ b/src/main/scala/millfork/parser/ZSourceLoadingQueue.scala @@ -11,7 +11,8 @@ class ZSourceLoadingQueue(initialFilenames: List[String], includePath: List[String], options: CompilationOptions) extends AbstractSourceLoadingQueue[ZLine](initialFilenames, includePath, options) { - override def createParser(filename: String, src: String, parentDir: String): MfParser[ZLine] = Z80Parser(filename, src, parentDir, options) + override def createParser(filename: String, src: String, parentDir: String, featureConstants: Map[String, Long]): MfParser[ZLine] = + Z80Parser(filename, src, parentDir, options, featureConstants) def enqueueStandardModules(): Unit = { // TODO diff --git a/src/test/scala/millfork/test/BasicSymonTest.scala b/src/test/scala/millfork/test/BasicSymonTest.scala index 3ac5630d..88dc1d7b 100644 --- a/src/test/scala/millfork/test/BasicSymonTest.scala +++ b/src/test/scala/millfork/test/BasicSymonTest.scala @@ -168,4 +168,45 @@ class BasicSymonTest extends FunSuite with Matchers { | } """.stripMargin){ m => () } } + + test("Preprocessor test") { + EmuUnoptimizedCrossPlatformRun(Cpu.Mos, Cpu.Z80)( + """ + | byte output @$c000 + | + | #use ARCH_6502 + | #use ARCH_Z80 + | + | #if 1 + | asm void main () { + | #if ARCH_6502 + | lda #ARCH_6502 + | sta output + | rts + | #elseif ARCH_Z80 + | ld a,ARCH_Z80 + | ld (output),a + | ret + | #else + | #error unsupported architecture + | #endif + | } + | #endif + | + | #if 1 + 1 == 2 + | #info 1 + | #if 1 == 3 + | #error 1 == 3 + | #elseif 1 == 5 + | #error 1 == 5 + | #else + | #info 2 + | #endif + | #else + | #error not( 1 + 1 == 2 ) + | #endif + """.stripMargin){ m => + m.readByte(0xc000) should equal(1) + } + } } diff --git a/src/test/scala/millfork/test/emu/EmuPlatform.scala b/src/test/scala/millfork/test/emu/EmuPlatform.scala index 70e8904b..997da715 100644 --- a/src/test/scala/millfork/test/emu/EmuPlatform.scala +++ b/src/test/scala/millfork/test/emu/EmuPlatform.scala @@ -16,6 +16,7 @@ object EmuPlatform { Nil, TextCodec.Ascii, TextCodec.Ascii, + Platform.builtInCpuFeatures(cpu), CurrentBankFragmentOutput(0, 0xffff), Map("default" -> new UpwardByteAllocator(0x200, 0xb000)), Map("default" -> new VariableAllocator( diff --git a/src/test/scala/millfork/test/emu/EmuRun.scala b/src/test/scala/millfork/test/emu/EmuRun.scala index b44d1079..ad78a514 100644 --- a/src/test/scala/millfork/test/emu/EmuRun.scala +++ b/src/test/scala/millfork/test/emu/EmuRun.scala @@ -16,7 +16,7 @@ import millfork.error.ErrorReporting import millfork.node.StandardCallGraph import millfork.node.opt.NodeOptimization import millfork.output.{MemoryBank, MosAssembler} -import millfork.parser.MosParser +import millfork.parser.{MosParser, Preprocessor} import millfork.{CompilationFlag, CompilationOptions, CpuFamily} import org.scalatest.Matchers @@ -119,7 +119,8 @@ class EmuRun(cpu: millfork.Cpu.Value, nodeOptimizations: List[NodeOptimization], if (!source.contains("_panic")) effectiveSource += "\n void _panic(){while(true){}}" if (source.contains("import zp_reg")) effectiveSource += Files.readAllLines(Paths.get("include/zp_reg.mfk"), StandardCharsets.US_ASCII).asScala.mkString("\n", "\n", "") - val parserF = MosParser("", effectiveSource, "", options) + val (preprocessedSource, features) = Preprocessor.preprocessForTest(options, effectiveSource) + val parserF = MosParser("", preprocessedSource, "", options, features) parserF.toAst match { case Success(unoptimized, _) => ErrorReporting.assertNoErrors("Parse failed") diff --git a/src/test/scala/millfork/test/emu/EmuZ80Run.scala b/src/test/scala/millfork/test/emu/EmuZ80Run.scala index ae14554f..bf01c9a8 100644 --- a/src/test/scala/millfork/test/emu/EmuZ80Run.scala +++ b/src/test/scala/millfork/test/emu/EmuZ80Run.scala @@ -10,7 +10,7 @@ import millfork.error.ErrorReporting import millfork.node.StandardCallGraph import millfork.node.opt.NodeOptimization import millfork.output.{MemoryBank, Z80Assembler} -import millfork.parser.Z80Parser +import millfork.parser.{Preprocessor, Z80Parser} import millfork.{CompilationFlag, CompilationOptions, CpuFamily} import millfork.compiler.z80.Z80Compiler import org.scalatest.Matchers @@ -33,7 +33,8 @@ class EmuZ80Run(cpu: millfork.Cpu.Value, nodeOptimizations: List[NodeOptimizatio ErrorReporting.verbosity = 999 var effectiveSource = source if (!source.contains("_panic")) effectiveSource += "\n void _panic(){while(true){}}" - val parserF = Z80Parser("", effectiveSource, "", options) + val (preprocessedSource, features) = Preprocessor.preprocessForTest(options, effectiveSource) + val parserF = Z80Parser("", preprocessedSource, "", options, features) parserF.toAst match { case Success(unoptimized, _) => ErrorReporting.assertNoErrors("Parse failed")