Coder Social home page Coder Social logo

lboasso / oberonc Goto Github PK

View Code? Open in Web Editor NEW
146.0 10.0 17.0 1.12 MB

An Oberon-07 compiler for the JVM

License: MIT License

Makefile 0.45% Modula-2 88.75% AMPL 1.64% Java 8.76% Batchfile 0.39%
oberon programming-language oberon-compiler jvm oberon-07 compiler

oberonc's Introduction

Oberon-07 compiler

oberonc is a single pass, self-hosting compiler for the Oberon-07 programming language. It targets the Java Virtual Machine (version >= 1.8).

This project was started to showcase Niklaus Wirth's approach to writing compilers (see "Compiler Construction - The Art of Niklaus Wirth" by Hanspeter Mössenböck for more details).

oberonc is inspired by Niklaus Wirth's compiler for a RISC processor available here.

The compiler is compact and does not depend on any third party libraries. It produces Java bytecode in one pass while parsing the source file. Although generating code for a stack machine is straightforward, this task is exacerbated by a complex class file format and the fact that the JVM was designed with the Java language in mind. In fact the JVM lacks many of the primitives required to support Oberon's features, specifically:

  • value types
  • pass by reference evaluation strategy
  • procedure variables (pointer to functions) and relative structural compatibility of types

Implementing those features with workarounds increased significantly the size of the compiler, totaling roughly 6000 lines of Oberon.

The source code is written following as much as possible Niklaus Wirth's coding style. oberonc compile itself in less than 300 ms on an old Intel i5 @ 2.80GHz (~ 100 ms with a hot VM).

How to build

You can build the compiler on Linux or Windows, you need a JDK >= 1.8 installed, with java and javac in the environment path.

Because you need an Oberon compiler to compile the sources in src, I have added to the repository the binaries of the compiler to perform the bootstrapping.

By typing make build on the shell, the compiler will compile itself and write the files in the out folder. The make bootstrap command is equivalent to make build, but it overwrites the files in the bin folder.

To run the compiler, you need to have the OBERON_BIN environmental variable set to the bin folder of the repository. This is taken care for you when using make.

How to run the tests

One typical test is to make sure that, by compiling the compiler, we get the same (bit by bit) class files originally included in the bin folder. To run this test simply type make bootstrapTest (available only on Linux). This will compile the sources into the bootstrapOut folder and compare these resulting class files with the ones in bin. If something goes wrong sha1sums will complain.

To run the tests included in the tests folder, type make test. The output should look like this:

...
TOTAL: 101
SUCCESSFUL: 101
FAILED: 0

Using the compiler

To use the compiler, you need to have the OBERON_BIN environmental variable set to the bin folder of the repository, for example on Linux export OBERON_BIN=~/oberonc/bin or set OBERON_BIN=C:\oberonc\bin on Windows. The command line syntax of oberonc is simple. Let's compile examples/Hello.Mod:

MODULE Hello;
  IMPORT Out; (* Import Out to print on the console *)
BEGIN
  Out.String("Hello 世界");
  Out.Ln (* print a new line *)
END Hello.

Assuming you are at the root of the repository, the following command will compile the Hello.Mod example and place the generated classes in the current folder:

Linux
java -cp $OBERON_BIN oberonc . examples/Hello.Mod

Windows
java -cp %OBERON_BIN% oberonc . examples/Hello.Mod

The first argument of oberonc is ., this is the existing folder where the generated class will be written, the next arguments specify module files to be compiled.

This will generate Hello.class and Hello.smb. The second file is a symbol file, it is used only during compilation and enables oberonc to perform separate compilation of modules that import Hello. In this simple case Hello.Mod does not export anything, but the other modules in the examples folder do.

To run Hello.class, you need the OberonRuntime.class and Out.class. These are present in the bin folder so they are already in the class path, we just need to include the current folder as well to locate Hello.class:

Linux
java -cp $OBERON_BIN:. Hello

Windows
java -cp %OBERON_BIN%;. Hello

If you want to compile and run automatically a simple example called fern, type make runFern. It should open a window like this one:

Fern

Lastly, make clean will delete the output folders generated by build, test, runFern and bootstrapTest.

License

The compiler is distributed under the MIT license found in the LICENSE.txt file.

oberonc's People

Contributors

itmm avatar johnperry-math avatar lboasso avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

oberonc's Issues

How to use double precision data type?

It seemed oberonc define REAL as float internally. As Oberon-07 only has the REAL data type but not also SHORTREAL I think REAL should be mapped to double. Other Oberon-07 compiler like OBNC allows redefining the size of numeric data types via compiler options. BTW, I have no problem with the current implementation but only want to know how to use double precision number in oberonc (alongside with the single precision REAL).

unexpected behavior in CASE ... OF

when using expressions or calls returning INTEGER in CASE ... OF, compiler gives errors or raises exceptions.
e.g.

MODULE CaseTest;

IMPORT Out;
 
VAR
  arr : ARRAY 3 OF INTEGER;
  i : INTEGER;
  
BEGIN
  FOR i := 0 TO 2 DO
    arr[i] := i;
  END;
  i := 1;
  CASE arr[i] OF
    1 : Out.String("1");
  END
END CaseTest.

gives
CaseTest.Mod:12:10: inadmissible type
CaseTest.Mod:13:10: undef
CaseTest.Mod:13:11: not a record
CaseTest.Mod:13:22: too many params
CaseTest.Mod:15:13: compilation FAILED

MODULE CaseTest2;
 
IMPORT Out;
 
VAR
  arr : ARRAY 3 OF CHAR;

BEGIN
  arr[0] := 'a';
  arr[1] := 'b';
  arr[2] := 'c';
  i := 1;
  CASE ORD(arr[1]) OF
    97 : Out.String("a");
  END
END CaseTest2.

