# Oberon compiler

The unit at [comp/oberon.fs] is an Oberon Compiler. It's very close to
specifications and is able to compile source from the Oberon Project almost
unchanged.

On top of that, it also follows the design philosophy of other compilers in
Dusk: good interoperability with the Dusk system.

## Usage

You compile a Oberon unit with "oberon<<":

  oberon<< path/to/source.mod

Oberon code kind of lives in its own world. Upon loading, all procedures created
during compilation are opaque to Forth. If you want to interact with Oberon,
you need to create aliases to some of its procedures:

  obalias MyModule.MyProc myforthname

See "Calling Oberon code from Forth" below.

## Specification compliance

As mentioned above, this compiler pretty much works exactly as specified, with
minor exceptions:

* The SYSTEM module doesn't exist. All procedures specified as being part of
  SYSTEM in the specs are in fact builtin.
  SYSTEM.VAL becomes "VAL", SYSTEM.GET becomes "GET" etc.
* LSL and ASR are operators, same priority as DIV and MOD. We also have LSR.
* There is a DUSK module, the gateway system words. See below.
* As specified, type checking is strict except for BYTE and INTEGER which are
  compatible. However, to simplify interfacing with the system, DWord, Word and
  Byte [lib/type] are also compatible.
* Specifications mention that SET width is type dependent. Ours is 32-bit.
* There is no such thing as procedure-local constants and types. Types and
  constants declared at the procedure level have the same effect as when they
  are declared outside a producedure.
* There is no such thing as a private record field. All fields are public, "*"
  in records are ignored.
* VAR in procedure signature has a slightly different meaning.
  See "VAR parameters" below.
* String literals allow escapes the same as Dusk's string literals.
* Has RAWPOINTER which is like pointer but can target anything. It creates a
  Pointer type instead of a RecordPointer one. Can't use with "IS" or
  typeguards.
* Has a OPAQUE type for values coming from Forth that are passed around without
  being worked on in Oberon code and thus, not worth creating proper types for.
  In procedure call, OPAQUE arguments will always match the signature, but they
  can't be used in arithmetics. Underneath, it's a Integer(4).
* The canonical Oberon implementation allows ARRAYs to be used with VAR in
  procedure arguments. ARRAYs are always pointers in arguments. Using VAR with
  ARRAYS doesn't do what you think it does. With this compiler, it adds an
  extra layer of indirection which will *not* be auto-dereferenced.
* There are no inline PROCEDUREs. See "Forward PROCEDURE references" below.
* Single quotes ' must be used for character literals instead of double quotes.
* IMPORT only maps the first 8 characters of the name to a filename. All files
  in Dusk must fit FAT's 8.3 filename scheme. Therefore, the first 8 chars of
  module names must be unique for auto-loading to work.

## Call convention

As with other compilers in Dusk, code compiled by this compiler follows the
regular Dusk calling convention, that is, it passes arguments through PS.

The first argument is the shallowest of the stack. Return value, when there is
one, is returned on the stack. For example:

  PROCEDURE foo(x,y:INTEGER): INTEGER;

has the Forth signature "( y x -- res )" and:

  PROCEDURE foo(x,y:INTEGER);

has the Forth signature "( y x -- )".

### Open arrays are tricky

So far, so good? Yeah, there's a hitch. Open arrays, that is, "ARRAY OF
Something", need to have their length passed around at runtime and the most
straightforward way is to use an extra PS slot, right "under" the array.
Therefore:

  PROCEDURE foo(x:INTEGER; array: ARRAY OF CHAR);

has the Forth signature "( arraylen array x -- )". For this reason, it's
generally a bad idea to expose Oberon code to Forth through a procedure that has
an open array in its arguments. It's a good way to create confusing bugs.

### VAR parameters with structs

Specifications mention that the "VAR" flag in argument indicate that we refer
to the variable itself. In this compiler, "VAR" means "auto-dereferenced
pointer".

This means that it's like a "POINTER TO" that can target any type, not just
RECORDs. It dereferences itself depending on the context. That means: it will
generally dereference itself (except on assigments), but if it's passed as an
argument to a procedure that requires a pointer, then it won't dereference
itself.

All in all, it's very much like what happens in "real" Oberon, but it's clearer
with regards to Forth interoperability.

... but there's a caveat when VAR is used with records.

First of all, you probably don't want to use VAR with a "POINTER TO RECORD". It
adds an extra level of indirection which isn't auto-dereferenced.

