This commit is contained in:
Irmen de Jong 2018-08-07 01:23:34 +02:00
parent 0e785fcfb3
commit b34ae4c91c
5 changed files with 595 additions and 357 deletions

73
docs/source/building.rst Normal file
View File

@ -0,0 +1,73 @@
==============================
Writing and building a program
==============================
What is a "Program" anyway?
---------------------------
A "complete runnable program" is a compiled, assembled, and linked together single unit.
It contains all of the program's code and data and has a certain file format that
allows it to be loaded directly on the target system. IL65 currently has no built-in
support for programs that exceed 64 Kb of memory, nor for multi-part loaders.
For the Commodore-64, most programs will have a tiny BASIC launcher that does a SYS into the generated machine code.
This way the user can load it as any other program and simply RUN it to start. (This is a regular ".prg" program).
Il65 can create those, but it is also possible to output plain binary programs
that can be loaded into memory anywhere.
Compiling program code
----------------------
Compilation of program code is done by telling the IL65 compiler to compile a main source code module file.
Other modules that this code needs will be loaded and processed via imports from within that file.
The compiler will link everything together into one output program at the end.
The compiler is invoked with the command:
``$ @todo``
Module source code files
------------------------
A module source file is a text file with the ``.ill`` suffix, containing the program's source code.
It consists of compilation options and other directives, imports of other modules,
and source code for one or more code blocks.
IL65 has a couple of *LIBRARY* modules that are defined in special internal files provided by the compiler:
``c64lib``, ``il65lib``, ``mathlib``.
You should not overwrite these or reuse their names.
.. _debugging:
Debugging (with Vice)
---------------------
There's support for using the monitor and debugging capabilities of the rather excellent
`Vice emulator <http://vice-emu.sourceforge.net/>`_.
The ``%breakpoint`` directive (see :ref:`directives`) in the source code instructs the compiler to put
a *breakpoint* at that position. Some systems use a BRK instruction for this, but
this will usually halt the machine altogether instead of just suspending execution.
IL65 issues a NOP instruction instead and creates a 'virtual' breakpoint at this position.
All breakpoints are then written to a file called "programname.vice-mon-list",
which is meant to be used by the Vice emulator.
It contains a series of commands for Vice's monitor, including source labels and the breakpoint settings.
If you use the vice autostart feature of the compiler, it will be processed by Vice automatically and immediately.
If you launch Vice manually, you'll have to use a command line option to load this file:
``$ x64 -moncommands programname.vice-mon-list``
Vice will then use the label names in memory disassembly, and will activate the breakpoints as well.
If your running program hits one of the breakpoints, Vice will halt execution and drop you into the monitor.
Troubleshooting
---------------
Getting an assembler error about undefined symbols such as ``not defined 'c64flt'``?
This happens when your program uses floating point values, and you forgot to import the ``c64lib``.
If you use floating points, the program will need routines from that library.
Fix it by adding an ``%import c64lib``.

View File

@ -40,6 +40,7 @@ Design principles
- The compiler outputs a regular 6502 assembly code file, it doesn't assemble this itself.
A third party cross-assembler tool is used to do this final step.
- Goto is considered harmful, but not here; arbitrary control flow jumps are allowed.
- No complicated error handling or overflow checks that would slow things down.
Required tools
@ -56,8 +57,8 @@ Required tools
:caption: Contents of this manual:
targetsystem.rst
building.rst
programming.rst
progstructure.rst
syntaxreference.rst
todo.rst

View File

