1. About this Document
2. What is NESFab?
NESFab is a statically-typed, procedural programming language for creating NES games. Designed with 8-bit limitations in mind, the language is more ergonomic to use than C, while also producing faster assembly code. It’s easy to get started with, and has a useful set of libraries for making your first — or hundredth — NES game.
2.1. Strengths of NESFab
-
Performance is generally superior to C and all other compiled languages.
-
Mapper banks are handled automatically and scale up without fuss.
-
Multi-byte and fixed-point arithmetic is well supported and simpler than other languages.
-
The compiler is easily configured, without needing complex build systems.
-
Some asset loading is built-in. There are less steps to get your ideas onto the screen.
2.2. Weaknesses of NESFab
-
NESFab code is only compatible with the NES. It cannot compile to other systems.
-
Only a select number of cartridge mappers are supported.
-
Although NESFab performance is good, writing assembly by hand can obviously surpass it.
-
NESFab is not as tried-and-true as other languages. They are likely bugs and missing features.
3. Quirks and Surprises
If you’re coming from another language, you might be surprised by a few of NESFab’s quirks. The most flagrant ones are listed below.
3.1. Types and Values
-
Like C, everything is passed by value. Nothing is passed by reference.
-
While arrays are supported, multi-dimensional arrays are not.
-
Most values cannot be addressed via pointers. Instead, only global variables of a specific type can be addressed.
-
Global variables and data are partitioned into used-defined groups; a concept unique to NESFab.
3.2. Operators
-
The operators
&
,|
, and^
have a higher precedence than in C. -
Combined-assignment operators, like
+=
, return a value of typeBool
, representing the carry. -
Division is unsupported at the language level.
-
Array operators (
[]
and{}
) are split into 8-bit and 16-bit versions, with the 8-bit versions having better performance. -
Types are not implicitly promoted. Different operators have different rules for how differing types are handled.
4. Illegal Instructions
By default, NESFab makes use of the system’s illegal opcodes, which provide small performance gains when used. Although widely supported, some emulators and clone devices may not handle these instructions and so it can be desirable to compile without them.
To compile without illegal instructions, pass ISA=LEGAL
to Make
when building the compiler.
See the __illegal
keyword for how to programatically check if illegal opcodes are supported.
5. Getting Started
5.1. Installation
NESFab is available on Github.
It is best to build it from scratch, but if that is not possible, download one of the releases.
On Unix systems, is recommended to place the nesfab
executable in a directory your PATH
variable searches.
On Linux, this is typically /usr/bin
, while on Mac, it is typically /usr/local/bin
.
You will also want a NES emulator with debugging features, like FCEUX, Mesen, or Nintendulator. It is often beneficial to test on multiple emulators, so there is no shame in downloading them all.
Syntax highlighting support can be found in the syntax_highlighting
directory of repository.
If your text editor is not supported, consider writing one yourself and submitting it to the repository.
5.2. Compiling your first ROM
The nesfab
tool compiles .fab
source code files into .nes
ROMs.
It can be used with the command-line, or by clicking and dragging the file you want to compile onto the nesfab
executable.
To compile your first ROM, create a file called main.fab
and save it with code below:
// This small program plays a sound effect. // Variables: vars /sound UU pitch = 1000 // Sends 'pitch' variable to the APU, emitting sound: fn play_sound() {$4015}(%100) {$4008}($FF) {$400A}(pitch.a) {$400B}(pitch.b & %111) // Game loop: mode main() {PPUCTRL}(%10000000) while true pitch *= 1.01 play_sound() nmi
If using the command-line, you can compile it using the command:
nesfab main.fab
Otherwise, drag the main.fab
file onto the nesfab
executable.
When done, the compiler should have produced an a.nes
file in the same directory,
which is the default name of compiled binaries.
Try running a.nes
in your NES emulator.
You should hear a sweeping tone being played.
5.3. Your first configuration file
The nesfab
compiler accepts options both on the command-line, and via configuration files.
For most projects, a single configuration file is ideal, so this section will focus on that.
Below is an example configuration file: hello_world.cfg
:
output = hello_world.nes input = main.fab
The output
option determines the name of the .nes
file, while input
lists a single source file.
To compile using this configuration file, either run:
nesfab hello_world.cfg
Or drag the hello_world.cfg
file onto the nesfab
executable.
It should produce the same result as before, but the ROM will be saved as hello_world.nes
instead of a.nes
because the output
option was set.
For more details about configuration files, see the config reference page.
5.4. Included Examples
Project examples can be found in the examples/
directory of the repository.
To build each example, compile their *.cfg
files.
6. Configuration Options
6.1. Comments
Comments in configuration files are specified as lines beginning with #
.
Comments are used for documentation; they have no effect on the configuration.
Comments are not available on the command-line.
Example:
# Hello world! This is a comment!
6.2. help
(-h
)
Prints a list of command-line options.
This option is only available via the command-line.
Command-line usage:
nesfab --help
6.3. version
(-v
)
Prints information about the NESFab executable, including its version history.
This option is only available via the command-line.
Command-line usage:
nesfab --version
6.4. input
(-i
)
Specifies a file to be compiled, which can either be a source file with extension .fab
,
a macro file with extension .macrofab
, or a configuration file with extension .cfg
.
This option can be used multiple times to compile multiple files.
Note: the flags --input
and -i
are optional when using the command line,
as any argument not belonging to another option will be interpreted as an input
.
Command-line usage:
nesfab --input "file1.fab" --input "file2.fab" --input "another_config.cfg"
or:
nesfab "file1.fab" "file2.fab" "another_config.cfg"
Configuration file usage:
input = file1.fab input = util/file2.fab input = another_config.cfg
6.5. output
(-o
)
Specifies the name of the executable .nes
file the compiler will produce.
This option can only be specified once.
By default, the value is "a.nes"
.
Command-line usage:
nesfab --output "game.nes"
Configuration file usage:
output = game.nes
6.6. code-dir
(-I
)
Specifies a directory to be searched when compiling source code files. This option can be used multiple times to specify multiple directories.
Commonly, this option is used when several source files exist in the same directory.
code-dir
can specify this directory, then input
can specify the files inside it
without having to reference the directory name.
Command-line usage:
nesfab --code-dir "some_directory/"
Configuration file usage:
code-dir = some_directory/
6.7. resource-dir
(-R
)
Specifies a directory to be searched when importing data files. This option can be used multiple times to specify multiple directories.
This behaves like code-dir
, but applies to the files imported by the file
keyword.
Command-line usage:
nesfab --resource-dir "some_directory/"
Configuration file usage:
resource-dir = some_directory/
6.8. nesfab-dir
(-N
)
Specifies a directory to be searched when importing source code and data files. This option can be used multiple times to specify multiple directories.
This behaves like defining both code-dir
and resource-dir
, but nesfab-dir
has a lower search priority.
Command-line usage:
nesfab --nesfab-dir "some_directory/"
Configuration file usage:
nesfab-dir = some_directory/
Note that the environment variable NESFAB
can also be used to set this value.
Example:
export NESFAB=/home/pubby/nesfab/
6.9. mapper
(-M
)
Specifies the mapper used. The argument is a mapper name. This option can only be specified once.
By default, the value is nrom
.
Command-line usage:
nesfab --mapper bnrom
Configuration file usage:
mapper = bnrom
6.10. mirroring
(-m
)
Specifies the mirroring used for mappers with fixed mirrorings. This option can only be specified once.
This option expects one argument. Any of the following arguments are valid:
Argument | Description |
---|---|
V |
Vertical Mirroring |
H |
Horizontal Mirroring |
4 |
4-Way Mirroring |
1 |
1-Way Switchable |
If the mapper supports multiple mirrorings, the default value is V
.
Command-line usage:
nesfab --mirroring H
Configuration file usage:
mirroring = H
6.11. prg-size
(-p
)
Specifies the size of PRG (the amount of memory for code) in increments of 1 KiB. This option can only be specified once.
The default value depends on the mapper.
Note
|
Just because the compiler accepts a prg-size does not mean that corresponding hardware exists in the real world.
Only the default size is asserted to be commonly available.
|
Command-line usage:
nesfab --prg-size 128
Configuration file usage:
prg-size = 128
6.12. chr-size
(-c
)
Specifies the size of CHR (the amount of memory for tilesets) in increments of 1 KiB. This option can only be specified once.
The default value depends on the mapper.
Note
|
Just because the compiler accepts a chr-size does not mean that corresponding hardware exists in the real world.
Only the default size is asserted to be commonly available.
|
Command-line usage:
nesfab --chr-size 32
Configuration file usage:
chr-size = 32
6.13. sector-size
Specifies the size of the PRG chip’s sectors, which is useful when implementing flash memory saves.
This option determines the language’s __sector_size
value.
It has no other purpose.
With most mappers, the default value is 4098.
Command-line usage:
nesfab --sector-size 1024
Configuration file usage:
sector-size = 1024
6.14. bus-conflicts
Specifies whether the mapper has bus conflicts. This option can only be specified once.
The following arguments are valid:
-
To disable:
0
,false,
oroff
. -
To enable:
1
,true,
oron
. -
For the default value:
default
.
The default value depends on the mapper.
Command-line usage:
nesfab --bus-conflicts true
Configuration file usage:
bus-conflicts = true
6.15. sram
Specifies whether the mapper should include 8KiB of RAM, mapped to addresses $6000-$7FFF.
The following arguments are valid:
-
To disable:
0
,false,
oroff
. -
To enable SRAM which saves across resets:
persistent
. -
To enable SRAM which does not save across resets:
volatile
. -
To enable and use the mapper’s default save behavior:
1
,true,
oron
. -
For the default value:
default
.
The default value depends on the mapper.
Command-line usage:
nesfab --sram volatile
Configuration file usage:
sram = volatile
Note
|
Just because the compiler accepts a sram value does not mean that corresponding hardware exists in the real world.
Only the default sram value is asserted to be commonly available.
|
6.16. system
(-S
)
Specifies the target NES system, which will be accessible using the system
keyword.
This option can only be specified once.
By default, the value is detect
.
Command-line usage:
nesfab --system ntsc
Configuration file usage:
system = ntsc
Argument | Description |
---|---|
Other systems |
|
|
Detect system at runtime |
Note
|
detect has a small runtime penalty.
|
6.17. controllers
(-C
)
Specifies the maximum number of controllers the game will use.
This option determines the language’s __controllers
value.
It has no other purpose.
Arguments from 1 to 8 are accepted.
By default, the value is 2.
Command-line usage:
nesfab --controllers 2
Configuration file usage:
controllers = 2
6.18. unsafe-bank-switch
By default, the compiler generates bank switching code which is resilient to hardware interrupts. For many games, this added safety is unnecessary and slows the code down. This option is used to disable safe bank switching behavior.
This option can only be specified once.
Note
|
Unsafe bank switches are best enabled when IRQ is not used and when NMI is always waited for (no lag frames possible). |
Note
|
Some mappers, such as BNROM, do not benefit from unsafe-bank-switch , as they always switch banks quickly.
|
Command-line usage:
nesfab --unsafe-bank-switch
Configuration file usage:
unsafe-bank-switch = 1
6.19. expansion-audio
This option determines the language’s __expansion_audio
value,
which is used to enable expansion audio in music drivers.
Command-line usage:
nesfab --expansion-audio
Configuration file usage:
expansion-audio = 1
6.20. multicart
This option is used to make the generated ROM compatible with specific multicarts. Often, this entails reserving a specific region in the ROM for multicart-specific code.
Argument | Multicart | Description |
---|---|---|
|
Reserves $FFD0-$FFF9. |
Note
|
The action53 setting should be used when entering the NESDev Competition.
|
Command-line usage:
nesfab --multicart action53
Configuration file usage:
multicart = action53
6.21. mlb
mlb
specifies a Mesen .mlb label file to output.
This file will contain addresses used by the program, for the purpose of debugging.
Command-line usage:
nesfab --mlb "my_labels.mlb"
Configuration file usage:
mlb = my_labels.mlb
6.22. ctags
ctags
specifies a Ctags file to output.
This file will contain source locations of global definitions, allowing text editors to better navigate.
Command-line usage:
nesfab --ctags ".tags"
Configuration file usage:
ctags = ".tags"
Note
|
To use CTags in VSCode, use the ctagsx extension. |
6.23. threads
(-j
)
Specifies how many threads the compiler can use, enabling parallel compilation. This option expects an integer argument, and can only be specified once.
By default, the value is 1.
In general, a value slightly above the number of CPU cores available is ideal. Performance may degrade if the number is too high.
Note
|
This option is currently not supported on MinGW builds of NESFab, due to that platform having a buggy implementation of threads. |
Command-line usage:
nesfab --threads 4
Configuration file usage:
threads = 4
6.24. error-on-warning
(-W
)
This option turns warnings into errors and halts compilation whenever a warning occurs. This option expects no arguments and can only be specified once.
Command-line usage:
nesfab --error-on-warning
Configuration file usage:
error-on-warning = 1
6.25. pause
This option pauses the compiler before exiting until input is received on stdin. It is intended to be used on Microsoft Windows to keep the Command Prompt window open until you’re ready to close it. This option expects no arguments.
Command-line usage:
nesfab --pause
Configuration file usage:
pause = 1
To make NESFab always pause on Microsoft Windows, first create a shortcut to the NESFab executable.
Then, in the shortcut’s properties, put --pause
after the target path.
6.26. sloppy
This option improves compilation speed at the cost of program optimization.
It can be disabled on a per-function basis with the modifier -sloppy
.
Command-line usage:
nesfab --sloppy
Configuration file usage:
sloppy = 1
6.27. --*ram-init
--ram-init
, --sram-init
, and --vram-init
cause their respective memory regions to be initialized to zero on reset.
This initialization happens by writing a 0
byte to each address, ignoring any banking behavior the mapper may have.
Note
|
It’s not recommended to use these compiler options, and it may result in brittle code. Instead, initialize variables and VRAM using code. |
Command-line usage:
nesfab --ram-init --sram-init --vram-init
Configuration file usage:
--ram-init = 1 --sram-init = 1 --vram-init = 1
7. Supported Mappers
NESFab supports a small set of mappers, which determine the capabilities of a cartridge. The choice of mapper determines the amount of space available for code, the nametable mirroring, and whether CHR data is stored in RAM or ROM.
For beginners: It is recommended to start with nrom
(the default),
and only consider switching once your program grows too large for it.
For information on how to configure NESFab for a specific mapper, see:
7.1. nrom
NROM is the simplest mapper. It is easy to use and offers good performance, but is lacking in features and memory size.
Note
|
16 KiB and 8 KiB variants of NROM are not currently supported. |
Memory Sizes:
Name | Min | Max | Default |
---|---|---|---|
32 KiB |
32 KiB |
32 KiB |
|
8 KiB |
8 KiB |
8 KiB |
Other Details:
Name | Description |
---|---|
Fixed H or V |
|
N/A |
|
By default, no |
|
N/A |
|
N/A |
7.2. anrom
ANROM is similar to BNROM, but allows mirroring to be changed on the fly.
Note
|
Related mappers like AMROM can be had using configuration options. |
Memory Sizes:
Name | Min | Max | Default |
---|---|---|---|
32 KiB |
512 KiB |
256 KiB |
|
8 KiB (RAM) |
8 KiB (RAM) |
8 KiB (RAM) |
Other Details:
Name | Description |
---|---|
1-Page switchable |
|
By default, no |
|
By default, no |
|
Bit 4 changes mirroring |
|
Acceptable risk |
7.3. bnrom
BNROM supports a huge amount of PRG, making it an excellent choice for large games.
Memory Sizes:
Name | Min | Max | Default |
---|---|---|---|
32 KiB |
8192 KiB |
128 KiB |
|
8 KiB (RAM) |
8 KiB (RAM) |
8 KiB (RAM) |
Other Details:
Name | Description |
---|---|
Fixed H or V |
|
By default, yes |
|
By default, no |
|
N/A |
|
N/A |
7.4. unrom
UNROM supports lots of PRG, like BNROM, but differs in that it has a fixed bank.
Because of this, UNROM requires manual ROM layout with the +static
modifier.
Note
|
Typically, BNROM is better for NESFab than UNROM, as it does not require the use of +static .
|
Memory Sizes:
Name | Min | Max | Default |
---|---|---|---|
32 KiB |
4096 KiB |
64 KiB |
|
8 KiB (RAM) |
8 KiB (RAM) |
8 KiB (RAM) |
Other Details:
Name | Description |
---|---|
Fixed H or V |
|
By default, yes |
|
By default, no |
|
N/A |
|
N/A |
7.5. 30
(UNROM 512)
Mapper 30 is an extended form of UNROM with CHRRAM banking.
Like UNROM, mapper 30 requires manual ROM layout with the +static
modifier.
Memory Sizes:
Name | Min | Max | Default |
---|---|---|---|
32 KiB |
512 KiB |
512 KiB |
|
32 KiB (RAM) |
32 KiB (RAM) |
32 KiB (RAM) |
Other Details:
Name | Description |
---|---|
Fixed H, V, 4, or 1 |
|
By default, no |
|
By default, no |
|
High 3 bits switch CHR and mirroring |
|
Acceptible risk |
7.6. cnrom
CNROM is similar to NROM, but has multiple CHR banks.
Memory Sizes:
Name | Min | Max | Default |
---|---|---|---|
32 KiB |
32 KiB |
32 KiB |
|
8 KiB |
2048 KiB |
32 KiB |
Other Details:
Name | Description |
---|---|
Fixed H or V |
|
N/A |
|
By default, no |
|
Sets CHR bank. |
|
N/A |
7.7. gnrom
GNROM offers both PRG and CHR banks.
Note
|
Related mappers like MHROM can be had using configuration options. |
Memory Sizes:
Name | Min | Max | Default |
---|---|---|---|
32 KiB |
512 KiB |
128 KiB |
|
8 KiB (RAM) |
128 KiB (RAM) |
32 KiB (RAM) |
Other Details:
Name | Description |
---|---|
Fixed H or V |
|
By default, yes |
|
By default, no |
|
Low 4 bits switch CHR |
|
Acceptable risk |
7.8. colordreams
COLORDREAMS is similar to GNROM, but reverses the bank switching nybbles.
Note
|
PRG above 128 KiB may not be supported on physical cartridges. |
Memory Sizes:
Name | Min | Max | Default |
---|---|---|---|
32 KiB |
512 KiB |
128 KiB |
|
8 KiB (RAM) |
128 KiB (RAM) |
128 KiB (RAM) |
Other Details:
Name | Description |
---|---|
Fixed H or V |
|
By default, yes |
|
By default, no |
|
High 4 bits switch CHR |
|
Acceptable risk |
7.9. gtrom
GTROM is a modern mapper designed to be cheap while offering a wide range of features.
Note
|
See the standard library file lib/mapper/gtrom.fab .
|
Memory Sizes:
Name | Min | Max | Default |
---|---|---|---|
32 KiB |
512 KiB |
512 KiB |
|
16 KiB (RAM) |
16 KiB (RAM) |
16 KiB (RAM) |
Other Details:
Name | Description |
---|---|
Fixed 4 |
|
Never |
|
By default, no |
|
High 4 bits switch nametable, CHR, and LEDs |
|
Acceptable risk |
7.10. mmc1
MMC1 is a flexible ASIC mapper with CHR banking and mirroring controls. Unfortunately, MMC1 is very slow to interface.
Note
|
See the standard library file lib/mapper/mmc1.fab .
|
Memory Sizes:
Name | Min | Max | Default |
---|---|---|---|
256 KiB |
256 KiB |
256 KiB |
|
128 KiB |
128 KiB |
128 KiB |
Other Details:
Name | Description |
---|---|
Switchable H, V, or 1 |
|
Never |
|
By default, no |
|
Sets internal $8000 register |
|
Not recommended |
7.11. mmc3
MMC3 is a flexible ASIC mapper with CHR banking, mirroring controls, and a scanline counter.
Because MMC3 uses fixed banks, it requires manual ROM layout with the +static
modifier.
See mapper 189
for an alternative.
Note
|
In NESFab’s implementation of MMC3, the two highest bits of $8000 cannot and should not be set.
When writing to $8000 , leave the two highest bits zero.
|
Note
|
See the standard library file lib/mapper/mmc3.fab .
|
Memory Sizes:
Name | Min | Max | Default |
---|---|---|---|
512 KiB |
2048 KiB |
512 KiB |
|
256 KiB |
256 KiB |
256 KiB |
Other Details:
Name | Description |
---|---|
Switchable H or V |
|
Never |
|
By default, no |
|
N/A |
|
Not recommended |
7.12. 189
(MMC3 Variant)
Mapper 189 is a MMC3 variant originally designed for bootleg games.
Is it an excellent choice for those wanting MMC3 features in NESFab, but has the caveat of being an uncommon mapper.
Unlike mmc3
, it does not support SRAM.
Note
|
Unlike MMC3, mapper 189 allows the highest bit of $8000 to be set.
|
Note
|
See the standard library file lib/mapper/mmc3.fab .
|
Memory Sizes:
Name | Min | Max | Default |
---|---|---|---|
32 KiB |
512 KiB |
128 KiB |
|
256 KiB |
256 KiB |
256 KiB |
Other Details:
Name | Description |
---|---|
Switchable H or V |
|
Never |
|
No |
|
N/A |
|
Acceptible risk |
7.13. mmc5
MMC5 is a powerful ASIC mapper with many features. Notably, it extends rendering with 8x8 background attributes, per-tile banking, and vertical splits, extends audio with expansion channels, provides a scanline counter, and even has hardware to perform multiplication.
Unfortunately, MMC5 is a difficult mapper to reproduce and emulate, meaning it’s not usually recommended for homebrew releases.
Note
|
See the standard library file lib/mapper/mmc5.fab .
|
Memory Sizes:
Name | Min | Max | Default |
---|---|---|---|
1024 KiB |
1024 KiB |
1024 KiB |
|
1024 KiB |
1024 KiB |
1024 KiB |
Other Details:
Name | Description |
---|---|
Switchable |
|
Never |
|
By default, yes |
|
N/A |
|
Recommended |
Note
|
Enabling unsafe-bank-switch enables NESFab to use the MMC5 multiplication hardware for arithmetic.
|
Note
|
Enabling expansion-audio enables NESFab to use the MMC5’s expanded sound channels.
|
7.14. rainbow
Rainbow is a modern ASIC mapper with many features similar to mmc5
,
designed for commercial homebrew releases.
Because it is so recent, emulation support may be spotty.
Note
|
See the standard library file lib/mapper/rainbow.fab .
|
Memory Sizes:
Name | Min | Max | Default |
---|---|---|---|
32 KiB |
8192 KiB |
8192 KiB |
|
8 KiB |
8192 KiB |
8192 KiB |
Other Details:
Name | Description |
---|---|
Switchable |
|
Never |
|
By default, yes |
|
N/A |
|
N/A |
Note
|
Enabling expansion-audio enables NESFab to use Rainbow’s expanded sound channels.
|
8. Identifiers
Identifiers may contain letters, numbers, and underscores, but they cannot start with a number. To differentiate types names from other identifiers, the following rules apply:
-
User-defined types are written in
PascalCase
-
Other definitions are written in
snake_case
orUPPERCASE_SNAKE_CASE
.
For top-level definitions, identifiers beginning with _
are visible only in their containing file.
In other languages, this behavior is sometimes called private
.
Example:
Foo // A type name. foo // A definition which isn't a type. _foo // An identifier only visible to this file.
9. Value Semantics
Values in NESFab are always passed and stored by value, not by reference. This means that when you call a function, its parameters will be copies of the arguments passed.
For example:
fn foo(U x) U x += 5 return x fn bar() U y = 10 U z = foo(y)
At the end of bar
, variable y
will have the value 10
, while variable z
will have the value 15
.
10. Indentation
Indentation refers to the spaces at the beginning of each line. In NESFab, indentation is significant and alters the behavior of code.
Indentation is used to create code blocks, where every line but the first is indented using spaces (not tabs). The amount of spaces is up to you, but it must be consistent throughout the block.
FIRST LINE INDENTED LINE INDENTED LINE INDENTED LINE
Code blocks can be nested:
FIRST BLOCK INDENTED LINE INDENTED LINE SECOND BLOCK INDENTED LINE INDENTED LINE THIRD BLOCK INDENTED LINE INDENTED LINE
11. Banks
The NES uses a 16-bit address space, but most games need more data than 16-bits can represent. To overcome this limitation, machine code can be broken up into segments called "banks", and hardware on the cartridge can switch between these banks at runtime.
In NESFab, banks are automatically handled for you, meaning you do not need to worry about them much. However, it is still useful to know a bit about them, to clarify how things work under the hood.
Pointers and Addressing: Implementation Details
When banks are involved, rather than addressing using 16-bit pointers, 24-bit pointers are used instead. A 24-bit pointer can be seen as a 16-bit address paired with an 8-bit integer representing the bank.
When dereferencing a 24-bit pointer, first the bank is swapped into memory using the 8-bit integer, then the data is read using the 16-bit address. The caveat is, the machine code performing the dereference needs to be in memory too. Depending on the mapper, this can involve duplicating the machine code across multiple banks, or storing the machine code in a specific location which won’t be switched out.
12. Groups
Groups organize globals together based on how they are used in the program. In NESFab, each global variable and pointer-addressible array is associated with a group.
There are two ways to declare groups: vars
and data
.
Furthermore, data
has two variants: data
and omni data
.
As a guideline, omni data
uses more ROM space, but has better performance than data
.
Typically, it is recommended to use data
for most everything, and only use omni data
for small look-up tables that are frequently used.
Note
|
The variables and data belonging to groups are always global and exist at top-level scope. |
Why groups?
To the programmer, the purpose of groups are:
-
To organize code.
-
To specify the storage of a variable.
To the compiler, the purpose of groups are:
-
To enable the compiler to smartly allocate variables by reusing RAM addresses.
-
To facilitate mappers with multiple banks, enabling smarter linking.
-
To simplify pointer aliasing optimizations.
When are groups mentioned?
How does one use groups?
For variables, it often makes sense to have at least one vars
group per mode
:
vars /main_menu U cursor_y vars /game U player_x U player_y
Often, certain variables will be used across different modes. These can receive their own groups:
vars /high_scores UU high_score = 0 vars /settings Bool swap_buttons = false Bool mute = false
You can use these variables without any special syntax. The compiler infers everything for you:
fn move_right() player_x += 1
The exception to this is when defining an asm fn
.
The compiler cannot infer the groups it uses, and so an employs
modifier is required:
asm fn move_right() : employs /game inc &player_x rts
For data, create a group for each schema.
data /levels [] level1 // ... [] level2 // ... data /songs [] song1 // ... [] song2 // ...
Then you can use pointers to access this data:
fn load_level(CCC/levels level) // ...
13. Comments
NESFab supports two kinds of comments: single-line and multi-line.
13.1. Single-Line Comments
Single-line comments begin with the character sequence //
, and terminate at the end of the line.
// This is a single-line comment. ct U foo = 10 // You can put them after lines of code to document it.
13.2. Multi-Line Comments
Multi-line comments begin with the character sequence /*
on a new line, and terminate with the character sequence */
, followed by a line ending.
Note
|
Unlike other languages, multi-line comments cannot share lines with code. |
/* This is a multi-line comment! */ /* This is also a multi-line comment! */ ct U foo = 10 /* This won't compile. Multi-line comments cannot share lines with code. */ /* This won't compile. Multi-line comments cannot share lines with code. */ ct U foo = 10
14. Byte Blocks
Byte blocks are a special syntax used to define inline assembly code and PAA data.
14.1. Typed Data
Data can be inserted into byte blocks using a syntax identical to casts.
Syntax:
Type(values...)
-
Type
is a type name. -
values
are a comma-separated list of expressions.
The value is cast, then inserted into the byte block with the following order:
-
For numeric types, the bytes are inserted in little-endian order.
-
For structures, the first member is inserted first, then the second, and so on.
-
For TEAs, the first element is inserted first, then the second, and so on.
-
For VECs, the first element is inserted first, then the second, and so on.
Example:
data /some_group [] some_data U(10) UU(2000) U[3](1,2,3)
14.2. Untyped Data
The type name of typed data can be elided, causing the type to inferred from the expression.
Syntax:
(values)
-
values
is an expression.
The value is inserted into the byte block following the rules of typed data.
Example:
data /some_group [] some_data (U(10) + U(20)) (UU(300).x)
14.3. Assembly Instructions
Assembly instructions can be inserted into byte blocks with a syntax similar to 6502 assemblers.
Syntaxes:
op // Implied op #num // Immediate op addr // Direct (Zero page or absolute) op addr // Relative op (addr) // Indirect op addr, x // Direct, X op addr, y // Direct, Y op (addr, y) // Indirect, X op (addr), y // Indirect, Y
Valid Op Codes:
adc and asl bcc bcs beq bit bmi bne bpl brk bvc bvs clc cld cli clv cmp cpx cpy dec dex dey eor inc inx iny jmp jsr lda ldx ldy lsr nop ora pha php pla plp rol ror rti rts sbc sec sed sei sta stx sty tax tay tsx txa txs tya lax axs anc alr arr sax skb ign dcp isc rla rra slo sre
Example:
data /some_group [] some_data lda #30 sta $2003 ldy #0 lda ($2000), y sta ($00, x)
15. Function Pointers and Function Sets
Although NESFab supports function pointers, their use is more limited when compared to other languages.
For a function to compatible with function pointers, it must belong to a function set. Function sets are defined by prefixing the function’s name with a set name, followed by a period:
fn foo.bar() // Declare a function 'bar' in the function set 'foo'. fn foo.qux() // Declare another function 'qux' in the function set 'foo'.
All functions belonging to the set must have the same type signature.
To reference a function belonging to a function set normally, you must include the function set name:
foo.bar() // Call 'bar'.
Using the @
operator, you can retrieve a function pointer.
Note that function pointers have Fn
types.
Fn.foo my_ptr = @(foo.bar)
Function pointers can be called using the regular function call syntax:
Fn.foo my_ptr = @(foo.bar) my_ptr() // Call it.
Limitation: One calling thread only
Unfortunately, function pointers can only be called from a single thread. For example, the code below will not compile, as it calls from two different threads:
vars Fn.foo my_ptr nmi my_nmi() my_ptr() irq my_irq() my_ptr()
If you need behavior like this, avoid function pointers and instead use switch
.
Limitation: No asm support
Currently, function pointers cannot be called from asm fn
contexts.
16. Keywords
16.1. if
The if
statement allows for conditional execution of code blocks.
It behaves like if
in most programming languages.
Syntax:
if expression code block
The conditional expression of if
will be converted to Bool
.
If this evaluates to true
, the body of the if
statement will be executed.
16.1.1. if
statement (byte block)
In byte blocks, the if
statement enables conditional compilation of byte block data.
Syntax:
if condition byte block
-
condition
is a compile-time constant value convertable toBool
.
Example:
lda #10 if MY_CONSTANT == 3 sta &foo tax
Note
|
In the current implementation, labels cannot exist inside conditional blocks. |
16.2. else
The else
statement allows for control flow to branch between two code blocks.
It behaves like else
in most programming languages.
This statement must be paired with a corresponding if.
Syntax:
if expression code block else code block
If the corresponding if
evaluates to false
, the body of the else
statement will be executed.
For visual appeal, other statements may follow the else
keyword on the same line, including if
, for
, and while
.
This looks like:
if expression code block else if expression code block else code block
16.2.1. else
(byte block)
Like if
statement (byte block), else
is also usable in byte blocks.
16.3. while
The while
statement allows for looping control flow.
It behaves like while
in most programming languages.
Syntax:
while condition code block
condition
is an expression converted to Bool
. While this expression evaluates to true
, the loop body will execute.
After the code in code block
executes, control flow jumps back to the condition
test.
Modifiers:
16.4. for
The for
statement allows for looping control flow, with more features than while
.
It behaves like for
in most programming languages.
Syntax:
for initialization ; condition ; iteration code block
-
initialization
executes before the loop and can be an expression or a variable initialization. -
condition
is an expression converted toBool
. While this expression evaluates totrue
, the loop body will execute. -
iteration
is an expression to be run at the end of every iteration (following the code block).
Any of these expressions may be empty. An empty condition
is equivalent to true
.
After the code in code block
executes, iteration
executes, and then control flow jumps back to the condition
test.
For visual appeal, the expressions of for
may be put on separate lines starting with the ;
character, like so:
for initialization ; condition ; iteration code block
Modifiers:
16.5. do
The do
keyword can be prefixed to either while
or for
to alter their behavior.
A loop with do
skips the condition
check of its first iteration.
Syntax:
do while condition code block do for initialization ; condition ; iteration code block
Modifiers:
Note
|
Loops written with do often have better runtime performance than loops written without.
|
16.6. break
break
ends the execution of the containing while
, for
, or switch
statement.
It behaves like break
in most programming languages.
Syntax:
break
Example:
for U i = 0; i < 10; i += 1 if array[i] == 0 break // Exits the loop
If you want to exit out of multiple nested statements, use goto
.
16.7. continue
continue
is used inside while
or for
statements,
and causes control flow to jump to the end of the loop’s code block.
It behaves like condition
in most programming languages.
Syntax:
continue
Example:
for U i = 0; i < 10; i += 1 if array[i] == 0 continue // If this executes, the line below it won't. array[i] += i
16.8. switch
The switch
statement branches control flow based on an byte value.
switch
is similar to if
, but instead of having a choice between two code blocks,
switch
allows multiple. It behaves like switch
in most programming languages.
Syntax:
switch expression code block
expression
must be of type U
or S
.
switch
is intended to be used with case
and default
.
Both of these label where control flow will jump.
Example:
switch player_state case 0 do_run() break case 1 do_jump() break case 2 do_kick() break default: do_nothing() break
16.8.1. switch
statement (byte block)
In byte blocks, the switch
statement causes the mapper to bank switch to a specified bank.
Syntax:
switch regs
-
regs
specifies which registers are holding the bank to switch to. The accepted values area,
x
,y
, andax
, whereax
requires registers A and X to hold the same value.
Example:
ldy &my_bank1 // Load the bank in registers Y switch y // Switch to the bank in that register lax &my_bank2 // Load the bank in registers A and X switch ax // Switch to the bank in those registers
16.9. case
case
is used inside of switch
statements as a label.
Control flow will jump to the case
from the switch
if the switch’s expression matches the case
value.
Syntax:
case constant expression code block
constant expression
is an expression which can be computed at compile-time.
The code block
of case
exists only to provide a scope.
There is no other difference between the syntax above, and this:
case constant expression code block
See more examples in switch
.
16.10. default
default
is used inside of switch
statements as a label.
Control flow will jump to the default
from the switch
if the switch’s expression matches no enclosed [hw_case] statement.
Syntax:
default code block
The code block
of default
exists only to provide a scope.
There is no other difference between the syntax above, and this:
default code block
See more examples in switch
.
16.11. goto
The keyword goto
has use in two different types of statements: goto
and goto mode
.
16.11.1. goto
statement
The goto
statement causes control flow to jump to a corresponding label
in the same function.
It behaves like goto
in most programming languages.
Syntax:
goto identifier
identifier
refers to the name of a label in the current function.
Example:
fn example() U i = 0 label loop i += 1 if i < 10 goto loop
16.11.2. goto mode
statement
The goto mode
statement causes control flow to jump to a mode
,
discarding the current call stack and starting anew.
In the process, global variables will be reset to their initial value,
unless they are explicitly preserved using preserves
in the goto mode
statement.
Syntax:
goto mode identifier(arguments) : preserves /groups
Note that preserves
is a required modifier of this statement.
Example:
vars /my_vars U some_var = 10 mode foo() goto mode bar(some_var + 1) : preserves mode bar(U some_argument) my_vars = some_argument goto mode foo() : preserves /my_vars
16.11.3. goto
(assembly byte block)
In assembly functions, the goto
statement causes control to switch execution to another function,
clobbering all registers in the process.
It behaves similar to the fn
assembly statement, but does not return.
Syntax:
goto fn_identifier
-
fn_identifier
is the name of a function.
If the function accepts arguments, those arguments must be set prior to the goto
statement.
Example:
fn foo(U x) // ... asm fn bar() : employs default lda #5 sta &foo.x // Set the argument goto foo
16.11.4. goto mode
(assembly byte block)
In assembly functions, the goto mode
statement causes control to switch execution to a mode,
clobbering all registers, discarding the current call stack, and starting anew.
In the process, global variables will be reset to their initial value,
unless they are explicitly preserved using preserves
in the goto mode
statement.
It behaves similar to the fn
assembly statement.
Syntax:
goto mode mode_identifier : preserves /groups
Note that preserves
is a required modifier of this statement.
Example:
vars /my_vars U some_var = 10 mode foo() // ... asm fn bar() goto mode foo : preserves /my_vars
16.12. label
The label
statement introduces a point which a goto
statement can jump to .
It has no effect otherwise.
It behaves like labels in most programming languages, albeit with a slightly different syntax.
Syntax:
label identifier code_block
-
identifier
is the unique name of the label. -
code_block
is an optional indented code block.
The code_block
of label
exists only to provide a scope.
There is no other difference between the syntax above, and this:
label identifier code_block
16.12.1. label
statement (byte block)
Labels give names to specific addresses inside of byte blocks.
They behave similarly to ct
definitions, defining values of type AA
and AAA
.
Syntax:
label identifier byte_block
-
identifier
is the unique name of the label. -
byte_block
is an optional indented byte block to be inserted into the containing byte block.
The byte_block
of label
exists only to provide a scope.
There is no other difference between the syntax above, and this:
label identifier byte_block
Example:
data /some_group [] some_data label foo jmp foo
16.13. return
16.13.1. return
statement
The return
statement ends the execution of the current function,
using its argument as the function’s return value.
It behaves like return
in most programming languages.
Syntax:
return expression
Syntax for functions lacking a return value:
return
16.13.2. return
expression
A return
expression does not cause functions to return.
Instead, it provides a handle to the current function’s return value.
Although the value itself cannot be used, the address of can be taken using unary operator &
,
This functionality exists because of inline assembly. Most often, it is used to allow inline assembly functions to return values by storing into the address.
Example:
AA return_addr = &return
16.14. swap
The swap
statement exchanges its arguments, assigning the first to the second and the second to the first.
Syntax:
swap a, b
-
a
andb
are lvalue expressions.
Example:
fn foo() U x = 10 U y = 20 swap x, y // Now x = 20 and y = 10.
16.15. push
The push
expression appends a value onto a VEC value.
The expression returns a copy of its second argument.
Syntax:
push(vec, elem)
-
vec
is an lvalue expression with a VEC type. -
elem
is an expression value to be pushed ontovec
.
Example:
ct fn foo() U{} vec = U{}() push(vec, 10) push(vec, 20) // Now vec = U{}(10, 20)
16.16. pop
The pop
expression removes the last value from a VEC value, returning the removed valued.
Syntax:
pop(vec)
-
vec
is an lvalue expression with a VEC type.
Example:
ct fn foo() U{} vec = U{}(10, 20) U x = pop(vec) U y = pop(vec) // Now x = 20, y = 10
16.17. fence
The fence
statement is used for both writing concurrent code, and for interacting with hardware.
It imposes constraints on how global variables are loaded and stored,
preventing the compiler from reordering them.
More precisely:
-
Every global variable the function is tracking will be stored before the
fence
executes. -
Every global variable the function is tracking will be loaded after the
fence
executes.
A function tracks a global variable if it reads or writes that variable, or if it calls another function that does. When dereferencing a pointer, the pointer’s groups define the set of globals to track.
Note
|
fence does not instruct the compiler which globals to track.
To do that, the modifier employs is required.
|
Why is fence
a thing in concurrent code?
The NESFab compiler performs optimizations which moves loads and stores around. This is normally fine, but issues arise due to interrupts.
To illustrate, take a look at the code below:
foo = 10 bar = 20
The compile is free to reorder these global variable assignments, storing into bar
before foo
.
However, imagine if an interrupt were to occur between these stores.
The interrupt would see that bar
equals 20
, but not foo
equals 10
,
as the store to foo
hasn’t happened yet.
To prevent this reordering, a fence
statement can be used:
foo = 10 fence bar = 20
Now if the interurpt sees that bar
equals 20
, foo
must equal 10
.
Why is fence
a thing in sequential code?
Optimizations which reorder code can affect sequential code, too.
For example, consider the following code which turns the grayscale bit of PPUMASK
on until game_update
completes.
Visually, this will depict how long it takes for game_update
to run by displaying a grayscale stripe on the screen.
while true {PPUMASK}(PPUMASK_GRAYSCALE_ON | PPUMASK_ON) game_update() {PPUMASK}(PPUMASK_ON) nmi
Unfortunately, this code may not work as intended.
The compiler is allowed to reorder the game_update
call and move it before the first PPUMASK
write, or after the second PPUMASK
write.
This is because the compiler sees no connection between the two; there is no dependency from one to another, as they do not involve the same global variables.
To fix the problem, two fence
statements are used:
while true {PPUMASK}(PPUMASK_GRAYSCALE_ON | PPUMASK_ON) fence game_update() fence {PPUMASK}(PPUMASK_ON) nmi
These force the game_update
call to remain between the PPUMASK
writes.
Another purpose for fence
:
fence
is also used when interacting with the hardware directly.
When reading or writing a global variable via its hardware address,
two fence
statements are recommended with the hardware access between them.
These fence
statements instruct the compiler to store the global before the hardware access,
and load the value after it.
A common example arises when doing OAM DMA:
fence {OAMDMA}((&oam).b) fence
Without the first fence
instruction, the compiler would not recognize that global variables are being read.
and so the resulting read may have incorrect results.
The second fence
, although largely uncessary, ensures that future reads to oam
occur after OAMDMA
completes.
Note that this only applies when an address is written, and that write has an effect which dereferences the address.
It is not necessary to use fence
when a value is passed normally:
// fence isn't needed here: {PPUDATA}(some_var)
Likewise, it is not necessary to use fence
when the address is not dereferenced:
// fence isn't needed here: {PPUDATA}(&some_var.a)
More on dependencies and side effects:
One way to think about fence
is that the program is outputting a list of hardware reads and writes (i.e. those involving the PPU), and the compiler makes sure the order and the data written matches the original code.
16.18. true
true
is an expression of type Bool
, and has a compile-time constant value.
When converted to an integer type, it will have the value 1
.
Syntax:
true
16.19. false
false
is an expression of type Bool
, and has a compile-time constant value.
When converted to an integer type, it will have the value 0
.
Syntax:
false
16.20. read
read
is an expression used to access the value a pointer is pointing at, advancing the pointer in the process.
Syntax:
read Type(ptr)
-
Type
is a type name. The expression will read a value of this type from the pointer, returning it. -
ptr
is an lvalue expression with a pointer type. The expression will increment the pointer bysizeof Type
bytes.
Example:
omni data [] my_data UU($1234) UU($5678) mode main() CC ptr = @my_data UU first = read UU(ptr) UU second = read UU(ptr)
16.21. write
write
is an expression used to store a value at an address pointed-to by a pointer, advancing the pointer in the process.
The expression returns no value.
Syntax:
write Type(ptr, expr)
-
Type
is a type name. The expression will write a value of this type to the pointer. -
ptr
is an lvalue expression with a pointer type. The expression will increment the pointer bysizeof Type
bytes. -
expr
is an expression of typeType
. The value will be written atptr
.
Example:
vars [] my_data UU($1234) UU($5678) mode main() MM ptr = @my_data write UU(ptr, $1234) write UU(ptr, $5678)
16.22. ready
ready
is an expression of type Bool
which returns true
if both an NMI is active and the program was waiting on one,
or false
otherwise.
It is intended to be used as a synchronization primitive (mutex)
to avoid race conditions inside of NMI handlers.
Syntax:
ready
In general, if ready
is true
, all global variables are in a stable, concurrent-safe state.
Likewise, if ready
is false
, either no NMI is happening, or the program is lagging one or more frames.
Example:
nmi foo() if ready upload_data() poll_controller() play_music()
The address of ready
can be taken using unary operator &
,
but the pointed-to value must never be modified by the program.
Note
|
There is more than one way to achieve concurrent safety. See fence , for example.
|
16.23. nmi_counter
nmi_counter
is an expression of type U
whose value is incremented after each NMI.
It can be used for timing purposes, to create simple animations, or to detect when NMI has occured.
Syntax:
nmi_counter
The address of nmi_counter
can be taken using unary operator &
,
but the pointed-to value must never be modified by the program.
16.24. state
Some mappers have registers which combine bank switching with other functionality.
For example, ANROM
uses a bit to track the cartridge’s mirroring, and lets the programmer switch it on the fly.
state
expressions read or write these mapper registers while correctly handling the bank.
See the mappers page what state
means for each mapper.
Note
|
The NESFab runtime duplicates the mapper’s register state to a fixed location in RAM. Reading the state will return this copy instead of polling the hardware. |
16.24.1. state
read
state
is an expression of type U
which returns the mapper’s last-set register state.
Syntax:
state()
Example:
U foo = state()
The address of state
can be taken using unary operator &
.
This address refers to the copy in RAM; modifying it does not notify the hardware.
16.24.2. state
write
state
is an expression of type Void
which sets the mapper’s register state.
Syntax:
state(expr)
-
expr
is an expression of typeU
. The state will be assigned this value.
Example:
state(5) // The state will have a value of 5
Note
|
You should not alter the bits reserved for the mapper’s bank.
Leave these bits set to 0 , or otherwise the program may crash.
|
16.25. system
system
is an expression of type U
which returns the current NES system.
Syntax:
system
The possible return values are listed below:
Enumeration | Value |
---|---|
0 |
|
1 |
|
2 |
|
3 |
Example:
fn foo() if system == SYSTEM_NTSC speed = 1.0 else speed = 1.2
When the system
option is set to detect
, the value will be determined at program startup.
Additionally, the address of system
can be taken using unary operator &
,
but the pointed-to value must never be modified by the program.
When the system
option is not set to detect
, the expression is a compile-time constant
and its address cannot be taken.
16.25.1. SYSTEM_NTSC
SYSTEM_NTSC
is an expression of type Int
, and has a compile-time constant value of 0
.
Syntax:
SYSTEM_NTSC
16.25.2. SYSTEM_PAL
SYSTEM_PAL
is an expression of type Int
, and has a compile-time constant value of 1
.
Syntax:
SYSTEM_PAL
16.25.3. SYSTEM_DENDY
SYSTEM_DENDY
is an expression of type Int
, and has a compile-time constant value of 2
.
Syntax:
SYSTEM_DENDY
16.25.4. SYSTEM_UNKNOWN
SYSTEM_UNKNOWN
is an expression of type Int
, and has a compile-time constant value of 3
.
Syntax:
SYSTEM_UNKNOWN
16.26. PPU Registers
The following PPU registers have keywords.
All of these are expressions of type AA
with compile-time constant values.
Enumeration | Value |
---|---|
$2000 |
|
$2001 |
|
$2002 |
|
$2003 |
|
$2004 |
|
$2005 |
|
$2006 |
|
$2007 |
|
$4014 |
16.26.1. PPUCTRL
PPUCTRL
is an expression of type AA
, and has a compile-time constant value of $2000
.
Syntax:
PPUCTRL
16.26.2. PPUMASK
PPUMASK
is an expression of type AA
, and has a compile-time constant value of $2001
.
Syntax:
PPUMASK
16.26.3. PPUSTATUS
PPUSTATUS
is an expression of type AA
, and has a compile-time constant value of $2002
.
Syntax:
PPUSTATUS
16.26.4. OAMADDR
OAMADDR
is an expression of type AA
, and has a compile-time constant value of $2003
.
Syntax:
OAMADDR
16.26.5. OAMDATA
OAMDATA
is an expression of type AA
, and has a compile-time constant value of $2004
.
Syntax:
OAMDATA
16.26.6. PPUSCROLL
PPUSCROLL
is an expression of type AA
, and has a compile-time constant value of $2005
.
Syntax:
PPUSCROLL
16.26.7. PPUADDR
PPUADDR
is an expression of type AA
, and has a compile-time constant value of $2006
.
Syntax:
PPUADDR
16.26.8. PPUDATA
PPUDATA
is an expression of type AA
, and has a compile-time constant value of $2007
.
Syntax:
PPUDATA
16.26.9. OAMDMA
OAMDMA
is an expression of type AA
, and has a compile-time constant value of $4014
.
Syntax:
OAMDMA
16.27. fn
The fn
keyword declares a
function
at global scope.
Syntax:
fn identifier(parameters) ReturnType code block
-
identifier
is the name of the function. -
parameters
is a comma-separated list of variables with the syntaxType name
. -
ReturnType
is a type name, but is optional. LeavingReturnType
blank is the same as specifying it asVoid
. -
code block
is the block of code which implements the function.
Functions can only be declared at global-scope. Unlike other programming languages, functions in NESFab cannot be nested or recursive.
Modifiers:
Example:
fn foo(U p1, U p2) U return p1 + p2
16.27.1. fn
statement (assembly byte block)
In assembly functions, the fn
statement calls a NESFab function,
clobbering all registers in the process.
Note
|
Unlike the JSR instruction, the fn statement correctly handles the NESFab calling convention and runtime.
|
Syntax:
fn fn_identifier
-
fn_identifier
is the name of a function.
If the function accepts arguments, those arguments must be set prior to the fn
statement.
If the function returns a value, it can be retrieved via return
.
Example:
fn foo(U x) U return x + x asm fn bar() : employs default lda #5 sta &foo.x // Set the argument fn foo // Call the function lda #&foo.return // Read the return value sta PPUDATA rts
16.28. ct
ct
is short for compile-time.
The keyword can be prefixed onto value and function declarations to insist that their computations occur at compile-time.
16.28.2. ct
value
Syntax:
ct TypeName identifier = value
ct
values are declared with the syntax of regular variables, but must be defined a value.
They can be declared at global scope, or inside functions.
16.29. mode
The mode
keyword declares a mode function at global scope.
Modes are similar to regular functions, but they do not return.
Instead, the only way to leave a mode function is via a goto mode
statement.
Syntax:
mode identifier(parameters) code block
-
identifier
is the name of the mode function. -
parameters
is a comma-separated list of variables with the syntaxType name
. -
code block
is the block of code which implements the mode function.
Every program is required to have a mode named main
defined, which takes no parameters.
When the program starts, execution will begin at main
.
This behavior is similar to main
functions found in other programming languages.
Modes can be assigned a corresponding nmi function, using a modifier.
While the mode function is executing, NMIs will be handled using the supplied nmi
function.
Modifiers:
Example:
mode main() : nmi my_nmi while true x = x + 1
Why do modes exist?
There are two reasons.
First, it is convenient to be able to change what the program is doing deep inside a function call.
For example, in a video game it can be useful to define one mode
for the main menu,
and another one for the actual gameplay.
To switch between the two, a goto mode
statement can be used anywhere in the program,
which is nicer than having to use variables and switch-cases.
But more importantly, modes allow the compiler to smartly allocate memory, enabling variables used in different modes to share RAM addresses. This happens transparently from the programmer; no sum types needed.
16.30. nmi
The keyword nmi
can be used as a statement, a declaration, or as a modifier.
16.30.1. nmi
statement
The nmi
statement blocks execution until an nmi
function occurs.
Until the nmi
statement returns, ready
will evaluate to true
.
Syntax:
nmi
16.30.2. nmi
statement (byte block)
In byte blocks, the nmi
statement blocks execution until an nmi
function occurs,
clobbering all registers in the process.
Until the nmi
statement returns, ready
will evaluate to true
.
Syntax:
nmi
16.30.3. nmi
function
The nmi
keyword declares an NMI interrupt function at global scope.
NMI interrupts are similar to regular functions, but they have no parameters, cannot return values, and cannot be called.
Instead, they execute once per frame at the start of VBLANK,
so long as bit 7 of PPUCTRL is set.
Syntax:
nmi identifier() code block
-
identifier
is the name of the mode function. -
code block
is the block of code which implements the mode function.
Modifiers:
Why do NMI interrupt functions exist?
NMI interrupts provide a way for code to detect the vertical blanking interval (VBLANK). This is important, as most modifications to the PPU’s state require that rendering be turned off, and VBLANK is one such time.
Since the NMI interrupt occurs once per frame, it’s also convenient to use it as a timer. Typically, game updates are run in sync with the NMI, as otherwise the game would speed up or slow down based on how much computation is happening.
16.31. irq
The keyword irq
can be used as a statement, a declaration, or as a modifier.
16.31.1. irq
statement
The irq
statement is used to enable or disable IRQ interrupt handling.
When disabled, no IRQ functions will be called.
Syntax:
irq expr
-
expr
is an expression of typeBool
.
Example:
irq true // Enable IRQs
Note
|
The irq statement corresponds to assembly instructions SEI and CLI .
|
16.31.2. irq
function
The irq
keyword declares an irq interrupt function at global scope.
IRQ interrupts are similar to regular functions, but they have no parameters, cannot return values, and cannot be called.
Instead, they are triggered by hardware such as the APU frame counter,
or MMC3 scanline counter.
Syntax:
irq identifier() code block
-
identifier
is the name of the mode function. -
code block
is the block of code which implements the mode function.
Modifiers:
Note
|
asm can be applied to irq , so long as +solo_interrupt and +static are used.
|
16.32. asm
The asm
keyword declares an function at global scope using byte block inline assembly syntax.
Syntax:
asm fn identifier(parameters) ReturnType : employs /groups vars local vars byte block
-
identifier
is the name of the function. -
parameters
is a comma-separated list of variables with the syntaxType name
. -
ReturnType
is a type name, but is optional. LeavingReturnType
blank is the same as specifying it asVoid
. -
/groups
is an optional list of groups that the function uses. Seeemploys
. -
local vars
is a line-separated list of variables with the syntaxType name
. -
byte block
is the byte block of code which implements the function.
A special default
label is required in each asm
function,
and specifies the entry point to the function.
Example:
asm fn waste_time() : employs vars U counter default lda #0 label loop sta &counter inc &countner bne loop rts
Modifiers:
The labels of an asm
function are visible using the .
operator.
Although the address cannot be taken of these labels, it is possible to call them like functions.
Example:
waste_time.loop()
Note
|
asm can be applied to irq , so long as +solo_interrupt and `+static are used.
asm is not currently supported with nmi .
|
16.33. struct
The struct
keyword is used to define new types (records) at global scope.
It behaves similarly to the struct
keyword in other languages.
Syntax:
struct NewTypeName fields
-
NewTypeName
is the name of thestruct
. -
fields
is a newline-separated list of fields, with the syntaxTypeName field_name
.
Example:
struct Circle S center_x S center_y UF radius
struct
types may contain arrays and other struct
types,
so long as multi-dimensional arrays are not created.
Like all values in NESFab
, struct
types are passed by value.
16.34. vars
The vars
keyword declares a block of global variables, and potentially their group.
Syntax:
vars /group_name variables
-
/group_name
is the optional name of the group that the variables will be part of. -
variables
are global variables definitions with the syntaxTypeName identifier
orTypeName identifier = value
.
Assigning to a global variable in a vars
block sets its initial value.
The variable will reset to this value at the start of the program,
but also whenever a goto mode
statement occurs and the variable’s group is not preserved
The same vars
group can be declared multiple times,
with each declaration defining additional global variables.
The group will be defined as the union of these declarations.
Variable modifiers:
Example:
vars /my_group U score = 0 // Set an initial value for 'score' UU player_x UU player_y vars /my_group U speed
16.35. data
The data
keyword declares a group and the pointer-addressable global constants inside of it.
Syntax:
data /group_name constants
-
group_name
is the mandatory name of the group that the constants will be part of. -
constants
are global constant definitions with the syntax[optional_size] identifier
, followed by a byte block.
The same data
group can be declared multiple times,
with each declaration defining additional global variables.
The group will be defined as the union of these declarations.
Constant modifiers:
Example:
data /my_group [4] player_speeds U(1) U(4) U(8) U(20) [4] player_attacks U(10) U(20) U(30) U(40)
16.36. omni
The omni
keyword can be prefixed to data
to alter its behavior.
Groups declared using omni
will have their data duplicated across every bank of the ROM.
Pointers to data inside this group will not include a bank field (e.g. type CC
instead of CCC
).
Syntax:
omni data /group_name constants
-
group_name
is the optional name of the group that the constants will be part of. -
constants
are global constant definitions with the syntax[optional_size] identifier
, followed by a byte block.
Why use omni
?
Data inside an omni
block can be accessed slightly quicker, at the expense of ROM size.
Additionally, pointers to omni
data take up only two bytes, as opposed to three.
When using a mapper without PRG banks (such as NROM), it is strictly better to use omni data
instead of data
.
16.37. charmap
The charmap
keyword defines character maps,
which are sets of characters with a mapping from each character to byte values.
It is used to specify text encoding, like
ASCII,
EBCDIC,
or MIK.
Syntax:
charmap identifier("string", 's', offset)
-
identifier
is the name of the charmap. This is optional. When left out, the defaultcharmap
is defined. -
"string"
is a string literal, defining the characters of the charmap. The first character in the string will map to a value ofoffset
(or zero ifoffset
is not defined), with other characters mapping to one higher than the character preceding them. -
's'
is an optional character literal, defining the sentinel. When left out, no sentinel is defined. -
offset
is an optional integer literal, defining the value of the first charmap element.
Modifiers:
Example:
charmap foo(" ,.!?ABCDEFGHIJKLMNOPQRSTUVWXYZ\0", '\0') // Defines the mapping: // ' ' = 0 // ',' = 1 // '.' = 2 // '!' = 3 // '?' = 4 // 'A' = 5 // 'B' = 6 // 'C' = 7 // ... and so on // with the sentinel being: '\0'
Example:
charmap bar("abcd", 10) : stows /strings // Defines the default charmap mapping: // 'a' = 10 // 'b' = 11 // 'c' = 12 // 'd' = 13 // with no sentinel, // and stowing its literals in group /strings.
Shared Characters
The escape sequence \/
has a special meaning inside of charmap
definitions.
A character preceding \/
will map to the same value as the character following it.
Commonly, \/
is used when multiple characters can use the same glyph,
such as 0
and O
, or 1
and I
.
charmap foo("_0\/O1\/I\/|X", '\0') // Defines the mapping: // '_' = 0 // '0' = 1 // 'O' = 1 // '1' = 2 // '|' = 2 // 'I' = 2 // 'X' = 3
Sizes and Members
The number of unique values in a charmap
can be accessed using the size
member,
which is a compile-time constant value of type Int
.
charmap foo("abc") // The member 'size' is defined as: // foo.size = 3 // Example use: ct U last_foo_char = foo.size - 1
To access the members of the default charmap
, the expression charmap
is used:
// Define the default charmap: charmap("xyz") // Access the default charmap using 'charmap': ct U last_default_char = charmap.size - 1
Sentinels
For charmaps
that define a sentinel character, two things occur:
-
String literals using the
charmap
have the sentinel character appended onto the end. -
The member
sentinel
of typeU
is defined forcharmap
.
The intention behind sentinel characters is to mark the end of strings. This can be used to mimic the behavior of the C programming language’s null-terminated strings.
charmap foo("abc", 'b') // String literals have 'b' tacked on: // "string"foo[6] = 'b' // len("string"foo) = 7 // The member 'sentinel' is defined for 'foo': // foo.sentinel = 1 charmap c_string("\0abc", '\0') // This literal is terminated by the value 0: // "hello world"c_string // The member 'sentinel' is defined for 'c_string': // c_string.sentinel = 0
Note that sentinels must have a mapping defined in the charmap
.
Doing so otherwise is an error.
charmap bad_charmap("abc", 'z') // Error! 'z' is not in the charmap!
stows
Group
charmap
accepts a single group in its stows
modifiers.
If defined, string literals using the charmap
become valid operands to operator @
and operator &
.
When using these operators, the contents of the string literal will exist in the group as data.
Example:
charmap foo("ABCD") : stows /strings // Can now reference strings using literals: ct CCC/strings some_ptr = @"AAA" // This is akin to defining the string inside a 'data' block first: data /strings [] some_string ("AAA") // ... and then referencing it: ct CCC/strings another_ptr = @some_string
16.38. chrrom
The chrrom
keyword is only used for mappers which use CHR ROM (as opposed to CHR RAM).
It specifies the data of the CHR ROM using a byte block syntax.
Syntax:
chrrom offset byte block
-
offset
is an optional offset which determines which address the data gets stored. If left out,offset
is treated as 0.
Example:
// Store at offset $0000: chrrom file(chr, "sprites.png") file(chr, "bg.png") // Store at offset $2000: chrrom $2000 file(chr, "more_sprites.png") file(chr, "more_bg.png")
The compiler will issue a warning if the supplied data does not match what the mapper expects.
16.39. file
The file
keyword imports and converts data from an external file.
It can be used as a statement in byte blocks, or as an expression.
Syntax:
file(target, "filename", args...)
-
target
specifies the output conversion target to use. -
"filename"
is a string literal path to the file. -
args…
is a list of arguments that the conversion script will use. (Most conversion scripts do not use arguments.)
Modifiers:
16.39.1. file
expression
file
expressions produce compile-time constant values of type U{}
.
To use modifiers with them, write the modifiers on the same line.
Example:
ct U{} my_data = file(chr, "sprites.png") : +spr_8x16
16.39.2. file
statement (byte block)
file
statements insert data into a byte block.
Unlike file
expressions, these statements can can introduce accessory definitions.
Example:
chrrom file(chr, "sprites.png") : +spr_8x16 file(chr, "bg.png")
16.39.3. Conversions
Input File Conversions
When loading a file, its data is first interpreted based on its filename extension. The following filenames are accepted:
File Format | Description |
---|---|
Raw binary data |
|
Raw binary data |
|
Raw binary data |
|
Nametable data |
|
Textual data |
|
Output Target Conversions
Once a file has been loaded, it is then converted based on its target. The following targets are accepted:
Conversion Target | Description |
---|---|
Raw binary data |
|
Formatted data |
|
Compressed graphical data |
|
Compressed graphical data |
|
Compressed data |
16.39.4. Accessory Definitions
In addition to defining a byte sequence, file
statements (but not expressions) may define compile-time constants in the byte block’s namespace.
These constants will have names prefixed by the previous label and the character _
, if such a label exists.
Example:
[] compressed_data file(pbz, "sprites.png") label bg file(pbz, "bg.png")
In the example above, the pbz
target is used.
This target has two accessory definitions: chunks
and tiles
.
Thus, compressed_data
would gain the following members:
-
compressed_data.chunks
-
compressed_data.tiles
-
compressed_data.bg_chunks
-
compressed_data.bg_tiles
Note that the first two refer to the first file
, while the second two refer to the second file
.
The second two are prefixed with bg_
, as the previous label is bg
.
16.39.5. Binary file formats
The filetypes .bin
, .chr
, and .nam
are loaded as raw binary data, with no conversions happening.
16.39.6. .txt
format
The filetype .txt
is interpreted as ASCII data, with newline sequences replaced with a single newline character.
The following newline sequences are replaced with \n
:
-
\r
-
\r\n
-
\n\r
Where \r
has an ASCII value of $0D
, and \n
has an ASCII value of $0A
.
16.39.7. .map
format
The filetype .map
, originating from the NEXXT tool, describes tile maps.
When imported into NESFab, the data is interpretted as a series of nametables, one after another.
Note that this interpretation requires that the tile map have dimensions evenly divisible by 32x30.
16.39.8. .png
format
The filetype .png
is interpreted as a PNG image representing CHR tileset data.
The input image must have dimensions that are multiples of 8 x 8 pixels.
If the PNG image is encoded using a palette, the resulting CHR will use the palette indexes as each pixel’s color, modulo 4. Otherwise, the PNG will be converted to a grayscale image with pixel values in the range [0, 3]; black represents color 0 and white represents color 3.
16.39.9. raw
target
The raw
target imports raw binary data, without performing any filetype conversions.
It accepts no arguments.
Example:
[] sin_table file(raw, "sin_table.bin")
Accessory Definitions
There are no accessory definitions for raw
.
16.39.10. fmt
target
The fmt
target imports data after first processing it using filetype conversions.
It accepts no arguments.
Example:
chrrom file(fmt, "tiles.png")
Accessory Definitions
There are no accessory definitions for fmt
.
16.39.11. pbz
target
The pbz
target compresses the data into the PBZ encoding after first processing it using filetype conversions.
It accepts no arguments.
Example:
[] compressed_data file(pbz, "sprites.png")
Accessory Definitions
-
chunks
: AnInt
equal to the decompressed size divided by 8. -
tiles
: AnInt
equal to the decompressed size divided by 16. If the size is not a multiple of 16, the value is left undefined.
Decompressing
The standard library file pbz.fab
can be used to decompress PBZ-encoded data.
Encoding Description
PBZ is a simple run-length encoding that is good for representing graphical data. As it decompresses into chunks of 8 bytes, it won’t work with arbitrarily-sized data.
The data is formatted as a sequence of compressed 8-byte chunks. The first byte of a chunk encodes it run-length encoding in a unary-encoded format. For each bit of this byte, starting from the highest bit:
-
0
bit: Read a byte from the sequence and output it. -
1
bit: Output the previous byte outputted for this chunk, or$00
if none was.
For example, given the sequence:
$AF $11 $22
The unary-encoded byte is $AF
, which has the binary representation %10101111
.
Starting from the highest bit and working to the lowest bit, the decompressed sequence is:
$00 $11 $11 $22 $22 $22 $22 $22
16.39.12. donut
target
The donut
target compresses the data into the Donut encoding after first processing it using filetype conversions.
It accepts no arguments.
Example:
[] compressed_data file(donut, "sprites.png")
Accessory Definitions
-
chunks
: AnInt
equal to the decompressed size divided by 64.
Decompressing
The standard library file donut.fab
can be used to decompress Donut-encoded data.
16.39.13. rlz
target
The rlz
target compresses the data into the RLZ encoding after first processing it using filetype conversions.
Arguments
-
1st (optional): Include terminator. If
true
, the byte sequence will have a$00
byte appended onto the end. Iffalse
, no$00
will be appended. By default, the value istrue
.
Example:
[] compressed_data file(rlz, "sprites.png", false) file(rlz, "sprites2.png")
Accessory Definitions
There are no accessory definitions for rlz
.
Decompressing
The standard library file rlz.fab
can be used to decompress RLZ-encoded data.
Encoding Description
RLZ is a simple run-length encoding that’s good for data with long sequences of repeating bytes.
The data is formatted as a sequence of runs, where the first byte, N, of a run determines the effect.
-
$00
byte: Terminate the data sequence. -
$01
to$7F
byte: Copy the next byte, (N + 2) times. -
$80
to$FF
byte: Copy the next (N - 127) bytes verbatim.
For example, given the sequence:
$03 $11 $81 $22 $33 $02 $44 $00
The decompressed sequence is:
$01 $01 $01 $01 $01 $22 $33 $44 $44 $44 $44
16.40. macro
The macro
keyword generates and compiles a new source file by substituting its arguments into an existing .macrofab
file.
It is only usable at top-level scope.
Syntax:
macro("macro_name", "args"...)
-
"macro_name"
is the string literal name of the macro file being invoked, without the.macrofile
extension or path. If the string is empty, no macro is invoked. -
"args"
are a comma-separated list of string literals to be substituted into the.macrofab
file.
Macro Files:
Macro (.macrofab
) files resemble regular .fab
files, but have additional syntax:
-
#:identifier:#
declares a macro parameter. The order of these declarations determines the argument order for themacro
keyword. -
#identifier#
expands a macro argument. -
#'identifier'#
expands a macro argument, putting it inside'
quotes and escaping its characters. -
#"identifier"#
expands a macro argument, putting it inside"
quotes and escaping its characters. -
#`test`#
expands a macro argument, putting it inside`
quotes and escaping its characters. -
\-"identifier"-
expands a macro argument, converting underscores to camel-case. -
\="identifier"=
expands a macro argument, converting underscores to snake-case.
Note that macro arguments are not parsed inside comments or string literals.
Example:
// Declare the parameters first: #:my_arg:# #:another_arg:# // Now expand them: vars U #my_arg# = #another_arg#
If this is saved as foo.macrofab
, the macro can be invoked in a .fab
file like so:
macro("foo", "something", "100")
Which would generate the following source file and compile it:
// Declare the parameters first: // Now expand them: vars U something = 100
Note
|
The generated file is not saved to disk. It is compiled, and then forgotten. |
Modifiers:
16.41. mapfab
The mapfab
keyword parses a .mapfab
file and invokes a series of macros based on the data.
It is only usable at top-level scope.
Note
|
MapFab is a level editor designed to be used with NESFab. |
Syntax:
mapfab(target, "mapfab_file", "chr_macro", "palette_macro", "metatiles_macro", "level_macro")
-
target
specifies the output target to use for the level tiles. -
"mapfab_file"
is the string literal path to the.mapfab
or.json
file. -
"chr_macro"
is the name of the macro to invoke for each CHR definition. -
"palette_macro"
is the name of the macro to invoke for each palette definition. -
"metatiles_macro"
is the name of the macro to invoke for each metatile set definition. -
"level_macro"
is the name of the macro to invoke for each level definition.
If any of the macro names are the empty string (""
), those macros are not invoked.
CHR Macro:
The following macro arguments are supplied for each CHR definition:
#:name:# // The name of the CHR definition #:path:# // The path to the CHR definition
Additionally, the following private definitions are defined:
ct Int _index // The unique index of the CHR definition.
Note
|
It can make sense to ignore path , and instead use name to derive the desired path.
|
Palette Macro:
The following macro arguments are supplied for each palette definition:
#:name:# // The name of the palette definition, which is an integer from 0 to 255.
Additionally, the following private definitions are defined:
ct Int _index // The unique index of the palette definition. ct U[25] _palette // The palette's data.
Metatiles Macro:
The following macro arguments are supplied for each metatile set definition:
#:name:# // The name of the metatile definition. #:chr_name:# // The name of the CHR definition the metatile set uses for display. #:palette_name:# // The name of the palette definition the metatile set uses for display.
Note
|
Typically, chr_name and palette_name should be ignored for metatile sets,
as level macros have this information too.
|
Additionally, the following private definitions are defined:
ct Int _index // The unique index of the metatile set definition. ct Int _num // The number of metatiles in the set. ct U[_num] _nw // The north-west tiles of each metatile. ct U[_num] _ne // The north-east tiles of each metatile. ct U[_num] _sw // The south-west tiles of each metatile. ct U[_num] _se // The south-east tiles of each metatile. ct U[_num] _attributes // The 2-bit attribute data of each metatile. ct U[_num] _collisions // The 6-bit collision data of each metatile. ct U[_num] _combined // The two arrays above combined: attribute | (collision << 2) ct U[_num] _combined_alt // The two arrays above combined: (attribute << 6) | collision
If the target is mmt_32
, the following definitions are also defined:
ct Int _mmt_num // The number of metametatiles in the set. ct U[_num] _mmt_nw // The north-west metatiles of each metametatile. ct U[_num] _mmt_ne // The north-east metatiles of each metametatile. ct U[_num] _mmt_sw // The south-west metatiles of each metametatile. ct U[_num] _mmt_se // The south-east metatiles of each metametatile. ct U[_num] _mmt_attributes // The combined 8-bit attribute data of each metametatile.
Levels Macro:
The following macro arguments are supplied for each level set definition:
#:name:# // The name of the level definition. #:chr_name:# // The name of the CHR definition the metatile set uses for display. #:palette_name:# // The name of the palette definition the metatile set uses for display. #:metatiles_name:# // The name of the metatile set definition the metatile set uses for display. #:macro_name:# // Contents of MapFab's macro field.
Additionally, the following private definitions are defined:
ct Int _index // The unique index of the level definition. ct Int _width // The width of the level, in tiles. ct Int _height // The height of the level, in tiles. ct U[_width * _height] _row_major // The level's contents in a row-major order. ct U[_width * _height] _column_major // The level's contents in a column-major order.
For each object class (CLASS
), the following VECs are defined:
ct Int{} _CLASS_x ct Int{} _CLASS_y
For each field (FIELD
) in CLASS
, additional VECs are defined, with the string of each field wrapped inside a cast.
ct TYPE{} _CLASS_FIELD
For example, if the class foo
had three objects in this level,
and each object had a field U bar
, the following definitions would exist:
ct Int{} _foo_x = Int{}(203, -3, 3099) ct Int{} _foo_y = Int{}(13, 991, -30) ct U{} _foo_bar = U{}(U(0), U(5), U(2))
Note
|
Objects are ordered based on their names. |
For each named object, addition Ints are defined. Each Int holds the object’s index in its object array:
ct Int _CLASS_name_NAME = ID
For example, if an object was named foo
and belonged to object class bar
with index 2, the following Int would be defined:
ct Int _bar_name_foo = 2
Output Target Conversions
The data of each level (_row_major
and _column_major
) are converted based on specified target:
Conversion Target | Description |
---|---|
No conversion |
|
Compress using PBZ compression |
|
Compress using RLZ compression (no terminator) |
|
Compress using 32x32 pixel metametatiles. |
Modifiers:
16.41.1. mmt_32
target
The mmt_32
target generates 32x32 pixel metametatiles, where each metametatile is comprised of four 16x16 metatiles.
These metametatiles are not user-defined. Instead, they are computed automatically by scanning the level data.
Note
|
An error will occur if more than 256 metametatiles are found. |
See examples/meta_meta_tiles
for code which decompresses this.
16.42. audio
The audio
keyword imports and converts audio data from an external file,
converting the data into code definitions.
It is only usable at top-level scope.
Syntax:
audio(target, args...)
-
target
specifies the output target to use. -
args…
is a comma-separated list of arguments that the conversion script will use.
Example:
audio(puf1_music, "music.txt")
Output Targets
The following targets are accepted:
Conversion Target | Description | puf1_music |
---|---|---|
Music |
Sound Effects |
16.42.1. puf1_music
target
The puf1_music
target converts music data and generates code compatible with the PUF music engine.
Example:
audio(puf1_music, "music.txt")
Arguments
-
1st (optional): Filename as a string literal. The file should be a
.txt
file exported by FamiTracker. If this argument is left out, definitions will still be generated, albeit with zero tracks.
Definitions
Every generated definition will be prefixed with puf_
, and will have /puf_data
or /puf_omni
as its group.
Because tracks are indexed by number, puf1_music
enumerates each track with a compile-time constant definition.
The names of these definitions are prefixed with puf_track_
, followed by the track’s name converted to lowercase,
with _
characters replacing spaces and other special characters.
For example, if the tracks are:
Main Menu Game Play 1 Death
The following definitions would be defined by puf1_music
:
ct U puf_track_main_menu = 0 ct U puf_track_game_play_1 = 1 ct U puf_track_death = 2
Use
The standard library file puf1.fab
can be used to play the converted music.
A description of how to compose compatible music can be found in that file.
Note
|
You will also need a puf1_sfx audio target.
|
16.42.2. puf1_sfx
target
The puf1_sfx
target converts sound effect data and generates code compatible with the PUF music engine.
Example:
audio(puf1_sfx, "music.txt", "music.nsf")
Arguments
-
1st (optional): Filename as a string literal. The file should be a
.txt
file exported by FamiTracker. If this argument is left out, definitions will still be generated, albeit with zero sound effects. -
2nd (optional): Filename as a string literal. The file should be a
.nsf
file exported by FamiTracker, from the same project as the.txt
. If both arguments are left out, definitions will still be generated, albeit with zero sound effects.
Definitions
Every generated definition will be prefixed with puf_
, and will have /puf_data
or /puf_omni
as its group.
Because sound effects are indexed by number, puf1_sfx
enumerates each track with a compile-time constant definition.
The names of these definitions are prefixed with puf_sfx_
, followed by the sound effect track’s name converted to lowercase,
with _
characters replacing spaces and other special characters.
For example, if the sound effect tracks are:
Attack Double Jump Death
The following definitions would be defined by puf1_sfx
:
ct U puf_sfx_attack = 0 ct U puf_sfx_double_jump = 1 ct U puf_sfx_death = 2
Use
The standard library file puf1.fab
can be used to play the converted sound effects.
A description of how to compose compatible sound effects can be found in that file.
Note
|
You will also need a puf1_music audio target.
|
16.43. stows
See stows
.
16.44. employs
See employs
.
16.45. Implementation Keywords
Keywords prefixed with two underscores (__
) provide access to specific niches of the compiler,
and are primarily intended for use in the standard library, not by users.
16.45.1. __controllers
__controllers
is a value of type Int
which returns the value set by the --controllers
option.
16.45.2. __sector_size
__sector_size
is a value of type Int
which returns the value set by the --sector-size
option.
16.45.3. __expansion_audio
__expansion_audio
is a value of type Int
, determined by the --expansion-audio
option
and the --mapper
option.
Its possible values are below:
Value | Meaning |
---|---|
0 |
No expansion audio |
1 |
MMC5 audio enabled |
2 |
VRC6 audio enabled |
16.45.4. Numeric Constant Types
16.45.5. __mapper
__mapper
is a value of type Int
which returns the INES mapper number set by the --mapper
option.
16.45.6. __illegal
__illegal
is a value of type Bool
which returns true when the compiler supports the generation of illegal instructions.
16.45.7. __mapper_state
__mapper_detail
is a value used by specific mappers (such as MMC1) to workaround interrupts that occur during bank switches.
It is not recommended to use this keyword.
16.45.8. __mapper_reset
__mapper_reset
is a function used by specific mappers (such as MMC1) to reset their state.
It is not recommended to use this keyword.
17. Modifiers
Modifiers add additional metadata to definitions.
Example:
fn foo(U x) U : employs /bar : +align return x + x
17.1. Function Modifier Flags
Modifier flags are specified prefixed with a -
or +
character.
-
is used to disable the modifier, while +
is used to enable it.
The following flags exist for function definitions:
-
+inline
,-inline
: Force / prevent the function from being inlined. -
+align
: Aligns the data to fit inside a 256-byte page (or to 256 bytes otherwise). -
+zero_page
,-zero_page
: Force / prevent variables from using fast zero-page RAM. -
+sram
,-sram
: Force / prevent variables from using SRAM (seesram
). -
+spr_8x16
: Reordersfile
CHR data from 8x16 tiles to 8x8 tiles. -
+graphviz
: Output the function’s intermediate representation in a graphviz file. -
+info
: Output the function’s intermediate representation in a text file. -
+dpcm
: Align and store the data in a ROM location suitable for DPCM. -
+sector
: Align and store the data in a ROM location aligned to the ROM chip’s sectors (for flash saving, etc). -
+static
: Allocate the function or data in every bank (or force them into the fixed bank, for mappers with one). This modifier is incompatible withasm
functions that return in a different bank than they started in. You must validate this yourself. -
+palette_3
: Converts 4-byte palettes into 3-byte palettes. -
+palette_25
: Converts 32-byte palettes into 25-byte palettes. -
+sloppy
,-sloppy
: Enables / disables faster compilation speed, at the cost of performance. -
+fork_scope
: The invoked macro(s) will have access to private definitions inside the invoking file. -
+solo_interrupt
: Disable switchable interrupts and always use this interrupt. -
+unused
: Do not warn if the definition is unused.
Example:
fn foo(U x) U : -inline : +align : +graphviz return x + x
17.2. Loop Modifier Flags
Modifier flags are specified prefixed with a -
or +
character.
-
is used to disable the modifier, while +
is used to enable it.
The following flags exist for loop statements:
-
-unroll
,+unroll
: Hint to prevent/enable loop unrolling. -
+unloop
: Hint to unroll a loop completely, replacing the loop.
Example:
for U i = 0; i < 10; i += 10 : -unroll {PPUDATA}(i)
17.3. nmi
Syntax:
: nmi nmi_handler
-
nmi_handler
is anmi
function handler to be used while this mode is executing.
The nmi
modifier is optional. Modes without one will use an NMI handler that immediately returns from the interrupt.
Example:
nmi my_nmi() {PPUMASK}(PPUMASK_ON) mode foo() : nmi my_nmi // ...
17.4. irq
Syntax:
: irq irq_handler
-
irq_handler
is anirq
function handler to be used while this mode is executing.
The irq
modifier is optional. Modes without one will use an IRQ handler that immediately returns from the interrupt.
Example:
irq my_irq() {PPUMASK}(PPUMASK_ON) mode foo() : irq my_irq // ...
17.5. stows
The stows
modifier is used inside charmap
definitions
to enable string literals to use said charmap
.
Syntax:
: stows /group_name
-
/group_name
is a singledata
group which string literals will be stored in.
17.5.1. stows omni
The stows omni
modifier behaves like stows
, except it stores its data inside an omni data
group.
Syntax:
: stows omni /group_name
-
/group_name
is a singleomni data
group which string literals will be stored in.
17.6. employs
The employs
modifier instructs a function to be dependent on a group.
From the time the function is called to the time the function returns,
the memory associated with that group will be usable by the function.
Normally, the compiler automatically infers the groups a function depends on.
The employs
modifier is only required in these circumstances:
-
A value is read or written using a hardware address (type
AA
orAAA
). -
The modified function is an
asm fn
.
Syntax:
: employs /group_names
-
/group_names
is an optional list of groups.
17.6.1. employs vars
and employs data
For additional control, employs vars
and/or employs data
modifiers can be used.
These behave like employs
, but only include the vars
and/or data
definitions of each groups.
Syntax:
: employs vars /group_names : employs data /group_names
-
/group_names
is an optional list of groups.
17.7. preserves
The preserves
modifier is used inside a goto mode
statement
to specify which variables are kept, and which are reset to their initial value.
Syntax:
: preserves /group_names
-
/group_names
is an optional list ofvars
groups.
If a global variable is not in a preserved group, it will be reset to its initial value if one exists. If no initial value was specified, the value will enter an undefined (garbage) state.
17.8. data
Syntax:
: data /group_names
-
/group_names
is an optional list ofdata
groups.
The function will be checked by the compiler to ensure it only uses data from the listed groups.
18. Operators
18.1. Operator Tables
18.1.1. Unary Operators
Note
|
Operators with lower precedence numbers come earlier in the order of operations. |
Operator | Precedence | Description |
---|---|---|
|
4 |
|
|
8 |
|
|
8 |
|
|
8 |
|
|
8 |
|
|
8 |
18.1.2. Binary Operators
Note
|
Operators with lower precedence numbers come earlier in the order of operations. |
Operator | Precedence | Associativity | Description |
---|---|---|---|
|
5 |
Left |
|
|
10 |
Left |
|
|
11 |
Left |
|
|
11 |
Left |
|
|
12 |
Left |
|
|
13 |
Right |
|
|
14 |
Left |
|
|
14 |
Left |
|
|
15 |
Left |
|
|
16 |
Left |
|
|
17 |
Left |
|
|
18 |
Left |
|
|
18 |
Left |
|
|
18 |
Left |
|
|
18 |
Left |
|
|
19 |
Left |
|
|
19 |
Left |
|
|
20 |
Left |
|
|
21 |
Left |
|
|
28 |
Right |
|
|
29 |
Left |
|
|
30 |
Right |
|
|
30 |
Right |
|
|
30 |
Right |
|
|
30 |
Right |
|
|
30 |
Right |
|
|
30 |
Right |
|
|
30 |
Right |
|
|
30 |
Right |
|
|
30 |
Right |
18.1.3. Function-like Operators
Note
|
All function-like operators have left associativity and evaluate first in the order of operations. |
Operator | Description |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18.2. Operator Listings
18.2.1. Get Pointer @
Converts an lvalue pointer-addressable array into a corresponding pointer.
18.2.2. Get Hardware Address &
Converts an lvalue into its corresponding hardware address, of type AA
or AAA
.
This operator is intended to be used with inline assembly code.
Although this operator by itself is safe, dereferencing the addresses it returns can easily cause undefined behavior.
For regular code, it’s recommended to use Get Pointer @
instead.
18.2.3. Unary Plus +
Returns its operand, type and value unchanged. The operand must be an arithmetic type.
Example:
+100 // Equivalent to 100
18.2.4. Unary Negate -
Returns its operand subtracted from zero, type unchanged. The operand must be an arithmetic type.
Example:
-100 // Equivalent to (0 - 100)
18.2.5. Unary Bitwise NOT ~
Returns its operand with every bit flipped (1 becomes 0, and vice versa), type unchanged. The operand must be an arithmetic type.
Example:
U bits = %1010 ~bits // Equivalent to %11110101
18.2.6. Unary Logical NOT !
Returns its operand, converted to type Bool
, then negated (true
becomes false
and vice versa). The operand must be an arithmetic type.
Example:
!0 // Equivalent to true !5 // Equivalent to false !true // Equivalent to false !false // Equivalent to true
18.2.7. Member Access .
Operator .
is used to access members and nested values, and works similarly to other languages.
Its behavior depends on the left hand side of the operator:
-
For structure values, returns the specified member as an lvalue.
-
For scalar values, returns the specified byte or pointer member as an lvalue.
-
For
fn
values and PAA values, returns thect
value in its scope.
Additionally, if the left hand side is an asm fn
and the right hand side is a label
,
the result is a callable function with the label being the entry point.
Example:
foo.bar = 10 // Modify a struct member some_uu.a = 10 // Modify a byte some_asm_fn.some_label(10) // Call an assembly function
Scalar value members:
Scalars of types such as UU
, SSSF
, or CCC
have the following members defined for them, when applicable:
Member | Defined For | Byte |
---|---|---|
|
Scalars |
1st (lowest) whole byte |
|
Scalars |
2nd whole byte |
|
Scalars |
3d whole byte |
|
Scalars |
1st (highest) fractional byte |
|
Scalars |
2nd fractional byte |
|
Scalars |
3rd fractional byte |
|
Only Pointers |
Bank byte |
|
Only Pointers |
Non-banked pointer |
These members can be read or set using operator .
like struct members can.
Example:
UUU foo = $123456 foo.a = 0 // Set the low byte. foo is now $123400 foo.b = 0 // Set the middle byte. foo is now $120000 foo.c = 0 // Set the high byte. foo is now $000000
18.2.8. Multiply *
Returns its operands multiplied together, of a type large enough to hold the product. The return type is signed if either operand is signed, but unsigned otherwise. The operands must be quantity types.
To be more precise, if the operand types have F
and F'
fractional bytes, the return type will have F + F'
.
Likewise, if the operand types have W
and W'
whole bytes, the return type will have W + W'
.
The return type will be truncated to fit the compiler’s available types.
Example:
5 * 3 // Equivalent to 15, of type Int U(5) * U(8) // Equivalent to 40, of type UU UF(5.5) * SS(-10) // Equivalent to -55, of type SSSF
Note
|
Multiplying two variables together is a very slow operation, but multiplying a variable by a constant is faster since the compiler can convert the expression to a sequence of shifts and adds. However, if you need to do lots of multiplications, consider using lookup tables instead. |
18.2.9. Assign by Multiply (*=
)
Multiplies its operands together, then assigns the value to the lvalue left operand, converting as needed. Returns the left operand’s new value.
Example:
U a a *= b // Equivalent to a = U(a * b)
18.2.10. Add +
Returns the sum of its operands.
The operands must be of the same quantity type, although Int
and Real
will convert.
Example:
3 + 7 // Equivalent to 10
18.2.11. Assign by Add +=
Converts the right operand to the left operand’s type, then performs an addition using both operands and assigns the value to the lvalue left operand.
Return the carry: a value of type Bool
that is true
when the resulting sum overflowed, and false
otherwise.
Example:
U x = 200 x += 50 // 'x' is now equal to 50. The expression returns 'false'. x += 100 // 'x' is now equal to 94 due to overflow. The expression returns 'true'.
Note
|
Unlike in other languages, this operator doesn’t return its left operand. |
18.2.12. Subtract -
Returns the difference of its operands (the right operand subtracted from the left).
The operands must be of the same quantity type, although Int
and Real
will convert.
Example:
10 - 7 // Equivalent to 3
18.2.13. Assign by Subtract -=
Converts the right operand to the left operand’s type, then performs a subtraction using both operands and assigns the value to the lvalue left operand.
Return the carry: a value of type Bool
that is false
when the resulting sum underflowed, and true
otherwise.
Example:
U x = 200 x -= 50 // 'x' is now equal to 150. The expression returns 'true'. x -= 300 // 'x' is now equal to 106 due to underflow. The expression returns 'false'.
18.2.14. Rotate Left <-<
Moves each of the bits of the left operand one place to the left, with the lowest bit being filled with the value of the right operand.
The left operand must be a type_quantity, and the right operand must be type Bool
.
The return type matches the left operand’s type.
Example:
U(%11001010) <-< false // Equivalent to U(%10010100) U(%11001010) <-< true // Equivalent to U(%10010101) U(%01111111) <-< false // Equivalent to U(%11111110)
18.2.15. Assign by Rotate Left <=<
Performs a left rotation using both operands, then assigns the value to the lvalue left operand.
Returns the carry: a value of type Bool
equal to left operand’s highest bit prior to the operation.
Example:
U foo = %11001010 foo <=< false // Sets 'foo' to U(%10010100). Returns true.
18.2.16. Rotate Right >->
Moves each of the bits of the right operand one place to the right, with the highest bit being filled with the value of the left operand.
The right operand must be a type_quantity, and the left operand must be type Bool
.
The return type matches the right operand’s type.
Example:
false >-> U(%11001010) // Equivalent to U(%01100101) true >-> U(%11001010) // Equivalent to U(%11100101) false >-> U(%11111110) // Equivalent to U(%01111111)
Note
|
This operation corresponds to the 6502 assembly instruction ROR .
|
18.2.17. Assign by Rotate Right >=>
Performs a right rotation using both operands, then assigns the value to the lvalue right operand.
Returns the carry: a value of type Bool
equal to right operand’s lowest bit prior to the operation.
Example:
U foo = %11001010 false >=> foo // Sets 'foo' to %01100101. Returns false.
Note
|
This operator requires an lvalue on the right side of the operator, which is unlike other assignment operators. |
18.2.18. Shift Left <<
Moves each of the bits of the left operand to the left N places, where N is the right operand of type U
,
and filling blank spaces with 0
.
The return type matches the left operand’s type.
Example:
U(%11110001) << 1 // Equivalent to U(%11100010) U(%11110001) << 3 // Equivalent to U(%10001000)
Note
|
The NES performs shifts one bit at a time, meaning x << 1 is five times faster than x << 5 ,
and shifting by a variable (x << y) generates a loop in the assembly.
|
18.2.19. Assign by Shift Left <<=
Performs a left shift using both operands, then assigns the value to the lvalue left operand.
Returns the carry: a value of type Bool
equal to last bit shifted out (or false
if no shifting occurred).
Example:
U foo = %11001010 foo <<= 2 // Sets 'foo' to U(%00101000). Returns true.
Note
|
Unlike in other languages, this operator doesn’t return its left operand. |
18.2.20. Shift Right >>
Moves each of the bits of the left operand to the right N places, where N is the right operand of type U
.
If the left operand is unsigned, the blank spaces are filled with 0
,
otherwise the blank spaces are filled with the highest bit of the left operand (this is called sign extension).
The return type matches the left operand’s type.
Example:
U(%11110001) >> 1 // Equivalent to U(%01111000) S(%11110001) >> 1 // Equivalent to S(%11111000) U(%11110001) >> 3 // Equivalent to U(%00011110) S(%11110001) >> 3 // Equivalent to S(%11111110) S(%01110001) >> 3 // Equivalent to S(%00001110)
Note
|
The NES performs shifts one bit at a time, meaning x >> 1 is five times faster than x >> 5 ,
and shifting by a variable (x >> y) generates a loop in the assembly.
|
18.2.21. Assign by Shift Right >>=
Performs a right shift using both operands, then assigns the value to the lvalue left operand.
Returns the carry: a value of type Bool
equal to last bit shifted out (or false
if no shifting occurred).
Example:
U foo = %11001010 foo >>= 2 // Sets 'foo' to U(%00110010). Returns true.
Note
|
Unlike in other languages, this operator doesn’t return its left operand. |
18.2.22. Bitwise AND &
Applies the AND operation across each bit of the operands, returning the result.
The operands must be of the same arithmetic type, although Int
and Real
will convert.
Example:
U(%11110000) & U(%10101010) // Equivalent to U(%10100000)
18.2.23. Assign by Bitwise AND &=
Converts the right operand to the left operand’s type, then performs a bitwise AND using both operands and assigns the value to the lvalue left operand. Returns the left operand’s new value.
Example:
U foo = %11110000 foo &= %10101010 // Sets 'foo' to U(%10100000)
18.2.24. Bitwise XOR ^
Applies the XOR operation across each bit of the operands, returning the result.
The operands must be of the same arithmetic type, although Int
and Real
will convert.
Example:
U(%11110000) ^ U(%10101010) // Equivalent to U(%01011010)
18.2.25. Assign by Bitwise XOR ^=
Converts the right operand to the left operand’s type, then performs a bitwise XOR using both operands and assigns the value to the lvalue left operand. Returns the left operand’s new value.
Example:
U foo = %11110000 foo ^= %10101010 // Sets 'foo' to U(%01011010)
18.2.26. Bitwise OR |
Applies the OR operation across each bit of the operands, returning the result.
The operands must be of the same arithmetic type, although Int
and Real
will convert.
Example:
U(%11110000) | U(%10101010) // Equivalent to U(%11111010)
18.2.27. Assign by Bitwise OR |=
Converts the right operand to the left operand’s type, then performs a bitwise OR using both operands and assigns the value to the lvalue left operand. Returns the left operand’s new value.
Example:
U foo = %11110000 foo |= %10101010 // Sets 'foo' to U(%11111010)
18.2.28. Logical AND &&
Implements the "short-circuit" version of the AND operation from boolean logic.
Evaluates the left operand and converts it to Bool
. If it is false
, the operator returns false
.
Otherwise, it evaluates the right operand and returns its value converted to Bool
.
Example:
false && false // Returns false true && false // Returns false false && true // Returns false true && true // Returns true
18.2.29. Logical OR ||
Implements the "short-circuit" version of the OR operation from boolean logic.
Evaluates the left operand and converts it to Bool
. If it is true
, the operator returns true
.
Otherwise, it evaluates the right operand and returns its value converted to Bool
.
Example:
false || false // Returns false true || false // Returns true false || true // Returns true true || true // Returns true
18.2.30. Less Than <
Compares the arithmetic type operands, returning true
if the left operand is less than the right and false
otherwise.
The operands may be of different types. No types conversions occur besides Int
and Real
conversions.
Example:
3 < 10 // Returns true 3 < -10 // Returns false
18.2.31. Less Than or Equal To <=
Compares the arithmetic type operands, returning true
if the left operand is less than or equal to the right and false
otherwise.
The operands may be of different types. No types conversions occur besides Int
and Real
conversions.
Example:
3 <= 3 // Returns true 3 <= -10 // Returns false
18.2.32. Greater Than >
Compares the arithmetic type operands, returning true
if the left operand is greater than the right and false
otherwise.
The operands may be of different types. No types conversions occur besides Int
and Real
conversions.
Example:
3 > 10 // Returns false 3 > -10 // Returns true
18.2.33. Greater Than or Equal To >=
Compares the arithmetic type operands, returning true
if the left operand is greater than or equal to the right and false
otherwise.
The operands may be of different types. No types conversions occur besides Int
and Real
conversions.
Example:
3 >= 3 // Returns true 3 >= 10 // Returns false
18.2.34. Equal To ==
Compares the arithmetic type operands, returning true
if the left operand is equal to the right and false
otherwise.
The operands may be of different types. No types conversions occur besides Int
and Real
conversions.
Example:
3 == 3 // Returns true 3 == 10 // Returns false
18.2.35. Not Equal To !=
Compares the arithmetic type operands, returning true
if the left operand is equal to the right and false
otherwise.
The operands may be of different types. No types conversions occur besides Int
and Real
conversions.
Example:
3 != 3 // Returns false 3 != 10 // Returns true
18.2.36. Assign =
Stores the right operand into the left and returns the new value of the left operand.
The right operand must have the same type as the left operand, although Int
and Real
will convert.
Example:
U foo foo = 10 // 'foo' is now equal to 10
18.3. Array and pointer access
18.3.1. []
U-indexed access
Accesses the element value at an offset of type U, with 0-based indexing.
Syntax:
value[offset]
Example:
array[20] = 10 // Set a TEA element x = @paa[10] // Read a PAA element
18.3.2. {}
UU-indexed access
Accesses the element value at an offset of type UU, with 0-based indexing.
Syntax:
value[offset]
Example:
array{2000} = 10 // Set a TEA element x = @paa{1000} // Read a PAA element
Note
|
{} often has significantly worse performance than [] .
|
18.4. Hardware operators
18.4.1. {}()
Hardware read
Returns the value at a hardware address.
Syntax:
{address}()
-
address
is a value of typeAA
.
Example:
U status = {PPUSTATUS}() // Read the PPU's status register
18.4.2. {,}()
Hardware read (pair)
Two compile-time constant addresses can be supplied to a hardware read expression, separated by a comma. Both addresses will be read, with the expression returning the result of the second one.
In the generated assembly, NESFab will ensure that both reads occur together, without any code between them.
Syntax:
{address_a, address_b}()
-
address_a
andaddress_b
are compile-time constant values of typeAA
.
Example:
U status = {PPUSTATUS, PPUSTATUS}() // Read the PPU's status register twice.
18.4.3. {}()
Hardware write
Assigns a value at a hardware address.
Syntax:
{address}(value)
-
address
is a value of typeAA
. -
value
is an expression of typeU
.
Example:
{PPUDATA}($80) // Write to the PPU's data register
18.4.4. {,}(,)
Hardware write (pair)
Two compile-time constant addresses can be supplied to a hardware write expression, separated by a comma. Both addresses will be written by their corresponding arguments.
In the generated assembly, NESFab will ensure that both writes occur together, without any code between them.
Syntax:
{address_a, address_b}(value_a, value_b)
-
address_a
andaddress_b
are compile-time constant values of typeAA
. -
value_a
andvalue_b
are expressions of typeU
.
Example:
{PPUDATA, PPUDATA}(10, 20) // Write to the PPU's data register twice
Note
|
This expression is most useful for mappers that require two hardware writes to interface, like MMC3. |
18.5. Function calls
Calls the named function It behaves similarly to function calls in other languages.
Syntax:
fn_expression(argument_expressions...)
-
fn_expression
is the function to be called. -
argument_expressions
are a comma-separated list of expressions to be passed to the function as argument.
Example:
foo(1, 2, 3)
18.6. sizeof
sizeof
expressions return the size in bytes of something.
18.6.1. sizeof
Type
Returns the size in bytes of values belonging to a type as a compile-time constant value of type Int
.
Syntax:
sizeof Type
-
Type
is a type name.
Example:
ct Int uu_size = sizeof UU
18.6.2. sizeof
(expression)
Returns the size in bytes of an expression’s value as a compile-time constant value of type Int
.
Syntax:
sizeof(expression)
-
expression
is an expression to calculate the size of. The expression will not be executed.
Example:
ct Int expr_size = sizeof(1 + 2 + 3)
18.7. len
len
expressions return the number of array elements.
18.7.1. len
Type
Returns the number of array elements of values belonging to a type as a compile-time constant value of type Int
.
Syntax:
len Type
-
Type
is a typed element array type.
Example:
ct Int array_len = len UU[10]
18.7.2. len
(expression)
Returns the number of array elements of an expression’s value as a compile-time constant value of type Int
.
Syntax:
len(expression)
-
expression
is an array expression to calcalute the length of. The expression will not be executed.
Example:
ct Int expr_len = len(U[10](3))
18.7.3. abs
(expression)
abs
first converts the argument to a signed representation,
then it returns the absolute value of it.
The argument must be an arithmetic type.
The return type is the argument type converted to unsigned.
Syntax:
abs(expression)
-
expression
is an expression with an arithmetic type.
Example:
UU positive = abs(SS(-10))
18.7.4. min
(expression)
Returns the minimum value of multiple quantity types.
The return type is equivalent to the argument types, which must be the the same,
although Int
and Real
will convert.
Syntax:
min(expressions...)
-
expressions
is a comma-separated list of at least two expressions with quantity types.
Example:
SS smallest = min(10, 20, 30)
18.7.5. max
(expression)
Returns the maximum value of multiple quantity types.
The return type is equivalent to the argument types, which must be the the same,
although Int
and Real
will convert.
Syntax:
max(expressions...)
-
expressions
is a comma-separated list of at least two expressions with quantity types.
Example:
SS largest = max(10, 20, 30)
18.8. Explicit Type Casts
Type casts convert values into different types.
Basic syntax:
Type(value)
Where type
is any type name, and value
is any expression.
Example:
fn example() U some_u = 10 SS some_ss = SS(some_u) // An explicit cast to SS UUF some_uuf = UUF(some_u) // An explicit cast to UUF
When casting between numeric types, the bytes closest to U
are preserved.
This means that casting UUU
to U
discards the two most significant bytes, while FFF
to F
discards the two least.
When casting from signed types to unsigned types of the same size, the bit pattern of the value is unchanged.
For example, S(-1)
will cast to U(255)
.
18.8.1. Zero Initializations
A type cast with no arguments is called a zero initialization. The value, its struct members, and its array elements are initialized to zero.
U() // equivalent to U(0) UF() // equivalent to U(0.0) U[3]() // equivalent to U[3](0, 0, 0) SomeStruct() // equivalent to SomeStruct(0, 0)
18.8.2. Banked Pointer Initializations
Banked pointers can be created with a two-argument cast:
CCC/my_group(address, bank)
The first argument is an unbanked pointer, while the second is the type U
bank.
18.8.3. Array Fills
Array fills are used to create arrays where every element holds identical values. They are specified as an array cast, where the cast’s singular argument can be converted to the element type.
S[2](50) // equivalent to S[2](50, 50) U[5](11) // equivalent to U[5](11, 11, 11, 11, 11)
The meaning of the indentation is determined by the first line - it might be a construct like if
or else
,
a definition like a function, or something else.
19. Literals
19.1. Boolean Literals
There are two boolean literals:
Boolean literals have the type Bool
.
19.2. Numeric Literals
Numeric literals can be either decimal, hexadecimal, or or binary. Hexadecimal literals are prefixed with a $
character, while binary with %
:
1234 // Decimal $89AB // Hexadecimal %1010 // Binary
Literals can use decimal points:
12.34 // Decimal $.89AB // Hexadecimal %1010. // Binary
Literals with decimal points have the type Real
, while those without are Int
.
19.3. String and Character Literals
19.3.1. Syntax and Semantics
Basic syntax:
'c' // Character using the default charmap "NES" // Uncompressed string using the default charmap `NES` // Compressed string using the default charmap 'c'some_charmap // Character using a specific charmap "NES"some_charmap // Uncompressed string using a specific charmap `NES`some_charmap // Compressed string using a specific charmap
Character literals have the type U
. String literals have the type U[N]
, where N
is the length of the string.
Each string and character literal uses a charmap
to translate the characters from Unicode into the charmap’s range.
The `charmap
is set by an identifier following the literal.
If no identifier follows, the default charmap
is used.
Strings can be uncompressed or compressed. Compressed strings use a byte-pair encoding, where unused values in the charmap represent pairs of bytes. These pairs are expanded recursively to decompress the string.
The nice thing about byte-pair encoding is that uncompressed strings are valid under the encoding too. This means functions for compressed strings also work with uncompressed strings.
19.3.2. Escape Sequences
Escape sequences denote characters that are either impossible or unwiedly to write otherwise.
Every escape sequence begins with backslash character \
, followed by one or more characters.
For example, to denote the apostrophe character, you must use an escape sequence, as '''
is not valid syntax.
Likewise, to write a string containing line breaks, you must use escape sequences.
'\'' // An apostrophe character literal "Hello\nWorld!" // A multi-line string literal
The valid escape sequences are listed below.
Escape Sequence | Unicode Code Point | Description |
---|---|---|
|
$00 |
Sentinel |
|
$07 |
Bell or alert |
|
$08 |
Backspace |
|
$09 |
Tab |
|
$0A |
Newline |
|
$0B |
Vertical tab |
|
$0C |
Form feed |
|
$0D |
Carriage return |
|
$22 |
Quotation mark |
|
$27 |
Apostrophe |
|
$5C |
Backslash |
|
$60 |
Backtick |
|
$FFFFFFFF |
Special meaning for |
Unicode code points can also be specified directly. Below, each N character represents any hexadecimal digit.
Escape Sequence | Unicode Code Point |
---|---|
|
$NN |
|
$NNNN |
|
$NNNNNNNN |
20. Types
20.1. Scalar Types
20.1.1. Integer Types
Unsigned integer types are expressed using the character U
, while signed integer types use S
.
The values of signed integers are expressed in two’s complement form.
The 6 integer types are listed below.
Type | Size (bytes) | Value range |
---|---|---|
|
1 |
[0, 255] |
|
1 |
[-128, 127] |
|
2 |
[0, 65535] |
|
2 |
[-32768, 32767] |
|
3 |
[0, 16777215] |
|
3 |
[-8388608, 8388607] |
20.1.2. Unit-Fractional Types
Type | Size (bytes) | Value range |
---|---|---|
|
1 |
[$0.00, $0.FF] |
|
2 |
[$0.0000, $0.FFFF] |
|
3 |
[$0.000000, $0.FFFFFF] |
20.1.3. Fixed-point Types
An integer type and a unit-fractional type can be combined to form a fixed-point type, merging the ranges of both. The syntax is the integer type, followed by the unit-fractional type, without no other characters in-between.
There are 18 possible fixed-point types, but only 3 are listed below. The rest can be inferred from the tables above.
Type | Size (bytes) | Value range |
---|---|---|
|
2 |
[$00.00, $FF.FF] |
|
3 |
[-$8000.00, $7FFF.FF] |
|
4 |
[$00.000000, $FF.FFFFFF] |
20.1.4. Numeric Constant Types
Int
and Real
are used for compile-time constants.
Literal expressions denoting integers, like 1234
or $40
, have the type Int
.
Likewise, literal expressions with a .
to denote fractions, like 3.14
, 0.5
, or 100.0
, have the type Real
.
Values of these types can only exist at compile-time. Any attempt to use them at run-time will error.
Type | Size (bytes) | Value range |
---|---|---|
|
4 |
[-2147483648, 2147483647] |
|
7 |
[-$80000000.000000, $7FFFFFFF.FFFFFF] |
In many cases, Int
and Real
implicitly convert to other numeric types based on context.
For example, when using Int
or Real
in an operator like +
,
the value converts to match the other operand’s type.
U(a) + Int(b) // 'b' will implicitly convert to type U.
When converting Real
to a smaller representation, the value will be rounded.
This is in contrast to other type conversions, which truncate.
The purpose of this rounding is to improve the accuracy of Real
constants.
U x = U(1.75) // 'x' is set to 2 via rounding, NOT 1. U x = U(UF(1.75)) // 'x' is set to 1 via truncation, NOT 2.
Although Int
and Real
cannot be used at run-time,
it is very useful to use them at compile-time.
For example, you may use them to define numeric constants:
ct Int MEANING_OF_LIFE = 42 ct Real PI = 3.14159265359 ct Real GOLDEN_RATIO= 1.61803398875
20.1.5. Bool
The type Bool
is short for "boolean", and represents the values true
and false
.
Type | Size (bytes) | Value range |
---|---|---|
|
1 |
[ |
The Bool
type is space-inefficient, as each value takes a byte if stored in memory.
If you need lots of boolean values, it’s recommended to combine them into bit-field flags using an integer type:
ct U IS_ALIVE = 1 << 0 ct U IS_MOVING = 1 << 1 ct U IS_HAPPY = 1 << 2 fn example() U flags = 0 flags |= IS_ALIVE // set flags flags &= ~IS_MOVING // clear flags if flags & IS_HAPPY // test flags // . . .
20.1.6. Void
The type Void
represents no value.
Its purpose is to represent the return type of functions that return no value.
Note that you do not have to use the Void
keyword,
as leaving the return type off of a function implies Void
.
Type | Size (bytes) | Value range |
---|---|---|
|
0 |
N/A |
20.2. Arithmetic Types
Arithmetic types are defined as all integer types, fixed-point types, and Bool
.
20.3. Quantity Types
Quantity types are types which can represent quantities. They are defined as all integer types, fixed-point types.
20.3.1. Pointer Types
Pointer types represent addresses. Unlike pointers in other languages, pointers in NESFab can only point to pointer-addressible arrays.
There are six types of pointers (not including function pointers, listed below:
Type | Size (bytes) | Value range | Pointed-to Memory |
---|---|---|---|
|
2 |
[$0000, $FFFF] |
ROM |
|
3 |
[$00:$0000, $FF:$FFFF] |
ROM |
|
2 |
[$0000, $FFFF] |
RAM |
|
3 |
[$00:$0000, $FF:$FFFF] |
RAM |
|
2 |
[$0000, $FFFF] |
RAM or ROM |
|
3 |
[$00:$0000, $FF:$FFFF] |
RAM or ROM |
Banked vs Unbanked
The two-letter pointers represent a 16-bit address, while the three-letter pointers represent a 16-bit address along with an 8-bit bank.
Mutable vs Constant
The letter C
in the names stands for "constant", while M
stands for "mutable".
It is possible to modify memory using M
pointers, but not C
pointers.
Commonly, C
pointers are used for ROM data, while M
pointers are used for RAM data.
Groups
Pointer types are associated with one or more groups, and their values can only point to values of those groups.
To include a group in a pointer type, append the group name onto it.
For example, to create a pointer that can reference values belonging to the groups /foo
and /bar
, the syntax is:
CC/foo/bar
20.3.2. Address Types
The address type AA
is used for 16-bit hardware addresses,
and has the same representation as pointers, ignoring groups.
This type is used for inline assembly and hardware expressions.
AAA
behaves like AA
, but has an additional byte which tracks the address’s bank.
Type | Size (bytes) | Value range |
---|---|---|
|
2 |
[$0000, $FFFF] |
|
3 |
[$00:$0000, $FF:$FFFF] |
20.3.3. Function Pointer Types
Function pointer types can be expressed using the prefix Fn
, followed by their corresponding set.
For more information, see the function pointer section.
Syntax:
Fn.set
-
set
is the name of a fn set.
The size of function pointer types is unspecified, although it is typically 2 or 3 bytes.
20.4. Arrays
NESFab has three kinds of array types with different restrictions for each. The first and second hav value semantics and behaves like arrays do in other languages, but cannot be referenced by pointers. The third can, but it very limited otherwise and can only represent elements of type U.
20.4.1. Typed Element Arrays (TEAs)
A TEA is an array of a specified type, of any size between 0 and 65536. Two TEA variables are shown below.
fn example() // Syntax is TYPE[SIZE] UU[10] a_tea_variable SomeStruct[1500] another_tea_variable
At the type level, TEAs are strictly one-dimensional. That is, one cannot have a TEA of a TEA. The compiler will enforce this even if TEAs are hidden inside structs.
struct SomeStruct U[30] struct_tea fn foo() U[10][20] // Error! Can't have multi-dimensional TEAs SomeStruct[50] // Error! Can't have multi-dimensional TEAs
A TEA value can be created using a cast. The size can be left blank and the compiler will infer it.
fn example() U[3] small_tea = U[3](10, 20, 30) // Initialize with value U[] inferred_tea = U[](3, 1, 4) // Inferred size
TEAs are always copied by value. To copy more or fewer elements, the TEA can be cast to a different size before copying. When casting to a larger TEA, the new elements at the end of the TEA are zero-initialized.
fn example() U[3] small_tea = U[3](10, 20, 30) U[3] copied_tea = small_tea U[2] smaller_tea = U[2](small_tea) U[5] larger_tea = U[5](small_tea)
Operator []
or operator {}
can be used to read or write the elements of a TEA value.
The indexing is zero-based.
-
[]
expects an index argument of typeU
, limiting it to the first 256 elements. -
{}
expects an index argument of typeUU
.
fn example() UU[1500] a_tea_variable a_tea_variable[4] = 64 // Set the 5th element to 64. a_tea_variable{999} = 10 // Set the 1000th element to 10. a_tea_variable[999] = 10 // Error! 999 can't convert to type U.
The rationale for two different indexing operators is performance;
in general, []
executes significantly faster than {}
,
as the NES only supports 8-bit indexing natively.
Note that in some cases, the compiler will be able to optimize operator {}
to take advantage of 8-bit indexing on the NES’s hardware.
One such pattern it recognizes is:
tea{U(index) + CONSTANT}
But in general, one should prefer to use []
, with TEAs of size 256 or less.
20.4.2. Vectors (VECs)
A VEC is an compile-time constant array of a specified type, of any size. They are similar to TEAs, but can be resized dynamically.
ct fn example() // Syntax is TYPE{} UU{} a_vec SomeStruct{} another_vec_variable
VECs do not store their size in their type.
Unlike TEAs, VECs can be multidimensional. However, VECs can only exist at compile-time.
A VEC value can be created using a cast. The size can be left blank and the compiler will infer it.
ct fn example() U{} small_vec = U{}(10, 20, 30) // Initialize with value
Like TEAs, operator []
or operator {}
can be used to read or write the elements of a VEC value.
The indexing is zero-based.
-
[]
expects an index argument of typeU
, limiting it to the first 256 elements. -
{}
expects an index argument of typeUU
.
ct fn example() UU{} a_vec_variable = UU{}(UU[1500]()) a_vec_variable[4] = 64 // Set the 5th element to 64. a_vec_variable{999} = 10 // Set the 1000th element to 10. a_vec_variable[999] = 10 // Error! 999 can't convert to type U.
Note
|
VECs are intended to be used for generating data at compile-time.
For example, one can write a ct fn which compresses level data, using VECs.
|
20.4.3. Pointer-Addressable Arrays (PAAs)
PAAs only support a single element type: U
, and can only be defined at global scope.
Like TEAs, they can be any size from 1 to 65536.
vars /example // Syntax is [SIZE] // (You do not specify a type, as it is always type U) [10] a_paa [1500] another_paa fn example([100] bad_param) // Error! Cannot use PAAs as fn parameters [100] bad_variable // Error! Cannot use PAAs as local variables
PAAs cannot be created using casts and there is no support for copying them.
Like TEAs, operator []
or operator {}
can be used to read or write the elements of a pointer value.
The indexing is zero-based.
-
[]
expects an index argument of typeU
, limiting it to the first 256 elements. -
{}
expects an index argument of typeUU
.
vars /example [10] a_paa fn example() MM/example ptr = @a_paa // Get a pointer to 'a_paa'
20.5. Structures
Structures (or "structs") are record types, which allows one to aggregate multiple types into a single one.
See struct
.