Programs must start with the “program” keyword. Variable declarations can happen anywhere after a var is defined. Procedures and functions are defined by their respective keywords. The main block starts with a “begin” and “end”, as does code blocks.
Program MyProgram; var a,b,c : byte = 0; Procedure Myprocedure(mp_i:byte); var somProcedureVariable : byte; begin // Do someting end; // This is the main block. begin // Call user-defined procedures etc MyProcedure(2); end.
TRSE supports several data types:
- byte : values 0-255. Supported on all systems.
- integer: values 0-65535. Supported on all systems.
- long : 32-bit double word. Supported on all x86 and m68k systems. On the 6502, “long” is a 24-bit number.
- pointer : Points to an address in memory. Uses zero pages on the 6502, “long” on the x86/m68k and is simply an integer on the (GB)Z80
- lpointer: 24-bit pointers for the 65c816.
- boolean: Syntactically identical to a byte.
- string : An array of characters. Syntactically identical to an array of bytes.
- incbin : include a binary file
- incsid : include SID music file (C64 only)
- incnsf : include NSF music file (NES only)
TRSE allows types to have modifiers in order to instruct the compiler to either keep certain things in line (such as forcing a parameter to be pure numeric/variable) or modify the data (automatic lz4 compression of data, or placement in special memory blocks etc). For instance, on some systems it is necessary to attribute a specific type in order to tell TRSE where the block should be placed. This is mostly relevant for the Amiga and the Gameboy, or systems that loads programs from ROM (Vectrex, NES etc).
The type modifiers must be added as a list of keywords directly before or after the type itself (“global” must be placed before the type).
Program MyProgram; var myArray: array[$100] of byte chipmem; // Places data in chipmem (AMIGA only) sprite_data : array[4*3] of byte sprram; // Puts data in sprite ram (GAMEBOY only) image_data : incbin("myImage.bin") compressed; // Automatically LZ77-compresses data and includes (allowed on all systems. Use the TRSE "decompression" library to decompress real-time.) myString : string = ("HELLO"); // normal null-terminated string myInfiniteString : string no_term = ("HI","THERE"); //no null-termination myVar : byte at $3030; // Place variable at an absolute value // "global" implies that the variable 'b' is already defined elsewhere, and will be reused (saving a byte) // "pure" means that the value *must* be a pure variable or constant, not an expression // Inline procedures famously inserts "b" as macros, hence the need for "pure" to eliminate the // possibility of the user inserting an advanced expressions. procedure MyProcedure( b : global pure byte ) inline;
On systems using the MOS6502, Motorola 68000, Z80 or the Motorola 6809 you can automatically LZ4-compress data using the “compressed” flag. Use the “compression” library to deflate your data:
@use "compression/compression" var image : incbin("mydata.bin") compressed; begin ... Compression::Decompress(#image,#videomemory);
A character is a single byte represented with a single letter encased in ”. ‘A’ and 65 are treated as identical in TRSE.
myByte := 'C'; // same as myByte:=67; if (myByte='5') then ...
TRSE supports arrays of base types (which includes addresses of symbols), records and classes.
If the number of elements in the array doesn’t match the declared count, TRSE will pad the missing items with “0”.
If you don’t know the number of elements in the array – but have a list of data – you can declare the count to be missing by using .
Arrays are accessed as such:
there is no out-of bounds testing in TRSE.
Note that on the 6502 you cannot have arrays of pointers, as they must reside in the zeropage address space. Instead, use an array of integers, and simply assign a pointer value to the integer array element in order to use it, i.e.
var addresses : array[ 10 ] of integer = (#sym1, $2020, ... ) zp : pointer; ... zp := addresses[i]; zp := ...
This restriction does not apply on the x86, z80 or m68k where pointers are simply 16/32 bit numbers.
It is possible to define your own types in TRSE:
type mat4 = array of integer; rotMat : mat4;
Pointers and References
Pointers are the back-bone of any programming language, and are simply variables that contain (“points to”) a memory address. A TRSE pointer p can point to any of the three standard built-in types : byte, integer or long, where the corresponding lookup p[ index ] automatically adjusts for 1,2 or 4 byte data lengths. Pointers can also point to class objects, but not to records (since these are laid out non-chunky in memory by TRSE).
The reference symbol in TRSE is #, analogous to C’s &. # returns the address of a variable/data, and can be assigned to pointers – or 16/32 bit integers/longs. Example:
var myArray of byte = BuildTable(" i*2 "); // builds a table with values 0,2,4,6,8 ..... someText : string="SOME TEXT"; p: pointer; // default byte pointer, same as Pascal ^byte or "pointer of byte" ip : pointer of integer; // integer pointer, same as Pascal ^integer ip2 : ^integer; // same as "pointer of integer" begin p:=#myArray; // p now points to myArray p := 10; // same as myArray := 10; p:=#myArray + i*8; // p now points to myArray + i*8 p:=#someText + 3; // points to the 3rd letter in the string ip := #myIntegerArray; // ip will access elements as integers (16-bit, or 2 byte numbers)
Array of arrays/strings
String lists in TRSE are declared as a list of pointers containing the addresses of the declared strings. Example:
var strlst : array[ 3 ] of string = ("HELLO", ("YOU",10,65), "WORLD"); zp : pointer; // a pointer ... printstring(strings,0,40); // will print out "HELLO" strlst:="NEW STRING";
An if-else block in TRSE is defined as such:
if (a=1) then DoSomething(); if (a<>3) then // not equals begin DoSomethingElse(); IgnoreMe(4); end; if (a*2=b*3) then=return() else somethingelse();
Note that an there should be no semicolon “;” before an “else”.
A final note: The “empty” conditional
if (a) then DoSomething() else DoSomethingElse();
is the same as typing “if (a<>false)”
Nested conditionals must be enclosed with parentheses in order to remove any ambiguity:
if (a and (b<>c or (d > e))) then DoSomething();
This requirement also applies when all the conditionals are “and”
if (a and (b and (c and (d and (e>2))))) then DoSomething();
While conditionals works similar to if statements:
while (a>0) do begin PrintSomething(a); dec(a); end;
Multiple conditionals needs to be nested in a hierarchy as such:
if (a>0 and (c<10 or d<>5)) then ... if (j<>k*2 or (c<10 or (b=c and d=e))) then ...
Cases tests a single variable for multiple values as such:
case i of 0: DoSomething(); 1: begin HelloThere(); b:=b+5; end; 2+i : ThisIsWeird(); else begin PerformElseBlock(); end;
The else block is optional.
TRSE supports the usual suspects when regards to control statements in loops: break and continue
for i:=0 to 10 do if (i=5) then continue; // if i=5 then the program jumps back to the start of the for loop where i is now 6 end; if (i=8) then break; // breaks off the loop when i=8
Procedures are defined as such:
Procedure NoParameters(); begin // .. code here end; // Procedure with single parameter and variable block procedure DoSomething(a,b:byte; zp: pointer); var localVar : byte; begin zp:=zp+a; localVar := a+b; // ... end;
Functions are procedures that can return values. TRSE currently only supports ‘byte’, ‘integer’ and ‘boolean’ as return values.
function Calc( p : byte ) : byte; begin // return some random calculation Calc := p*p + 2; end; begin // Call function i:=Calc( j+2 ); // ... end;
A note on Returnvalue (semi-obsolete)
ReturnValue is a semi-obsolete method that was used before TRSE supported functions.
Calling “ReturnValue( int / byte )” will load the parameter value into the internal
byte / int registers for the current CPU, and return from the function (rts/ret etc), allowing
for function-like return values as such:
procedure Calc( p : byte ); begin // return some random calculation Calc := p*p + 2; end; begin // Call function i:=Calc( j+2 ); // ... end;
Note: A major flaw with “Returnvalue” – and why functions should be used instead – is that
the compiler is unable to obtain the correct return value type. The following code will therefore fail:
// The compiler doesnt know whether "Calculate" returns a byte or an integer, but since // "someInteger" is an int it assumes this incorrectly. // Buggy on the 6502 (since the contents of "y" is unknown) // Fatal on the Z80 since bytes are stored in "a" whereas ints in "hl" someInteger := Calculate( j );
You can forward declare a function by using the “forward” keyword:
function Calc( p : byte ) : byte; forward; // requires "Calc" to be defined procedure Hello(j : byte); begin k:=Calc(j); end; // Actual definition function Calc( p : byte ) : byte; begin // return some random calculation Calc := p*p + 2; end;
You can declare a procedure/function as inline by using the inline keyword. This will effectively remove the procedure declaration and simply insert the function code directly. As this effectively works like a macro, note that function parameters are evaluated on the fly – meaning that if you have an advanced function you’ll get a lot of overhead. Also, since inline functions behave like macros, parameters are non-existent so you cannot access them via assembler. Inline is useful for system calls and small, fast routines.
// Example from the trs-80 coco3 system unit that initialises the stack pointer, // requiring inline injection (since a jsr would fail when returning as the stack pointer is modified) procedure InitSystem() inline; begin DisableInterrupts(); DisableROM(); asm(" clra tfr a,dp lds #System_stack ; modifies the stack "); SetFast(); end; // Bad use of inline procedure Calc( p : byte ) inline; begin c:= p * p +2; end; // because if you do this: Calc(i*3 + j); // the inlining of the parameter will effectively insert this expression: c:= (i*3 + j) * (i*3 + j) +2;
Note that you can *enforce* inline parameters to be pure – either a pure numerical value or pure variable by using the “pure” keyword
procedure Calc( p : pure byte ) inline; begin c:= p * p +2; end; ... // the following will raise a compiler error because the parameter // isn't a pure variable or number Calc(i*3 + j);
For loops in TRSE are by default non-inclusive, meaning that the below expression will count i from 0 to 9
for i:=0 to 10 do begin PrintNumber(i); // will print 0-10 end;
If you prefer inclusive for, use the fori keyword:
fori i:=0 to 10 do begin PrintNumber(i); // will output 0-10 end;
You can change the step length by using the step keyword:
for i:=0 to 10 step 2 do begin PrintNumber(i); // will output 0,2,4,6,8 end;
You can specify the compiler to unroll your loops for faster execution, at the cost of larger code:
for i:=0 to 5 unroll do begin doSomething(i+1); end; // syntactically the same as doSomething(1); doSomething(2); doSomething(3); doSomething(4); doSomething(5);
Inline assembly code
TRSE allows the user to insert assembly code anywhere in a statement block. You can mix between Pascal and asm at will.
for i:=0 to 5 do begin v += 5; asm(" lda v asl tax myLoop: ... "); end;
Note that since variables are internally sometimes stored with prefixes (such as unit variables etc), you will have to be careful when accessing these variables from an inline assembly block. Variables with the same name as internal CPU registers are always prefixed (such as “hl”,”a”, “bc” on the z80 and “ax”,”bx” etc on the x86). Z80 example:
Unit SomeUnit; var screen : ^byte; ... // screen variable is internally stored as "SomeUnit_screen" asm(" ld hl, [ SomeUnit_screen ] ; ...
Inline assembly unrolling
The internal OrgAsm 6502/z80 assembler allows for 1d and 2d unrolling of code. The internal unroll values are expressed through [i] and [j].
asm(" repeat 50 lda $8000 + [i*8] sta $4400 + [i] repend "); /* The above code will generate the following unrolled code: lda $8000 + 0 sta $4400 + 0 lda $8000 + 8 sta $4400 + 1 lda $8000 + 16 sta $4400 + 2 ... */
TRSE has a lot of preprocessor directives defined. These can be used to give messages to the compiler before the actual parsing of the program is executed.
Constants are preprocessors that will be string replaced before compilation occurs.
@define myConstant 4 @define myText "WHOO PARTY" ... a:=@myConstant * 5; // same as 4*5
Preprocessor ifdef / if / else / endif
You can define preprocessor constants and use them in preprocessor expressions as such:
@define L 1 @define N 10 @ifdef L // If @L is defined... @if N = 10 // if @N holds the value of 10 screen_bg_col:=1; @else screen_bg_col:=2; @endif @else // if L is not defined screen_bg_col:=4; @endif
Pre-existing preprocessor defines
You can define your own preprocessor for a given project in the project settings->target output->global defines. Simply type “@define IS_DEBUG 1” etc in order to add the preprocessor to your project.
TRSE also provides the user with some handy preprocessor defines for determining the current CPU / system:
... @ifdef C64 // the following line will only be compiled if the current project is for the C64 screen_bg_col := black; @endif @ifdef CPU_Z80 // the following line will only be compiled if the current project is for a Z80 CPU asm(" push bc "); @endif
The syntax for cpu-specific defines are CPU_MOS6502, CPU_Z80, CPU_PX86, CPU_M68K, CPU_GBZ80 and CPU_M6809.
Other preprocessor commands
TRSE contains a lot of preprocessor commands that can perform a variety of function. Some examples are:
@raiseerror "This text will halt compilation and display this message" @raisewarning "This text will continue compilation and display this message as a warning" @deletefile "myfile.bin" // will export 10 sprites from mysprites.flf // @export works on all .flf files, and will export to the system's // native video format @export "mysprites.flf" "mysprites.bin" 10 // will export a subregion in a file. Only works on select systems @exportsubregion "tiles.flf" "tiles.bin" 0 0 8 8 // Sprite compiler will convert sprites to pure .asm functions for that extra speed. Only works on select systems. @spritecompiler "images/sprites1.flf" "tank" 3 0 16 8
There are a lot of undocumented built-in preprocessor commands for automatically building noise data, path tools, conversion, file handling, sprite compiling etc. If you have an idea for a neat preprocessor tool command for your favourite system, don’t hesitate requesting getting it implemented into TRSE!
A record is similar to a “typedef” in C, and lets the programmer bundle a set of variables together to a primitive object. TRSE allows for creation of these objects as arrays. Internally, each element of the record (or array) is stored as individual arrays. This allows for fast lookup on the 6502 and z80 etc, but prevents any pointers to records as the data isn’t bundled together.
Since records are internally stored as arrays of data, having arrays within records is not possible. You will need to define a class instead, which will store data sequentially instead of a distributed manner.
program SomeRecords; var monster_type = record x,y : byte; // Current position of monster color : byte; // Current colour of monster health : byte; end; player : monster_type; // Declare "player" as a single monster type const noMonsters : byte = 10; // Declare an array of monsters_type monsters : array[ noMonsters ] of monster_type;
A class is similar to a Record, but has a couple major distinctions. First, you can define methods (procedures) within the class that operates on its properties. Also, class data is stored sequentially – or chunky – as opposed to records. This makes lookup a bit slower on the 6502, but you can now have pointers to the objects. Third, it is possible to have arrays and strings located inside classes (as opposed to records).
Use the “this” pointer in order to access local class variables within a method.
var Monster = class x,y : byte; // Current position of monster color : byte; // Current colour of monster health : byte; procedure Draw(); begin moveto(this.x, this.y,hi(screen_char_loc)); screenmemory := 0; // draw a @ end; end; player : Monster; // Declare "player" as a single monster type const noMonsters : byte = 10; // Declare an array of monsters_type monsters : array[noMonsters] of Monster; pm : pointer of Monster; .. pm := #monsters; // Point to the 3rd monster pm.x := 10; pm.Draw(); monsters.Draw(); // same as pm.Draw();
Please note: Classes are still in its very early implementation, and does not yet support inheritance/constructors/private/public properties etc. Also, in order to ensure good coding practice (= I haven’t managed to write an optimizer for this yet), setting properties directly of arrays of objects is not allowed:
monsters[i].x := 10; // NOT allowed in TRSE monsters[i].y := 20; // NOT allowed in TRSE
These calls would have to calculate i*sizeof(Monster) on every step and set a temporary pointer to the memory location. Instead, you’ll have to do this manually – enforcing good coding standards:
var pm : pointer of Monster; ... pm := #monsters[i]; // Points to the i'th element pm.x := 10; pm.y := 20;
A Turbo Rascal Unit file (.tru) is a re-usable library (containing rascal source code) that can be included and compiled on build. Units are extremely handy when creating re-usable pieces of code (also across all platforms). Units are not pre-compiled, but are included compile-time.
Units can be placed in 4 different levels, depending on the usage:
- Project level: The .tru file is located somewhere in your project folder, and can only be accessed from your project. Example: game monster library, inventory library
- System level: The .tru file (located in TRSE/units/[SYSTEM]) can be accessed from all projects for the given system (ie C64, Amstrad, X86, Amiga). These libraries should typically contain system-specific functions such as screen setup, text output, line drawing, file handling etc.
- CPU level: The .tru file (located in TRSE/units/cpu_specific/[CPU]) can be accessed from all projects for any system that uses the specific CPU (ie 6502, Z80, M68K, X86 etc). These libraries should contain system-independent methods that are useful for all projects of the same CPU. Examples include: compression/decompression, math methods (matrix operations, vectors, operations etc)
- Global level: the .tru file (located in TRSE/units/global) is accessible for *all* systems using *all* CPU types. This means that the code is completely platform independent. Examples include: string manipulation libraries, 3d engine
It is possible to mix levels of units, where global units contain “high level code” and system-specific units contain “low-level” code (perhaps with direct asm + system-specific calls). As an example, think of a wireframe 3D renderer:
- A global unit containing high-level methods such as mesh creation, draw calls, sorting methods, list of of 3d objects etc. Accessible from all systems.
- CPU-specific units containing mathematical matrix/vector operations. These units are used in the main global 3d egnine. (example: 6502 matrix class, z80 matrix class etc)
- A system-specific graphics class for line drawing: when the “global” unit calls the respective “draw line” method from the graphics unit, the actual implementation is performed on a system level (since a drawline method would vary greatly between the C64, BBC Micro and Amiga 500).
Unit MyUnit; var var1, var2 : byte; const ANumber : integer = 200; Monster = record x,y,health : byte; end; // keyword "global" reuses var1 procedure Proc1(var1 : global byte); begin // .. do some stuff end; end.
Program myProgram; @use "MyUnit" var monsters : array[ MyUnit::ANumber ] of MyUnit::Monster; ... MyUnit::Proc1( 10 );
Note that you access unit variable/methods by “Unit::var”, where “::” is the default separator. “::” is also synonymous to “_”, so you can also access the members by “Unit_var” instead. Using “_” us mandatory when accessing variables/methods within assembly blocks.