@ -1,73 +1,335 @@
==============================
Writing and building a program
==============================
.. _programstructure:
What is a "Program" anyway?
---------------------------
===================
Programming in IL65
===================
A "complete runnable program" is a compiled, assembled, and linked together single unit.
It contains all of the program's code and data and has a certain file format that
allows it to be loaded directly on the target system. IL65 currently has no built-in
support for programs that exceed 64 Kb of memory, nor for multi-part loaders.
For the Commodore-64, most programs will have a tiny BASIC launcher that does a SYS into the generated machine code.
This way the user can load it as any other program and simply RUN it to start. (This is a regular ".prg" program).
Il65 can create those, but it is also possible to output plain binary programs
that can be loaded into memory anywhere.
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.
Compiling program code
----------------------
Compilation of program code is done by telling the IL65 compiler to compile a main source code module file.
Other modules that this code needs will be loaded and processed via imports from within that file.
The compiler will link everything together into one output program at the end.
The compiler is invoked with the command:
``$ @todo``
Module source code files
------------------------
A module source file is a text file with the ``.ill`` suffix, containing the program's source code.
It consists of compilation options and other directives, imports of other modules,
and source code for one or more code blocks.
IL65 has a couple of *LIBRARY* modules that are defined in special internal files provided by the compiler:
``c64lib``, ``il65lib``, ``mathlib``.
You should not overwrite these or reuse their names.
.. _debugging:
Debugging (with Vice)
Elements of a program
---------------------
There's support for using the monitor and debugging capabilities of the rather excellent
`Vice emulator <http://vice-emu.sourceforge.net/>`_.
Program
Consists of one or more *modules*.
The ``%breakpoint`` directive (see :ref:`directives`) in the source code instructs the compiler to put
a *breakpoint* at that position. Some systems use a BRK instruction for this, but
this will usually halt the machine altogether instead of just suspending execution.
IL65 issues a NOP instruction instead and creates a 'virtual' breakpoint at this position.
All breakpoints are then written to a file called "programname.vice-mon-list",
which is meant to be used by the Vice emulator.
It contains a series of commands for Vice's monitor, including source labels and the breakpoint settings.
If you use the vice autostart feature of the compiler, it will be processed by Vice automatically and immediately.
If you launch Vice manually, you'll have to use a command line option to load this file:
Module
A file on disk with the ``.ill`` suffix. It contains *directives* and *code blocks*.
Whitespace and indentation in the source code are arbitrary and can be tabs or spaces or both.
You can also add *comments* to the source code.
One moudule file can *import* others, and also import *library modules*.
``$ x64 -moncommands programname.vice-mon-list``
Comments
Everything after a semicolon ``;`` is a comment and is ignored by the compiler.
If the whole line is just a comment, it will be copied into the resulting assembly source code.
This makes it easier to understand and relate the generated code. Examples::
Vice will then use the label names in memory disassembly, and will activate the breakpoints as well.
If your running program hits one of the breakpoints, Vice will halt execution and drop you into the monitor.
A = 42 ; set the initial value to 42
; next is the code that...
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.
Code block
A block of actual program code. It defines a *scope* (also known as 'namespace') and
can contain IL65 *code*, *variable declarations* 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.
They define the name and type of the variable, and its initial value.
IL65 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. There are different kinds of instructions
('statements' is a better name):
- value assignment
- looping (for, while, repeat, unconditional jumps)
- conditional execution (if - then - else, 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 result values.
It can define its own variables but it's not possible to define subroutines nested in other subroutines.
To keep things simple, you can only define subroutines inside code blocks from a module.
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).
Troubleshooting
---------------
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 scopes in IL65:
Getting an assembler error about undefined symbols such as ``not defined 'c64flt'``?
This happens when your program uses floating point values, and you forgot to import the ``c64lib``.
If you use floating points, the program will need routines from that library.
Fix it by adding an ``%import c64lib``.
- global (no prefix)
- code block
- subroutine
Modules are *not* a scope! Everything defined in a module is merged into the global scope.
.. _blocks:
Blocks, Scopes, and accessing Symbols
-------------------------------------
Blocks are the separate pieces of code and data of your program. They are combined
into a single output program. No code or data can occur outside a block. Here's an example::
~ main $c000 {
; this is code inside the block...
}
The name of a block must be unique in your entire program.
Also 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.
It's possible to omit this name, but then you can only refer to the contents of the block via its absolute address,
which is required in this case. If you omit *both* name and address, the block is *ignored* by the compiler (and a warning is displayed).
This is a way to quickly "comment out" a piece of code that is unfinshed or may contain errors that you
want to work on later, because the contents of the ignored block are not fully parsed either.
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).
The address must be >= ``$0200`` (because ``$00``--``$ff`` is the ZP and ``$100``--``$200`` is the cpu stack).
A block is also a *scope* in your program so the symbols in the block don't clash with
symbols of the same name defined elsewhere in the same file or in another file.
You can refer to the symbols in a particular block by using a *dotted name*: ``blockname.symbolname``.
Labels inside a subroutine are appended again to that; ``blockname.subroutinename.label``.
Every symbol is 'public' and can be accessed from elsewhere given its dotted name.
**The special "ZP" ZeroPage block**
Blocks named "ZP" are treated a bit differently: they refer to the ZeroPage.
The contents of every block with that name (this one may occur multiple times) are merged into one.
Its start address is always set to ``$04``, because ``$00 - $01`` are used by the hardware
and ``$02 - $03`` are reserved as general purpose scratch registers.
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 data
------------------
::
12345 ; integer number
"Hi, I am a string" ; text string
-33.456e52 ; floating point number
byte counter = 42 ; variable of size 8 bits, with initial value 42
Integers
^^^^^^^^
Integers are 8 or 16 bit numbers and can be written in normal decimal notation,
in hexadecimal and in binary notation.
@todo right now only unsinged integers are supported (>=0)
Strings
^^^^^^^
Strings are a sequence of characters enclosed in ``"`` quotes.
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 in your source code files will be encoded (translated from ASCII/UTF-8) into either CBM PETSCII or C-64 screencodes.
PETSCII is the default choice. If you need screencodes (also called 'poke' codes) instead,
you have to use the ``str_s`` variants of the string type identifier.
If you assign a string literal of length 1 to a non-string variable, it is treated as a *byte* value instead
with has the PETSCII value of that single character,
Floating point numbers
^^^^^^^^^^^^^^^^^^^^^^
Floats are stored in the 5-byte 'MFLPT' format that is used on CBM machines,
and also most float operations are specific to the Commodore-64.
This is because routines in the C-64 BASIC and KERNAL ROMs are used for that.
So floating point operations will only work if the C-64 BASIC ROM (and KERNAL ROM)
are banked in (and your code imports the ``c64lib.ill``)
The largest 5-byte MFLPT float that can be stored is: **1.7014118345e+38** (negative: **-1.7014118345e+38**)
Initial values across multiple runs of the program
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The initial values of your variables will be restored automatically when the program is (re)started,
*except for string variables*. It is assumed these are left unchanged by the program.
If you do modify them in-place, you should take care yourself that they work as
expected when the program is restarted.
Indirect addressing and address-of
----------------------------------
The ``#`` operator is used to take the address of the symbol following it.
It can be used for example to work with the *address* of a memory mapped variable rather than
the value it holds. You could take the address of a string as well, but that is redundant:
the compiler already treats those as a value that you manipulate via its address.
For most other types this prefix is not supported and will result in a compilation error.
The resulting value is simply a 16 bit word. Example::
AX = #somevar
**Indirect addressing:**
@todo
**Indirect addressing in jumps:**
@todo
For an indirect ``goto`` statement, the compiler will issue the 6502 CPU's special instruction
(``jmp`` indirect). A subroutine call (``jsr`` indirect) is emitted
using a couple of instructions.
Loops
-----
The *for*-loop is used to iterate over a range of values. Iteration steps by 1,
but you can set it to something else as well.
The *while*-loop is used to repeat a piece of code while a certain condition is still true.
The *repeat--until* loop is used to repeat a piece of code until a certain condition is true.
You can also create loops by using the ``goto`` statement, but this should be avoided.
Conditional Execution
---------------------
@todo
Conditional execution means that the flow of execution changes based on certiain conditions,
rather than having fixed gotos or subroutine calls. IL65 has a *conditional goto* statement for this,
that is translated into a comparison (if needed) and then a conditional branch instruction::
if[_XX] [<expression>] goto <label>
The if-status XX is one of: [cc, cs, vc, vs, eq, ne, true, not, zero, pos, neg, lt, gt, le, ge]
It defaults to 'true' (=='ne', not-zero) if omitted. ('pos' will translate into 'pl', 'neg' into 'mi')
@todo signed: lts==neg?, gts==eq+pos?, les==neg+eq?, ges==pos?
The <expression> is optional. If it is provided, it will be evaluated first. Only the [true] and [not] and [zero]
if-statuses can be used when such a *comparison expression* is used. An example is::
if_not A > 55 goto more_iterations
Conditional jumps are compiled into 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.
You'll have to restructure your gotos in the code (place target labels closer to the branch)
if you run into this type of assembler error.
Assignments
-----------
Assignment statements assign a single value to a target variable or memory location.
Augmented assignments (such as ``A += X``) are also available, but these are just shorthands
for normal assignments (``A = A + X``).
Expressions
-----------
In most places where a number or other value is expected, you can use just the number, or a full constant expression.
The expression is parsed and evaluated by Python itself at compile time, and the (constant) resulting value is used in its place.
Ofcourse the special il65 syntax for hexadecimal numbers (``$xxxx``), binary numbers (``%bbbbbbbb``),
and the address-of (``#xxxx``) is supported. Other than that it must be valid Python syntax.
Expressions can contain function calls to the math library (sin, cos, etc) and you can also use
all builtin functions (max, avg, min, sum etc). They can also reference idendifiers defined elsewhere in your code,
if this makes sense.
Arithmetic
^^^^^^^^^^
@todo
Logical expressions
^^^^^^^^^^^^^^^^^^^
@todo
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 input- and output parameters.
For now, only register based parameters are supported (A, X, Y and paired registers,
the carry status bit SC and the interrupt disable bit SI as specials).
For subroutine return values, the special SZ register is also available, it means the zero status bit.
Calling a subroutine
^^^^^^^^^^^^^^^^^^^^
The output variables must occur in the correct sequence of return registers as specified
in the subroutine's definiton. It is possible to not specify any of them but the compiler
will issue a warning then if the result values of a subroutine call are discarded.
If you don't have a variable to store the output register in, it's then required
to list the register itself instead as output variable.
Arguments should match the subroutine definition. You are allowed to omit the parameter names.
If no definition is available (because you're directly calling memory or a label or something else),
you can freely add arguments (but in this case they all have to be named).
To jump to a subroutine (without returning), prefix the subroutine call with the word 'goto'.
Unlike gotos in other languages, here it take arguments as well, because it
essentially is the same as calling a subroutine and only doing something different when it's finished.
**Register preserving calls:** use the ``!`` followed by a combination of A, X and Y (or followed
by nothing, which is the same as AXY) to tell the compiler you want to preserve the origial
value of the given registers after the subroutine call. Otherwise, the subroutine may just
as well clobber all three registers. Preserving the original values does result in some
stack manipulation code to be inserted for every call like this, which can be quite slow.

View File

@ -1,164 +0,0 @@
.. _programstructure:
=================
Program Structure
=================
This chapter describes a high level overview of the elements that make up a program.
Details about each of them, and the syntax, are discussed in the :ref:`syntaxreference` chapter.
Elements of a program
---------------------
.. data:: Program
Consists of one or more *modules*.
.. data:: Module
A file on disk with the ``.ill`` suffix. It contains *directives* and *code blocks*.
Whitespace and indentation in the source code are arbitrary and can be tabs or spaces or both.
You can also add *comments* to the source code.
One moudule file can *import* others, and also import *library modules*.
.. data:: Comments
Everything after a semicolon ``;`` is a comment and is ignored by the compiler.
If the whole line is just a comment, it will be copied into the resulting assembly source code.
This makes it easier to understand and relate the generated code. Examples::
A = 42 ; set the initial value to 42
; next is the code that...
.. data:: 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.
.. data:: Code block
A block of actual program code. It defines a *scope* (also known as 'namespace') and
can contain IL65 *code*, *variable declarations* and *subroutines*.
More details about this below: :ref:`blocks`.
.. data:: 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.
They define the name and type of the variable, and its initial value.
IL65 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.
.. data:: Code
These are the instructions that make up the program's logic. There are different kinds of instructions
('statements' is a better name):
- value assignment
- looping (for, while, repeat, unconditional jumps)
- conditional execution (if - then - else, and conditional jumps)
- subroutine calls
- label definition
.. data:: 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 result values.
It can define its own variables but it's not possible to define subroutines nested in other subroutines.
To keep things simple, you can only define subroutines inside code blocks from a module.
.. data:: Label
To label a position in your code where you can jump to from another place, you use a label like this::
nice_place:
; code ...
It's an identifier followed by a colon ``:``. It's allowed to put the next statement on
the same line, after the label.
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).
.. data:: 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 scopes in IL65:
- global (no prefix)
- code block
- subroutine
Modules are *not* a scope! Everything defined in a module is merged into the global scope.
.. _blocks:
Blocks, Scopes, and accessing Symbols
-------------------------------------
Blocks are the separate pieces of code and data of your program. They are combined
into a single output program. No code or data can occur outside a block. Here's an example::
~ main $c000 {
; this is code inside the block...
}
The name of a block must be unique in your entire program.
Also 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.
It's possible to omit this name, but then you can only refer to the contents of the block via its absolute address,
which is required in this case. If you omit *both* name and address, the block is *ignored* by the compiler (and a warning is displayed).
This is a way to quickly "comment out" a piece of code that is unfinshed or may contain errors that you
want to work on later, because the contents of the ignored block are not fully parsed either.
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).
The address must be >= ``$0200`` (because ``$00``--``$ff`` is the ZP and ``$100``--``$200`` is the cpu stack).
A block is also a *scope* in your program so the symbols in the block don't clash with
symbols of the same name defined elsewhere in the same file or in another file.
You can refer to the symbols in a particular block by using a *dotted name*: ``blockname.symbolname``.
Labels inside a subroutine are appended again to that; ``blockname.subroutinename.label``.
Every symbol is 'public' and can be accessed from elsewhere given its dotted name.
**The special "ZP" ZeroPage block**
Blocks named "ZP" are treated a bit differently: they refer to the ZeroPage.
The contents of every block with that name (this one may occur multiple times) are merged into one.
Its start address is always set to ``$04``, because ``$00 - $01`` are used by the hardware
and ``$02 - $03`` are reserved as general purpose scratch registers.
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.

