lboasso / oberonc

An Oberon-07 compiler for the JVM
MIT License
146 stars 17 forks source link

Variable Declared in Parent Procedure Inaccessible from Inner Procedure #27

Closed geekstakulus closed 11 months ago

geekstakulus commented 12 months ago

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.

johnperry-math commented 12 months ago

FWIW

Perhaps I misread this 2018 thread at ETH, or perhaps there has been more dicsussion since then, but this message in particular indicates that the desirable behavior is that

access to any kind of object from an intermediate scope is not allowed in Oberon-07/16.

EchedelleLR commented 12 months ago

I guess the point is being similar to PHP and pass every variable as explicit parameter to the internal procedure.

geekstakulus commented 12 months ago

FWIW

Perhaps I misread this 2018 thread at ETH, or perhaps there has been more dicsussion since then, but this message in particular indicates that the desirable behavior is that

access to any kind of object from an intermediate scope is not allowed in Oberon-07/16.

I guess this depends on what he smoke the night before answering this. In the report it says nothing about it, and to be honest it doesn't make any sense. I would understand invalidating local type declarations, but variables and constants?

Wirth just keeps crippling the languages. I guess his focus is academia, creating toy languages for teaching. Oberon-2 and Component Pascal are by far the best iterations of Oberon, in my opinion.

The reports says nothing about, so I guess, as some have stated in the discussion, it is up to the compiler implementer to decide on this. As usual, all Wirth's reports are too vague and open to lots of interpretations. He does, however, make clear in Programming in Oberon that this is valid. That's the only place where he actually states the scope rules.

So, we have to deal with this crippled feature, and accept the fact that you can use types and constants. No wonder all his languages have thousands of dialects. Couldn't he just learn from C? It kept the same for years and the difference in compilers, if any, was minimal. Anyway...

geekstakulus commented 12 months ago

I guess the point is being similar to PHP and pass every variable as explicit parameter to the internal procedure.

I haven't programmed in PHP for a long time, but wasn't there a use function that allows to "inherit" it inside the function?

EchedelleLR commented 12 months ago

use is for classes or closure format.

Traditionally, you just add a parameter.

lboasso commented 12 months ago

I agree, this behavior is not well specified in the official language report. I decided to follow the same behavior of the original Oberon-07 RISC compiler when I implemented oberonc. The reasoning behind Wirth's design choice was explained in On Programming Styles. I think it makes sense to restrict access to strictly local and to strictly global objects only, it follows the minimalist spirit of Oberon. An alternative design would be to provide full first class functions (i.e. closures), a nice paper discussing the difference and comparing some languages (including Modula-2) is First-class functions in an imperative world.

geekstakulus commented 11 months ago

I agree, this behavior is not well specified in the official language report. I decided to follow the same behavior of the original Oberon-07 RISC compiler when I implemented oberonc. The reasoning behind Wirth's design choice was explained in On Programming Styles. I think it makes sense to restrict access to strictly local and to strictly global objects only, it follows the minimalist spirit of Oberon. An alternative design would be to provide full first class functions (i.e. closures), a nice paper discussing the difference and comparing some languages (including Modula-2) is First-class functions in an imperative world.

Hi, @lboasso !

Thanks for the pointers. I agree with you. To me, it doesn't really make much of a difference, as I know there is an alternative, but it is weird. Specially when you are accustomed to this in other of his languages.

I guess we can close this issue and leave it as it is.

geekstakulus commented 11 months ago

I read the papers, and I believe I had read his paper before. I agree that using global variables and nesting procedures is a bad thing. In fact, I have never done it, and never saw a need for them when I used to program in Pascal. However, studying some of his early compilers, particularly as a way of bypassing the limitation of a one-pass compiler, one has to follow one of three paths:

1) Nest procedures, particularly if there is circular dependency as in expression, term and factor 2) Create a procedure variable, a procedure with a similar name as the variable name so that you can later assign the procedure to this variable 3) Forward procedure declaration

I think 1) and 3) are the more elegant solutions, and in your compiler you opted for the third option. But, for option 2), if we think about it, are there other functions that call term and factor other than expression? Aren't they part of expression? So, nesting sort of makes sense as well, because you only need expression. Everything else is part of expression. Anyhow, I have always found nesting a bad practice. But, closures are a different thing altogether, and I do use them quite often in Go, in a functional programming style.

