Preprocessor. Z80 improvements. Library improvements.

This commit is contained in:
Karol Stasiak 2018-07-12 18:30:35 +02:00
parent 35f3638a4f
commit 215d8d92b4
91 changed files with 1560 additions and 169 deletions

View File

@ -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

View File

@ -34,6 +34,7 @@ no extension for BBC micro program file,
* `-r <program>` Run given program after successful compilation. Useful for automatically launching emulators without any external scripting.
* `-D <feature>=<value>` Defines a feature value for the preprocessor.
## Verbosity options

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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

96
docs/lang/preprocessor.md Normal file
View File

@ -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 <expr>
#elseif <expr>
#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

23
docs/stdlib/c64.md Normal file
View File

@ -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.

40
docs/stdlib/nes.md Normal file
View File

@ -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.

53
docs/stdlib/other.md Normal file
View File

@ -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.

33
docs/stdlib/stdlib.md Normal file
View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1 +1,6 @@
#if not(BBC_MICRO)
#warn bbc_hardware module should be used only on BBC Micro-compatible targets
#endif

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()
}
byte value
poke($dc03, 0)
value = peek($dc01)
mouse_rbm = (value & 1) ^ 1
if value & 16 == 0 { mouse_lbm = 1 } else { mouse_lbm = 0}
}

View File

@ -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

View File

@ -1 +1,5 @@
#if not(CBM_264)
#warn c264_hardware module should be only used on C264-compatible targets
#endif
import c264_ted

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
word irq_pointer @$314

View File

@ -23,4 +23,4 @@ void _panic() {
STA $D020
}
while(true){}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,3 +1,6 @@
#if not(NES)
#warn nes_routines module should be only used on NES/Famicom targets
#endif
asm void on_reset() {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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

50
include/stdlib_6502.mfk Normal file
View File

@ -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]

45
include/stdlib_z80.mfk Normal file
View File

@ -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]

10
include/string.mfk Normal file
View File

@ -0,0 +1,10 @@
byte strzlen(pointer str) {
byte index
index = 0
while str[index] != 0 {
index += 1
}
return index
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

6
include/x_coord.mfk Normal file
View File

@ -0,0 +1,6 @@
#if WIDESCREEN
alias x_coord = word
#else
alias x_coord = byte
#endif

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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._

View File

@ -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("<feature>=<value>").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:", "")

View File

@ -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("-")

View File

@ -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))
}

View File

@ -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))
}
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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 {

View File

@ -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("<decimal digits>") ~ !("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("<binary digits>")
} 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("<hex digits>")
} 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("<octal digits>")
} 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("<quaternary digits>")
} 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("||", "^^"),

View File

@ -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)

View File

@ -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) {

View File

@ -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))
}
}

View File

@ -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))

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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(

View File

@ -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")

View File

@ -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")