View File

@ -10,20 +10,22 @@ Module file
This is a file with the ``.ill`` suffix, containing *directives* and *code blocks*, described below.
The file is a text file wich can also contain:
.. data:: Lines, whitespace, indentation
Lines, whitespace, indentation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Line endings are significant because *only one* declaration, statement or other instruction can occur on every line.
Other whitespace and line indentation is arbitrary and ignored by the compiler.
You can use tabs or spaces as you wish.
Line endings are significant because *only one* declaration, statement or other instruction can occur on every line.
Other whitespace and line indentation is arbitrary and ignored by the compiler.
You can use tabs or spaces as you wish.
.. data:: Source code comments
Source code comments
^^^^^^^^^^^^^^^^^^^^
Everything after a semicolon ``;`` is a comment and is ignored.
If the whole line is just a comment, it will be copied into the resulting assembly source code.
This makes it easier to understand and relate the generated code. Examples::
Everything after a semicolon ``;`` is a comment and is ignored.
If the whole line is just a comment, it will be copied into the resulting assembly source code.
This makes it easier to understand and relate the generated code. Examples::
A = 42 ; set the initial value to 42
; next is the code that...
A = 42 ; set the initial value to 42
; next is the code that...
.. _directives:
@ -168,6 +170,19 @@ Also read :ref:`blocks`. Here is an example of a code block, to be loaded at ``
}
Labels
------
To label a position in your code where you can jump to from another place, you use a label::
nice_place:
; code ...
It's just an identifier followed by a colon ``:``. It's allowed to put the next statement on
the same line, after the label.
Variables and value literals
----------------------------
@ -178,7 +193,23 @@ data types below you can see how they should be written.
Variable declarations
^^^^^^^^^^^^^^^^^^^^^
@todo
Variables should be declared with their exact type and size so the compiler can allocate storage
for them. You can give them an initial value as well. That value can be a simple literal value,
but you can put a (constant) expression there as well. The syntax is::
<datatype> <variable name> [ = <initial value> ]
Various examples::
word thing
byte counter = 0
byte age = 2018 - 1974
float wallet = 55.25
str name = "my name is Irmen"
word address = #counter
byte[5] values = [11, 22, 33, 44, 55]
byte[5][6] empty_matrix
Data types
@ -211,121 +242,132 @@ type identifier type storage size example var declara
=============== ======================= ================= =========================================
**String encoding:**
Strings in your code will be encoded (translated from ASCII/UTF-8) into either CBM PETSCII or C-64 screencodes.
PETSCII is the default, so if you need screencodes (also called 'poke' codes)
you have to use the ``_s`` variants of the string type identifier.
A string literal of length 1 is considered to be a *byte* instead with
that single character's PETSCII value. If you really need a *string* of length 1 you
can only do so by assigning it to a variable with one of the string types.
**Floating point numbers:**
Floats are stored in the 5-byte 'MFLPT' format that is used on CBM machines,
and also most float operations are specific to the Commodore-64.
This is because routines in the C-64 BASIC and KERNAL ROMs are used for that.
So floating point operations will only work if the C-64 BASIC ROM (and KERNAL ROM)
are banked in (and your code imports the ``c64lib.ill``)
The largest 5-byte MFLPT float that can be stored is: **1.7014118345e+38** (negative: **-1.7014118345e+38**)
**Initial values over multiple runs:**
The initial values of your variables will be restored automatically when the program is (re)started,
*except for string variables*. It is assumed these are left unchanged by the program.
If you do modify them in-place, you should take care yourself that they work as
expected when the program is restarted.
**@todo pointers/addresses? (as opposed to normal WORDs)**
**@todo signed integers (byte and word)?**
Reserved names
^^^^^^^^^^^^^^
Indirect addressing and address-of
----------------------------------
The following names are reserved, they have a special meaning::
**Address-of:**
The ``#`` prefix is used to take the address of something.
It can be used for example to work with the *address* of a memory mapped variable rather than
the value it holds. You could take the address of a string as well, but that is redundant:
the compiler already treats those as a value that you manipulate via its address.
For most other types this prefix is not supported and will result in a compilation error.
The resulting value is simply a 16 bit word.
**Indirect addressing:**
@todo
**Indirect addressing in jumps:**
@todo
For an indirect ``goto`` statement, the compiler will issue the 6502 CPU's special instruction
(``jmp`` indirect). A subroutine call (``jsr`` indirect) is emitted
using a couple of instructions.
A X Y ; 6502 hardware registers
AX AY XY ; 16-bit pseudo register pairs
SC SI SZ ; bits of the 6502 hardware status register
Conditional Execution
---------------------
Operators
---------
Conditional execution means that the flow of execution changes based on certiain conditions,
rather than having fixed gotos or subroutine calls. IL65 has a *conditional goto* statement for this,
that is translated into a comparison (if needed) and then a conditional branch instruction::
.. data:: # (address-of)
if[_XX] [<expression>] goto <label>
Takes the address of the symbol following it: ``word address = #somevar``
The if-status XX is one of: [cc, cs, vc, vs, eq, ne, true, not, zero, pos, neg, lt, gt, le, ge]
It defaults to 'true' (=='ne', not-zero) if omitted. ('pos' will translate into 'pl', 'neg' into 'mi')
@todo signed: lts==neg?, gts==eq+pos?, les==neg+eq?, ges==pos?
.. data:: + - * / // ** % (arithmetic)
The <expression> is optional. If it is provided, it will be evaluated first. Only the [true] and [not] and [zero]
if-statuses can be used when such a *comparison expression* is used. An example is::
``+``, ``-``, ``*``, ``/`` are the familiar arithmetic operations.
if_not A > 55 goto more_iterations
``//`` means *integer division* even when the operands are floating point values: ``9.5 // 2.5`` is 3 (and not 3.8)
``**`` is the power operator: ``3 ** 5`` is equal to 3*3*3*3*3 and is 243.
``%`` is the modulo operator: ``25 % 7`` is 4.
Conditional jumps are compiled into 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.
You'll have to restructure your gotos in the code (place target labels closer to the branch)
if you run into this type of assembler error.
.. data:: << >> <<@ >>@ & | ^ ~ (bitwise arithmetic)
``<<`` and ``>>`` are bitwise shifts (left and right), ``<<@`` and ``@>>`` are bitwise rotations (left and right)
``&`` is bitwise and, ``|`` is bitwise or, ``^`` is bitwise xor, ``~`` is bitwise invert (this one is an unary operator)
Assignments
-----------
.. data:: = (assignment)
Assignment statements assign a single value to a target variable or memory location.::
target = value-expression
Sets the target on the LHS (left hand side) of the operator to the value of the expression on the RHS (right hand side).
Augmented Assignments
---------------------
.. data:: += -= *= /= //= **= <<= >>= <<@= >>@= &= |= ^= (augmented assignment)
A special assignment is the *augmented assignment* where the value is modified in-place.
Several assignment operators are available: ``+=``, ``-=``, ``&=``, ``|=``, ``^=``, ``<<=``, ``>>=``
Syntactic sugar; ``A += X`` is equivalent to ``A = A + X``
Expressions
-----------
.. data:: ++ -- (postfix increment and decrement)
In most places where a number or other value is expected, you can use just the number, or a full constant expression.
The expression is parsed and evaluated by Python itself at compile time, and the (constant) resulting value is used in its place.
Ofcourse the special il65 syntax for hexadecimal numbers (``$xxxx``), binary numbers (``%bbbbbbbb``),
and the address-of (``#xxxx``) is supported. Other than that it must be valid Python syntax.
Expressions can contain function calls to the math library (sin, cos, etc) and you can also use
all builtin functions (max, avg, min, sum etc). They can also reference idendifiers defined elsewhere in your code,
if this makes sense.
Syntactic sugar; ``A++`` is equivalent to ``A = A + 1``, and ``A--`` is equivalent to ``A = A - 1``.
Because these operations are so common, we have these short forms.
Subroutines
-----------
.. data:: == != < > <= >= (comparison)
Defining a subroutine
^^^^^^^^^^^^^^^^^^^^^
Equality, Inequality, Less-than, Greater-than, Less-or-Equal-than, Greater-or-Equal-than comparisons.
The result is a 'boolean' value 'true' or 'false' (which in reality is just a byte value of 1 or 0).
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 input- and output parameters.
For now, only register based parameters are supported (A, X, Y and paired registers,
the carry status bit SC and the interrupt disable bit SI as specials).
For subroutine return values, the special SZ register is also available, it means the zero status bit.
.. data:: not and or xor (logical)
These operators are the usual logical operations that are part of a logical expression to reason
about truths (boolean values). The result of such an expression is a 'boolean' value 'true' or 'false'
(which in reality is just a byte value of 1 or 0).
.. data:: .. (range creation)
Creates a range of values from the LHS value to the RHS value, inclusive.
These are mainly used in for loops to set the loop range. Example::
0 .. 7 ; range of values 0, 1, 2, 3, 4, 5, 6, 7 (constant)
A = 5
X = 10
A .. X ; range of 5, 6, 7, 8, 9, 10
byte[4] array = 10 .. 13 ; sets the array to [1, 2, 3, 4]
for i in 0 .. 127 {
; i loops 0, 1, 2, ... 127
}
.. data:: [ ... ] (array indexing)
When put after a sequence type (array, string or matrix) it means to point to the given element in that sequence::
array[2] ; the third byte in the array (index is 0-based)
matrix[4,2] ; the byte at the 5th column and 3rd row in the matrix
.. data:: ( ... ) (precedence grouping in expressions, or subroutine parameter list)
Parentheses are used to chose the evaluation precedence in expressions.
Usually the normal precedence rules apply (``*`` goes before ``+`` etc.) but with
parentheses you can change this: ``4 + 8 * 2`` is 20, but ``(4 + 8) * 2`` is 24.
Parentheses are also used in a subroutine call, they follow the name of the subroutine and contain
the list of arguments to pass to the subroutine: ``big_function(1, 99)``
Subroutine calls
----------------
You call a subroutine like this::
[ result = ] subroutinename_or_address ( [argument...] )
; example:
outputvar1, outputvar2 = subroutine ( arg1, arg2, arg3 )
Arguments are separated by commas. The argument list can also be empty if the subroutine
takes no parameters.
If the subroutine returns one or more result values, you must use an assignment statement
to store those values somewhere. If the subroutine has no result values, you must
omit the assignment.
Subroutine definitions
----------------------
The syntax is::
@ -333,13 +375,33 @@ The syntax is::
... statements ...
}
**proc_parameters =**
; example:
sub triple_something (amount: X) -> A {
return X * 3
}
The open curly brace must immediately follow the subroutine result specification on the same line,
and can have nothing following it. The close curly brace must be on its own line as well.
Pre-defined subroutines that are available on specific memory addresses
(in system ROM for instance) can be defined by assigning the routine's memory address to the sub,
and not specifying a code block::
sub <identifier> ([proc_parameters]) -> ([proc_results]) = <address>
; example:
sub CLOSE (logical: A) -> (A?, X?, Y?) = $FFC3
.. data:: proc_parameters
comma separated list of "<parametername>:<register>" pairs specifying the input parameters.
You can omit the parameter names as long as the arguments "line up".
(actually, the Python parameter passing rules apply, so you can also mix positional
and keyword arguments, as long as the keyword arguments come last)
**proc_results =**
.. data:: proc_results
comma separated list of <register> names specifying in which register(s) the output is returned.
If the register name ends with a '?', that means the register doesn't contain a real return value but
is clobbered in the process so the original value it had before calling the sub is no longer valid.
@ -349,49 +411,53 @@ The syntax is::
what the changed registers are, assume the worst")
Pre-defined subroutines that are available on specific memory addresses
(in system ROM for instance) can also be defined using the 'sub' statement.
To do this you assign the routine's memory address to the sub::
Loops
-----
sub <identifier> ([proc_parameters]) -> ([proc_results]) = <address>
for loop
^^^^^^^^
@todo::
example::
sub CLOSE (logical: A) -> (A?, X?, Y?) = $FFC3"
for <loopvar> in <range> [ step <amount> ] {
; do something...
}
Calling a subroutine
^^^^^^^^^^^^^^^^^^^^
while loop
^^^^^^^^^^
@todo::
You call a subroutine like this::
while <condition> {
; do something...
}
subroutinename_or_address ( [arguments...] )
or::
repeat--until loop
^^^^^^^^^^^^^^^^^^
@todo::
subroutinename_or_address ![register(s)] ( [arguments...] )
repeat {
; do something...
} until <condition>
If the subroutine returns one or more values as results, you must use an assignment statement
to store those values somewhere::
outputvar1, outputvar2 = subroutine ( arg1, arg2, arg3 )
Conditional Execution
---------------------
The output variables must occur in the correct sequence of return registers as specified
in the subroutine's definiton. It is possible to not specify any of them but the compiler
will issue a warning then if the result values of a subroutine call are discarded.
If you don't have a variable to store the output register in, it's then required
to list the register itself instead as output variable.
Must align this with the various status bits in the cpu... not only true/false....
Arguments should match the subroutine definition. You are allowed to omit the parameter names.
If no definition is available (because you're directly calling memory or a label or something else),
you can freely add arguments (but in this case they all have to be named).
@todo::
To jump to a subroutine (without returning), prefix the subroutine call with the word 'goto'.
Unlike gotos in other languages, here it take arguments as well, because it
essentially is the same as calling a subroutine and only doing something different when it's finished.
if <condition> {
; do something....
}
[ else {
; do something else...
} ]
@todo::
if <condition> goto <location>
**Register preserving calls:** use the ``!`` followed by a combination of A, X and Y (or followed
by nothing, which is the same as AXY) to tell the compiler you want to preserve the origial
value of the given registers after the subroutine call. Otherwise, the subroutine may just
as well clobber all three registers. Preserving the original values does result in some
stack manipulation code to be inserted for every call like this, which can be quite slow.