The Portabl E language & compiler

  by Chris Handley, for Portabl E r6b (24.11.2022)
  For my email address, please see the "About the author" chapter.
  Manual last updated 24.11.2022.

Portabl E logo


CONTENTS


1. The biggest recent changes

This is the finished r6 release, with the "beta" tag removed, although it hasn't really been beta since the last couple of release.

The changes in the r6b release:

The changes in the r6a release:

The biggest changes since the last r6 beta release in 2016 include:

The other biggest changes since the r5 release in 2009 include:

Please see the 23. History chapter 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

Portabl E is my recreation of the AmigaE programming language, along with most of the improvements I have wanted. And while AmigaE only worked on AmigaOS3, Portabl E also works on AmigaOS4, AROS, MorphOS, as well as even Linux & Windows!

Portabl E works by translating your E code into C++, and then using GCC to compile it into an executable that you can run. But you don't usually need to worry about those details, because the PEGCC program will do it for you. Portabl E does need an installation of GCC, which is free & easily installed.

Previous versions of Portabl E were aimed at existing AmigaE users, but the r6 release of Portabl E aims to be attractive for new users too.

2.1. What is 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.

Jason Hulance's good AmigaE beginners guide: cshandley.co.uk/JasonHulance (TIP: If you want to use Portabl E rather than AmigaE with this guide, then make sure to use the OptAmigaE switch described in 5.1. Usage description.)

The original AmigaE manual: cshandley.co.uk/amigae/

The home page of AmigaE: strlen.com/amiga-e/

The Wikipedia entry on AmigaE: en.wikipedia.org/wiki/AmigaE

Aminet AmigaE stuff: aminet.net/dev/e

Amiga E Tutorials and Code Samples: web.archive.org/web/20091026160233/http://www.amigau.com/c-programming/amigae/etut.htm

The original AmigaE shareware package: (Amiga only) aminet.net/package/dev/e/amigae33a and wouter.fov120.com/files/lang/e/amigae33a.lha

The full AmigaE compiler released for free: (Amiga only) wouter.fov120.com/files/lang/e/ec33a.lha and wouter.fov120.com/files/lang/e/ec33a.readme

2.2. What should I read?

New users should only bother reading the first part of the 4. A quick start chapter in this manual. Then they should look at the "Standard Functionality" document, and also at the "Examples/std" folder. Eventually they may want to come back to this manual, and look at the 15. The type system, 5. Usage of the compiler & 7. Modules chapters. They might also consider joining the official Portabl E forum, or the AmigaE mailing list.

For Linux & Windows users, some familiarity with the Amiga is assumed, particularly the ReadArgs syntax used to specify command parameters & what assignments are, as both are 'emulated' by Portabl E on Linux & Windows systems.

Existing AmigaE users should try to read most of this manual, as it describes the differences between Portabl E & AmigaE. They can look at the "Examples/Amiga" folder, to see how Portabl E handles classic AmigaE programs. They are also advised to look at the "Standard Functionality" document, and the "Examples/std" folder, as these explain & demonstrate the new modules that come with Portabl E

Portabl E's "Standard Functionality" document can be found here: cshandley.co.uk/portable/StandardFunctionality.html

Portabl E's official forum for questions & discussions: ae.amigalife.org/index.php?board=10.0 (this covers all OSes, not just AROS!)

The current forum lost most of it's content after an upgrade, but the old content is archived here: web.archive.org/web/20181007125741/http://aros-exec.org/modules/newbb/viewforum.php?sortname=p.post_time&sortorder=DESC&since=0&forum=28&type=&refresh=Go

The AmigaE mailing list is still running: www.freelists.org/list/positron

Annotate is a nice (Amiga only) editor with syntax highlighting: aminet.net/package/text/edit/Annotate_usr or os4depot.net/index.php?function=showfile&file=utility/text/edit/annotate.lha

I have written an add-on for Annotate, which allows you to compile & run Portabl E code from inside Annotate: cshandley.co.uk/portable/AnnPEGCC_r1.lha

The homepage of Portabl E: cshandley.co.uk/portable/

2.3. Current status

Portabl E is capable of generating code for the C++ language (as well as for AmigaE itself!), which is then compiled to a proper executable. It supports Linux, Windows, AmigaOS3, AmigaOS4, AROS & MorphOS. (It can also potentially support other OSes & generate code for other languages.)

Portabl E has native executables for Linux (64bit x86), Windows, AmigaOS3, AmigaOS4, AROS (x86) & MorphOS, because Portabl E is written in E itself!