raises
CaseTest2.Mod:9:15: undef
CaseTest2.Mod:10:15: undef
CaseTest2.Mod:11:15: undef
CaseTest2.Mod:12:3: undef
CaseTest2.Mod:13:10: OF expected
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1
at OJG.CaseOut(OJG.Mod:1930)
at OJP.CasePart(OJP.Mod:759)
at OJP.StatSequence(OJP.Mod:910)
at OJP.Module(OJP.Mod:1356)
at OJP.Compile(OJP.Mod:1384)
at oberonc.Main(oberonc.Mod:41)
at oberonc.main(oberonc.Mod)

SYSTEM.VAL pointer conversions

Hello

I tried to do an "unchecked" pointer-to-pointer conversion using SYSTEM.VAL. I discovered that (a) oberonc doesn't actually allow that in its current version, and (b) after I modified it to allow it, the performance seemed indistinguishable from a "checked" conversion via type guard.

To be clear, I'm talking about (unchecked)

   y := SYSTEM.VAL(Ptr, x);

and (checked)

   y := x(Ptr);

I could pass on (via pull request if need be) the modifications I made that enable the unchecked pointer-to-pointer conversion, but I'm not sure it's worth the effort if it makes no measurable difference in code. (It doesn't seem to make a difference in OBNC's implementation of SYSTEM.VAL, either, and from what I can tell that does a direct C typecast.)

I would be interested in your thoughts on this.