But it's not those that are special, it's VAR used with straight RECORD types.
Oberon code expects to be able to run dynamic type checks on those variables
too, but such RECORD don't live in the GC!

Therefore, when passing such pointers around, we do like with open arrays: we
push an extra argument with the struct type on PS. This way, such arguments can
be used with "IS" and typeguards.

This is important with regards to Forth interoperability. First, if you call a
Oberon word that has a VAR struct in its argument, you need to push the type of
that struct along with it.

Second, the only way to call a Forth word with a struct pointer in its signature
is by passing it an OPAQUE variable. Passing a real struct will make it compile
an extra PS push.

## Forward PROCEDURE declaration

This Oberon compiler doesn't have inline procedures. They are silly and seldom
used in actual code. Moreover, their implementation is needlessly complex. For
this implementation to work, one has to have the ability to save and restore
local symbol declarations, something we're not willing to do.

They are silly because the private/public system already takes care of reducing
namespace pollution. The only legitimate use of inline procedures is forward
references when complex recursion is needed. In these cases, a PROCEDURE needs
the ability to call another PROCEDURE that has yet to be defined.

For this, we have forward declarations. That is: PROCEDURE statements can stand
there alone, without an accompanying VAR and BEGIN. It's then possible to call
that procedure. Then, later, you can copy the same PROCEDURE declaration and
implement it.

Forward referenced calls have an extra layer of indirection. Instead of
branching to a "i)", they branch to a "m)".

## String literals, comparisons and null-termination

Oberon specifications mention that comparison operators, including ordering
ones, can be used on strings (array of chars). This means that strings aren't
considered as pointers by the relation operators, their contents is compared.

Specs don't mention exactly how it should be done. For example, if we compare
arrays of different length, what happens? We compare using the smallest length?
Would "foo" = "foobar" yield true? No, it shouldn't.

In Dusk's Oberon, we consider all array of chars to be null-terminated. String
literals null-terminate their data. Therefore, string comparisons ignore array
length entirely. They just compare until a zero is met in either string.

## Calling Forth code from Oberon

Forth words can be called through the DUSK module. Lookups for the DUSK
module are made in the sysdict through "findannotated" [comp/sig]. If found, we
compile a call to it.

## Calling Oberon code from Forth

Oberon compilation doesn't generate any Forth word. All those artifacts live
in the "Oberon world". You can hook into that world with "obalias", which has
semantics similar to "alias" except that its first argument is a fully qualified
(that is, "Module.ident") name. Example:

  obalias MyModule.MyProc myforthname

Then, you can call "myforthname" using the call convention described above.

## Sharing types between Oberon and Forth

Type lookup generally works as specified, that is, types belong to modules and
have to be fully qualified to be referred to.

But there's more. Unqualified type lookups first look into the current module,
as expected, but if that lookup fails, we look in the whole sysdict through
"findtype" [lib/type].

This allows you, for example, to share structures [lib/struct] between Forth and
Oberon without the need for duplicating the declaration.

## IMPORT autoloading

The IMPORT line goes through the specified list of module names, checking
whether each module is loaded in memory (exists in the "modules" dictionary).
If it doesn't, run the equivalent of "oberon<< oberon/<modulename>.mod".

The name "DUSK" is ignored and has no effect.

## The DUSK module

Semantically, "DUSK" is used like any other module, but under the hood, it's
special. It's not a real module. What happens when a lookup occurs in that
module is that it occurs in the whole sysdict.

If a word of that name is found, we check if it has a SIGT tag from [comp/sig].
If it does, then we've found a procedure identifier.

The Oberon compiler implement DUSK words from Oberon specs as forth words with
a SIGT tag.

On top of procedures included in the Oberon specs, this compiler also implement
a few helper procedures:

DUSK.DebugStr(str: ARRAY OF CHAR);
  Proxy for "rtype".

DUSK.DebugVal(n: OPAQUE);
  Proxy for ".x".

## API

oberon<< ( "path" -- )
  Compile the Oberon module at "path".

obalias ( "qualident" "name" -- )
  Look for procedure "qualident" (modulename.name) in the Oberon world and
  export it as a Forth word named "name".

obtype ( "qualident" "name" -- )
  Look for type "qualident" (modulename.name) in the Oberon world and
  export it using "addtype" [lib/type].

obvar' ( "qualident" -- a )
  Look for variable "qualident" (modulename.name) in the Oberon world and yield
  its address.