It comes with portable modules to cover stuff like file & directory access, shell parameter parsing, graphics, sound & GUIs. These portable modules are NOT always supported by all OSes, especially not by Linux & Windows yet. You can find a complete list & description of all of them in the "Standard Functionality" document, but essentially Linux & Windows only supports Shell-like stuff.

Many Amiga modules are also provided for AmigaOS3/OS4/AROS/MorphOS: AmigaGuide, AmigaLib, Asl, Commodities, Console, DataTypes, Devices (inc. AHI & Timer), Diskfont, Dos, Exec, Gadgets, Gadtools, Graphics, Icon, Identify, IFF, IFFParse, Images, Intuition, Layers, Locale, Keymap, MPEGA, MUIMaster, Resources, ReqTools, RexxSysLib, Utility & Workbench. Plus part of class, mui/*_mcc, Other & Tools. Also 'libraries/ahi_sub' & 'libraries/cd_play'. (Additional modules can be added if there is interest, but the original idea for Portabl E was to provide abstract modules that did not expose the OS.)

Additionally, some modules contributed by others are currently only available for some targets: 'mui/Lamp_mcc' (OS3/OS4), 'mui/TheBar_mcc' (OS3/OS4).

Modules that are specific to certain OSes: 'Picasso96API' (OS4), 'target/application' (OS4's application.library), 'target/cybergraphics' (AROS/MOS).


For Amiga-like OSes, 256MB of installed memory is the recommended minimum. A stack of at least 100KB is also required.

2.4. Background & motivation

I only became interested in AmigaE in 1996, after becoming dissatisfied with the other languages available (such as the buggy AMOS Pro). 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 (sort of Amiga-like) Psion platform, which meant loosing all my code & switching languages...

First I considered writing a crude translator from AmigaE to Psion's OPL 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 Portabl E 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 something like this, 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. It was finally officially released in June 2008. Windows support was added in May 2009. Linux support was finally added in November 2022, as I had recently switched from Windows to Linux.

2.5. The biggest changes from AmigaE

AmigaE hadn't been developed by Wouter for about a decade by the time I first released PortablE, but if it had then it would have likely looked quite different from the AmigaE v3.3 (and close descendants) that we were stuck with. Think of Portabl E as what AmigaE v4 or v5 might have looked like - except without Wouter's penchant for adding features from completely unrelated languages!

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.

3.1. Errr, what does that mean?

In Plain English (and only as an aid to understanding) the above is intended to roughly mean: Portabl E 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 it works well, but I can't guarantee it, so all use is soley at your risk. Backups are a wonderful invention, make use of them.


Go back to CONTENTS


4. A quick start

This chapter is to help get existing AmigaE users quickly writing new programs in Portabl E. 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.

For the Amiga, Portabl E 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

Linux & Windows user do not need to worry about this of course!

And if you want a list or explanation of all the standard (portable) functions, then please read the "Standard Functionality" document.

4.1. Hello world

Using a text editor like Notepad or EditPad, type the following program into a text file called Hello.e :

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

If Portabl E and GCC have been correctly installed, and you have saved Hello.e to your Work: partition, then you can compile it by typing the following at a Shell or Command prompt:

  PEGCC Work:Hello.e

This will produce an executable that you can run straight away. If you are feeling lazy, then you can have the executable automatically run as soon as it is compiled:

  PEGCC Work:Hello.e RUN


More advanced users may wish to generate C++ code that they can compile themselves, by typing the following at a Shell or Command prompt:

  PortablE Work:Hello.e

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


If you wanted to produce C++ code for AmigaOS4, using say the Windows or AROS version of Portabl E, then you would type the following at a Shell or Command 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 .


4.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 Portabl E 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 Portabl E (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 (or inlining) 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 Portabl E.

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

4.3. Floating point maths

Portabl E 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 Portabl E 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 Portabl E will report an error, because precision will be lost. In that case you must cast it to an integer first:

  number := 12.34 !!LONG

4.4. Strings

Lets get Portabl E 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, Portabl E 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 Portabl E it is always better to use NEW & END if you can. Where-as AmigaE's NEW could only allocate an array, Portabl E'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, Portabl E 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. (Actually, you can declare the return type(s) on the PROC line using RETURNS instead, but I don't want to over-complicate things yet!)


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 Portabl E 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 Portabl E 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, Portabl E 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.

4.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 & 64-bit values). An immediate list looks the same in Portabl E 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 Portabl E'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 is thrown.

Portabl E 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...

4.6. A glance at object orientation

Portabl E 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 a great game\n')
  	Print( test.get() )		->This prints "Portal is a great game"
  	
  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 Portabl E 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.

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:

Portabl E 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. Alternatively, we can specifically declare the return types:

  PROC get() OF example RETURNS value:ARRAY OF CHAR IS self.string

It's worth mentioning that when a child class inherits from a parent class, Portabl E places restrictions on the procedure 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 procedure parameters than the parent. Please read the 16. Object orientation chapter for a full explanation.

4.7. Other stuff

This whirl-wind tour of Portabl E 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 some of the later chapters (particularly from 8. How to compile your old AmigaE programs up to 13. AmigaE features that are missing). But don't let that stop you from experimenting - Portabl E 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 yet implemented, but ENDWHILE IF & ENDFOR IF reduces the need for it. JUMP is not supported.

I am willing to try to increase Portabl E's backwards compatibility with AmigaE, but please be aware that some things are not easy to fix, and a few things are impossible if Portabl E is to remain truely portable.


Go back to CONTENTS


5. Usage of the compiler

The main Portabl E command is PortablE without a space.

For the Amiga, 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

Linux & Windows users do not need to worry about the stack.

5.1. Usage description

PortablE is a Shell program, and it's parameter template is:

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

5.2. Usage example

For the Amiga, 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, you would enter the following line:

  PortablE Work:Code/PE/example.e

Similarly, for Linux users to compile the program ~/Code/example.e, they should enter:

  PortablE ~/Code/example.e

Similarly, for Windows users to compile the program C:\PortablE\Code\example.e, they should enter:

  PortablE C:\PortablE\Code\example.e

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 example.cpp in the same folder, which you can then compile using a C++ compiler. (Note that PortablE can also generate AmigaE v3 compatible code, even when using new features not supported by AmigaE.)

To compile the C++ code for all Amiga targets on Linux (and possibly Windows) please see the 6.2. Cross-compilation of C++ code sub-chapter.

5.3. Usage of PEGCC

Alternatively, you can use the PEGCC program to combine both of those steps, so that it automatically compiles the generated C++ code into an executable. You use it just like PortablE:

  PEGCC Work:Code/PE/example.e
or
  PEGCC ~/Code/example.e
or
  PEGCC C:\PortablE\Code\example.e

The end result should be an example executable file.


In addition to PortablE's shell parameters, PEGCC has these additional parameters:

TargetDir/K, LeaveTargetFile/S, NoStrip/S, Debug/S, GccOpts/F/K, Run/S, RunUsing=Using/K, RunParams=Params/F/K


Similarly, you can use the PE-EC script to automatically compile the generated AmigaE code with EC (if it is installed on your Amiga).

5.4. Obfuscation mode

The Obfuscate switch causes the chosen module to be obfuscated, and then written back out under a new file name (ending in _OBFUSCATED.e).

This makes the module almost unreadable by people, but it should behave exactly the same as the original. Some of the ways it obfuscates a module:

Obfuscation does have some unavoidable limitations:

In case anyone wonders, the reason for obfuscating a module is if you want to release it publically, but you want to discourage people from modifying it & releasing their own version. This should be nearly (but not quite) as effective as releasing a binary blob (e.g. a .o file), like you can do for some other languages. And it should be at least as effective as compiling Java code, since Java 'binaries' can be automatically turned back into source code (minus the comments, names of private elements, etc).

5.5. DeleteModuleCache utility

This shell program recursively deletes all .PEM (module cache) files. It's parameter template is:

  Folder, Verbose/S

If no Folder parameter is specified, then it defaults to the whole PEmodules: folder, which is usually what you want. (In some cases you might have local (i.e. *) modules, which are stored outside of PEmodules: )

If the Verbose parameter is specified, then it reports the path of every module cache file that is deleted.

NOTE: Currently this program handles both the old-style module cache files (which are stored in the same folder as the source module itself), and the new-style module cache files (which are always stored somewhere inside PEmodules:PE/cache ). At some point support for the old-style module cache files will be dropped.

5.6. CleanModuleCache utility

This shell program checks that all module cache files correspond to source code that still exists, and deletes the cache files if not.

You shouldn't normally need to use this, as PortablE automatically does it gradually for you anyway (every time it is run). It's only provided for occasions where you've moved or deleted a large quantity of modules, and can't bear to think of all that wasted disk space!

NOTE: This program only works with new-style module cache files (which are always stored somewhere inside PEmodules:PE/cache ).


Go back to CONTENTS


6. Compiling the code generated by Portabl E

6.1. C++ code

Older C++ compilers (as often used for the Amiga) have their own bugs, and also have 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:

Note that OS4's 'libraries/mpega' module was modified to work with the corrected headers from: www.os4depot.net/index.php?function=showfile&file=development/example/mpega_demo.lha

6.2. Cross-compilation of C++ code

PortablE supports generating C++ code for OSes other than the current one, using to the TargetOS parameter. However, you still need to compile that code - which won't work without a special version of GCC.

You can get special "cross-compiler" versions of GCC for Linux from here: cshandley.co.uk/crosscompilers/

Once installed, you should be able to use PEGCC with the TargetOS parameter.

Or if you use Windows, then AmiDevCpp did support compiling for other Amiga OSes - but it's no-longer available: web.archive.org/web/20160509140125/http://amidevcpp.amiga-world.de/index.php?HR_LANG=english

I still have copies of the main AmiDevCpp downloads, if there is any interest...

6.3. 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.


Beware 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: web.archive.org/web/20130322222429/http://www.tbs-software.com/fp/author.phtml


Go back to CONTENTS


7. Modules

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

Portabl E 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. There are also system modules within the targetShared 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).

7.1. Module cache

The automatic module cache provides a big speed boost (up to 3 times faster), by storing the result of parsing 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. So 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.

7.2. Module options (OPT)

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

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

7.3. OPT OPTIMISE

This is not (yet) as good as the GCC optimiser, and it will impair the GCC optimiser, thus resulting in a slower program if you use the GCC optimiser as well! So it is currently only recommended for target languages that are badly optimised (such as AmigaE).

But you may wish to use OPT INLINE instead, as this can give a significant speed-up, without impacting GCC's optimiser.

7.4. OPT POINTER

Portability will be impaired - it will not be possible to generate code for target languages which do not support real pointers, like Java. 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.

7.5. 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. You can avoid messy C++ code by using OPT NOPTRTOCHAR.

7.6. OPT NOPTRTOCHAR

This is only useful for source code that was generated using OPT AMIGAE, as it stops untyped variables from defaulting to the PTR TO CHAR type. Obviously it reduces compatibility with AmigaE code, but usually you just have to declare a few variables as being PTR TO CHAR when Portabl E gives an error that they can't be indexed as an array.

Apart from preventing messy C++ code being generated, it used to be necessary to work-around wierd C++ compiler errors like "cast from 'char*' to 'XXX' loses precision" or "cast to pointer from integer of different size". If this option fails to solve such compiler errors, then please let me know.

7.7. OPT NATIVE

Portability could 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.

7.8. OPT MULTITHREADED

For best speed, multi-threading support is normally disabled in programs compiled by Portabl E. But if you use this option in your main program, then multi-threading support is enabled for the whole program.

Without it, semaphores (and some internal) functions are 'dummies' that literally do nothing. But if you use this option then the semaphore/etc functions are properly implemented. This allows you to write functions that support multi-threading (using semaphores), but avoid the multi-threading overhead unless your program is multi-threaded!

And in particular, FastNew()/FastDispose()/NEW/END have multi-threaded support, but they won't be slowed down by it unless you use OPT MULTITHREADED.


WARNING: Unless you use the 'CSH/pAmiga_fakeNewProcess' module to create a new process on the Amiga, you will find that certain Portabl E features do not work (reliably) with multi-threaded programs:

Note that using the 'CSH/pAmiga_fakeNewProcess' module will mean your new processes will not share their global variables. So if you want to share any state, you must pass a pointer to it when the process is created.


Go back to CONTENTS


8. How to compile your old AmigaE programs

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

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

While you can just try compiling with the OptAmigaE switch, and then fixing errors as they are reported (see the 8.2. Compatibility hints sub-chapter for hints), you will likely have less problems if you follow these suggested steps:

1.First skim the 12. Other changes from AmigaE & 13. 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 8.2. Compatibility hints sub-chapter 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).

It USED to be the case that some C++ compilers gave errors like "cast from 'char*' to 'XXX' loses precision" or "cast to pointer from integer of different size" when you used the OptAmigaE switch. This should no-longer happen, but if it still does then please let me know (although a work-around is to use the OptNoPtrToChar switch).

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 Portabl E's stricter rules, particularly it's type-checking rules. So read the 11. Reversible changes from AmigaE & 15. 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 15. 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 15.9. 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 Portabl E's funky new features... So read the 9. New features compared to AmigaE & 10. Current improvements over AmigaE chapters, and maybe the 16. 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
  var := 123
  test(ADDRESSOF var)

Note that you need to use OPT POINTER for CALLBACK to be supported.

Note that unlike AmigaE, your callback procedure cannot have less parameters than it will be called with. For example, hook procedures MUST have 3 parameters.

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.

Or if it returns multiple values, then use "call2many" instead of "call2". This last function is not yet available for AmigaE targets.

However, if the function pointer originated from within the E code, then it could be worth considering using FUNC procedures instead, as these are safer & more portable.

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 AmigaE 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 Amiga modules are supported?

A.Please see the 2.3. Current status sub-chapter of the "Introduction" chapter. Or You can see exactly what is supported, by looking within PEmodules: but ignoring the PE, target & targetShared folders.

While the original idea for Portabl E 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.Portabl E 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 Portabl E 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 Portabl E'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

You can use these with #ifdef & #ifndef to enable or disable sections of code for different targets. They are best used to work-around subtle differences between different Amiga-like OSes. But you are strongly advised to use the special target module folder instead, if: (1) You have large sections of code (especially whole procedures) for different targets, or (2) You have a different section of code for every target OS.

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.

Note that ArrayCopy()'s offset parameters are in items not bytes.

9.3. Statements, expressions & functions

Similarly, LittleEndianINT(), LittleEndianLONG() & LittleEndianBIGVALUE() should be used when reading or writing a value that is stored in Little Endian format.

While SwapEndianINT(), SwapEndianLONG() & SwapEndianBIGVALUE() always return a value with swapped endianness, whatever processor is used. These functions should not normally be needed, but are provided for completeness.

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. mem := SUPER New(size, noClear)

Beware that only the current module (and modules that use it) will make use of the replacement. (The exception to this is when a PROTOTYPE procedure is being replaced - see below.)

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.

However, you can explicitly state the return type using RETURNS instead:

  PROC foo() RETURNS text:ARRAY OF CHAR
    text := 'bar'
  ENDPROC

It can also be used by in-line procedures:

  PROC foo() RETURNS text:ARRAY OF CHAR IS 'bar'

Although since the string 'bar' already has the type ARRAY OF CHAR, you could just type this instead:

  PROC foo() IS 'bar'

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 may 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

Note that exceptions are actively prevented from being thrown out of destructor end() 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 7.2. 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 Portabl E. 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 assumed 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

Note that you need to use OPT POINTER for CALLBACK to be supported.

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

12.4. Procedures & methods

If the function pointer is only used by E code (and not AmigaOS), then you should consider using FUNC procedures instead (see below).

Note that you need to use OPT POINTER for CALLBACK to be supported.

Note that unlike AmigaE, your callback procedure cannot have less parameters than it will be called with. For example, hook procedures MUST have 3 parameters.

But if the function pointer originated from E code, then you should consider using FUNC procedures instead (see below).

12.5. Object orientation

Go back to CONTENTS


13. AmigaE features that are missing

And there may be other changes I have forgotten, or maybe didn't even realise! Please report anything that you think I may have missed.


Go back to CONTENTS


14. Expression evaluation

14.1. Operator precedence

Portabl E 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

14.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.

14.3. Order of evaluation

Portabl E 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


15. 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.

15.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 .

15.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.

15.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 Portabl E'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.

15.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'

15.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

15.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 15.7. User-defined types sub-chapter).

15.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 20. Advanced usage - accessing native OS/language elements chapter.

15.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 (unless you need to enforce 32-bits), 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.

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

15.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

But due to how the type system works, you can actually get-away with casting to just PTR. For example:

  expression !!PTR


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.

15.10. The type resulting from an operation

Portabl E implements a fairly advanced type system, which is able to accurately deduce & represent the type of a result for operations like +, -, *, /, OR and AND. But it's one big limitation is that currently this doesn't work for VALUE types.

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. Portabl E 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 Portabl E 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 Portabl E 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 Portabl E 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


16. 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. Portabl E 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 Portabl E does things a little differently to C++ & many other OOP languages, I will begin with a little OOP theory...

16.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 Portabl E, 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...

16.2. How this applies to Portabl E

Unlike C++ or AmigaE, Portabl E 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 Portabl E, 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!

16.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 Portabl E 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.]

16.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 Portabl E 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 inheritance, 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 Portabl E for completeness, but it will have far less uses than simply ORPHANing a class, so don't worry about it too much.

16.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.


Go back to CONTENTS


17. Functional programming

The basis of functional programming is that functions (or procedures as they are called in E) are first class, which basically means that they can be treated like any other value and stored or passed around.

AmigaE allowed this using function pointers, but it was untyped so that a slight mistake could cause a crash. Portabl E has a similar-looking capability, but it is typed checked, so mistakes are caught at compile time.

17.1. FUNC procedures

FUNC procedures look like normal PROC procedures, but they have a (pseudo) function pointer. For example:

  FUNC add(a,b) IS a+b
  
  PROC main()
     DEF function:PTR TO add
     function := add
     
     add(1,2)          ->returns 3
     function(1,2)     ->returns 3
  ENDPROC

The function variable must have the type "PTR TO add" to allow it to be treated as a function. In particular, the type indicates the parameters & return values of the function.

Of course, this would not be much use if a variable could only ever call one function, so you can use inheritance for flexibility:

  FUNC funcParam2(x,y) IS x
  
  FUNC add(a,b) OF funcParam2 IS a+b
  
  PROC main()
     DEF function:PTR TO funcParam2
     function := add
     
     add(1,2)          ->returns 3
     function(1,2)     ->returns 3
  ENDPROC

This has the same result as the previous example, but this time we could have called any function that took two parameters, as long as the function inherited funcParam2.

Portabl E defines many such "base" functions, to cover various number of parameters, with the idea that code from different people will still be able to work together. For example:

  ->these return nothing
  FUNC funcParam0empty() IS EMPTY
  FUNC funcParam1empty(p1) IS EMPTY
  FUNC funcParam2empty(p1, p2) IS EMPTY
  ->these return one value
  FUNC funcParam0() OF funcParam0empty RETURNS value IS EMPTY
  FUNC funcParam1(p1) OF funcParam1empty RETURNS value IS EMPTY
  FUNC funcParam2(p1, p2) OF funcParam2empty RETURNS value IS EMPTY

These are currently stored in the 'std-alpha/functions' module, but in practice I haven't found much use for this module, so you are probably safe to ignore it.

The "std-alpha" folder indicates it is intended to eventually become standard (by moving to the std/ folder), but that currently they are an "alpha" version, which means they may change significantly (or may have major bugs).


WARNING: The function pointer provided by FUNC is not implemented using a real function pointer, so passing it to AmigaOS (say for a hook) will not work. If you need to provide real function pointers, then use a PROC with CALLBACK proc(). In the unlikely even that you need to call real function pointers, the 'std/pCallback' module provides a klunky solution. See the 8. How to compile your old AmigaE programs chapter for more information.

17.2. Standard functional programming procedures

The 'std-alpha/functions' module currently also contains these two procedures:

  PROC Fmap(function:PTR TO funcParam1, list:LIST)
  	DEF i
  	FOR i := 0 TO ListLen(list)-1 DO list[i] := function(list[i])
  ENDPROC list
  
  PROC Freduce(function:PTR TO funcParam2, list:ILIST, init)
  	DEF i, sum
  	sum := init
  	FOR i := 0 TO ListLen(list)-1 DO sum := function(sum, list[i])
  ENDPROC sum

These are LIST-based implementations of Map & Reduce, which are well known in functional programming. Fmap takes a list, and replaces each item with the value returned by the supplied function (which is called using the original item value). Freduce applies a function across the entire list, and returns the 'running total' at the end.

For example, to multiply every item in a list by two, and then add all the items together, you could use:

  FUNC multiply(value) OF funcParam1 IS 2*value
  FUNC add(a,b) OF funcParam2 IS a+b
  
  PROC main()
     DEF list:LIST
     list := NEW [1,2,3]
     Fmap(multiply, list)   ->list now contains [2,4,6]
     Freduce(add, list, 0)  ->returns 12 = 2+4+6
  ENDPROC

Fmap() & Freduce() can work on more than just numbers, although numbers are easier. For example, if you had a list of e-strings, then you could use Fmap() to preprocess those strings (say removing spaces), and then Freduce() to concatentate those strings together.

In practice I haven't found much use for this module, so you are probably safe to ignore it.

17.3. Child functions are flexible

If you have a child function that inherits a parent function, then the child can have more parameters & more return values, just like you can with normal OOP methods:

  ->one parameter & one return value
  FUNC parent(one) IS one*2
  
  ->three parameters & two return values
  FUNC child(one, two=2, three=1) OF parent IS one*two, one*three

In this case child(x) does the same as parent(x), i.e. it returns x*2, but it also returns an extra (optional) value, and it has extra (optional) parameters. So child() can be used where parent() is expected, but when you know that you have child(), then you can make use of it's extra (optional) capabilities.

17.4. Implementation

As has been previously mentioned, functions are not implemented using real function pointers. So you might wonder how it is actually implemented. In the case of this code:

  FUNC add(a,b) IS a+b
  
  PROC main()
     DEF function:PTR TO add
     function := add
     
     add(1,2)          ->returns 3
     function(1,2)     ->returns 3
  ENDPROC

It is converted by Portabl E into something like this:

  CLASS add OF function
  ENDCLASS
  
  PROC call(a,b) OF add IS a+b
  
  DEF add:PTR TO add	->this gets NEWed automatically
  
  
  PROC main()
     DEF function:PTR TO add
     function := add
     
     add.call(1,2)          ->returns 3
     function.call(1,2)     ->returns 3
  ENDPROC

First FUNC is converted into a method of a new class, then a global variable is created which contains that object. Finally any function calls to variables (of the right type) are converted into method calls. Result: Magic! ;-)


While it isn't necessary to understand the implementation, it may help you understand clever uses of functions, especially if you understand how Portabl E does OOP. For example, you can give your function a memory (effectively static variables), by knowing how functions are implemented:

  FUNC parent(one) IS one*2
  
  
  CLASS child OF parent
     lastValue     ->this is the function's memory
  ENDCLASS
  
  PROC call(one, two=2, three=1) OF child
     self.lastValue := one
  ENDPROC one*two, one*three
  
  DEF child:PTR TO child
  
  PROC new()
     NEW child.new()
  ENDPROC
  
  PROC end()
     END child
  ENDPROC

Such a child can even be inherited by another FUNCtion.


Go back to CONTENTS


18. Known bugs

Go back to CONTENTS


19. Technical discussion

Am I right to class Portabl E 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 - which C arguably is anyway! What decided it for me is the fact that Portabl E implements almost everything you would see in a "real" compiler, and in principle the "missing" low-level functionality could be added.

Also, PEGCC *does* produce an executable that isn't interpreted - which is exactly what a real compiler does. Unlike say Python or Hollywood. So from the end-users point of view there doesn't seem much difference!

Or an alternative solution is to say that Portabl E is a "meta compiler". But I find that description to be messy, and it may just lead to the question of what a "meta compiler" is...


Portabl E 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.


Portabl E 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. The idea was that as more target languages were supported, the easier it would be to support further languages - but in practice the biggest difficulty in supporting a language has turned out to be writing all the necessary target & 'wrapper' modules, so I have ended-up sticking to just C++ (with the hope of also supporting plain C eventually).


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 carefully think about it. You have two ways to support different NATIVE code for multiple OSes:

1. You can enclose the NATIVE code inside preprocessor sections, such as #ifdef pe_TargetOS_Linux . This works well when you only have small amounts of NATIVE code.

2. You can put the NATIVE code in separate modules. In this case the 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.

20.1. The basics

Native code is simply plain text that passes through Portabl E 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 this:

  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 Portabl E 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.

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 AmigaE target language? 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 Portabl E 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 Portabl E 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 Portabl E, 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 Portabl E, 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 Portabl E code. However, since Portabl E does not know about undeclared members, it is possible (but unlikely) that there could be a name-space clash (typically with another member of the same name). In the future I intend to provide a simply solution to this.


Each member must also have it's correct type declared, which is not always obvious when we are using Portabl E's type system - because the real C++ object uses C++ types! For example, we may have an object who's C++ member is:

  char *string;

While the obvious equivalent is "string:PTR TO CHAR", Portabl E's strings are ARRAY OF CHAR, so it's real equivalent is "string:ARRAY OF CHAR". It may help to know that this still results in the C++ type "char* string".


Another example:

  UBYTE *CS_Buffer;     /* Optional string parsing space. */

