The PortablE language & compiler

  by Chris Handley, for PortablE r1 (04-Jul-2008)
  For my email address, please see the "About the author" chapter.
  Manual last updated 04-Jul-2008.

PortablE logo

This manual *may* be out of date! The latest version can be found here:

http://cshandley.co.uk/portable/PortablE.html

CONTENTS


1. Recent major news

Please see the "History" chapter (at the end of this manual) for a full summary of recent changes. For the details of any particular changes, you can read the relevant sections of this manual.


Go back to CONTENTS


2. Introduction

PortablE is my attempt to recreate AmigaE from scratch, with all the improvements I have wanted - but with backwards compatibility not a top priority (although I feel it is pretty good now), and with esoteric stuff (like Lisp cells) left out, at least for the moment.

PortablE is not a traditional compiler, because it doesn't output machine code, or even assembler (although it could), but rather it translates your code into another language (not just C++) for a chosen OS (not just AmigaOS). You would then be expected to give the output code to a 'real' compiler. The output code is referred to as the "target", while the input code is referred to as the "source".

When PortablE is generating the target code, it tries to preserve most of the formatting & layout of the (original) source code. The aim here is that someone could abandon PortablE and edit the target code instead. Perhaps you could code in PortablE, but provide your team with nice looking C++ code...? You can also be confident in the future of your code, because if you ever wish to leave PortablE then you can simply switch to the generated C++ (or AmigaE) code instead. There are many possibilities - use your imagination!

2.1. What is AmigaE?

This manual is aimed at existing AmigaE users, as it mainly describes the differences between PortablE & AmigaE. Someone with no prior experience of the E language should first become familiar with AmigaE.

In 1991 Wouter van Oortmerssen started working on the friendly but powerful AmigaE language, he released the first version in 1993, and continued to improve it up till 1997, by which time it had become a very popular programming language for the Amiga. But during 1998 development stalled, and it was finally officially abandoned in 1999, partly due to the Amiga platform's own lack of development since Commodore went under.

The original AmigaE shareware package:

http://aminet.net/package/dev/e/amigae33a
http://wouter.fov120.com/files/lang/e/amigae33a.lha

The full AmigaE compiler released for free:

http://wouter.fov120.com/files/lang/e/ec33a.lha
http://wouter.fov120.com/files/lang/e/ec33a.readme

Jason Hulance's great beginners guide:

http://cshandley.co.uk/JasonHulance/beginner_1.html

The following links may also help you.

The original AmigaE home page:

http://wouter.fov120.com/e

The AmigaE mailing list:

http://www.freelists.org/list/positron

The AmigaE IRC channel:

  #amigae on irc.freenode.net

Aminet AmigaE stuff:

http://aminet.net/search.php?path=dev/e

Amiga E Tutorials and Code Samples

http://www.amigau.com/c-programming/amigae/etut.htm

The homepage of PortablE:

http://cshandley.co.uk/portable

The homepage of ECX:

http://home.swipnet.se/blubbe/ECX

2.2. Current status

PortablE is capable of generating code for both the C++ and AmigaE(!) languages, and it supports Amiga OS3, OS4 and AROS. MorphOS is supported via ECX. In the future it will support more OSes & languages (likely including Windows).

PortablE is available as a 68k executable for OS3 & MorphOS, a PPC executable for OS4, and an x86 executable for AROS - all compiled by GCC using AmiDevCpp on Windows, from C++ code generated by PortablE itself!

Modules for accessing Amiga OS are currently limited to Asl, Commodities, Console, Devices (inc. Timer), Diskfont, Dos, Exec, Gadtools, Graphics, Icon, Intuition, Resources, Utility & Workbench, plus part of AmigaLib, class, Other & Tools. Additional modules could be added if there is interest, but the original idea for PortablE was to provide abstract modules that did not expose the OS.


128MB of installed memory is the recommended minimum, and 256MB will probably be required to compile reasonably large applications. A stack of at least 100KB is also required.

It will run fastest as a native executable (under Amiga OS4 & AROS), followed by an emulated Amiga (such as WinUAE or Amithlon), with AROS on QEmu and 68k emulation on MorphOS being slowest. I *suspect* that it may be too slow & memory hungry for all but the most powerful Classic Amigas, but would be happy to be told otherwise.

2.3. Background & motivation

I only became interested in AmigaE in 1996, after becoming dissatisfied with the other languages available. Luckily I was hooked before AmigaE was abandoned, and due to it's lack of bugs I happily continued to use it after that. Around 2000 severe problems with my Blizzard PPC forced me to contemplate ditching the Amiga, for the (AmigaOS-like) Psion platform, which meant loosing all my code & switching languages...

First I considered writing a crude translator from AmigaE to a Psion language, but it became clear something more advanced would be needed. Then in 2001 Amithlon appeared, just as Psion was abandoning my planned replacement platform, which allowed me to stay with the Amiga. But by this time I was very aware of how fragile & limiting it was to tie my programs to any single platform or OS, so my plans crystalised on writing a "universal" E translator that would future-proof my code & also allow my programs to run on all the other platforms I used. Anything less would likely be a waste of my time & effort!

I started properly designing PortablE in about 2001, and gradually resolved to improve the E language, with each new feature spurring ideas for further improvements. Due to a lack of time, a desire to get the design "right first time", a lack of any prior experience writing a compiler, and in general being overly ambitious :-) , it took me until early 2006 before it was complete enough to start working. And it then took me until January 2008 before it was able to successfully compile itself - a major milestone, which should guarantee that PortablE will continue to be developed, even *if* I stop using Amiga OS for daily tasks. It was finally officially released in June 2008.

2.4. The biggest changes from AmigaE

Remember that AmigaE hasn't been developed by Wouter for 10 years, but if it had then it would have likely looked quite different from the AmigaE v3.3 (and close descendants) that we have been stuck with. Think of PortablE as what AmigaE v4 or v5 *might* have looked like - except without Wouter's penchant for adding features from completely unrelated languages!