(Some detail: I modified StandFunc when fct = 16 to invoke a new OJG.PointerToPointer procedure when x and y are both pointers. OJG.PointerToPointer then generates a CHECKCAST opcode. As it turns out, this seems to generate the exact same byte code as a type guard, so perhaps there's a better way to make an unchecked pointer-to-pointer conversion, but that's the best I could divine from my very limited knowledge of JVM byte codes. Plus, it also seems to be what the Java language it does when you cast. But I'm just learning, so...)

Tests fails because of locales, that differ from en_US

For example:

$ LANG=ru_RU.UTF-8 make test
...
Test 'tests/base/TestImport62.Mod' FAILED:
EXPECTING:
'AY 3.140000 -999999999'
FOUND:
'AY 3,140000 -999999999'
---END---

TOTAL: 97
SUCCESSFUL: 85
FAILED: 12

Other example:

$ LANG=en_UK.UTF-8 make test
...
Test 'tests/base/UTF8String.Mod' FAILED:
EXPECTING:
'Hello, ? ?'
FOUND:
'Hello, ? ?'
---END---
...
TOTAL: 97
SUCCESSFUL: 95
FAILED: 2

How to format errors?

I mentioned I would make some suggestions for more helpful error messages. That could be subjective, so I wanted to make sure the following general approach would be OK with you. After all, I don't want to get so far into this that it becomes too hard to undo.

Suppose someone tries to cast two types that don't belong together. Right now the error will be incompatible types or something like that. I thought it might be more helpful to name the types, which leads to the following changes to TypeTest:

  PROCEDURE TypeTest(VAR x: OJG.Item; T: OJB.Type; guard: BOOLEAN);
    VAR xt: OJB.Type; i: INTEGER;
  BEGIN xt := x.type;
    IF (T.form = xt.form) &
       ((T.form = OJB.Pointer) OR
        (T.form = OJB.Record) & (x.mode = OJB.ParStruct)) THEN
      WHILE (xt # T) & (xt # NIL) DO xt := xt.base END ;
      IF xt # T THEN xt := x.type;
        IF xt.form = OJB.Pointer THEN
          IF IsExtension(xt.base, T.base) THEN
            OJG.TypeTest(x, T.base, guard); x.type := T
          ELSE
            i := 0;
            i := Strings.Write(xt.typobj.name, error, i);
            i := Strings.Write(" is not an extension of ", error, i);
            i := Strings.Write(T.typobj.name, error, i);
            OJS.Mark(error)
          END
        ELSIF (xt.form = OJB.Record) & (x.mode = OJB.ParStruct) THEN
          IF IsExtension(xt, T) THEN  OJG.TypeTest(x, T, guard); x.type := T
          ELSE
            i := 0;
            i := Strings.Write(xt.typobj.name, error, i);
            i := Strings.Write(" is not an extension of ", error, i);
            i := Strings.Write(T.typobj.name, error, i);
            OJS.Mark(error)
          END
        ELSE
          i := 0;
          i := Strings.Write(xt.typobj.name, error, i);
          i := Strings.Write(" and ", error, i);
          i := Strings.Write(T.typobj.name, error, i);
          i := Strings.Write(" are incompatible types", error, i);
          OJS.Mark(error)
        END
      ELSIF ~guard THEN OJG.MakeConstItem(x, OJB.boolType, 1)
      END
    ELSE
      i := 0;
      i := Strings.Write("type mismatch for ", error, i);
      i := Strings.Write(xt.typobj.name, error, i);
      i := Strings.Write(" and ", error, i);
      i := Strings.Write(T.typobj.name, error, i);
      OJS.Mark(error)
    END ;
    IF ~guard THEN x.type := OJB.boolType END
  END TypeTest;

Does this sort of thing look OK to you, or would you suggest a different way of going about it?

Unexpected Runtime Exception When Casting INTEGER to BYTE

Introduction

In Oberon-07, it's possible to convert one scalar data type to another through the SYSTEM.VAL function. This is a system dependent function, hence it belongs to the SYSTEM module. However, in the particular system, we are expected to get the corresponding value casted to the identity of the target data type.

Current Behavior

When casting a variable of type INTEGER to a variable of type BYTE, we get no compile-time error, but when executing we get an error saying that the variable of type INTEGER does not exist.

Expected Behavior

Compiling the following code and executing it:

MODULE WriteByteTestFailing;
  IMPORT Out, SYSTEM;
  VAR
    x : INTEGER;
    b : BYTE;
BEGIN
  x := 0ECFFH;
  b := SYSTEM.VAL(BYTE, x);

  Out.String("BYTE = ");
  Out.Int(b, 0); Out.Ln;
END WriteByteTestFailing.

should render the following output:

BYTE = 255

However, we get a runtime exception instead.

Repro Steps

  1. Compile the code using the Oberonc compiler.

  2. Observe the exception being raised:

Exception in thread "main" java.lang.NoSuchFieldError: x
	at WriteByteTestFailing.<clinit>(WriteByteTestFailing.Mod:8)

Other Information

This issue seems to be specific to the class file generated by the Oberonc compiler, because if we reverse engineer the code, we get the following java class:

public final class WriteByteTestFailing {
    public static String[] args = new String[0];
    public static int x = 60671;
    public static byte b = (byte)(x & 0xFF);

    public static void main(String[] stringArray) {
        args = stringArray;
    }

    static {
        Out.String((char[])"BYTE = \u0000".toCharArray());
        Out.Int((int)(b & 0xFF), (int)0);
        Out.Ln();
    }
}

Compiling it with javacand executing it, renders the expected output.

Variable Declared in Parent Procedure Inaccessible from Inner Procedure

Introduction

The Oberon-7, as all reports written by Wirth, isn't very clear about scope rules. However, in Programming in Oberon, he sort of addresses this. The following bug appears to be related to inner procedures accessing variables declared in the parent procedure.

Current Behavior

When trying to refer to a variable declared in an outer procedure from an inner procedure, I get an error saying that the variable is inaccessible.

Expected Behavior

According to the tutorial book Programming in Oberon by Niklaus Wirth, the scope of a variable should extend to the inner procedures. Hence, if I declare variable x in procedure Outer, I should be able to access it in procedure Inner, and all subsequent inner procedures.

Repro Steps

  1. Write a valid Oberon-7 code that involves use of a variable declared in an Outer procedure from an Inner procedure.
MODULE InnerProcedures;
  IMPORT Out;

  PROCEDURE Outer;
    VAR
      x : INTEGER;
    PROCEDURE Inner;
    BEGIN
      x := 234;
    END Inner;
  BEGIN
    Inner;
    Out.String("Value of x is ");
    Out.Int(x, 0); Out.Ln
  END Outer;
BEGIN
  Outer
END InnerProcedures.
  1. Compile the code using the Oberonc compiler.
  2. Observe the type compatibility error being raised shown below:
InnerProcedures.Mod:9:0: not accessible
InnerProcedures.Mod:18:20: compilation FAILED

There are two interesting observations from this error message. The first, naturally, is that it does not let variable x be accessible by procedure Inner. The second is the location of the error. It reports that it is found in line 9, column 0. This is also incorrect.

Modified Code (Working Solution)

To work around this issue, use a VAR parameter in procedure Inner. This approach eliminates the error.

MODULE InnerProceduresFix;
  IMPORT Out;

  PROCEDURE Outer;
    VAR
      x : INTEGER;
    PROCEDURE Inner(VAR x : INTEGER);
    BEGIN
      x := 234;
    END Inner;
  BEGIN
    Inner(x);
    Out.String("Value of x is ");
    Out.Int(x, 0); Out.Ln
  END Outer;
BEGIN
  Outer
END InnerProceduresFix.
  1. Compile and run the modified code using the Oberonc compiler.
  2. Observe that the modified code works correctly without raising type compatibility errors.

Other Information

This issue appears to be specific to variables within inner procedures. This scope is valid in Oberon, and all other compilers I have tried accept this code as valid, as it should.

The compiler accepts usage of any other type of declarations from within an inner procedure: types, constants and procedures.

Out Module missing format width exception

Trying to print an Integer with

MODULE OutInt;

IMPORT Out;

BEGIN
  Out.Int(10, 0); Out.Ln;
END OutInt.

gives exception

Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.util.MissingFormatWidthException: %0d
at java.util.Formatter$FormatSpecifier.checkNumeric(Formatter.java:3022)
at java.util.Formatter$FormatSpecifier.checkInteger(Formatter.java:2982)
at java.util.Formatter$FormatSpecifier.(Formatter.java:2729)
at java.util.Formatter.parse(Formatter.java:2560)
at java.util.Formatter.format(Formatter.java:2501)
at java.io.PrintStream.format(PrintStream.java:970)
at java.io.PrintStream.printf(PrintStream.java:871)
at Out.Int(Out.java:22)
at OutInt.(OutInt.Mod:6)

Not sure if this is actually defined anywhere, values other than 0 work here, but so far I am used to use 0 from Oxford and Vishaps Oberon compilers.

Incorrect Handling of Pointer Type Base and Incompatible Assignment

Introduction

In Oberon-07, two pointer types are considered equal if they have the same base type. Consequently, if a record field is declared as a pointer to a specific type, and later another type is declared as a pointer to the same base type, both types should be equal and assignment compatible.

Current Behavior

Consider the following code snippet:

MODULE PointerCompat;
  TYPE
    Rec = RECORD
      x, y : INTEGER;
    END;

    PRec1 = POINTER TO Rec;
    PRec2 = POINTER TO Rec;

  VAR
    p1 : PRec1;
    p2 : PRec2;
BEGIN
  NEW(p1);
  p2 := p1;
  p2.x := 12;
  p2.y := 10;
  ASSERT(p1 = p2);
  ASSERT(p1.x = p2.x);
  ASSERT(p1.y = p2.y)
END PointerCompat.

In this example, PRec1 and PRec2 are equal types and assignment compatible, which is expected behavior.

However, when declaring a record with a field of type POINTER TO Rec and subsequently attempting to assign it to a variable declared as POINTER TO Rec, as shown below:

MODULE PointerCompat2;
  IMPORT Out;
  TYPE
    Rec = RECORD
      x, y : INTEGER;
      next : POINTER TO Rec
    END;

    PRec1 = POINTER TO Rec;
    PRec2 = POINTER TO Rec;

  VAR
    p1 : PRec1;
    p2 : PRec2;
    p : PRec1;
BEGIN
  NEW(p1);
  NEW(p2);
  p1.x := 13;
  p1.y := 19;
  p1.next := p2;
  p2.x := 12;
  p2.y := 10;
  
  p := p1;
  WHILE p # NIL DO
    Out.String("x = "); Out.Int(p.x, 0); Out.Ln;
    Out.String("y = "); Out.Int(p.y, 0); Out.Ln;
    p := p.next
  END
END PointerCompat2.

The compiler reports the following errors:

PointerCompat2.Mod:16:0: undefined pointer base: END
PointerCompat2.Mod:21:16: illegal assignment
PointerCompat2.Mod:30:0: illegal assignment
PointerCompat2.Mod:31:19: compilation FAILED

The compiler does not recognize the pointer base of PRec1 declared as the type of variable p. Consequently, it does not recognize the assignment of a variable of type POINTER TO Rec, which is the same as the one defined for the field next of Rec.

The issue is resolved when the field next is defined with a pre-declared pointer type, as demonstrated in the following code:

MODULE PointerCompat3;
  IMPORT Out;
  TYPE
    PRec1 = POINTER TO Rec;
    PRec2 = POINTER TO Rec;
    Rec = RECORD
      x, y : INTEGER;
      next : PRec1
    END;

  VAR
    p1 : PRec1;
    p2 : PRec2;
    p : PRec1;
BEGIN
  NEW(p1);
  NEW(p2);
  p1.x := 13;
  p1.y := 19;
  p1.next := p2;
  p2.x := 12;
  p2.y := 10;
  
  p := p1;
  WHILE p # NIL DO
    Out.String("x = "); Out.Int(p.x, 0); Out.Ln;
    Out.String("y = "); Out.Int(p.y, 0); Out.Ln;
    p := p.next
  END
END PointerCompat3.

Expected Behavior

The compiler should successfully compile the code, regardless of whether record fields are declared with a pre-declared pointer type or directly as a pointer to the same type.

Repro Steps

  1. To observe this behavior:
    1. Compile the code in the three examples using the Oberonc compiler.
    2. Note that the compiler successfully compiles the code for PointerCompat.Mod and PointerCompat3.Mod, but reports errors for PointerCompat2.Mod.

Other Information

This issue has not been tested with the original compiler. However, when compiled with OBNC, it does not present this problem and behaves as expected.

Changes to make it easy to recompile

Here's another issue I found while trying to find the problem I was having. The Makefile has the following lines:

MOD_SOURCES = src/Out.Mod src/Os.Mod src/Files.Mod src/Strings.Mod src/OJS.Mod \
              src/CpCache.Mod src/Opcodes.Mod src/ClassFormat.Mod+ src/OJB.Mod \
              src/OJG.Mod src/OJP.Mod src/oberonc.Mod

As is, making a change to the compiler will lead to an error, something like "New symbol file inhibited." This is because the compiler, by default, doesn't want to overwrite a symbol file. Unfortunately, the compiler actually deletes the old class file(s) first (personally, I think this is a bug) so now the compiler doesn't work anymore. Argh thunk as head slams against table in Don Music style

Changing it to the following makes it possible to overwrite those class files, recompile the compiler, and so forth. Joy!!! smiles

MOD_SOURCES = src/Out.Mod+ src/Os.Mod src/Files.Mod+ src/Strings.Mod+ src/OJS.Mod+ \
              src/CpCache.Mod+ src/Opcodes.Mod+ src/ClassFormat.Mod+ src/OJB.Mod+ \
              src/OJG.Mod+ src/OJP.Mod+ src/oberonc.Mod+

Compile errors when importing a file

I found it's impossible to compile the Rectangles.Mod file. These are the errors detected:

Rectangles.Mod:22:28: undef
Rectangles.Mod:26:11: not a function
Rectangles.Mod:31:20: illegal assignment
Rectangles.Mod:32:21: undef
Rectangles.Mod:33:20: undef
Rectangles.Mod:35:0: undef
Rectangles.Mod:35:15: compilation FAILED

It seems that the elements that supposedly have been exported are not really exported.

Figures.Mod

MODULE Figures; (* Abstract module *)

TYPE
   Figure*    = POINTER TO FigureDesc;
   Interface* = POINTER TO InterfaceDesc;

   InterfaceDesc* = RECORD
      draw*  : PROCEDURE (f : Figure);
      clear* : PROCEDURE (f : Figure);
      mark*  : PROCEDURE (f : Figure);
      move*  : PROCEDURE (f : Figure; dx, dy : INTEGER);
   END;

   FigureDesc* = RECORD
      if : Interface;
   END;

PROCEDURE Init* (f : Figure; if : Interface);
BEGIN
   f.if := if
END Init;

PROCEDURE Draw* (f : Figure);
BEGIN
   f.if.draw(f)
END Draw;

(* Other procedures here *)

END Figures.

Rectangles.Mod

MODULE Rectangles;

IMPORT Figures;

TYPE
   Rectangle* = POINTER TO RectangleDesc;

   RectangleDesc* = RECORD
      (Figures.FigureDesc)
      x, y, w, h : INTEGER;
   END;

VAR
   if : Figures.Interface;

PROCEDURE New* (VAR r : Rectangle);
BEGIN
   NEW(r);
   Figures.Init(r, if)
END New;

PROCEDURE Draw* (f : Figure);
   VAR
      r : Rectangle;
BEGIN
   r := f(Rectangle); (* f AS Rectangle *)
   (* ... *)
END Draw;

(* Other procedures here *)

BEGIN (* Module initialisation *)
   NEW(if);
   if.draw  := Draw;
   if.clear := Clear;
   if.mark  := Mark;
   if.move  := Move
END Rectangles.

Name Conflict in Inner Procedures with Different Parent Procedures

Here's an enhanced version of the report:

Introduction

In Oberon-07, it's possible to declare inner procedures, which are accessible within the scope of the outer procedure only. This allows the use of inner procedures with the same name, similar to how variables and constants can have the same name in different scopes.

Current Behavior

When declaring two procedures, A and C, each with its own inner procedure named B, the compiler throws an error indicating that procedure names must be unique.

Expected Behavior

It's expected that declaring identifiers (procedures, variables, or any other type of identifiers) within different scopes should not be problematic. The current issue may arise due to the way the code is translated into JVM class files. However, it should be possible to have multiple inner procedures with the same name as long as they are within different outer procedures.

Repro Steps

  1. Write a valid Oberon-7 code that involves the use of inner procedures with the same name in distinct outer procedures.
MODULE Scopes;
  IMPORT Out;

  PROCEDURE A;
    PROCEDURE B;
    BEGIN
      Out.String("Hello from A/B"); Out.Ln
    END B;
  BEGIN
    B
  END A;

  PROCEDURE C;
    PROCEDURE B;
    BEGIN
      Out.String("Hello from C/B"); Out.Ln
    END B;
  BEGIN
    B
  END C;
BEGIN
  A;
  C
END Scopes.
  1. Compile the code using the Oberonc compiler.
  2. Observe the name conflict error being raised:
Scopes.Mod:15:0: procedure names must be unique
Scopes.Mod:24:11: compilation FAILED

Modified Code (Working Solution)

To work around this issue, one needs to rename the procedures, and in the example that follows the inner procedure name is prefixed with the name of the outer procedure in lower case. This approach eliminates the error.

MODULE ScopesSolution;
  IMPORT Out;

  PROCEDURE A;
    PROCEDURE aB;
    BEGIN
      Out.String("Hello from A/B"); Out.Ln
    END aB;
  BEGIN
    aB
  END A;

  PROCEDURE C;
    PROCEDURE cB;
    BEGIN
      Out.String("Hello from C/B"); Out.Ln
    END cB;
  BEGIN
    cB
  END C;
BEGIN
  A;
  C
END ScopesSolution.
  1. Compile and run the modified code using the Oberonc compiler.
  2. Observe that the modified code works correctly without raising naming conflicts.

Other Information

This issue seems to be specific to the Oberonc compiler and the way it generates Java class files, as indicated by the following decompiled code using javap:

public final class ScopesSolution {
  public static java.lang.String[] args;

  public static final void aB();
    Code:
       0: ldc           #7                  // String Hello from A/B\u0000
       2: invokevirtual #13                 // Method java/lang/String.toCharArray:()[C
       5: invokestatic  #19                 // Method Out.String:([C)V
       8: invokestatic  #23                 // Method Out.Ln:()V
      11: return

  public static final void A();
    Code:
       0: invokestatic  #28                 // Method aB:()V
       3: return

  public static final void cB();
    Code:
       0: ldc           #31                 // String Hello from C/B\u0000
       2: invokevirtual #13                 // Method java/lang/String.toCharArray:()[C
       5: invokestatic  #19                 // Method Out.String:([C)V
       8: invokestatic  #23                 // Method Out.Ln:()V
      11: return

  public static final void C();
    Code:
       0: invokestatic  #34                 // Method cB:()V
       3: return

  public static void main(java.lang.String[]);
    Code:
       0: aload_0
       1: putstatic     #39                 // Field args:[Ljava/lang/String;
       4: return

  static {};
    Code:
       0: iconst_0
       1: anewarray     #9                  // class java/lang/String
       4: putstatic     #39                 // Field args:[Ljava/lang/String;
       7: invokestatic  #43                 // Method A:()V
      10: invokestatic  #45                 // Method C:()V
      13: return
}

It's possible that a solution could involve generating procedure names that are prefixed with the parent procedure's name to avoid naming conflicts.

Please note that this bug report is intended to highlight the observed behavior and suggest a possible solution for consideration.

Type Incompatibility with Self-Recursion in Oberonc Compiler

Introduction

The Oberon-7 code provided below encounters an unexpected behavior when compiled with the Oberonc compiler. This bug appears to be related to self-recursion in the code that utilizes certain features of the Oberon-7 language specification.

Current Behavior

When using self-recursion in the Oberonc compiler with valid Oberon-7 code, the compiler raises an error indicating incompatible types when passing a type-extended variable of Node.

Expected Behavior

Self-recursion should work as expected without raising type compatibility errors, as it does in other compilers.

Repro Steps

  1. Write a valid Oberon-7 code that involves self-recursion and type-extended variables of Node.
MODULE AstExample;

IMPORT Out;

CONST
  Add = 1; 
  Subtract = 2; 
  Multiply = 3; 
  Divide = 4;

TYPE
  AstNode = POINTER TO AstNodeDesc;
  AstNodeDesc = RECORD
  END;

  AstNumberNode = POINTER TO AstNumberNodeDesc;
  AstNumberNodeDesc = RECORD(AstNodeDesc)
      value: INTEGER
  END;

  AstBinaryNode = POINTER TO AstBinaryNodeDesc;
  AstBinaryNodeDesc = RECORD(AstNodeDesc)
      opType: INTEGER;
      left, right: AstNode
  END;

PROCEDURE CreateNumberNode(value: INTEGER): AstNumberNode;
  VAR
    node: AstNumberNode;
  BEGIN
    NEW(node);
    node.value := value;
    RETURN node
  END CreateNumberNode;

PROCEDURE CreateBinaryNode(opType: INTEGER; left, right: AstNode): AstBinaryNode;
  VAR
    node: AstBinaryNode;
  BEGIN
    NEW(node);
    node.opType := opType;
    node.left := left;
    node.right := right;
    RETURN node
  END CreateBinaryNode;

PROCEDURE DepthFirstTraversal(node: AstNode);
  BEGIN
    IF node # NIL THEN
      CASE node OF
        AstNumberNode:
          Out.Int(node.value, 0)
        | AstBinaryNode:
          CASE node.opType OF
            Add:
              Out.String("(");
              DepthFirstTraversal(node.left);
              Out.String(" + ");
              DepthFirstTraversal(node.right);
              Out.String(")")
            | Subtract:
              Out.String("(");
              DepthFirstTraversal(node.left);
              Out.String(" - ");
              DepthFirstTraversal(node.right);
              Out.String(")")
            | Multiply:
              Out.String("(");
              DepthFirstTraversal(node.left);
              Out.String(" * ");
              DepthFirstTraversal(node.right);
              Out.String(")")
            | Divide:
              Out.String("(");
              DepthFirstTraversal(node.left);
              Out.String(" / ");
              DepthFirstTraversal(node.right);
              Out.String(")")
          END
      END
    END
  END DepthFirstTraversal;

PROCEDURE Main;
  VAR
    root: AstNode;
  BEGIN
    (* Build the AST: 2 * (3 + 4) *)
    root := CreateBinaryNode(Multiply,
      CreateNumberNode(2),
      CreateBinaryNode(Add,
        CreateNumberNode(3),
        CreateNumberNode(4)
      )
    );

    (* Perform depth-first traversal *)
    Out.String("Expression: ");
    DepthFirstTraversal(root);
    Out.Ln
  END Main;

END AstExample.
  1. Compile the code using the Oberonc compiler.
  2. Observe the type compatibility error being raised shown below:
AstExample.Mod:57:44: incompatible parameters
AstExample.Mod:59:45: incompatible parameters
AstExample.Mod:63:44: incompatible parameters
AstExample.Mod:65:45: incompatible parameters
AstExample.Mod:69:44: incompatible parameters
AstExample.Mod:71:45: incompatible parameters
AstExample.Mod:75:44: incompatible parameters
AstExample.Mod:77:45: incompatible parameters
AstExample.Mod:103:15: compilation FAILED

Modified Code (Working Solution)

To work around the self-recursion issue, use an intermediary procedure for the AstBinaryNode traversal. This approach eliminates the type compatibility error.

MODULE AstExample2;

IMPORT Out;

CONST
  Add = 1; 
  Subtract = 2; 
  Multiply = 3; 
  Divide = 4;

TYPE
  AstNode = POINTER TO AstNodeDesc;
  AstNodeDesc = RECORD
  END;

  AstNumberNode = POINTER TO AstNumberNodeDesc;
  AstNumberNodeDesc = RECORD(AstNodeDesc)
      value: INTEGER
  END;

  AstBinaryNode = POINTER TO AstBinaryNodeDesc;
  AstBinaryNodeDesc = RECORD(AstNodeDesc)
      opType: INTEGER;
      left, right: AstNode
  END;

VAR
  TraverseBinaryNode : PROCEDURE(node : AstBinaryNode);

PROCEDURE CreateNumberNode(value: INTEGER): AstNumberNode;
  VAR
    node: AstNumberNode;
  BEGIN
    NEW(node);
    node.value := value;
    RETURN node
  END CreateNumberNode;

PROCEDURE CreateBinaryNode(opType: INTEGER; left, right: AstNode): AstBinaryNode;
  VAR
    node: AstBinaryNode;
  BEGIN
    NEW(node);
    node.opType := opType;
    node.left := left;
    node.right := right;
    RETURN node
  END CreateBinaryNode;

PROCEDURE DepthFirstTraversal(node: AstNode);
  BEGIN
    IF node # NIL THEN
      CASE node OF
        AstNumberNode:
          Out.Int(node.value, 0)
      | AstBinaryNode:
          TraverseBinaryNode(node)
      END
    END
  END DepthFirstTraversal;

PROCEDURE TraverseBinaryNode0(node : AstBinaryNode);
  BEGIN
    IF node # NIL THEN
      CASE node.opType OF
        Add:
          Out.String("(");
          DepthFirstTraversal(node.left);
          Out.String(" + ");
          DepthFirstTraversal(node.right);
          Out.String(")")
      | Subtract:
          Out.String("(");
          DepthFirstTraversal(node.left);
          Out.String(" - ");
          DepthFirstTraversal(node.right);
          Out.String(")")
      | Multiply:
          Out.String("(");
          DepthFirstTraversal(node.left);
          Out.String(" * ");
          DepthFirstTraversal(node.right);
          Out.String(")")
      | Divide:
          Out.String("(");
          DepthFirstTraversal(node.left);
          Out.String(" / ");
          DepthFirstTraversal(node.right);
          Out.String(")")
      END (* CASE *)
    END (* IF *)
  END TraverseBinaryNode0;

PROCEDURE Main;
  VAR
    root: AstNode;
  BEGIN
    (* Build the AST: 2 * (3 + 4) *)
    root := CreateBinaryNode(Multiply,
      CreateNumberNode(2),
      CreateBinaryNode(Add,
        CreateNumberNode(3),
        CreateNumberNode(4)
      )
    );

    (* Perform depth-first traversal *)
    Out.String("Expression: ");
    DepthFirstTraversal(root);
    Out.Ln
  END Main;
BEGIN
  TraverseBinaryNode := TraverseBinaryNode0
END AstExample2.
  1. Compile and run the modified code using the Oberonc compiler.
  2. Observe that the modified code works correctly without raising type compatibility errors.

Other Information

This issue appears to be specific to self-recursion within the Oberonc compiler. Self-recursion is a common programming technique, and the type compatibility error in this context is unexpected.

Here is another code that also doesn't work:

MODULE ExpressionCalculator;

IMPORT Out;

TYPE
  TreeNode = POINTER TO Node;
  Node = RECORD
    value: INTEGER;
    operator: CHAR;
    left, right: TreeNode;
  END;

VAR
  expressionTree: TreeNode;

(* Function to create a new TreeNode *)
PROCEDURE CreateNode(value: INTEGER; operator: CHAR): TreeNode;
  VAR newNode: TreeNode;
BEGIN
  NEW(newNode);
  newNode.value := value;
  newNode.operator := operator;
  newNode.left := NIL;
  newNode.right := NIL;
  RETURN newNode
END CreateNode;

(* Function to evaluate an expression tree *)
PROCEDURE EvaluateExpression(root: TreeNode): INTEGER;
  VAR
    result : INTEGER;
BEGIN
  result := 0;

  IF root # NIL THEN
    IF root.operator = '+' THEN
      result := EvaluateExpression(root.left) + EvaluateExpression(root.right);
    ELSIF root.operator = '-' THEN
      result := EvaluateExpression(root.left) - EvaluateExpression(root.right);
    ELSIF root.operator = '*' THEN
      result := EvaluateExpression(root.left) * EvaluateExpression(root.right);
    ELSIF root.operator = '/' THEN
      result := EvaluateExpression(root.left) DIV EvaluateExpression(root.right);
    ELSE
      result := root.value;
    END
  END;

  RETURN result
END EvaluateExpression;

BEGIN
  (* Creating an expression tree for (3 + 4) * 2 *)
  expressionTree := CreateNode(0, '*');
  expressionTree.left := CreateNode(3, ' ');
  expressionTree.right := CreateNode(0, '+');
  expressionTree.right.left := CreateNode(4, ' ');
  expressionTree.right.right := CreateNode(2, ' ');

  (* Evaluate the expression and print the result *)
  Out.String("Expression result: ");
  Out.Int(EvaluateExpression(expressionTree), 0);
  Out.Ln;
END ExpressionCalculator.

assigning records from pointers causes error

The code snipplet

MODULE RecTest;

TYPE RP = POINTER TO R;
  R = RECORD
  rp : RP;
  i : INTEGER;
END;

VAR
globRP : RP;

PROCEDURE DoSmth(locRP : RP);
BEGIN
  globRP^.rp^ := locRP^;
  (*
  globRP^.rp^.rp := locRP^.rp;
  globRP^.rp^.i := locRP^.i;
  *)
END DoSmth;

BEGIN
END RecTest.

Throws an error
..\examples\RecTest.Mod:14:24: Reg Stack
..\examples\RecTest.Mod:19:12: compilation FAILED

Other compilers compile this assignment (e.g. Oxford)
Assigning single members works well with oberonc.

CASE statements & tableswitch

This code won't compile:

MODULE TestCase;
IMPORT Out;
VAR a: INTEGER;
BEGIN
  a := 10;
  CASE a OF
     5: Out.String("Five")
  | 10: Out.String("Ten")
  | 15: Out.String("Fifteen")
  | 2000: Out.String("Two thousand!!!")
  END;
  Out.Ln
END TestCase.

The output is

TestCase.obn:11:0: too many cases or no case in case statement
TestCase.obn:13:13: compilation FAILED

Apparently the problem is that the compiler wants to use a tableswitch, and limits the number of cases to a maximum of 256. From what I read about the JVM, a lookupswitch is a better option in this case.

I have a workaround for now (the obvious one that uses IF).

Undetected Syntax Error: Record Type's Final Field Declaration Ends with a Semicolon

Introduction

In Oberon-07, semicolons serve as separators for both statements and field declarations within records. However, Niklaus Wirth's implementation diverged from the specified syntax. Specifically, the END keyword in record declarations should not be preceded by a semicolon.

Current Behavior

Currently, when fields in a record type are declared, and the last field is terminated with a semicolon, the compiler does not report a syntax error. For example:

MODULE Records;
  TYPE
  	rec = RECORD
  		x : INTEGER;
  		y : CHAR;
  	END;
END Records.

This code compiles without any problem, although it deviates from the expected behavior.

Expected Behavior

According to the Oberon-07 specification, the syntax for record declarations should adhere to the following pattern:

RecordType = "RECORD" ["(" BaseType ")"] [FieldListSequence] "END".
FieldListSequence = FieldList {";" FieldList}.
FieldList = IdentList ":" type.
IdentList = ident {"," ident}.

In this syntax, the semicolon functions as a separator, not a terminator. Consequently, the last field declaration should not conclude with a semicolon.

Repro Steps

To observe this behavior:

  1. Compile the code using the Oberonc compiler.
  2. Note that the compiler compiles the code without any error.

Other Information

Niklaus Wirth's implementation in the compiler code treats the semicolon as optional in this context. This behavior is not aligned with the Oberon-07 specification and originates from the original Oberon compiler. As a result, all compilers that rely on his code as a foundation fail to address this issue and accept it as valid. The only compiler that I know to adhere to the correct behavior is the OBNC compiler, implemented in C. The OBC compiler has a condition to handle this in the grammar. However, when you pass the -07 option, it fails to report the syntax error, and it compiles as if it were an Oberon-2 file.

Here's the output of the OBNC compiler:

obnc-compile: Records.Mod:6: syntax error, unexpected END, expecting IDENT
obnc: build process failed

Wirth's implementation in the code can be represented by the following EBNF rule:

RecordType = "RECORD" ["(" BaseType ")"] [FieldListSequence] [";"] "END".

Here, the semicolon is optional, mirroring the behavior implemented in the procedure RecordType.

This issue was identified by my TreeSitter parser, which adheres strictly to the specified grammar. The parser correctly flags this syntax as an error in record declarations. In contrast, the compiler handles it without issue. This mismatch highlights the discrepancy between the actual compiler code and the Oberon-07 specification, as well as the legacy nature of the codebase, that are nothing but a rewrite of his previous compilers. Namely: Algol-W, Pascal, Pascal-S, Modula and Modula-2.

Despite the challenges, working with legacy systems and updating them to modern standards can be a rewarding experience. This case serves as a reminder of how past limitations and workarounds can persist in code, even in systems designed by influential figures like Niklaus Wirth.

Unexpected java.lang.VerifyError (Stack size too large)

Trying to initialise the following module,

MODULE M;
  VAR i: INTEGER;

  PROCEDURE I(): INTEGER;
  BEGIN RETURN 1
  END I;

  PROCEDURE R(): REAL;
  BEGIN RETURN 1.0
  END R;

BEGIN
  i := I() * FLOOR(R());        (* or: MOD, +, etc. *)
END M.

results in

Error: Unable to initialize main class M
Caused by: java.lang.VerifyError: (class: M, method: <clinit> signature: ()V) Stack size too large

As indicated in the comment, the same is true of I() MOD FLOOR(R()) etc.

The above is as minimal a reproduction as I could find. One workaround for this issue is FLOOR(0.0 + R()).

Bug with invalid pointers

There seems to be a bug in internalNameAt that manifests with this code:

MODULE TestPt;
TYPE
  PT = POINTER TO ARRAY 5 OF BOOLEAN;
VAR
  pt: PT;
BEGIN
  pt := NIL;
END TestPt.

The output I get is

$ java -cp $OBERON_BIN oberonc build TestPt.obn 
TestPt.obn:5:37: must point to record
Exception in thread "main" java.lang.NullPointerException
	at OJG.internalNameAt(OJG.Mod:139)
	at OJG.DescriptorR(OJG.Mod:171)
	at OJG.DescriptorAt(OJG.Mod:209)
	at OJG.Descriptor(OJG.Mod:214)
	at OJG.Header(OJG.Mod:2310)
	at OJP.Module(OJP.Mod:1378)
	at OJP.Compile(OJP.Mod:1410)
	at oberonc.Main(oberonc.Mod:40)
	at oberonc.main(oberonc.Mod)

It looks as if it's because type.base.typobj is NIL. If I change it as follows:

      IF type.base.typobj # NIL THEN
        i := Strings.Write(type.base.typobj.name, out, i)
      ELSE
        i := Strings.Write(type.typobj.name, out, i)
      END

...then the compiler doesn't crash. Though maybe it doesn't need the ELSE case anyway; could it be left blank?

jar integration

any chance I can import and use other jars directly in Oberon?

DescLenMax is too small

Compilation failed because DescLenMax in ClassFormat.Mod is only 100. That's not long enough for most function names, where there can be many variables with long type names. Increasing it to 1000 got a program to move past one compilation error for me, but I'm not sure 1000 is enough, either.

I can submit a pull request if you like, but this seems easy enough to fix for now.

IN Module

Is there such a module to get user input. Very Important to have one!! Thx.

Need list of language expression that compiler do understand correctly

Good day. Saw many bug reports of using compiler. As far as I understand the compiler does not understand some language expression. Would you please write the list of languge expressions, declaration if variables and so on that compiler do understand correctly, so I could use them in my code?

Inconsistent Behaviour of For Loop

Introduction

In Oberon-07 and its predecessors, the for loop is designed to iterate a fixed number of times. As such, the control variable within the loop should remain constant, and the expression following the "TO" keyword should also be fixed.

Current Behavior

Currently, the Oberonc compiler flags modifications to the control variable within the loop body as errors. However, it doesn't enforce the immutability of the expression following the "TO" keyword. Consider the following two code snippets:

MODULE ForLoop1;
	IMPORT Out;
	VAR
		n : INTEGER;
BEGIN
	FOR n := 1 TO 5 DO
		DEC(n);
		Out.String("i = ");
		Out.Int(n, 0);
		Out.Ln
	END
END ForLoop1.

This code fails to compile due to the attempt to decrement n, which triggers an error. This behavior is expected and thus considered correct.

However, consider the next code snippet:

MODULE ForLimit;
 IMPORT Out;
 VAR
   i, limit : INTEGER;
BEGIN
 limit := 4;

 FOR i := 1 TO limit + 1 DO
   DEC(limit);
   Out.Int(limit, 0);
   Out.String(", ");
   Out.Int(i, 0);
   Out.Ln
 END;
 Out.String(":");
 Out.Int(limit, 0);
 Out.Ln
END ForLimit.

This code compiles successfully, but produces unexpected output. The expression limit + 1 should only be evaluated once, but in this case, it appears to be reevaluated with each iteration of the loop, resulting in the following incorrect output:

3, 1
2, 2
1, 3
:1

Expected Behavior

The expected behavior would be for the expression limit + 1 to be evaluated only once, producing the following output:

3, 1
2, 2
1, 3
0, 4
-1, 5
:-1

Reproduction Steps

To reproduce this behavior:

  1. Compile the code using the Oberonc compiler.
  2. Observe that the compiler throws an error on the first example.
  3. Observe that the compiler compiles the code without error on the second example, but produces incorrect output.

Additional Information

Interestingly, compiling the same code with obc yields the correct output for the second example. However, it allows the modification of the control variable within the body of the for loop, which is contrary to the expected behavior. oberonc behaves similarly to Java and C.

Problem with reference parameter/typeguard combination

Hi Luca,

The following module compiles but crashes on execution.

MODULE Hard;

    TYPE
        P0 = POINTER TO R0;
        P1 = POINTER TO R1;
        R0 = RECORD x: INTEGER END;
        R1 = RECORD (R0) y: INTEGER END;

    VAR
        p0: P0;
        p1: P1;

    PROCEDURE proc(VAR p1: P1);
    BEGIN p1.y := 4
    END proc;

BEGIN
    NEW(p1);
    p0 := p1;
    proc(p0(P1))
END Hard.

Kevin

VAR parameter passing not working

The following code assigns 0 to j.

MODULE ParTest;

    IMPORT Out;

    VAR j: INTEGER;

    PROCEDURE P(x: INTEGER; VAR y, z: INTEGER);
    BEGIN
        IF x > 0 THEN y := x ELSE z := x END
    END P;

BEGIN
    P(-1, j, j);
    Out.String("j = "); Out.Int(j, 0)  (*j should be -1*)
END ParTest.

Oakwood's Math module

Currently oberonc lacks the Math module. I have implemented a Math module compatible with obnc's using Apache's Common Math library. I use this library because Java Math doesn't offer asinh, acosh, atanh and implementing them myself is erroneous and inefficient, so I use a well known and widely used library. Please consider the possibility of adding them to the stardard distribution of oberonc. Thanks.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.