A cursed Modula 2 compiler
Cleanup readme a little
Ok I'm a little done rn
Ok that was easy to sort out

heads

tip
browse log

clone

read-only
https://hg.sr.ht/~icefox/even-or-odd-p
read/write
ssh://hg@hg.sr.ht/~icefox/even-or-odd-p

#even-or-odd-p

A cursed modula-2 compiler.

It's modula 2, with a different syntax. Because modula-2 is surprisingly good, and the syntax and naming is just tedious. So the goal is to be exactly Modula-2 in terms of semantics, even the bits I don't particularly care for, with a syntax that is a little less bleh. (Well, I might leave out some of the multiprocessing bits and such.) I'm going to play around with various syntax ideas that have been bouncing around in my head anyway to see how they work in practice. I expect the result to be the most success-immune language of all time.

Name: because it's modulo 2! But lispy! HAH. Thanks, MBones.

#Building

Install Dotnet Core on your machine per instructions from https://fsharp.org/ . For Debian this is at https://docs.microsoft.com/en-us/dotnet/core/install/linux-debian. Dotnet Core should come with F# itself.

Then you should be able to just do:

dotnet build
dotnet run programs/fib.m2

Other useful commands:

dotnet test
dotnet clean

#Status

Probably doesn't work.

The syntax is more or less done, which is the fun part anyway, I am just like halfway through the parser.

#Examples

Let's rewrite some stuff from the book to play with the ideas. Examples from chapter 6.2 of the book Programming In Modula-2 (4th ed).

MODULE Oscillation;
  FROM InOut IMPORT ReadInt, WriteString, WriteLn;
  FROM RealinOut IMPORT ReadReal, WriteReal;
  FROM MathLib0 IMPORT exp, cos;
  CONST dx = 0.19634953; (*pi/16*)
  VAR i, n: INTEGER;
    x, y, r: REAL;
BEGIN
  WriteString(" n = "); Readlnt(n);
  WriteString(" r = "); ReadReal(r); WriteLn;
  i := 0; x := 0.0;
  REPEAT x := x + dx; i := i+1;
    Y := exp ( -r*x) * cos (x);
    WriteReal(x, 15); WriteReal(y, 15); WriteLn
  UNTIL i >= n
END Oscillation.
mod Oscillation
  use InOut.ReadInt, WriteString, WriteLn
  use RealinOut.ReadReal, WriteReal
  use MathLib0.exp, cos

consts
  dx = 0.19634953  ; pi/16
vars
  ; Var initialization semantically happens at the beginning of
  ; the `do` block?  That might be more rewriting than I want.
  i: int = 0
  n: int = 0
  x: float = 0.0
  y: float = 0.0
  r: float = 0.0

do
   (WriteString " n = ") (ReadInt n)
   (WriteString " r = ") (ReadReal r) (WriteLn)
   repeat
     x <- (+ x dx)
     i <- (+ i 1)
     y <- (* (exp (* (- r) x)) (cos x))
     (WriteReal x 15) (WriteReal y 15) (WriteLn)
   until (>= i n)

Thoughts: += operator? <- or = for assignment? = or == for equals? repeat ... until loop becomes do ... while, inverting the sense of the test? do ... end block syntax change?

