# 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:

* LSL and ASR are operators, same priority as DIV and MOD. We also have LSR.
* Single quotes ' must be used for character literals instead of double quotes.
* There are no inline PROCEDUREs. See "Forward PROCEDURE references" below.
* 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.
* 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.
* 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.
* Specifications mention that SET width is type dependent. Ours is 32-bit.

... and a few additions:

* 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.
* String literals allow escapes the same as Dusk's string literals.
* 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).
* Has a LOADFORTH top-level directive.

## 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 are tricky too

When a straight RECORD (not a POINTER) is passed as a VAR argument, things are
tricky in a similar way as open arrays: we need to pass the type around. Unlike
the POINTER TO RECORD, which points to a GCed struct, which contains its type,
here, we have nothing.

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

## The LOADFORTH directive

LOADFORTH is a top-level directive that does exactly the same thing as "f<<"
[fs/core]. Its argument is not terminated by a ";" character.

This directive is used in Oberon modules needing hooks into the system. For
example, the Files module has "LOADFORTH oberon/Files.pre" which loads a Forth
source file that creates hooks like DUSK.Lookup, which the module needs.

This is generally called at the top of a module, but it can be anywhere, as long
as it's top-level and inside MODULE.

## API

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

?obimport ( name -- )
  If module "name" isn't loaded yet, load it like an IMPORT would do.

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.