Here UBYTE is actually declared by AmigaOS as "typedef unsigned char UBYTE;". So we should make CS_Buffer an ARRAY OF UBYTE. Since Portabl E does not have any unsigned types built-in, the UBYTE type is defined within the 'exec/types' module.


Another example:

  UWORD ed_OwnerUID;

Here UWORD is actually declared by AmigaOS as "typedef unsigned short UWORD;". Again, this is defined within 'exec/types'. So we can use "ed_OwnerUID:UWORD".


What happens if you declare a member as the wrong type? Maybe it will still seem to work, but in some cases Portabl E may fail to provide an appropriate cast. If you are unsure that a type is the correct choice, then simple write a dummy program that contains that type, and see what it get's translated to by Portabl E.

20.5. NATIVE options

In some cases it may be necessary to provide additional hints to tell Portabl E how it should handle a NATIVE declaration. Options are provided using the following format:

  NATIVE {name option1 option2 etc} ...

Which is equivalent to:

  NATIVE {name} ...

But with the additional options "option1" and "option2". Currently NATIVE options are only supported by OBJECT & CLASS declarations, and the only options available are "Typedef" and "Union", both of which tell Portabl E to not put the word "struct" before the objects name when referring to it.

  e.g. NATIVE {__sigset_t Typedef} OBJECT __sigset_t