The V programming language forbids, by default, global variables. Specially because it can cause some problems. You can use them through an option, but by default they are not allowed, and it is a good thing as it spares you from a lot of trouble. Unless, of course, you are using it as part of a module when creating an abstract data structure, as opposed to abstract data type. Oberon-7 introduced the concept of read-only export variables, something that you can optionally do it in Component Pascal with the "-" appended to the name of the variable and/or field.

I am porting his early one-pass Modula-2 compiler to Oberon-7, and this is where I am encountering all of these problems. In fact, I just found a bug in the Number function, whereas integers allow letters beyond "F", as in hexadecimal. So, a number such as "0AZH", is valid in his Modula-2 compiler and renders the value 160, which is correct, because it subtracts 48 from the ordinal value of the letter, and if the letter is greater than 22, it subtracts 7 from it, meaning that the hexadecimals in his original single-pass Modula-2 compiler include all the letters of the alphabet, and this hasn't been fixed in all the derivative works of that particular compiler, particularly the MacMeth one, which is the one I am using as a basis to convert to Oberon.

Another thing I had never realized until I began to unit test his code, which forced me to look into the grammar, is that "10." is a valid real number in all of his languages, and that includes Oberon. And guess what? I have just found another bug in your compiler, because it reports this number as an error, when according to the grammar it should be a valid real number. So should "10.E+32", etc... the digit after the "dot" is optional. I will report it as well.

Thanks once again!

Best Regards,

Fidel H Viegas

geekstakulus commented 11 months ago

Sorry! The compiler interprets real numbers correctly. In the code I was using, I had an integer being multiplied to a real number. False alarm!

lboasso commented 11 months ago

I am porting his early one-pass Modula-2 compiler to Oberon-7, and this is where I am encountering all of these problems.

That is great, I would be interested to see the final code! If you find more bugs in oberonc please file new GitHub issues. I'll be happy to fix them.

geekstakulus commented 11 months ago

I am porting his early one-pass Modula-2 compiler to Oberon-7, and this is where I am encountering all of these problems.

That is great, I would be interested to see the final code! If you find more bugs in oberonc please file new GitHub issues. I'll be happy to fix them.

The conversion is nearly one to one. The code is not much different from your compiler. In fact, I think they reused the code of this compiler for the Oberon one. It is quite identical. But, yes, I will release it after I am done. I will, in fact, use your JVM code generator, as I will be porting it to the JVM. Initially, I had modified your code for Modula-2. The scanner was complete, except for the Numbers, that I had to work on, and it is what I am fiddling with right now. They wrote some really long code that could have been simplified. It is almost twice the size of your Number function in the scanner module.

In the loop where it scans for digits, it doesn't stop when it finds a digit or a letter up to "F". It stops only when it finds a digit and a letter up to "Z". Worse yet, both upper and lower case. So, I fixed that condition, and allowed only for capital letters, because that's what the specs say. But, now that no longer recognizes hexadecimals (H), long integers (D), octal character (C) and octal decimal (B). If I put the "Z" back on the condition, it all works again, but with the bug that it accepts numbers such as 0AZH, 0ZJH, etc.... I have tracked down the area where the bug is, but I am now debugging it.

Thanks once again!

PS: I found another error, where the compiler throws an exception in the ClassFormat.Mod module. But I need to investigate it further, because I think it should just report just the error and not the exception.

lboasso commented 11 months ago

The code is not much different from your compiler. In fact, I think they reused the code of this compiler for the Oberon one. It is quite identical.

My scanner + parser borrowed heavily from Wirth's original RISC compiler.

I have tracked down the area where the bug is, but I am now debugging it.

Good luck :)

PS: I found another error, where the compiler throws an exception in the ClassFormat.Mod module. But I need to investigate it further, because I think it should just report just the error and not the exception.

Any exception thrown by oberonc is a bug. Please open a new issue and I'll fix it.

geekstakulus commented 11 months ago

Any exception thrown by oberonc is a bug. Please open a new issue and I'll fix it.

OK, I will do so.

I noticed that your code inherits from Wirth's original code by looking at source code of both compilers