We are NOT changing:

  • Block structure
  • Function names (though builtin's may be lowercased or altered a little?)
  • Anything that can't be trivially translated to/from stock Modula-2

Let's iterate slightly

MODULE Power;
 FROM InOut IMPORT Readlnt. WriteString, WriteLn;
 FROM RealinOut IMPORT ReadReal, Done, WriteReal;
 VAR i: INTEGER; x, z: REAL;
BEGIN
 WriteString( "x = "); ReadReal (x);
 WHILE Done DO
   WriteString(" t i = "); ReadInt(i);
   z := 1.0;
   WHILE i > 0 DO
     (* z * x^i = x0^i0 *)
     IF ODD(i) THEN z := z*x END;
     x := X*X; i := i DIV 2
   END;
   WriteReal(z,16); WriteLn;
   WriteString("x = "); ReadReal(x)
 END;
 WriteLn
END Power.
mod Power
  use InOut.ReadInt, WriteString, WriteLn
  use RealinOut.ReadReal, Done, WriteReal
vars
  i: int = 0
  x, z: float = 0.0

do
  (WriteString "x = ") (ReadReal x)
  while (Done)
    (WriteString " ^ i = ") (ReadInt)
    z <- 0.0
    while (> i 0)
      ; z * x^i = x0^i0
      if (odd i) z <- z * x end
      x <- x * x
      i <- (div i 2)
    end
    (WriteReal z 16) (WriteLn)
    (WriteString "x = ") (ReadReal x)
  end
  (WriteLn)

Thoughts:

  • I kinda like the mod Name ... is ...
  • // instead of div?

From chapter 14 of the Book:

MODULE Permute;
  FROM InOut IMPORT Read, Write, WriteLn;

  VAR n: INTEGER; ch: CHAR;
    a: ARRAY [1 .. 20] OF CHAR;

  PROCEDURE output;
    VAR i: INTEGER;
  BEGIN
    FOR i := 1 TO n DO Write(a[I]) END;
    WrlteLn
  END output;

  PROCEDURE permute(k: INTEGER);
    VAR i: INTEGER; t: CHAR;
  BEGIN
    IF k = 1 THEN output
    ELSE permute(k-1);
      FOR i := 1 TO k-1 DO
        t := a [i] ; a [i] := a [k] ; a [ k] := t;
        permute(k-1) ;
        t := a (i]; a (i] := a [k]; a [k] := t
      END
    END
  END permute;

  BEGIN Write(")"); n := 0; Read(ch);
    WHILE ch > " " DO
      n := n+1; a[n1 := ch; Write(ch); Read(ch)
    END;
  WriteLn; permute(n)
END Permute.
mod Permute
  use InOut.Read, Write, WriteLn
vars
  n: int = 0
  ch: char = '\0'
  a: char[1..20] = TODO

fn output(a: char[])
  vars i: int = 0
  for i = 1 to n do
    (Write a[i])
  end
  (WriteLn)
end

fn swap(a: char[], i: int, k: int)
  vars tmp: char = 0
  tmp <- a[i]
  a[i] <- a[k]
  a[k] <- tmp
end

fn permute(k: int)
  vars i: int = 0
  if (= k 1)
    (output)
  else
    (permute (- k 1))
    for i = 1 to (- k 1) do
      (swap a i k)
      (permute (- k 1))
      (swap a i k)
    end
  end
end

do
  (write "Enter chars to permute, end with space.  >")
  (read ch)
  while (/= ch " ")
    n <-+ 1
    a[n] <- ch
    (Read ch)
  end
  (WriteLn)
  (permute n)

Hm, can we avoid needing to declare our for loop vars explicitly? Yeah you know what I heckin' think so.

Downside of <- for assignment is that <-+ and <-- and <-* and such look weird. <+ maybe? Oops that would make minus <- so that doesn't work. Idk, try them out and see how they feel; it's a little cute.

Feedback: fn (swap a:char[] i:int k:int) instead of fn swap(...) makes declarations more consistent with calls. On a line with a function call you can drop the first and last parens and still be unambiguous. Also using sexprs for expressions alone apparently triggers a bigger flame war than using them for your whole language. People, amirite?

mod Fib
  ; Playin' with import syntax.  I like the rust approach 'cause
  ; you don't need to reorder keywords to change `import Foo` to
  ; `from Foo import A, B, C`
  use InOut.ReadInt, WriteInt, Write, WriteLn
  ; Maybe something like:
  ; use InOut.(ReadInt, WriteInt, Write, WriteLn)
  ; ...nah, I'll give the parens a miss.
vars
  n: int = 0

; I do like the `(fib i: int)` fn decl syntax for this.
; I could honestly remove the `fn` but let's keep a
; *little* tribute to the decl-heavy style.
;
; What do I do for return types?  -> for now I suppose.
fn (fib i: int) -> int
  if (<= i 2)
    1
  else
    (+ (fib (- i 1)) (fib (- i 2)))
  end
end

do
  (Write "Enter number to calculate the fib for >")
  (ReadInt n)
  n <- (fib n)
  (WriteInt n)
  (WriteLn)

#Ideas

What should the backend be? Really I was only actually interested in a new syntax to see what it looks like. Options to consider:

  • Modula-2 (Actually kinda tempting but I'd need to find a compiler for it. GNU GM2 is apparently a thing that exists?)
  • F#/C#/CIL (native platform)
  • Rust (hilarious and easy)
  • Common Lisp (to ensure it's 100% success-proof)
  • Cranelift? (to learn how it works)

Obfusticated Modula-2 (more work, but funnier) -- Very tempting actually. Stuff everything into one module, rename everything to lI1ll1l, turn all math into bitsets operations... Oh, there's a whole list: https://tigress.wtf/transformations.html

Rejected options:

  • C++ (the meme isn't worth the frustration)
  • Brainfuck (too much work)
  • LLVM (too useful)
  • QBE (too annoying/undocumented)

#License

Apache-2.0