Is used on Linux because the __sigset_t object is declared using a typedef:

  typedef struct { ... } __sigset_t;

20.6. Types

For those cases where Portabl E has no type that maps exactly onto an underlying type, you can use so-called native types. For example, as previously mentioned, Portabl E does not have any unsigned types built-in, so they are defined like this within the 'exec/types' module:

  TYPE UBYTE IS NATIVE {UBYTE} CHAR
  TYPE UINT  IS NATIVE {UWORD} INT
  TYPE ULONG IS NATIVE {ULONG} VALUE
  TYPE UBIGVALUE IS NATIVE {unsigned long long} BIGVALUE

What this says is that the UBYTE type behaves like a CHAR type, as far as the user is concerned, but when generating code the {UBYTE} causes UBYTE to be written instead of the type CHAR. Instead of CHAR, we could have chosen any type, such as VALUE, but in this case CHAR minimises the amount of casting that the user needs to do for old AmigaE programs.


As another example, if an object contained the following C++ function pointer:

  VOID (*freefunc)();

You can declare a Portabl E equivalent like this:

  freefunc:NATIVE {VOID (*)()} PTR

The "PTR" at the end of this declaration can be any type we like, such as VALUE, since it will not effect the generated code. But for function pointers you should use PTR, because that is the type that CALLBACK returns.