[NOTE: While some of the basic classes have already been completed, they aren't provided yet. So for the moment there is direct access to a selection of common AmigaOS modules.]

I have many plans for great future improvements, and these will appear slowly - as and when I have the time.


Go back to CONTENTS


3. Legal Disclaimer

This program is distributed for free, in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.


(Rough translation into English: PortablE cost you nothing, so don't expect me to take responsibility for any problems that it causes you. If it erases your harddisk, or directs your car into a ditch, that's your problem. Of course, I try to ensure that PortablE works well, but I can't guarantee it, so all use is soley at your risk.)


Go back to CONTENTS


4. Installation & usage of the compiler

PortablE requires a 100KB stack, and will refuse to run with less. Either have the STACK=100000 tooltype set in your Shell's icon, or otherwise type this before running PortablE:

  Stack 100000

4.1. Installation

1. The PortablE compiler can be put anywhere - use Sys:C or E:bin if you want. Basically anywhere that is part of your Shell's search path.

If you are using Amiga OS4, then use the PortablE-OS4 executable, but rename it to just PortablE. Similarly, if you are using AROS, then use the PortablE-AROS executable, but rename it.

2.(a) If you already have a previous version of PortablE installed, simply delete the old PEmodules:PE & PEmodules:target folders, and then copy the contents of the PEmodules folder into PEmodules:.

2.(b) Otherwise put the PEmodules folder somewhere safe, for example Work:PEmodules . You must then assign PEmodules: to it, for example add the following line to your S:user-startup file:

  Assign PEmodules: Work:PEmodules

If you have an old AmigaE installation, then you might like to copy the source code for any favourite modules from Emodules: to PEmodules:.

4.2. Usage description

PortablE is a Shell program, like AmigaE, and it's argument template is:

  Source/A, TargetFile/K, TargetOS=OS, TargetLanguage=Lang,
  OptOptimise/S, OptPointer/S, OptAmigaE/S, OptNoPtrToChar/S,
  NoOptInline/S, RefreshCache/S

4.3. Usage example

If you haven't already followed the stack warning given at the beginning of this chapter, then type the following in your Shell first:

  Stack 100000

To compile the program Work:Code/PE/example.e, with optimisations enabled, you would enter the following line:

  PortablE Work:Code/PE/example.e OptOptimise

It should not take long to compile, although it has to use some large system modules - which will slow it down UNTIL their module cache files have been automatically created the first time they are used.

The end result should be the file Work:Code/PE/example.cpp , which you can then compile using your favourite C++ compiler. Remember that PortablE can also generate AmigaE v3 compatible code, even when using new features not supported by AmigaE.

4.4. Modules

PortablE expects that the code for any module ends with '.e', and can be found using the PEmodules: assignment.

PortablE requires several special "system" modules; most of these modules are stored within the main 'PE' folder, but some of them are stored inside 'PE' folders that can be found within the 'target' folder. Please do not try to use these modules yourself!

The 'target' folder is special, as it allows the same module declaration to refer to different implementations depending on which target your code is compiled for, and is described later.

As with AmigaE, module paths starting with * use local paths (i.e. paths which start in the same directory as the current file).


4.5. Module cache

The automatic module cache provides a big speed boost (3-6 times faster), by storing the result of compiling a module for re-use next time, somewhat like .o files in traditional compilers (and .m files in AmigaE). Each module cache ends in ".pem" .

But the cache will not be used for modules that have been modified, nor for modules that depend on modified modules. Thus modifying a low-level module (upon which most modules ultimately depend) will temporarily loose you the speed benefit of the module cache.

Also, the module cache will (temporarily) not be used when you change one of the command line OPT switches, such as OptOptimise or OptAmigaE. So I strongly recommend being consistent in your usage of such switches.

PLEASE DO NOT replace any module cache files (which end in ".pem") with different or backup versions, as this could break the logic used by the automatic module cache (unless you use the REFRESHCACHE switch). But you can of course replace the module files themselves (which end in ".e") with different or backup versions. At some point I will add additional checks to avoid this potential problem.


Go back to CONTENTS


5. Compiling the code generated by PortablE

5.1. C++ code

Each C++ compiler has it's own bugs, and also different restrictions on what C++ code is allowed. So it has proven tricky to get the C++ code generated by PortablE to consistently work, but so far I have managed it for all the compilers available to me.

However, I have not tested everything, nor have I tested every single version of every C++ compiler. So please email me if you find any problems with the C++ code generated by PortablE.

Each compiler I've tried has it's own quirks:

You can have this automatically done for all projects, by going to the menu Tools/Compiler Options, and choosing the Compiler tab. Add -noixemul to the linker command line (which is the second box).

Note that AROS does not need nor support -noixemul.

5.2. AmigaE code

The code generated is AmigaE v3 code, which is compatible with CreativE. It is *not* completely compatible with ECX - but you can use TargetLanguage=ECX (instead of =AmigaE) to make the code completely compatible with ECX.


Warning: The ECX mode is a slight hack, because it uses the same modules as AmigaE, and it shares the module cache with AmigaE. The upshot of this is that if you compile something for AmigaE, compiling it again for ECX will *not* generate ECX compatible code, unless you use the REFRESHCACHE option. Therefore I recommend that you either use "AmigaE" or "ECX", but not both. The ECX code can still be compiled by AmigaE.


Please note that AmigaE has a bug where SUPER methods cannot be called when they have the same name as a member in *any* object (even from an OS module). Making PortablE work-around this bug would make the generated source code look very messy, and in the case of the end() method it is not really possible to fix. I have an unreleased version of the CreativE that fixes this problem, and if you are interest then please ask it's author (Tomasz Wiszkowski) about it. I suggest using his GMail address, which can be found here:

http://www.tbs-software.com/fp/author.phtml

Go back to CONTENTS


6. The module options (OPT)

Like AmigaE you can specify module options using OPT . The current options are:

  OPT OPTIMISE    ->enables optimisations.
  OPT INLINE      ->enables just the procedure in-lining optimisation.
  OPT POINTER     ->enables pointer arithmetic/manipulation support.
  OPT AMIGAE      ->enables the AmigaE backwards compatibility mode,
                    including pointer arithmetic/manipulation.
  OPT NOPTRTOCHAR ->stops variables defaulting to PTR TO CHAR,
                    when OPT AMIGAE is used.
  OPT NATIVE      ->enables the use of native target language elements.
  OPT PREPROCESS  ->enables the preprocessor.
  OPT ELSEIF      ->enables ELSEIF and disables ELSE IF support.

You can apply certain options to all modules from the command line (see the earlier "Installation & usage of the compiler" chapter). The original AmigaE options are also present, but they do nothing.

6.1. OPT POINTER

Portability will be impaired - it will not be possible to generate code for target languages like Java, which do not support real pointers. Better to use arrays if you can, but it's your choice :-)

I suggest restricting this to (low-level) modules that absolutely require it, so that only those modules need rewriting if you want to support more target languages later.

6.2. OPT AMIGAE

Portability will be impaired - it will not be possible to generate valid code for strongly-typed target languages, such as Java.

It also tends to produce messy C++ code, because untyped variables have the type PTR TO CHAR (rather than VALUE), yet C++ has many restrictions on the use of pointers that must be avoided using casts.

6.3. OPT NOPTRTOCHAR

Use this if you get C++ compiler errors like "cast from 'char*' to 'XXX' loses precision" or "cast to pointer from integer of different size" for source code that was generated using OPT AMIGAE. Obviously it reduces compatibility with AmigaE code, but usually you just have to declare a few variables as being PTR TO CHAR when PortablE gives an error that they can't be indexed as an array.

If this option fails to solve the compiler errors, then please let me know.

6.4. OPT NATIVE

Portability will be completely lost - so you are strongly recommended to not use it, except for low-level abstractions (modules) that will need to be rewritten for every target language/OS.


Go back to CONTENTS


7. A quick start

This chapter is to help get AmigaE users quickly writing new programs in PortablE. I still strongly suggest that users read the full list of differences from AmigaE, which are given in later chapters, as well as the chapters on the type system & possibly on object orientation.

PortablE needs a stack of at least 100KB to run, so either have the STACK=100000 tooltype set in your Shell's icon, or otherwise type the following each time you start the Shell:

  Stack 100000

7.1. Hello world

Using an editor like EditPad or CygnusEd, type the following program into a text file called "Hello.e" (without the quotes) :

  PROC main()
  	Print('Hello world!\n')
  ENDPROC

Assuming that you have put the PortablE compiler in the C: folder, and have saved "Hello.e" to your Work: partition, then you can compile it to C++ code by typing the following at a Shell prompt:

  PortablE Work:Hello.e

This should produce the file "Work:Hello.cpp", which you can then compile & run using your favourite C++ compiler (but for details on the quirks of each compiler please read the "Compiling the code generated by PortablE" chapter).


If you wanted to produce C++ code for OS4, using the OS3 (68k) version of PortablE, then you would type the following at a Shell prompt:

  PortablE Work:Hello.e OS=AmigaOS4


And if you wanted to produce 'old skool' AmigaE code instead, then you would type the following at a Shell prompt:

  PortablE Work:Hello.e LANG=AmigaE

This should produce the file "Work:Hello_OUTPUT.e".


If you are using OS4 then please note that the native OS4 executable is called PortablE-OS4, unless you have renamed it. Similarly for AROS, the executable is called PortablE-AROS.

7.2. What happened to WriteF?

With this program, the only difference from AmigaE is that we use the Print() procedure, rather than PrintF() or WriteF(). If you really prefer the old name, then you could try 'emulating' it:

  PROC main()
  	writeF('Hello world!\n')
  ENDPROC
  
  PROC writeF(string:ARRAY OF CHAR, param1=0, param2=0)
  	Print(string, param1, param2)
  	PrintFlush()
  ENDPROC

Here we see another difference - strings now have the type ARRAY OF CHAR, rather than PTR TO CHAR. And dynamically allocated arrays can be passed around (like pointers), rather than just being fixed-sized arrays preallocated on the stack. The reason for this is that PortablE is now type-checked, and for portability pointers may not be indexed like an array (unless you enable the 'dangerous' pointer arithmetic option).

Now, you are perhaps thinking that this 'emulation' is a bit inefficient, and would be better done using a line like:

  #define writeF(a,b,c) (Print(a,b,c) BUT PrintFlush())

Well, such macros are not recommended for PortablE (although they will work), because it provides an alternative which is generally better:

  PROC writeF(string:ARRAY OF CHAR, param1=0, param2=0) IS Print(string, param1, param2) BUT PrintFlush()

This is an "in-line procedure", which means that any calls to writeF() will be substituted by the given Print() code, when optimisations are enabled. So this is just as efficient as a macro, but it is more portable, supports default parameters, you don't need to worry about enclosing parameters within brackets, and parameters with side-effects can't cause problems.

Still, this is writeF() not WriteF(), because E does not let you declare procedures that begin with a capital letter, which means that it won't work with old programs. *There is a way around this*, but I don't want to introduce bad habits early on, so just think of this as an incentive to read the rest of this manual ;-) .

It's worth noting that the backwards compatibility mode *does* provide WriteF() and PrintF(), but you should only use that mode as a stepping stone when porting your programs over to PortablE.

Also note that while Print() is usually buffered, you can flush the buffer with PrintFlush(), so you shouldn't miss the unbuffered WriteF().

7.3. Floating point maths

PortablE is properly typed, so you no-longer need to use confusing ! symbols everywhere when dealing with floating point numbers. Here's a simple example:

  PROC main()
  	DEF result:FLOAT, number
  	
  	number := 5
  	result := number / 2.5
  	
  	IF result > 2
		Print('Result was > 2\n')
	ELSE
		Print('Result was <= 2\n')
	ENDIF
  ENDPROC

You will note that an integer variable was divided by a floating point number, and then stored in a floating point variable, without any worries. Similarly, a floating point variable was compared with an integer number.

Like C, you only need worry when dividing one integer by another integer. In that case PortablE will give you an integer, unless you cast one of the numbers as a floating point like so:

  result := 12 !!FLOAT / 34

The !! symbol is used for casting any type. It is always wise to cast the first number of a division, since E's left-to-right order of evaluation means that trying to cast the second number would end-up casting the result instead (unless you used brackets around it).

It is also worth mentioning that if you try to store a floating point variable in an integer variable, then PortablE will report an error, because precision will be lost. In that case you must cast it to an integer first:

  number := 12.34 !!LONG

7.4. Strings

Lets get PortablE to do something a bit more interesting:

  PROC main()
  	DEF newString:ARRAY OF CHAR
  	
  	newString := combine('one two ', 'three four\n')
  	Print(newString)
  ENDPROC
  
  PROC combine(first:ARRAY OF CHAR, second:ARRAY OF CHAR)
  	DEF eString:STRING
  	NEW eString[ StrLen(first) + StrLen(second) ]
  	
  	StrAdd(eString, first)
  	StrAdd(eString, second)
  	
  	Print('Third character=\c\n', eString[2])	->will show "e"
  ENDPROC eString

This example demonstrates quite a few changes. Let's deal with the combine() procedure first:


The eString variable has the e-string type, but where-as AmigaE only allowed you to declare fixed-sized STRINGs preallocated on the stack, PortablE also allows you to declare dynamically allocated STRINGs, as shown here.

We could have (dynamically) created an e-string using the NewString() procedure, which is the new name for our old friend String(), but with PortablE it is always better to use NEW & END if you can. Where-as AmigaE's NEW could only allocate an array, PortablE's NEW can be used to allocate an e-string or an e-list (amoung other talents!), as shown here.

The use of StrAdd() should be obvious, but it is worth pointing out that if you accidentally typed StrAdd(first, eString) then rather than crashing at run-time as AmigaE would, PortablE will report an error at compile-time, because "first" does not have the required type of STRING.

The final "ENDPROC eString" declares that the combine() procedure returns a value of type STRING, since eString has that type. All procedures that return something must declare their return type(s) in this way, even if they always use RETURN.


Everything from main() should be pretty self-explanatory, and in fact look very similar to something written for AmigaE. But wait a minute, doesn't combine() return a STRING, so why does PortablE allow it to be assigned to "newString" which has the type ARRAY OF CHAR? Well, while e-strings are a specialised kind of ARRAY OF CHAR, they can still be treated as ARRAY OF CHAR, so it's all right - in technical terms STRING is a subtype of ARRAY OF CHAR.

However, you may have spotted that something is wrong... main() fails to deallocate the e-string, so we have a memory leak, at least until the program quits & all remaining memory is reclaimed. Not a problem for a silly little program like this, but in a proper program it could be a serious issue. You could try adding this line at the bottom of main():

  DisposeString(newString)

But PortablE will complain that newString has the wrong type. So you could try this instead:

  END newString

The compiler won't complain about this, but it *is* wrong, because END is treating newString as an ARRAY OF CHAR, rather than a STRING. So the only way to correctly deallocate something is to have it stored in a variable of the correct type:

  DEF newString:STRING

Both the previous suggestions will now compile & work correctly. And as you may have noticed, PortablE does not allow you to specify an array's size when it is being deallocated using END, although you can end it with an empty [] if you want.


Before we move on, here's one final trick with NEW:

  eString := NEW 'immediate string'

This creates an e-string copy of the immediate string, and is significantly less tedious than what you'd need to do in AmigaE.

7.5. Lists

Having looked at e-strings, you should have a good idea what to expect from e-lists. All lists are some kind of ARRAY OF VALUE, and so can contain any value you want (except for floating point). An immediate list looks the same in PortablE as it does in AmigaE:

  e.g.  [1, 2, variable, 4]

This actually has the type ILIST, because unlike a plain ARRAY OF VALUE it knows it's own length. So just as in AmigaE, you could do this:

  length := ListLen( [1,4,9] )		->stores the value 3

But just as in AmigaE, you should not try to modify the contents of an immediate list - and in fact no list-modifying procedure will accept an ILIST. If you want to modify a list, then you must allocate a proper LIST (note it doesn't start with an "I"):

  PROC main()
  	DEF eList:LIST
  	NEW eList[ 5 ]
  	
  	ListAdd(eList, [1,2,3])
  	ListAdd(eList, [4,5,6])
  	
  	Print('List length=\d\n', ListLen(eList))   ->will show 5, not 6
  	Print('Third value=\d\n', eList[2])         ->will show 3
  	
  FINALLY
  	END eList
  ENDPROC

As with e-strings, we could have created an e-list using the NewList() procedure, which is the new name for our old friend List(), but using NEW is preferred. Similarly, we could have used DisposeList() for deallocation, instead of END.

We could also have avoided dynamic allocation altogether, and simply said:

  DEF eList[5]:LIST

You should note that the procedure ListLen(list:ILIST) accepts both ILIST & LIST types, because LIST is a specialised kind of ILIST - in technical terms LIST is a subtype of ILIST.


But what's this "FINALLY" doing? It's PortablE's equivalent of EXCEPT DO, which means that the code after it will always be executed - whether or not an exception is raised. It is used here to ensure that the memory does not leak, even if an exception was thrown.

PortablE has no equivalent of EXCEPT, so things that should only happen during an exception must be contained within an IF exception ... ENDIF statement.

FINALLY does a couple of things which EXCEPT DO does not. First, it automatically rethrows any exception at the end, so that exceptions cannot be accidentally lost. Use exception:=0 if you want to clear an exception. Second, it is *always* executed, however you exit the procedure - even if you use RETURN! This means that your clean-up code really does need to be in only one place - so no more accidental memory leaks...

7.6. A glance at object orientation

PortablE is heavily focused on improving OOP, which was a bit basic in AmigaE. But OOP is far too big a subject to tackle in one page, which is why it has it's own chapter later on. But I can still give you a taste of the differences right now:

  
  PROC main()
  	DEF test:PTR TO example
  	NEW test.new()
  	
  	test.set('Portal is great\n')
  	Print( test.get() )		->This prints "Portal is great"
  	
  FINALLY
  	END test
  ENDPROC
  
  
  CLASS example
  	string[20]:STRING
  ENDCLASS
  
  PROC new() OF example
  	StrCopy(self.string, 'default')
  ENDPROC
  
  PROC set(string:ARRAY OF CHAR) OF example
  	StrCopy(self.string, string)
  ENDPROC
  
  PROC get() OF example IS self.string

The first difference is that any procedure, such as main(), can refer to the "example" object - even if it is declared after that procedure. This means you can put your code in the order which makes most sense to you, rather than the order that the compiler wants.

With AmigaE, if a variable like "test" could be ENDed when an exception was thrown, then it should be initialised to NIL, so that END could never be passed a nonsense value. But PortablE guarantees that all pointers are initialised to NIL, so we don't need to. In fact, within DEF you cannot initialise any variables, for a good reason which is explained elsewhere. But please beware that automatic pointer initialisation will partially change in the future, so do not rely on it, *except* for deallocation.

The next difference is that objects with methods are known as classes. Such an object must (eventually) inherit the "class" object, and we can ensure this if we declare the object using the CLASS keyword (instead of OBJECT).

Another useful difference is that classes can contain preallocated arrays, such as the fixed-size e-string in this example. So we don't have to remember to allocate it in the new() constructor (nor deallocate it later).

Apart from strings being ARRAY OF CHAR, the rest of the code is standard AmigaE, albiet type-checked! But there is a hidden requirement that you should be aware of:

PortablE needs to know which methods are constructors. Either we hint at that with the method's name, or we must explicitly say so using a keyword. If the method's name is new(), or it begins with "new" & is then followed by a capital letter, such as newThing(), then it is assumed to be a constructor. But if your constructor needs a different name, then you must use the NEW keyword like this:

  PROC myConstructor() NEW OF example

Another thing which happens automatically is that the return type of the in-line method get() is deduced to be STRING, because it returns "self.string". But what if we don't want the user to get an e-string, because he might try to modify it? Then we must cast the returned e-string to a plain old string:

  PROC get() OF example IS self.string!!ARRAY OF CHAR

As you see here, the !! symbol is used to cast whole types. Note that you can still use :: to cast something as a pointer to an object.

It's worth mentioning that when a child class inherits from a parent class, PortablE places restrictions on the parameters & return values of the child class, to ensure that the child can always be used where the parent is expected. But it is more flexible than AmigaE, because it allows the child class to have more parameters than the parent. Please read the "Object orientation" chapter for a full explanation.

7.7. Other stuff

This whirl-wind tour of PortablE has only covered some of the more obvious changes - there are many more improvements, as well as a few things which have been lost from AmigaE. So I strongly recommend that you read the following chapters. But don't let that stop you from experimenting - PortablE will warn you of most problems!

The other changes include: RAISE now works with user procedures *and* methods too! Modules are more flexible. For procedures & methods that return objects, you are allowed to do something like procedure().method().member . Constant names can be most keywords. You can have 5 return values. ELSE IF is normally used instead of ELSEIF. exceptionInfo replaces exceptioninfo. E-list linking is not implemented. EXIT is not implemented, but ENDWHILE IF & ENDFOR IF reduces the need for it. JUMP is not supported.

Not to mention that I have many more planned improvements in the future. I am also willing to try to increase PortablE's backwards compatibility with AmigaE, but please be aware that some things are not easy to fix, and a few things are impossible if PortablE is to remain truely portable.


Go back to CONTENTS


8. How to compile your old AmigaE programs

There are a few restrictions on compiling your old programs using PortablE:

If these restrictions are no problem, then have a go! And please email to let me know how you get on - good or bad :-)

8.1. Suggested steps

Here are the steps that I suggest:

1.First skim the "Other changes from AmigaE" & "AmigaE features that are missing" chapters, to get a feel for any problems you may encounter.

2.You should compile your program using both the OptAmigaE and OptPointer switches (see an earlier chapter), to enable maximum compatibility for all modules. You will probably still get a few errors, which will need to be fixed, so read the following "Compatibility hints" section for help. If you get many errors of a particular kind, which you think are silly, then please email me, and I will see what I can do...

If you run across missing modules, then you might like to consider porting them too (if their E source code exists). Or if they have C++ counterparts, then I might be willing to add native support for them - and any help doing that would be most appreciated (I have a semi-automated tool that greatly reduces the effort required, please contact me for details).

3.If you got your old program to compile, you can now take the generated target code (which will be C++ unless you said otherwise), and use it with a 'real' compiler. Run the program to see if it works. If not, then check your E code for assumptions which may no-longer be true, and if that fails then try debugging the generated (C++) code (in case that contains a mistake).

Note that some C++ compilers will give errors like "cast from 'char*' to 'XXX' loses precision" or "cast to pointer from integer of different size" when you use the OptAmigaE switch. The solution is to also use the OptNoPtrToChar switch. If this option fails to solve the errors, then please let me know.

4.Assuming that your program actually compiles & runs OK, then you should stop using the OptAmigaE switch (but keep using the OptPointer switch). You will now have to modify your program to stick to PortablE's stricter rules, particularly it's type-checking rules. So read the "Reversible changes from AmigaE" & "The type system" chapters.

If an untyped variable is treated as an array, you will usually need to change that variable's type to ARRAY OF CHAR.

If a variable is of the wrong type, then you should first try changing it to the correct type. If there is a conflict in the expected type of a variable, then you could try using the parent type shared by those different types (see the type diagram in "The type system" chapter). Only rarely & as a last resort should you need to use !! to cast the variable to the required type; read the "Casting hints" sub-chapter of the "The type system" chapter if you get stuck.

5.Finally, you can try to get rid of direct pointer manipulation in some modules, say by replacing pointers with arrays. For any modules where you can't do this, you will need to add OPT POINTER to it. You can now stop using the OptPointer switch!

6.After that you might like to consider how you can use some of PortablE's funky new features... So read the "New features compared to AmigaE" & "Current improvements over AmigaE" chapters, and maybe the "Object orientation" chapter too.

8.2. Compatibility hints

Here are solutions to common problems that you may encounter when trying to compile your old programs using the compatibility mode:

If you are JUMPing out of a loop or other structure, then it gets a little more complicated - you will usually need to use a variable to remember that the loop/etc should be left immediately, and then use that variable with the appropriate replacement loop/IF. See below for an example.

You could easily modify it, like this code:

  DEF exit
  WHILE test
  	IF (exit := foo()) = FALSE
  		test := bar()
  	ENDIF
  ENDWHILE IF exit

Which is not so pretty, but in this case it can be rewritten a bit better as:

  IF test
  WHILE foo() = FALSE
  	test := bar()
  ENDWHILE IF test = NIL
  ENDIF

Minor differences in the original code could allow it to be simplified much better than this.

Then you can easily modify it, like this code:

  DEF var[1]:ARRAY OF VALUE
  var[0] := 123
  test(var)

Or if it does not return a value, then use "call2empty" instead of "call2". And make sure that all parameters (and any return value) of the function have the default type (i.e. VALUE), or else the compiled code *may* crash.

8.3. Language hints and FAQ

Q.What is the type given to something like "DEF variable" without a specific type?

A.The type is VALUE, hence it is equivalent to "DEF variable:VALUE". Well, unless you use the compatibility mode, as that defaults to PTR TO CHAR.


Q.Why can't I pass a string to my PTR TO CHAR parameter?

A.That's because strings are now ARRAY OF CHAR . But you can temporarily allow it by enabling the POINTER arithmetic or AMIGAE compatibility option.


Q.Which AmigaOS modules are supported?

A.For OS3/OS4/AROS currently all of the v45/v52 Asl, Commodities, Console, Devices (inc. Timer), Diskfont, Dos, Exec, Gadtools, Graphics, Icon, Intuition, Resources, Utility & Workbench modules, plus part of AmigaLib, class, Other & Tools. You can see exactly what is supported, by looking within PEmodules: but ignoring the "PE" & "target" folders.

While the original idea for PortablE was to provide abstract modules that did not expose the OS, additional OS modules may be added if there is interest - and any help doing that would be most appreciated (I have a semi-automated tool that greatly reduces the effort required, please contact me for details).


Q.I want an in-line procedure to call another procedure (which returns a value), but I don't want my in-line procedure to return that value.

A.Make it return EMPTY:

  e.g. PROC meef(x) IS function(x) BUT EMPTY


Q.How do I force floating point maths, such as when dividing one integer by another? Before I could use ! to do this.

A.What you need to do is cast one of the integers as a float:

  answer := firstInt!!FLOAT / secondInt

It is wise to cast the first integer, because otherwise E's left-to-right order of evaluation will likely mean you end-up casting the RESULT of the division - not what you intended! (Alternatively you can enclose your cast in brackets.)


Q.Why won't it let me cast one value into a different type?!?

A.PortablE does not allow a type to be cast to a completely unrelated type (i.e. a different branch of the type tree), for safety reasons. However, you can usually get around this by first casting to the VALUE type. For example:

  DEF string:STRING, number:INT
  number := string !!VALUE!!INT

So PortablE still allows bad or unsafe casts, but they are not as easy to do, which I think is good language design, since bad code should be harder to write. It is also easier to search for unsafe casts.


Q.Why does it tell me the "return type is less restrictive than for the inherited method"?

A.You must either make the parent method's return type more general (bigger), or you must make the current method return a less general (smaller) type. Have a look at PortablE's type diagram. To help you decide, you will be told the return type of both the current method & the inherited method.

Or, if you don't need to pass your current object where the parent is expected, then you can simply declare that your object is an ORPHAN, then declare that method is an ORPHAN.


Q.Why does it tell me the "return type is different for the inherited method"?

A.Either you must make the parent method's return type be more general (bigger) than that returned by your current method, or if you don't need to pass your current object where the parent is expected, then you can simply declare that your object is an ORPHAN, then declare that method is an ORPHAN.


Go back to CONTENTS


9. New features compared to AmigaE

To make the large list of funky new features easier to digest, I have grouped them into sections.

9.1. General

Thus the same module declaration can refer to a different module file for each target - so that different implementations can be used when necessary. Different implementations can share (some of) the same code (from other modules) using PUBLIC module declarations (see below).

Instead of #ifdef/#endif, you can use IF/ENDIF with a real constant, and replacing "#define MAGIC_SWITCH" with "CONST MAGIC_SWITCH = TRUE". Any such IFs will be "inlined" or omitted when optimisations are enabled.

Instead of #define stringName 'a string' or #define listName [1 2 3], you can use STATIC stringName = 'a string' or STATIC listName = [1 2 3].

Instead of #defining a complex constant, you can simply use a real CONST.

Instead of defining a preprocessor procedure like #define fakeProc(foo), you can replace it with a real in-line procedure. For example, replace "#define MAX(a,b) (IF (a)>(b) THEN a ELSE b)" with "PROC max(a,b) IS IF a>b THEN a ELSE b". And unlike macros, procedure calls can be used with parameters that have side-effects, and you don't have to enclose variables within brackets.

9.2. Maths, types, arrays, pointers & strings

You can also cast to pointers or arrays, although pointer manipulation must be enabled if the value is not already a pointer or array.

  e.g. string := value!!ARRAY OF CHAR

Arrays can also be dynamically allocated with NewArray(sizeInItems, sizeOfItem), and deallocated with DisposeArray(array).

  e.g. bar := NewArray(5, SIZEOF LONG)
       DisposeArray(bar)

But NewArray() can only be used when pointer arithmetic is enabled, since it *may* not be implementable in a Java-like language; this is enforced using a type-checking kludge that may give an odd error message.

9.3. Statements, expressions & functions

9.4. Procedures & methods

The original procedure can be accessed by preceeding it with the SUPER keyword, but only from within the module declaring the replacement.

  e.g. memory := SUPER New(size, noClear)

Beware that you are not allowed to replace the same procedure twice, from within different modules, because that would likely result in ambiguity as to which replacement procedure should be used. So you must be *very* careful when using REPLACEMENT, because it can cause two modules to be incompatible with each other!

However, there is nothing to stop you from replacing the first replacement procedure. In that case you would get an inheritance-like chain of replacement procedures, where the most recent one takes precedence over all the others.

9.5. Object orientation

You can also do member/method access on any expression which evaluates to an object, such as the (IF x THEN object1 ELSE object2) expression - even when object1 & object2 are slightly different types! This is real value polymorphism.

Go back to CONTENTS


10. Current improvements over AmigaE

These changes are improvements over existing AmigaE features.

10.1. General

10.2. Maths, types, arrays, pointers & strings

WARNING: This will partially change in the future, so do not rely on it, *except* for deallocation!

Unallocated arrays hold the constant NILA.

10.3. Statements, expressions & functions

10.4. Procedures & methods

10.5. Object orientation

Go back to CONTENTS


11. Reversible changes from AmigaE

These changes are the ones which are hidden by the AmigaE backwards compatibility mode, or in a few cases by other means. See "The module options (OPT)" chapter for more information on these modes.

11.1. General

11.2. Maths, types, arrays, pointers & strings

11.3. Statements, expressions & functions

11.4. Procedures & methods

[Java-style "camel hump" naming has been adopted, and is the recommended standard for user programs, if they wish to be consistent with PortablE. This may seem a trivial change, but if I can't fix aesthetic issues now, then we will be stuck with them for a long time...]

11.5. Object orientation

A method will automatically be presumed a constructor if the method's name is new(), or if it begins with "new" and is followed by an uppercase letter (thus newer() would NOT be a constructor). A method may also be explicitly declared as a constructor by putting NEW before the method's OF keyword.

  e.g. PROC myConstructor() NEW OF myClass

Go back to CONTENTS


12. Other changes from AmigaE

These changes are the ones which cannot be avoided by any special mode.

12.1. General

12.2. Maths, types, arrays, pointers & strings

12.3. Statements, expressions & functions

12.4. Procedures & methods

[A new type-safe solution is planned for users that want to use function pointers themselves, rather than just passing function pointers to existing (AmigaOS) modules.]

12.5. Object orientation

Go back to CONTENTS


13. AmigaE features that are missing

^ can be replaced by GetLong() or [], while { } can be replaced by an array or CALLBACK. See the "How to compile your old AmigaE programs" chapter for help.

Note that { } now has a different purpose, which is disabled by default.

And there are probably other changes I have forgotten, or maybe didn't even realise! Please report anything that I missed.


Go back to CONTENTS


14. Planned changes that you should be aware of

[NOTE: All the string functions will remain present, and in fact conversion between those strings & the string class will be quick & easy, so there will be no need to convert your programs to the string class in one big jump - or perhaps even at all.]

Go back to CONTENTS


15. Expression evaluation

15.1. Operator precedence

PortablE lacks operator precedence, just like AmigaE, so all expressions are simply parsed left to right. Parentheses must be used to group (sub) expressions together.

  e.g. w+x*y+z is parsed the same as ((w+x)*y)+z

15.2. The NOT operator

The NOT operator does not strictly obey the left to right parsing rule, as it operates on the first expression it can.

  e.g. w+NOT x+y is parsed the same as w+(NOT x)+y
       w AND NOT x OR y is parsed the same as w AND (NOT x) OR y

This choice was made, so that the presence or absence of a NOT doesn't completely change the whole meaning of an expression. You can consider NOT to behave in a similar fashion to the - (negation) operator.

15.3. Order of evaluation

PortablE does not guarantee the order in which an expression will be evaluated, but this should not be a problem since AmigaE did not either. (Of course, anyone relying on AmigaE's current implementation will be in trouble. Slap your wrist for using undocumented behaviour!)

  e.g.  x+y could evaluate x or y first, which would be a problem if you wrote
something wierd like w.set(5) + w.get(), or like (x:=5) + x

If this was a wide-spread problem (which I find hard to imagine) then I might be able to implement a special mode that would guarantee the correct order (or even implement it as part of the compatibility mode), at the expense of very messy target code.


Go back to CONTENTS


16. The type system

The basic type relationships are shown in the picture below:

type diagram

Smaller types point to the larger type that they can fit within. So BYTE fits within INT, LONG, VALUE, etc. Thus a BYTE value may be assigned to an INT variable, or passed to an INT parameter. Conversely, a LONG value cannot be assign to a BYTE variable, at least not without casting it first.

The VALUE type is used for variables that do not have a stated type.

The idea is to allow anything that is physically possible & sensible, with explicit casts being needed in the cases where it might not be possible or sensible. This is of course a big change from AmigaE, but so far I have found the type-checking to be much more of a help than a hinderance, and it has even revealed some subtle bugs in old AmigaE programs that I tried compiling.

Please read the following sub-chapters for a more detailed description.

16.1. Primitive types

The primitive numerical types are FLOAT, VALUE, QUAD, LONG, INT, BYTE & BOOL in order of decreasing size. Smaller types can fit within larger types, so you can pass a smaller type where a larger one is expected. e.g. a BYTE may be used where an INT is expected.

A quick explanation: BOOL is 1-bit and holds the values -1 to 0. BYTE is 8-bits and holds the values -128 to 127. INT is 16-bits and holds the values -32768 to 32767. LONG is 32-bit and it's value is also signed. QUAD represents the type needed to hold the value of 4 characters, like "abcd". VALUE is a special type which can hold all integer types, *and* is the type which variables have when no type is specified. FLOAT is a floating point type of unspecified accuracy (typically it is the most accurate float available in the target language). All types with X-bit sizes may be implemented with more bits, for example BOOL may be implemented using 8-bits.

Note that this means FLOAT cannot fit within a VALUE, and so must always be cast as an integer if it needs to be supplied to something which expects an integer.

The character type CHAR is considered smaller than (or equal in size to) INT, but you can't pass any other type where a CHAR is expected (so it does not fit directly between INT & BYTE). On some systems CHAR may be a 16-bit Unicode value.

Notice that CHAR may be signed or unsigned, so if you want to compare it to a value it is best to use "\xHH" where HH is your value in hexadecimal. However, if that is not easy, then you can use CharToUnsigned() & UnsignedToChar().

The special type ANY is an abstract type which represents any possible type, such as values, pointers, objects or arrays. While it may be considered similar to the VALUE type, it cannot be used directly (unlike VALUE). Thus you cannot write foo:ANY, or even bar:PTR TO ANY .

16.2. Pointer types

The special type PTR is normally used to point to a particular type, like PTR TO LONG, but it can also be specified on it's own as PTR (without any TO ...). PTR (on it's own) is a special type, and while most pointer types have limitations about which pointers they can be used as, PTR may be used where any pointer is expected. This is so that NIL can be assigned to any pointer, amoung other uses.

A pointer to one primitive type may not be passed as a pointer to another type. For example you cannot give a PTR TO CHAR where a PTR TO INT is expected. One special case is that PTR TO VALUE will accept all pointers to primitive types (although I am not sure if this is a good idea!). Pointers to objects follow the inheritance relationships, therefore if we have declared OBJECT foo OF bar, then PTR TO bar will accept values from both PTR TO bar & PTR TO foo.

Pointers are considered larger than QUAD (& LONG), but smaller than VALUE. Also a PTR will not accept a QUAD, but neither will a QUAD accept a PTR.

In case it is not obvious, this means that VALUE can also store pointers, but LONG cannot.

16.3. Array types

The special type ARRAY is normally used to hold arrays of a particular type, like ARRAY OF LONG, but it can also be on it's own as ARRAY (without any OF ...). Unlike AmigaE, arrays do not need to be of a fixed-sized (i.e. preallocated using [n] ), thus they may be passed around in a type-safe manner.


No array may be passed as an array of any other type (including pointers), therefore only arrays of exactly the same type may be passed. Arrays of classes (i.e. objects with methods) are not allowed - although arrays of pointers to classes are fine.

ARRAY (on it's own) is a special type, and may be used where any ARRAY (OF ...) is expected. This is so that NILA can be assigned to any array, amoung other uses. It also means that memory returned by New() can be used by any array. But ARRAY may not be of a fixed-size (i.e. preallocated using [n]).

Preallocated arrays cannot be replaced by an assignment. Also, arrays may contain any type, including pointers or arrays - but arrays of arrays cannot preallocate the inner arrays. Thus given that strings are ARRAY OF CHAR, you can have arrays of strings using ARRAY OF ARRAY OF CHAR, but the strings will not be preallocated themselves. So PortablE's arrays behave a lot like Java's.

If you use the pointer manipulation mode, then an ARRAY OF x will be accepted as a PTR TO x, and vice versa.

Tip: While NewArray() & DisposeArray() may be used to dynamically (de)allocate arrays, it is easier to use NEW & END on an ARRAY variable, with the size specified in [square brackets] after the variable for NEW. And actually NewArray() can only be used with OPT POINTER enabled, since it *may* not be implementable in a Java-like language; this is enforced using a type-checking kludge that may result in an odd error message.

Tip: ArrayCopy() may be used to copy arrays. But it can only be used with OPT POINTER enabled, since it *may* not be implementable in Java-like languages; this is enforced using another type-checking kludge.

16.4. E-string type

The type STRING is an "e-string", which is a subtype of ARRAY OF CHAR (i.e. a normal string). Since it is an array, it may be of a fixed-size (i.e. preallocated using [n]), but unlike AmigaE it does not need to be. Thus a procedure that expects an e-string can ensure this is the case, by specifying the parameter like so:

  PROC procedure(parameter:STRING)

Unallocated e-strings hold the value NILS.

Tip: While NewString() & DisposeString() may be used to dynamically (de)allocate e-strings, it is easier to use NEW & END on a STRING variable, with the size specified in [square brackets] after the variable for NEW.

Tip: StrCopy() can be used to copy a string into a e-string. Also, NEW can be used to create a dynamic e-string copy of an immediate string, like so:

  eString := NEW 'immediate string'

16.5. List types

Similarly, the type LIST is an "e-list", which is a subtype of ARRAY OF VALUE. So e-lists can contain most expressions, including strings. Unallocated e-lists hold the value NILL.

Immediate lists (such as [1,2,var]) have the type ILIST, which is also a subtype of ARRAY OF VALUE. In fact, LIST is really a subtype of ILIST, which means you can use a LIST were an ILIST is expected (but not vice versa). Note that for technical reasons, immediate lists cannot be used within the 'read only' main expression of CASE statements - unless the list only contains constants.

Just as with AmigaE, you should never modify the contents of immediate lists. None of the List functions will allow you to do so, because they expect a LIST (which as mentioned above is not an ILIST).


Typed lists (such as [1,2,var]:INT) are plain arrays of the given type, just like in AmigaE. You should never modify the contents of typed lists. And none of the List functions will work on typed lists, since they are not a LIST.


Tip: While NewList() & DisposeList() may be used to dynamically (de)allocate e-lists, it is easier to use NEW & END on a LIST variable, with the size specified in [square brackets] after the variable for NEW.

Tip: ListCopy() can be used to copy a list (except typed lists) into an e-list. Also, NEW can be used to create a dynamic e-list copy of an immediate list, but they must be destroyed using DisposeList(). And in fact NEW can be used to create a dynamic array copy of a typed list, like so:

  array := NEW [1,2,3]:INT

16.6. Miscellaneous types

The BIGVALUE type is only guaranteed to hold 32-bit signed values, but it will handle 64-bit signed values if the target supports it.


The special type EMPTY is used to represent an expression which evaluates to nothing (such as a procedure which returns nothing). You cannot declare a variable as being of type EMPTY, and it is considered unrelated to any other type (so EMPTY cannot be passed where any type is expected). However, EMPTY may be used as an expression element, so that for example PROC meef() IS foo() BUT EMPTY is a valid way to indicate that meef() returns nothing.

[It is interesting that the E guide by Wouter hints that he was considering something like this usage of EMPTY as a future addition to AmigaE. This in turn implies he was planning some sort of return value/type checking, however basic!]


The RANGE type allows you to specify a type which only accepts a range of values. For example, the "RANGE -128 TO 127" type is equivalent to the "BYTE" type. You would not often use the RANGE type on it's own, but it can be very useful when used as part of a TYPE declaration (see the "User-defined types" sub-chapter).

16.7. User-defined types

You are able to create your own types, using TYPE declarations. For example:

  TYPE SMALL IS BYTE

This allows you to have (say) parameters of type SMALL, and if you ever need something larger than a BYTE, then you only need to change SMALL's declaration (instead of editing every place that SMALL is used). If you get a type error involving SMALL, it will be reported as:

  SMALL (BYTE)

The RANGE type comes into it's own with TYPE declarations:

  TYPE SMALL IS RANGE -128 TO 127

TYPE can also be very handy for verbose NATIVE types, which are described in the "Advanced usage - accessing native OS/language elements" chapter.

16.8. Final comments on types

Type declarations (especially for parameters or object members) tend to spread to many other parts of your program, which can be tedious if they don't provide any obvious benefit. From experience I can say that LONG is not normally worth it, so simply use the default (VALUE) type instead. INT & BYTE are likely only needed to save memory in objects. Where-as CHAR & BOOL are very useful, so use them when you can. PTRs & ARRAYs are of course often required.


As my type system has gone through MANY changes, it is possible that it may not work right in some rare situations. So if you cannot work out why the type system prevents you from doing something (or it allows something silly), please contact me, rather than assuming it's your fault! Even if you do turn out to be mistaken, I think that explaining the oddities of the type system to someone else should help me understand how it (or at least this manual) could be improved.


Some variables with legal types:

  DEF x,                  x:QUAD,              x:CHAR
  
  DEF x:VALUE,            x:PTR,               x:ARRAY
  DEF x:PTR TO VALUE,     x:PTR TO PTR,        x:PTR TO ARRAY
  DEF x:ARRAY OF VALUE,   x:ARRAY OF PTR,      x:ARRAY OF ARRAY
  
  DEF x:STRING,           x[5]:STRING
  DEF x:LIST,             x[5]:LIST
  DEF x:ARRAY,            x[5]:ARRAY OF CHAR,  x:ARRAY OF CHAR
  
  DEF x:PTR TO CHAR,      x:PTR TO LONG,       x:PTR TO class
  DEF x:ARRAY OF CHAR,    x:ARRAY OF LONG,     x:ARRAY OF PTR TO class
  
  DEF x[5]:ARRAY OF PTR,  x[5]:ARRAY OF PTR TO CHAR
  DEF x[5]:ARRAY OF VALUE,x[5]:ARRAY OF ARRAY OF CHAR


And some variables with ILLEGAL types:

  DEF x:ANY,              x:PTR TO ANY,        x:ARRAY OF ANY
  DEF x[5]:ARRAY,         x:ARRAY OF class
  DEF x:PTR TO PTR TO CHAR

16.9. Casting hints

With a bit of experience you should find that on average most programs only need 1 cast every 200 lines of code. Even in the worst case, such as for low-level coding or heavy OS access, on average you still should not need more than 1 cast every 20 lines of code.

Declaring your variables with the right type is the most important way to avoid the need to cast. But since casts are sometimes still necessary, here are some hints...


If you want to cast an expression from any type x to any other type y, and the compiler says something like "illegal cast to an unrelated type (not super- or sub-type)", then you can usually get it to work like this:

  expression !!VALUE !!y

For example, to cast from a variable of type CHAR to type BYTE:

  character !!VALUE !!BYTE

This (usually) works because VALUE is the parent of (nearly) every type. So the first cast is to VALUE, which is a direct parent, and this is then followed by a cast to BYTE, which is a direct child of VALUE.


However, casting everything to VALUE is a bit of an extreme measure, and will not always work if pointer arithmetic isn't enabled. So it pays to be conservative, and choose the closest parent type that is shared by both types. You can find the closest parent by simply looking at the type diagram that begins this chapter. However, I will give a few examples, to get you going:

The closest parent for CHAR & BYTE is INT, so the previous example could be done this way:

  character !!INT !!BYTE

If you wish to cast from ARRAY OF x to ARRAY OF y, then the shared parent type is ARRAY. For example:

  expression !!ARRAY !!ARRAY OF y

If you wish to cast from PTR TO x to PTR TO y, then the shared parent type is PTR. For example:

  expression !!PTR !!PTR TO y


I must emphasis that this sort of casting shouldn't normally be necessary. So if you do think you need to do it, then I strongly recommend that you first check what you're doing truely makes sense, or couldn't be done in a safer way.

16.10. The resulting type of an operation

PortablE implements a fairly advanced type system, which is able to accurately deduce & represent the type of a result for operations like +, -, *, /, OR and AND. While the aim is a type system which always acts sensibly, it may sometimes still be helpful for you to know how it works:

All primitive types (except QUAD & CHAR) cover a specific range of values. For example, BYTE covers -128 to 127. For our discussion, imagine that we have declared the following types:

  TYPE UBYTE IS RANGE 0 TO   255
  TYPE UINT  IS RANGE 0 TO 65535

So if we add 1001 to a variable of type UBYTE, then we'd expect the result to be somewhere between 1001 & 1256. PortablE actually uses the range of a result like this as the type itself, which is far more accurate than simply saying the result fits within a UINT, or worse that it still fits within a UBYTE!

So if you tried to assign the result of a UBYTE + 1001 to another UBYTE, then PortablE would complain that the type "RANGE 1001 TO 1256" does not fit within the type UBYTE. If you multiplied the result by 100, before trying to assign it to a UBYTE, then it would complain that the type "RANGE 100100 TO 125600" does not fit. This result doesn't fit within a UINT either.

So PortablE is pretty good at estimating whether a maths calculation can be assigned to a variable, etc. It is even able to correctly deduce that something like "long AND $FF" will always fit within a UBYTE, even if "long" is a LONG.


But this accuracy leads to a problem: It is often common to write something like "ubyte := ubyte + 1", even though we know that the type "RANGE 1 TO 256" will not always fit within "RANGE 0 TO 255"! So strictly following the above rules would require the programmer to cast "ubyte + 1" as a UBYTE, before PortablE would allow it.

I decided to allow that kind of operation, where a *small* value is used on a primitive type, by letting the result have that primitive type (instead of an accurate range of values). For addition & subtraction, "small" is defined as half the size of the primitive type, e.g. under 128 for a BYTE or UBYTE. For multiplication, "small" is defined as the square root of the size of the primitive type, e.g. under 8 for a BYTE or UBYTE.


Go back to CONTENTS


17. Object orientation

AmigaE's support for Object Orientated Programming (OOP) was rather basic, which made it easy to learn, but it could also be quite limiting, and crashes from mistakes were quite likely. PortablE provides greatly improved support for OOP, while staying pretty backwards compatible - and I have attempted to avoid many of the unnecessary complexities seen in C++.

I will assume that you are already familiar with OOP concepts, and in particular with AmigaE's version of OOP. But since PortablE does things a little differently to C++ & many other OOP languages, I will begin with some basic OOP theory...

17.1. Some essential OOP theory

When a child class inherits from it's parent, not only does it inherit the parent's *implementation* (i.e. the code inside each method), but it also inherit's the parent's *interface* (i.e. each method's parameters & return types).

In languages that support overloading (such as C++) it is required that a child method has exactly the same interface as it's parent, because otherwise it would be treated as a different method. This guarantees that a child object can be used where it's parent is expected, because the child always supports exactly the same methods as it's parent.

But PortablE, like AmigaE, does not support overloading, and this provides room for some extra OOP freedom - it is possible for a child method to have a different interface to it's parent! An interesting idea, but I hope that you can see the potential problem here:


If a child method cannot be used where it's parent would be, then the child object itself is no-longer a valid substitute for it's parent, and therefore it is not a true child. For example, if the parent's first parameter was foo:LONG, but the child's first parameter was foo:INT, then it wouldn't make much sense for the child to be passed a LONG value when it was expecting an INT, because a LONG is larger than an INT!

While this might seem to prove that a child's interface must be exactly the same as it's parent, you would be wrong: If the *parent's* first parameter was foo:INT, but the child's first parameter was foo:LONG, then it would be perfectly fine for the child to be passed an INT value when it was expecting a LONG, because an INT is smaller than a LONG.

We can generalise this to a rule which says that a child's parameters can be more general (i.e. larger) than it's parent's, but not less general. In a similar fashion, a child can have more default parameters & more return values than it's parent, but not less. This is more formally known as the Liskov Substitution Principle (LSP), under which a child must be usable in all situations that it's parent is.

It is interesting to note that this principle gives a reverse rule for return types - a child's return types can be less general (i.e. smaller) than it's parent's, but not more general. This is because if a child tried to return a larger value than was expected, then that value would simply not fit.


If you find any of this confusing, then you may wish ignore the theory, and just read the next sub-chapter...

17.2. How this applies to PortablE

Unlike C++ or AmigaE, PortablE has a proper type system, where each less general (i.e. smaller) type inherits from a more general (i.e. larger) type. This means that it is possible for the previously mentioned LSP to be used to decide whether a child method is valid in all situations that it's parent is.

For PortablE, the LSP boils down to these rules for children:

Typically a child will have the same types for parameters & return values, but it can be very useful for a child to have additional parameters & return values. So even if you find it difficult to remember whether types should be more or less general, MAKE SURE TO REMEMBER THAT CHILDREN CAN HAVE MORE PARAMETERS & MORE RETURN VALUES!

17.3. Relaxing restrictions on constructor methods

The above restrictions apply to all methods, including constructor methods, even though constructors are not really part of a class's interface. This can be quite limiting, because often a constructor will need to be quite different in a child class. The solution is to declare that the class is UNGENERIC:

  CLASS child UNGENERIC OF parent
  ENDCLASS

As PortablE currently stands, there is no down side to doing this, but in the future it will probably prevent the child from being used in a more advanced OOP system known as "generics". Hence the keyword UNGENERIC.

[Note that I am not entirely happy with the word I chose for this keyword, so I will change it if I can think of a better word, and of course anyone else may make a suggestion too.]

17.4. Relaxing restrictions on all methods

While the above restrictions can be limiting for constructors, they can sometimes be limiting for normal methods too, so PortablE provides a novel solution that doesn't break the LSP. If a child is declared an ORPHAN of a parent object, then it is no-longer restricted by it's parent:

  CLASS child ORPHAN OF parent
      meef
  ENDCLASS

Effectively the child class inherits the *implementation* of the parent (it's genes if you like), but it does NOT inherit the *interface* of the parent (say the language it's taught). Thus the child can access the parent's methods, but it is free to completely change the interface when redeclaring those methods. Unlike C++'s private interitance, the inherited methods are still publically visible (unless they have been redefined)!

Because the child class is free to completely change it's interface, it will be treated as if it doesn't have a parent - and therefore will not be accepted where it's parent is expected. This ensures that the LSP is not broken.


There is a catch though - when you make a child method's interface incompatible with it's parent, the parent class will not be able to call your child method, even though polymorphism (aka virtual methods) normally means that it would. Instead the parent class will only be able to call it's own method, as it knows that interface. Since this can be a major problem, you are required to indicate which methods change their interface, like so:

  PROC method() OF child ORPHAN

Note that if you only do this for constructor methods, then it would usually make more sense to just declare that your class is UNGENERIC instead.


If being free to completely change the class's interface sounds like TOO much freedom, then you can limit the child to the interface of one of it's grandparents:

  CLASS parent OF grandparent
  ENDCLASS
  
  CLASS child ORPHAN OF parent IMPLEMENTS grandparent
  ENDCLASS

This means that the child will be accepted as a child of the grandparent, but not the parent! This is present in PortablE for completeness, but it will have far less uses than simply ORPHANing a class, so don't worry about it too much.

17.5. Run Time Type Information (RTTI)

RTTI is provided for classes, and is better than that provided by C++. It is extremely useful for larger OOP programs.


You can get a value which represents the type of a named class using TYPEOF className . Alternatively, every class has the method InfoClassType(), which returns the object's actual (dynamic) type. All such values have the type CLASSTYPE.

You can then see if two types are the same, using the procedure HaveSameClassTypes(first:CLASSTYPE, second:CLASSTYPE), which returns TRUE if they are the same. Alternatively, every class has the method IsSameClassTypeAs(type:CLASSTYPE), which indicates whether the object's actual (dynamic) type is the same as the one provided. It is effectively implemented like this:

  PROC IsSameClassTypeAs(type:CLASSTYPE) OF class IS HaveSameClassTypes(type, self.InfoClassType())

Finally, every class has the method IsOfClassType(parent:CLASSTYPE), which indicates whether the class is a sub-type of the provided type. Thus it returns TRUE if the class has the same type the provided type, or if it is a child of the provided type. This is very handy, because you can check whether it is really OK to treat an object as a child class (before casting it to that sub-type).


Please note that the (CLASSTYPE) value representing a class's type may be different each time a program is run, so there is no point in writing such values to a file. Additionally, there may even be multiple values representing the same class, therefore you can only use the provided procedure & methods to compare them.

17.6. Future plans

In the future I would like to allow orphans to implement any class (not just grandparents), and possibly even allow non-orphans to implement other interfaces (similar to what Java allows).

I also plan to support a generics systems that is based upon types, rather than anything like C++'s ad-hoc template system. It will bear some similarity to Java's generics system, but it will be far easier to use, while also allowing the construction of generic objects (so it won't be limited to just generic collections can only store supplied objects).

I can't say when (or if) any of these will ever be implemented, but I am quite keen on adding them as time & interest allows.


Go back to CONTENTS


18. The future

While I have many plans for PortablE, frequent public releases will depend on the feedback received, because documenting, testing & releasing a new version is extra effort that needs some justification. i.e. No feedback means I assume that everyone is happy with the current release! :-)


Go back to CONTENTS


19. Technical discussion

Am I right to class PortablE as a compiler, rather than a translator? Well, it's debatable, but all traditional compilers do is translate from a high-level language to a low-level one. What decided it for me is the fact that PortablE implements almost everything you would see in a "real" compiler, and in principle the "missing" functionality could be added.

An alternative solution is to say that PortablE is a "meta compiler".


PortablE has basically no hard-coded limits (really!), so you could make your programs as large & complex as you want. The only limitations I've noted are:

Of course you will still be limited by any hard-coded limits of the target language's compiler. However, should any of these limits be very small (such as OPL's limit of 8 nested IF statements) then I will likely work-around them, if possible.


PortablE uses a flexible "refactoring" system to replace features not supported by the target language. The same system is used to implement optimisations. The refactoring system is designed to allow maximum code reuse. The target code is then generated by doing a fairly trivial translation, although the actual translation system is also very flexible, to support maximum code reuse. What this means is the more target languages that are supported, the easier it is to support further languages.


Go back to CONTENTS


20. Advanced usage - accessing native OS/language elements

For those who find the supplied OS calls too limiting, they may like to know that it is possible to access most (if not all) of the underlying OS & language. Of course, this will tie the code to that OS & language.

You can do this in any module you want, as long as OPT NATIVE is specified, but it is recommended that you think about it carefully, and at the very least try to declare all the native stuff in a separate module. (Such modules should be stored under the appropriate sub-folder within the 'PEmodules:target/' folder, so that they are automatically used for the right OS & language.)

The module 'PE/AmigaE/base' is an example of how this is done - although you can ignore the early CONST stuff, which is related the the AmigaE language rather than AmigaOS.

20.1. The basics

Native code is simply plain text that passes through PortablE without being modified, and is enclosed within curly braces like so:

  {CtrlC()}

But native code is rarely useful on it's own. Usually we want to use it as an expression, like so:

  IF {CtrlC()} !!BOOL THEN doSomething()

The !!BOOL cast is needed, because normally native code has the EMPTY type (i.e. it is assumed to return nothing). If the target language was AmigaE, then the generated code would look something like this:

  IF CtrlC() THEN doSomething()

If the target language was C++ then the generated code would look something like this:

  if (CtrlC()) {doSomething();}

About now you are probably imagining that you can write something like this:

  IF {Not( var )} !!VALUE THEN doSomething()

You would be wrong! When PortablE generates code, there is no guaratee that a variable's name will be unchanged, because of possible name space clashes. So what you must do is avoid enclosing variables within curley braces. Your first suggestion would probably look like this:

  IF {Not(} var {)} !!VALUE THEN doSomething()

Unfortunately, the IF statement expects a single expression - but you have given it *three* expressions! Each pair of curley braces is treated as a whole expression, as are any variables. What we have to do is mark the beginning & end of the native expression, like so:

  IF NATIVE {Not(} var {)} ENDNATIVE !!VALUE THEN doSomething()

This is perfectly legal code, and for AmigaE would result in something like the following code being generated:

  IF Not(var2) THEN doSomething()

I think you will agree that this NATIVE stuff is a bit messy, and not something you want to litter your main code with. Not a problem! Just put it in a one-line wrapper procedure:

  PROC Not(a) IS NATIVE {Not(} a {)} ENDNATIVE !!VALUE

And in fact, if you look in the 'PE/AmigaE/base' module, you will find exactly this line of code. But you may be thinking this is a bit inefficient - an unwanted extra procedure call. Not so! Procedures containing such native code will be automatically inlined. So I hope you can see that storing all your native wrappers in a separate module is actually a good idea.

If you wish to quickly test that all your declared 'native' procedures are totally correct, use the NOOPTINLINE switch, and then try to compile the generated code.

Rather than simply duplicate every feature & wart of the native functions, the ideal solution is to actually devise some simple procedures or methods which abstract what you want to do, so that the same procedures or methods can then be implemented using the native features of other OSes & languages. In this way you can ensure that porting your program to another OS/language will ONLY require the rewriting of a simple module. This is what I will be doing for my own needs, and I will typically make them available for other people to use, as it will benefit PortablE as a whole. I would be very happy if other people did the same :-)

20.2. Further issues

Some of the quicker readers may have wondered how we can declare the Not() procedure, when one already exists in the target language (AmigaE)? The simple answer is that AmigaE does not allow the declaration of procedures with an uppercase first letter, so the generated code would look something like this:

  PROC not(a) IS Not(a)

If the target language DID contain a procedure who's name clashed with this, then PortablE would simply rename the new procedure to something else & all generated code would use that new name instead. While this can't happen in AmigaE, it COULD happen in most other target languages. In those cases we must tell PortablE about these pre-existing procedures, like so:

  NATIVE {Pre_existing_name} PROC

Similarly, if there exists something which could clash with constants, global variables, or object/type names, then it should be declared:

  NATIVE {PRE_EXISTING_NAME} CONST
  NATIVE {pre_existing_name} DEF
  NATIVE {pre_existing_name} OBJECT

You can also combine name space reservations using commas:

  NATIVE {pre_existing_name} DEF, OBJECT

Typically all such potential clashes will have already been declared by me within the appropriate target module, so you don't have to worry about this. But if you decided to add some additional code to your module, for example:

  {MODULE 'MyCustomModule'}

or like this in C:

  {#include "MyCustomInclude.h"}

Then you WOULD need to worry about declaring potential new name clashes.

Lastly, I should mention that procedures beginning with an uppercase letter may only be declared in a module that has OPT NATIVE specified. Don't abuse it!

20.3. Constants & global variables

Allowing access to native constants & global variables is very easy, as it works just like normal declarations, except that they must start with a NATIVE {} part. For example, to allow access to C++'s NULL constant from PortablE, the following declaration is used:

  NATIVE {NULL} CONST NIL = 0 !!VALUE!!PTR

This simply declares that the NIL constant has the value 0 with type PTR, and that when generating target code NIL should appear as NULL.

Similarly, to allow access to C++'s stdout global variable from PortablE, the following declarion is used:

  NATIVE {stdout} DEF stdout:PTR

This simply declares that the stdout global has the type PTR (which is equivalent to void* in C++), and when generating target code it must always appear as stdout (rather than possibly being automatically renamed).

20.4. Objects

Allowing access to native objects is a bit more tricky than constants or globals, but it follows a similar pattern. Here is what the "datestamp" declaration looks like for C++:

  NATIVE {DateStamp} OBJECT datestamp
     {ds_Days}   days  :LONG
     {ds_Minute} minute:LONG
     {ds_Tick}   tick  :LONG
  ENDOBJECT

What this says is that the "datestamp" object is actually called "DateStamp" in the C++ code, which I hope is fairly obvious. What may be less obvious is that each member must also have it's C++ equivalent declared; so the "days" member is actually called "ds_Days" in the C++ code, etc.

It is worth mentioning that not all object members need to be declared - if they are not listed, then they are simply inaccessible to PortablE code. However, since PortablE does not know about undeclared members, it is possible (but un