prog8/docs/source/programming.rst
2024-11-09 13:31:54 +01:00

904 lines
46 KiB
ReStructuredText

====================
Programming in Prog8
====================
This chapter describes a high level overview of the elements that make up a program.
Details about the syntax can be found in the :ref:`syntaxreference` chapter.
Elements of a program
---------------------
Program
Consists of one or more *modules*.
Module
A file on disk with the ``.p8`` suffix. It can contain *directives* and *code blocks*.
Whitespace and indentation in the source code are arbitrary and can be mixed tabs or spaces.
A module file can *import* other modules, including *library modules*.
It should be saved in UTF-8 encoding.
Comments
Everything on the line after a semicolon ``;`` is a comment and is ignored by the compiler.
If the whole line is just a comment, this line will be copied into the resulting assembly source code for reference.
There's also a block-comment: everything surrounded with ``/*`` and ``*/`` is ignored and this can span multiple lines.
This block comment is experimental for now: it may change or even be removed again in a future compiler version.
The recommended way to comment out a bunch of lines remains to just bulk comment them individually with ``;``.
Directive
These are special instructions for the compiler, to change how it processes the code
and what kind of program it creates. A directive is on its own line in the file, and
starts with ``%``, optionally followed by some arguments. See the syntax reference for all directives.
Code block
A block of actual program code. It has a starting address in memory,
and defines a *scope* (also known as 'namespace').
It contains variables and subroutines.
More details about this below: :ref:`blocks`.
Variable declarations
The data that the code works on is stored in variables ('named values that can change').
The compiler allocates the required memory for them.
There is *no dynamic memory allocation*. The storage size of all variables
is fixed and is determined at compile time.
Variable declarations tend to appear at the top of the code block that uses them, but this is not mandatory.
They define the name and type of the variable, and its initial value.
Prog8 supports a small list of data types, including special 'memory mapped' types
that don't allocate storage but instead point to a fixed location in the address space.
Code
These are the instructions that make up the program's logic.
Code can only occur inside a subroutine.
There are different kinds of instructions ('statements' is a better name) such as:
- value assignment
- looping (for, while, do-until, repeat, unconditional jumps)
- conditional execution (if - then - else, when, and conditional jumps)
- subroutine calls
- label definition
Subroutine
Defines a piece of code that can be called by its name from different locations in your code.
It accepts parameters and can return a value (optional).
It can define its own variables, and it is also possible to define subroutines within other subroutines.
Nested subroutines can access the variables from outer scopes easily, which removes the need and overhead to pass everything via parameters all the time.
Subroutines do not have to be declared in the source code before they can be called.
Label
This is a named position in your code where you can jump to from another place.
You can jump to it with a jump statement elsewhere. It is also possible to use a
subroutine call to a label (but without parameters and return value).
Scope
Also known as 'namespace', this is a named box around the symbols defined in it.
This prevents name collisions (or 'namespace pollution'), because the name of the scope
is needed as prefix to be able to access the symbols in it.
Anything *inside* the scope can refer to symbols in the same scope without using a prefix.
There are three scope levels in Prog8:
- global (no prefix), everything in a module file goes in here;
- block;
- subroutine, can be nested in another subroutine.
Even though modules are separate files, they are *not* separate scopes!
Everything defined in a module is merged into the global scope.
This is different from most other languages that have modules.
The global scope can only contain blocks and some directives, while the others can contain variables and subroutines too.
Some more details about how to deal with scopes and names is discussed below.
.. _blocks:
Blocks, Scopes, and accessing Symbols
-------------------------------------
**Blocks** are the top level separate pieces of code and data of your program. They have a
starting address in memory and will be combined together into a single output program.
They can only contain *directives*, *variable declarations*, *subroutines* and *inline assembly code*.
Your actual program code can only exist inside these subroutines.
(except the occasional inline assembly)
Here's an example::
main $c000 {
; this is code inside the block...
}
The name of a block must be unique in your entire program.
Be careful when importing other modules; blocks in your own code cannot have
the same name as a block defined in an imported module or library.
.. sidebar::
Using qualified names ("dotted names") to reference symbols defined elsewhere
Every symbol is 'public' and can be accessed from anywhere else, when given its *full* "dotted name".
So, accessing a variable ``counter`` defined in subroutine ``worker`` in block ``main``,
can be done from anywhere by using ``main.worker.counter``.
Unlike most other programming langues, as soon as a name is scoped,
Prog8 treats it as a name starting in the *global* namespace.
Relative name lookup is only performed for *non-scoped* names.
The address can be used to place a block at a specific location in memory.
Usually it is omitted, and the compiler will automatically choose the location (usually immediately after
the previous block in memory).
It must be >= ``$0200`` (because ``$00``--``$ff`` is the ZP and ``$100``--``$1ff`` is the cpu stack).
*Symbols* are names defined in a certain *scope*. Inside the same scope, you can refer
to them by their 'short' name directly. If the symbol is not found in the same scope,
the enclosing scope is searched for it, and so on, up to the top level block, until the symbol is found.
If the symbol was not found the compiler will issue an error message.
**Subroutines** create a new scope. All variables inside a subroutine are hoisted up to the
scope of the subroutine they are declared in. Note that you can define **nested subroutines** in Prog8,
and such a nested subroutine has its own scope! This also means that you have to use a fully qualified name
to access a variable from a nested subroutine::
main {
sub start() {
sub nested() {
ubyte counter
...
}
...
txt.print_ub(counter) ; Error: undefined symbol
txt.print_ub(main.start.nested.counter) ; OK
}
}
**Aliases** make it easier to refer to symbols from other places. They save
you from having to type the fully scoped name everytime you need to access that symbol.
Aliases can be created in any scope except at the module level.
You can create and use an alias with the ``alias`` statement like so::
alias score = cx16.r7L ; 'name' the virtual register
alias prn = txt.print_ub ; shorter name for a subroutine elsewhere
...
prn(score)
.. important::
Emphasizing this once more: unlike most other programming languages, a new scope is *not* created inside
for, while, repeat, and do-until statements, the if statement, and the branching conditionals.
These all share the same scope from the subroutine they're defined in.
You can define variables in these blocks, but these will be treated as if they
were defined in the subroutine instead.
Program Start and Entry Point
-----------------------------
Your program must have a single entry point where code execution begins.
The compiler expects a ``start`` subroutine in the ``main`` block for this,
taking no parameters and having no return value.
As any subroutine, it has to end with a ``return`` statement (or a ``goto`` call)::
main {
sub start () {
; program entrypoint code here
return
}
}
The ``main`` module is always relocated to the start of your programs
address space, and the ``start`` subroutine (the entrypoint) will be on the
first address. This will also be the address that the BASIC loader program (if generated)
calls with the SYS statement.
Variables and values
--------------------
Variables are named values that can change during the execution of the program.
They can be defined inside any scope (blocks, subroutines etc.) See :ref:`blocks`.
When declaring a numeric variable it is possible to specify the initial value, if you don't want it to be zero.
For other data types it is required to specify that initial value it should get.
Values will usually be part of an expression or assignment statement::
12345 ; integer number
$aa43 ; hex integer number
%100101 ; binary integer number (% is also remainder operator so be careful)
false ; boolean false
-33.456e52 ; floating point number
"Hi, I am a string" ; text string, encoded with default encoding
'a' ; byte value (ubyte) for the letter a
sc:"Alternate" ; text string, encoded with c64 screencode encoding
sc:'a' ; byte value of the letter a in c64 screencode encoding
byte counter = 42 ; variable of size 8 bits, with initial value 42
**putting a variable in zeropage:**
If you add the ``@zp`` tag to the variable declaration, the compiler will prioritize this variable
when selecting variables to put into zeropage (but no guarantees). If there are enough free locations in the zeropage,
it will try to fill it with as much other variables as possible (before they will be put in regular memory pages).
Use ``@requirezp`` tag to *force* the variable into zeropage, but if there is no more free space the compilation will fail.
It's possible to put strings, arrays and floats into zeropage too, however because Zp space is really scarce
this is not advised as they will eat up the available space very quickly. It's best to only put byte or word
variables in zeropage. By the way, there is also ``@nozp`` to keep a variable *out of the zeropage* at all times.
Example::
byte @zp smallcounter = 42
uword @requirezp zppointer = $4000
**shared variables:**
If you add the ``@shared`` tag to the variable declaration, the compiler will know that this variable
is a prog8 variable shared with some assembly code elsewhere. This means that the assembly code can
refer to the variable even if it's otherwise not used in prog8 code itself.
(usually, these kinds of 'unused' variables are optimized away by the compiler, resulting in an error
when assembling the rest of the code). Example::
byte @shared assemblyVariable = 42
**uninitialized variables:**
All variables will be initialized by prog8 at startup, they'll get their assigned initialization value, or be cleared to zero.
This (re)initialization is also done on each subroutine entry for the variables declared in the subroutine.
There may be certain scenarios where this initialization is redundant and/or where you want to avoid the overhead of it.
In some cases, Prog8 itself can detect that a variable doesn't need a separate automatic initialization to zero, if
it's trivial that it is not being read between the variable's declaration and the first assignment. For instance, when
you declare a variable immediately before a for loop where it is the loop variable. However Prog8 is not yet very smart
at detecting these redundant initializations. If you want to be sure, check the generated assembly output.
In any case, you can use the ``@dirty`` tag on the variable declaration to make the variable *not* being (re)initialized by Prog8.
This means its value will be undefined (it can be anything) until you assign a value yourself! Don't use such
a variable before you have done so. 🦶🔫 Footgun warning.
**memory alignment:**
A string or array variable can be aligned to a couple of possible interval sizes in memory.
The use for this is very situational, but two examples are: sprite data for the C64 that needs
to be on a 64 byte aligned memory address, or an array aligned on a full page boundary to avoid
any possible extra page boundary clock cycles on certain instructions when accessing the array.
You can align on word, 64 bytes, and page boundaries::
ubyte[] @alignword array = [1, 2, 3, 4, ...]
ubyte[] @align64 spritedata = [ %00000000, %11111111, ...]
ubyte[] @alignpage lookup = [11, 22, 33, 44, ...]
Integers
^^^^^^^^
Integers are 8 or 16 bit numbers and can be written in normal decimal notation,
in hexadecimal and in binary notation. There is no octal notation.
You can use underscores to group digits to make long numbers more readable.
A single character in single quotes such as ``'a'`` is translated into a byte integer,
which is the PETSCII value for that character.
Unsigned integers are in the range 0-255 for unsigned byte types, and 0-65535 for unsigned word types.
The signed integers integers are in the range -128..127 for bytes,
and -32768..32767 for words.
.. attention::
Doing math on signed integers can result in code that is a lot larger and slower than
when using unsigned integers. Make sure you really need the signed numbers, otherwise
stick to unsigned integers for efficiency.
Booleans
^^^^^^^^
Booleans are a distinct type in Prog8 and can have only the values ``true`` or ``false``.
It can be casted to and from other integer types though
where a nonzero integer is considered to be true, and zero is false.
Logical expressions, comparisons and some other code tends to compile more efficiently if
you explicitly use ``bool`` types instead of 0/1 integers.
The in-memory representation of a boolean value is just a byte containing 0 or 1.
If you find that you need a whole bunch of boolean variables or perhaps even an array of them,
consider using integer bit mask variable + bitwise operators instead.
This saves a lot of memory and may be faster as well.
Floating point numbers
^^^^^^^^^^^^^^^^^^^^^^
You can use underscores to group digits to make long numbers more readable.
Floats are stored in the 5-byte 'MFLPT' format that is used on CBM machines.
Floating point support is available on the c64 and cx16 (and virtual) compiler targets.
On the c64 and cx16, the rom routines are used for floating point operations,
so on both systems the correct rom banks have to be banked in to make this work.
Although the C128 shares the same floating point format, Prog8 currently doesn't support
using floating point on that system (because the c128 fp routines require the fp variables
to be in another ram bank than the program, something Prog8 doesn't do).
Also your code needs to import the ``floats`` library to enable floating point support
in the compiler, and to gain access to the floating point routines.
(this library contains the directive to enable floating points, you don't have
to worry about this yourself)
The largest 5-byte MFLPT float that can be stored is: **1.7014118345e+38** (negative: **-1.7014118345e+38**)
Arrays
^^^^^^
Array types are also supported. They can be formed from a list of booleans, bytes, words, floats, or addresses of other variables
(such as explicit address-of expressions, strings, or other array variables) - values in an array literal
always have to be constants. Here are some examples of arrays::
byte[10] array ; array of 10 bytes, initially set to 0
byte[] array = [1, 2, 3, 4] ; initialize the array, size taken from value
ubyte[99] array = [255]*99 ; initialize array with 99 times 255 [255, 255, 255, 255, ...]
byte[] array = 100 to 199 ; initialize array with [100, 101, ..., 198, 199]
str[] names = ["ally", "pete"] ; array of string pointers/addresses (equivalent to array of uwords)
uword[] others = [names, array] ; array of pointers/addresses to other arrays
bool[2] flags = [true, false] ; array of two boolean values (take up 1 byte each, like a byte array)
value = array[3] ; the fourth value in the array (index is 0-based)
char = string[4] ; the fifth character (=byte) in the string
char = string[-2] ; the second-to-last character in the string (Python-style indexing from the end)
.. note::
Right now, the array should be small enough to be indexable by a single byte index.
This means byte arrays should be <= 256 elements, word arrays <= 128 elements (256 if
it's a split array - see below), and float arrays <= 51 elements.
Arrays can be initialized with a range expression or an array literal value.
You can write out such an initializer value over several lines if you want to improve readability.
You can assign a new value to an element in the array, but you can't assign a whole
new array to another array at once. This is usually a costly operation. If you really
need this you have to write it out depending on the use case: you can copy the memory using
``sys.memcopy(sourcearray, targetarray, sizeof(targetarray))``. Or perhaps use ``sys.memset`` instead to
set it all to the same value, or maybe even simply assign the individual elements.
Note that the various keywords for the data type and variable type (``byte``, ``word``, ``const``, etc.)
can't be used as *identifiers* elsewhere. You can't make a variable, block or subroutine with the name ``byte``
for instance.
Using the ``in`` operator you can easily check if a value is present in an array,
example: ``if choice in [1,2,3,4] {....}``
**Arrays at a specific memory location:**
Using the memory-mapped syntax it is possible to define an array to be located at a specific memory location.
For instance to reference the first 5 rows of the Commodore 64's screen matrix as an array, you can define::
&ubyte[5*40] top5screenrows = $0400
This way you can set the second character on the second row from the top like this::
top5screenrows[41] = '!'
**Array indexing on a pointer variable:**
An uword variable can be used in limited scenarios as a 'pointer' to a byte in memory at a specific,
dynamic, location. You can use array indexing on a pointer variable to use it as a byte array at
a dynamic location in memory: currently this is equivalent to directly referencing the bytes in
memory at the given index. In contrast to a real array variable, the index value can be the size of a word.
Unlike array variables, negative indexing for pointer variables does *not* mean it will be counting from the end, because the size of the buffer is unknown.
Instead, it simply addresses memory that lies *before* the pointer variable.
See also :ref:`pointervars_programming`
**LSB/MSB split word arrays:**
For (u)word arrays, you can make the compiler layout the array in memory as two separate arrays,
one with the LSBs and one with the MSBs of the word values. This makes it more efficient to access
values from the array (smaller and faster code). It also doubles the maximum size of the array from 128 words to 256 words!
The ``@split`` tag should be added to the variable declaration to do this.
In the assembly code, the array will then be generated as two byte arrays namely ``name_lsb`` and ``name_msb``.
.. caution::
Not all array operations are supported yet on "split word arrays".
If you get an error message, simply revert to a regular word array and please report the issue,
so that more support can be added in the future where it is needed.
Strings
^^^^^^^
Strings are a sequence of characters enclosed in double quotes. The length is limited to 255 characters.
They're stored and treated much the same as a byte array,
but they have some special properties because they are considered to be *text*.
Strings (without encoding prefix) will be encoded (translated from ASCII/UTF-8) into bytes via the
*default encoding* for the target platform. On the CBM machines, this is CBM PETSCII.
Alternative encodings can be specified with a ``encodingname:`` prefix to the string or character literal.
The following encodings are currently recognised:
- ``petscii`` PETSCII, the default encoding on CBM machines (c64, c128, cx16)
- ``sc`` CBM-screencodes aka 'poke' codes (c64, c128, cx16)
- ``iso`` iso-8859-15 text (supported on cx16)
So the following is a string literal that will be encoded into memory bytes using the iso encoding.
It can be correctly displayed on the screen only if a iso-8859-15 charset has been activated first
(the Commander X16 has this feature built in)::
iso:"Käse, Straße"
You can concatenate two string literals using '+', which can be useful to
split long strings over separate lines. But remember that the length
of the total string still cannot exceed 255 characters.
A string literal can also be repeated a given number of times using '*', where the repeat number must be a constant value.
And a new string value can be assigned to another string, but no bounds check is done!
So be sure the destination string is large enough to contain the new value (it is overwritten in memory)::
str string1 = "first part" + "second part"
str string2 = "hello!" * 10
string1 = string2
string1 = "new value"
There are several 'escape sequences' to help you put special characters into strings, such
as newlines, quote characters themselves, and so on. The ones used most often are
``\\``, ``\"``, ``\n``, ``\r``. For a detailed description of all of them and what they mean,
read the syntax reference on strings.
Using the ``in`` operator you can easily check if a character is present in a string,
example: ``if '@' in email_address {....}`` (however this gives no clue about the location
in the string where the character is present, if you need that, use the ``string.find()``
library function instead)
**Caution:**
This checks *all* elements in the string with the length as it was initially declared.
Even when a string was changed and is terminated early with a 0-byte early,
the containment check with ``in`` will still look at all character positions in the initial string.
Consider using ``string.find`` followed by ``if_cs`` (for instance) to do a "safer" search
for a character in such strings (one that stops at the first 0 byte)
.. hint::
Strings/arrays and uwords (=memory address) can often be interchanged.
An array of strings is actually an array of uwords where every element is the memory
address of the string. You can pass a memory address to assembly functions
that require a string as an argument.
For regular assignments you still need to use an explicit ``&`` (address-of) to take
the address of the string or array.
.. hint::
You can declare parameters and return values of subroutines as ``str``,
but in this case that is equivalent to declaring them as ``uword`` (because
in this case, the address of the string is passed as argument or returned as value).
.. note:: Strings and their (im)mutability
*String literals outside of a string variable's initialization value*,
are considered to be "constant", i.e. the string isn't going to change
during the execution of the program. The compiler takes advantage of this in certain
ways. For instance, multiple identical occurrences of a string literal are folded into
just one string allocation in memory. Examples of such strings are the string literals
passed to a subroutine as arguments.
*Strings that aren't such string literals are considered to be unique*, even if they
are the same as a string defined elsewhere. This includes the strings assigned to
a string variable in its declaration! These kind of strings are not deduplicated and
are just copied into the program in their own unique part of memory. This means that
it is okay to treat those strings as mutable; you can safely change the contents
of such a string without destroying other occurrences (as long as you stay within
the size of the allocated string!)
Special types: const and memory-mapped
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When using ``const``, the value of the 'variable' cannot be changed; it has become a compile-time constant value instead.
You'll have to specify the initial value expression. This value is then used
by the compiler everywhere you refer to the constant (and no memory is allocated
for the constant itself). Onlythe simple numeric types (byte, word, float) can be defined as a constant.
If something is defined as a constant, very efficient code can usually be generated from it.
Variables on the other hand can't be optimized as much, need memory, and more code to manipulate them.
Note that a subset of the library routines in the ``math``, ``string`` and ``floats`` modules are recognised in
compile time expressions. For example, the compiler knows what ``math.sin8u(12)`` is and replaces it with the computed result.
When using ``&`` (the address-of operator but now applied to a datatype), the variable will point to specific location in memory,
rather than being newly allocated. The initial value (mandatory) must be a valid
memory address. Reading the variable will read the given data type from the
address you specified, and setting the variable will directly modify that memory location(s)::
const byte max_age = 2000 - 1974 ; max_age will be the constant value 26
&word SCREENCOLORS = $d020 ; a 16-bit word at the address $d020-$d021
.. _pointervars_programming:
Direct access to memory locations ('peek' and 'poke')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Normally memory locations are accessed by a *memory mapped* name, such as ``cbm.BGCOL0`` that is defined
as the memory mapped address $d021 (on the c64 target).
If you want to access a memory location directly (by using the address itself or via an uword pointer variable),
without defining a memory mapped location, you can do so by enclosing the address in ``@(...)``::
color = @($d020) ; set the variable 'color' to the current c64 screen border color ("peek(53280)")
@($d020) = 0 ; set the c64 screen border to black ("poke 53280,0")
@(vic+$20) = 6 ; you can also use expressions to 'calculate' the address
This is the official syntax to 'dereference a pointer' as it is often named in other languages.
You can actually also use the array indexing notation for this. It will be silently converted into
the direct memory access expression as explained above. Note that unlike regular arrays,
the index is not limited to an ubyte value. You can use a full uword to index a pointer variable like this::
pointervar[999] = 0 ; set memory byte to zero at location pointervar + 999.
Converting types into other types
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Sometimes you need an unsigned word where you have an unsigned byte, or you need some other type conversion.
Many type conversions are possible by just writing ``as <type>`` at the end of an expression::
uword uw = $ea31
ubyte ub = uw as ubyte ; ub will be $31, identical to lsb(uw)
float f = uw as float ; f will be 59953, but this conversion can be omitted in this case
word w = uw as word ; w will be -5583 (simply reinterpret $ea31 as 2-complement negative number)
f = 56.777
ub = f as ubyte ; ub will be 56
Sometimes it is a straight reinterpretation of the given value as being of the other type,
sometimes an actual value conversion is done to convert it into the other type.
Try to avoid those type conversions as much as possible.
Initial values across multiple runs of the program
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When declaring values with an initial value, this value will be set into the variable each time
the program reaches the declaration again. This can be in loops, multiple subroutine calls,
or even multiple invocations of the entire program.
If you omit the initial value, zero will be used instead.
This only works for simple types, *and not for string variables and arrays*.
It is assumed these are left unchanged by the program; they are not re-initialized on
a second run.
If you do modify them in-place, you should take care yourself that they work as
expected when the program is restarted.
(This is an optimization choice to avoid having to store two copies of every string and array)
Loops
-----
The *for*-loop is used to let a variable iterate over a range of values. Iteration is done in steps of 1, but you can change this.
.. sidebar::
Optimization
Usually a loop in descending order downto 0 or 1, produces more efficient assembly code than the same loop in ascending order.
The loop variable must be declared separately as byte or word earlier, so that you can reuse it for multiple occasions.
Iterating with a floating point variable is not supported. If you want to loop over a floating-point array, use a loop with an integer index variable instead.
If the from value is already outside of the loop range, the whole for loop is skipped.
The *while*-loop is used to repeat a piece of code while a certain condition is still true.
The *do--until* loop is used to repeat a piece of code until a certain condition is true.
The *repeat* loop is used as a short notation of a for loop where the loop variable doesn't matter and you're only interested in the number of iterations.
(without iteration count specified it simply loops forever). A repeat loop will result in the most efficient code generated so use this if possible.
You can also create loops by using the ``goto`` statement, but this should usually be avoided.
Breaking out of a loop prematurely is possible with the ``break`` statement,
immediately continue into the next cycle of the loop with the ``continue`` statement.
(These are just shorthands for a goto + a label)
The *unroll* loop is not really a loop, but looks like one. It actually duplicates the statements in its block on the spot by
the given number of times. It's meant to "unroll loops" - trade memory for speed by avoiding the actual repeat loop counting code.
Only simple statements are allowed to be inside an unroll loop (assignments, function calls etc.).
.. attention::
The value of the loop variable after executing the loop *is undefined* - you cannot rely
on it to be the last value in the range for instance! The value of the variable should only be used inside the for loop body.
(this is an optimization issue to avoid having to deal with mostly useless post-loop logic to adjust the loop variable's value)
Conditional Execution
---------------------
if statement
^^^^^^^^^^^^
Conditional execution means that the flow of execution changes based on certain conditions,
rather than having fixed gotos or subroutine calls::
if xx==5 {
yy = 99
zz = 42
} else {
aa = 3
bb = 9
}
if xx==5
yy = 42
else if xx==6
yy = 43
else
yy = 44
if aa>4 goto some_label
if xx==3 yy = 4
if xx==3 yy = 4 else aa = 2
Conditional jumps (``if condition goto label``) are compiled using 6502's branching instructions (such as ``bne`` and ``bcc``) so
the rather strict limit on how *far* it can jump applies. The compiler itself can't figure this
out unfortunately, so it is entirely possible to create code that cannot be assembled successfully.
Thankfully the ``64tass`` assembler that is used has the option to automatically
convert such branches to their opposite + a normal jmp. This is slower and takes up more space
and you will get warning printed if this happens. You may then want to restructure your branches (place target labels closer to the branch,
or reduce code complexity).
There is a special form of the if-statement that immediately translates into one of the 6502's branching instructions.
This allows you to write a conditional jump or block execution directly acting on the current values of the CPU's status register bits.
The eight branching instructions of the CPU each have an if-equivalent (and there are some easier to understand aliases):
====================== =====================
condition meaning
====================== =====================
``if_cs`` if carry status is set
``if_cc`` if carry status is clear
``if_vs`` if overflow status is set
``if_vc`` if overflow status is clear
``if_eq`` / ``if_z`` if result is equal to zero
``if_ne`` / ``if_nz`` if result is not equal to zero
``if_pl`` / ``if_pos`` if result is 'plus' (>= zero)
``if_mi`` / ``if_neg`` if result is 'minus' (< zero)
====================== =====================
So ``if_cc goto target`` will directly translate into the single CPU instruction ``BCC target``.
.. caution::
These special ``if_XX`` branching statements are only useful in certain specific situations where you are *certain*
that the status register (still) contains the correct status bits.
This is not always the case after a function call or other operations!
If in doubt, check the generated assembly code!
.. note::
For now, the symbols used or declared in the statement block(s) are shared with
the same scope the if statement itself is in.
Maybe in the future this will be a separate nested scope, but for now, that is
only possible when defining a subroutine.
if expression
^^^^^^^^^^^^^
You can also use if..else as an *expression* instead of a statement. This expression selects one of two
different values depending of the condition. Sometimes it may be more legible if you surround the condition expression with parentheses.
An example, to select the number of cards to use depending on what game is played::
ubyte numcards = if game_is_piquet 32 else 52
; it's more verbose with an if statement:
ubyte numcards
if game_is_piquet
numcards = 32
else
numcards = 52
when statement ('jump table')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Instead of writing a bunch of sequential if-elseif statements, it is more readable to
use a ``when`` statement. (It will also result in greatly improved assembly code generation)
Use a ``when`` statement if you have a set of fixed choices that each should result in a certain
action. It is possible to combine several choices to result in the same action::
when value {
4 -> txt.print("four")
5 -> txt.print("five")
10,20,30 -> {
txt.print("ten or twenty or thirty")
}
else -> txt.print("don't know")
}
The when-*value* can be any expression but the choice values have to evaluate to
compile-time constant integers (bytes or words). They also have to be the same
datatype as the when-value, otherwise no efficient comparison can be done.
.. note::
Instead of chaining several value equality checks together using ``or`` (ex.: ``if x==1 or xx==5 or xx==9``),
consider using a ``when`` statement or ``in`` containment check instead. These are more efficient.
Assignments
-----------
Assignment statements assign a single value to a target variable or memory location.
Augmented assignments (such as ``aa += xx``) are also available, but these are just shorthands
for normal assignments (``aa = aa + xx``).
It is possible to "chain" assignments: ``x = y = z = 42``, this is just a shorthand
for the three individual assignments with the same value 42.
Only for certain subroutines that return multiple values it is possible to write a "multi assign" statement
with comma separated assignment targets, that assigns those multiple values to different targets in one statement.
Details can be found here: :ref:`multiassign`.
.. attention::
**Data type conversion (in assignments):**
When assigning a value with a 'smaller' datatype to variable with a 'larger' datatype,
the value will be automatically converted to the target datatype: byte --> word --> float.
So assigning a byte to a word variable, or a word to a floating point variable, is fine.
The reverse is *not* true: it is *not* possible to assign a value of a 'larger' datatype to
a variable of a smaller datatype without an explicit conversion. Otherwise you'll get an error telling you
that there is a loss of precision. You can use builtin functions such as ``round`` and ``lsb`` to convert
to a smaller datatype, or revert to integer arithmetic.
Expressions
-----------
Expressions tell the program to *calculate* something. They consist of
values, variables, operators such as ``+`` and ``-``, function calls, type casts, or other expressions.
Here is an example that calculates to number of seconds in a certain time period::
num_hours * 3600 + num_minutes * 60 + num_seconds
Long expressions can be split over multiple lines by inserting a line break before or after an operator::
num_hours * 3600
+ num_minutes * 60
+ num_seconds
In most places where a number or other value is expected, you can use just the number, or a constant expression.
If possible, the expression is parsed and evaluated by the compiler itself at compile time, and the (constant) resulting value is used in its place.
Expressions that cannot be compile-time evaluated will result in code that calculates them at runtime.
Expressions can contain procedure and function calls.
There are various built-in functions that can be used in expressions (see :ref:`builtinfunctions`).
You can also reference identifiers defined elsewhere in your code.
Read the :ref:`syntaxreference` chapter for all details on the available operators and kinds of expressions you can write.
.. note::
**Order of evaluation:**
The order of evaluation of expression operands is *unspecified* and should not be relied upon.
There is no guarantee of a left-to-right or right-to-left evaluation. But don't confuse this with
operator precedence order (multiplication comes before addition etcetera).
.. attention::
**Floating point values used in expressions:**
When a floating point value is used in a calculation, the result will be a floating point, and byte or word values
will be automatically converted into floats in this case. The compiler will issue a warning though when this happens, because floating
point calculations are very slow and possibly unintended!
Calculations with integer variables will not result in floating point values.
if you divide two integer variables say 32500 and 99 the result will be the integer floor
division (328) rather than the floating point result (328.2828282828283). If you need the full precision,
you'll have to make sure at least the first operand is a floating point. You can do this by
using a floating point value or variable, or use a type cast.
When the compiler can calculate the result during compile-time, it will try to avoid loss
of precision though and gives an error if you may be losing a floating point result.
Arithmetic and Logical expressions
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Arithmetic expressions are expressions that calculate a numeric result (integer or floating point).
Many common arithmetic operators can be used and follow the regular precedence rules.
Logical expressions are expressions that calculate a boolean result: true or false
(which in reality are just a 1 or 0 integer value). When using variables of the type ``bool``,
logical expressions will compile more efficiently than when you're using regular integer type operands
(because these have to be converted to 0 or 1 every time)
Prog8 applies short-circuit aka McCarthy evaluation for ``and`` and ``or`` on boolean expressions.
You can use parentheses to group parts of an expression to change the precedence.
Usually the normal precedence rules apply (``*`` goes before ``+`` etc.) but subexpressions
within parentheses will be evaluated first. So ``(4 + 8) * 2`` is 24 and not 20,
and ``(true or false) and false`` is false instead of true.
.. attention::
**calculations keep their datatype even if the target variable is larger:**
When you do calculations on a BYTE type, the result will remain a BYTE.
When you do calculations on a WORD type, the result will remain a WORD.
For instance::
byte b = 44
word w = b*55 ; the result will be 116! (even though the target variable is a word)
w *= 999 ; the result will be -15188 (the multiplication stays within a word, but overflows)
*The compiler does NOT warn about this!* It's doing this for
performance reasons - so you won't get sudden 16 bit (or even float)
calculations where you needed only simple fast byte arithmetic.
If you do need the extended resulting value, cast at least one of the
operands explicitly to the larger datatype. For example::
byte b = 44
w = (b as word)*55
w = b*(55 as word)
Subroutines
-----------
Defining a subroutine
^^^^^^^^^^^^^^^^^^^^^
Subroutines are parts of the code that can be repeatedly invoked using a subroutine call from elsewhere.
Their definition, using the ``sub`` statement, includes the specification of the required parameters and return value.
Subroutines can be defined in a Block, but also nested inside another subroutine. Everything is scoped accordingly.
With ``asmsub`` you can define a low-level subroutine that is implemented directly in assembly and takes parameters
directly in registers. Finally with ``extsub`` you can define an external subroutine that's implemented outside
of the program (for instance, a ROM routine, or a routine in a library loaded elsewhere in RAM).
Trivial ``asmsub`` routines can be tagged as ``inline`` to tell the compiler to copy their code
in-place to the locations where the subroutine is called, rather than inserting an actual call and return to the
subroutine. This may increase code size significantly and can only be used in limited scenarios, so YMMV.
Note that the routine's code is copied verbatim into the place of the subroutine call in this case,
so pay attention to any jumps and rts instructions in the inlined code!
Inlining regular Prog8 subroutines is at the discretion of the compiler.
Calling a subroutine
^^^^^^^^^^^^^^^^^^^^
The arguments in parentheses after the function name, should match the parameters in the subroutine definition.
If you want to ignore a return value of a subroutine, you should prefix the call with the ``void`` keyword.
Otherwise the compiler will issue a warning about discarding a result value.
.. note::
**Order of evaluation:**
The order of evaluation of arguments to a single function call is *unspecified* and should not be relied upon.
There is no guarantee of a left-to-right or right-to-left evaluation of the call arguments.
.. caution::
Note that due to the way parameters are processed by the compiler,
subroutines are *non-reentrant*. This means you cannot create recursive calls.
If you do need a recursive algorithm, you'll have to hand code it in embedded assembly for now,
or rewrite it into an iterative algorithm.
Also, subroutines used in the main program should not be used from an IRQ handler. This is because
the subroutine may be interrupted, and will then call itself from the IRQ handler. Results are
then undefined because the variables will get overwritten.
Deferred ("cleanup") code
^^^^^^^^^^^^^^^^^^^^^^^^^
Usually when a subroutine exits, it has to clean up things that it worked on. For example, it has to close
a file that it opened before to read data from, or it has to free a piece of memory that it allocated via
a dynamic memory allocation library, etc.
Every spot where the subroutine exits (return statement, jump, or the end of the routine) you have to take care
of doing the cleanups required. This can get tedious, and the cleanup code is separated from the place where
the resource allocation was done at the start.
To help make this easier and less error prone, you can ``defer`` code to be executed automatically,
immediately before any moment the subroutine exits. So for example to make sure a file is closed
regardless of what happens later in the routine, you can write something along these lines::
sub example() -> bool {
ubyte file = open_file()
defer close_file(file) ; "close it when we exit from here"
uword memory = allocate(1000)
if memory==0
return false
defer deallocate(memory) ; "deallocate when we exit from here"
process(file, memory)
return true
}
In this example, the two deferred statements are not immediately executed. Instead, they are executed when the
subroutine exits at any point. So for example the ``return false`` after the memory check will automatically also close
the file that was opened earlier because the close_file() call was scheduled there.
At the bottom when the ``return true`` appears, *both* deferred cleanup calls are executed: first the deallocation of
the memory, and then the file close. As you can see this saves you from duplicating the cleanup logic,
and the logic is declared very close to the spot where the allocation of the resource happens, so it's easier to read and understand.
It's possible to write a defer for a block of statements, but the advice is to keep such cleanup code as simple and short as possible.
.. caution::
Defers only work for subroutines that are written as regular Prog8 code.
If a piece of inlined assembly somehow causes the routine to exit, the compiler cannot detect this.
Defers will not be handled in such cases.
Library routines and builtin functions
--------------------------------------
There are many routines available in the compiler libraries or as builtin functions.
The most important ones can be found in the :doc:`libraries` chapter.