For C++ targets, please also note that NATIVE types of function pointers should only be used (1) for members of NATIVE objects, and (2) for NATIVE global variables, because Portabl E will not create the correct C++ syntax for other uses.

20.7. Unlimited parameters for a procedure

Unlike some other languages, Portabl E does not (yet) support procedures with an unlimited number of parameters - except for NATIVE procedures. It's done using the special ... symbol, but it's mostly easier to demonstrate it's usage rather than explain it:

  PROC Print(fmtString:ARRAY OF CHAR, arg=0, ...) IS NATIVE {printf(} fmtString {,} arg {,} ... {)} ENDNATIVE

The above is how the Print() procedure is implemented. Beware that all ... parameters are expected to have the same type as the previous parameter, in this case the "arg" parameter which has the default type VALUE.

Note that the native expression before the ... (which is {,} in this case) is used to separate the extra parameters when generating code.

20.8. Converting C++ headers

Once you've had a bit of practice, doing the above is quite easy. But it can still be a chore if there is a large quantity of code to support, such as you might see in an Amiga C++ header file.

Therefore I have written a dumb tool that will semi-automatically convert a C++ header file for you. You still need to go through the generated code yourself, filling-in or removing certain things it was not sure about, but it probably gives a 10 times speed-up to the overall process.

I have NOT yet provided the tool with Portabl E, but if you think that you might find it useful, then please email me.


Go back to CONTENTS


21. About the author

My name is Christopher S Handley, but you don't really want to hear about me do you? Suffice to say that I am an AmigaE fanatic, and live in England :-)

You can you can get my current email address from this web page: cshandley.co.uk/email But please do not post my email address in public, because I might get spammed!


Go back to CONTENTS


22. Thanks

While there are many people I could thank, this isn't the Hollywood Oscars, so in alphabetical order I will just give my biggest thanks to:

Others who have helped me are acknowledged within the 23. History chapter, as are the details of how the above people have helped me.


Go back to CONTENTS


23. History

key: NEW = new or significantly improved feature CHG = changed or removed feature BUG = bug fix.

23.1. r6

r6b (24.11.2022)

r6a (13.11.2022)

r6 (07.11.2022)

And from much longer ago:

The above changes are all since the last 23.2. r6 beta in 2016. There are too many changes to list since r5 in 2009, see the 23.2. r6 beta sub-chapter for more details.

23.2. r6 beta

key: + = something different from the last official release - = something different from the last beta release

r6 beta (26.04.2016)

r6 beta (11.12.2015)

r6 beta (06.12.2015)

r6 beta (20.11.2014)

r6 beta (14.07.2013)

r6 beta (07-10-2012)

r6 beta (09-04-2012)

r6 beta (21-03-2012)

r6 beta (05-03-2012)

r6 beta (27-10-2011)

r6 beta (28-04-2011)

r6 beta (23-04-2011)

r6 beta (20-03-2011)

r6 beta (13-03-2011)

r6 beta (09-03-2011)

r6 beta (13-02-2011)

r6 beta (06-02-2011)

23.3. r5

r5 (27-11-2009)

23.4. r4

r4 (29-05-2009)

23.5. r3

r3 (25-12-2008)

r3 beta2 (11-12-2008)

r3 beta1 (11-11-2008)

23.6. r2

r2 (04-07-2008)

23.7. r1

r1 (15-06-2008)

23.8. pre-release

These versions were limited to the AmigaE mailing list, and could be considered as coming before "v1.0".

r1 RC2 (14-06-2008)

r1 RC1 (31-05-2008)

r1 beta5 (02-05-2008)

r1 beta4 (11-04-2008)

r1 beta3 (07-03-2008)

r1 beta2 (15-02-2008)

r1 beta1 (24-06-2007)

r1 alpha5 (03-10-2006)

r1 alpha4 (05-08-2006)

r1 alpha3 (23-07-2006)

r1 alpha2 (23-06-2006)

r1 alpha1 (17-06-2006)