Going “Retro” and Loving It!

Continuing my concatenative language kick, I’ve been having fun playing with Retro (https://www.retroforth.com) and couldn’t resist making an F#-based VM on which to run it. It is an elegant, minimal Forth with an important twist. What caught my eye is that it supports quotations and combinators much like Joy, Cat, Factor, ... I may have to add this to TransForth, and I may have to do a follow up post to “Programming is Pointless” showing off the beauty of quotations and combinators in a pure composition-based world.

Stop Thinking About the Stack

Quotations allow you to push anonymous functions to the stack and combinators are words taking functions as input. Here’s just one example (in Factor, taken from Aaron Schaefer’s blog) of how they change the flavor of your code. To find the average of a sequence of numbers we could:
 

{ 1 2 3 } dup sum swap length / .
 

Push a list of numbers, dup the list so that we can sum one copy, then swap and get the length of the other. Finally divide the sum by the length and display the result. This works fine, but with stack shuffling words like dup and swap, you have to keep the stack in mind. I would rather put that out of my mind and more directly express the intent:
 

{ 1 2 3 } [ sum ] [ length ] bi / .

The square brackets delimit quotations, treated as data. Here they contain single words but could just as easily contain longer sequences; something like anonymous functions. The bi word then consumes the list of numbers and the two quotations, applying each quotation to the list, leaving the sum and length to be consumed by / . This isn’t necessarily more concise, but it is certainly more direct without any stack juggling.

The Ngaro VM (in F#)

Retro runs on a tiny VM called Ngaro which has been ported to a growing list of languages and platforms – C, Python, Ruby, ANS Forth, C#, Common Lisp, Go, Java, Lua, Perl, Javascript, .... Missing from the list was F# (and as for .NET in general, the C# version was incomplete). I went ahead and made my own implementation (below) and submitted it back to the project. I like that for concision, the only implementation that beats this F# one is the one in Lisp – only slightly and it’s lacking file I/O features :-)

To use it, just grab the latest retroImage and drop it in the same directory. Like any respectable Forth, Retro is written in Retro of course and can compile itself. You can grab the source (core.rx) in the latest release, make tweaks and run the VM below “--with core.rx” to compile a new image. There are lots of interesting samples and libraries as well. Have fun!
 

open System

open System.IO

open System.Text

let MEM_SIZE = 1024 * 1024

let IMAGE_FILE = "retroImage"

let MAX_OPEN_FILES = 8

let PORTS = 12

letmutable ip = 0

letmutable ports = Array.create PORTS 0

letmutable inputs = Array.create PORTS ""

letmutable memory = Array.create MEM_SIZE 0

letmutable (files : FileStream[]) = Array.create MAX_OPEN_FILES null

letmutable isp = 0

letmutable offset = 0

letmutable shrink = false

letmutable halt = false

let data = ref []

let address = ref []

let pushVal d x = d := x :: !d

let popVal d () = match !d with h :: t -> d := t; h | _ -> failwith "Underflow"

let push = pushVal data

let pushr = pushVal address

let pop = popVal data

let popr = popVal address

let tos () = (!data).Head

let load() =

    use binReader = new BinaryReader(File.Open(IMAGE_FILE, FileMode.Open))

    for i in 0 .. int (binReader.BaseStream.Length / 4L) - 1 do

        memory.[i] <- binReader.ReadInt32()

let saveImage () =

    let j = if shrink then memory.[3] else MEM_SIZE

    use binWriter = new BinaryWriter(File.Open(IMAGE_FILE, FileMode.Create))

    Array.iter (fun (c : int) -> binWriter.Write(c)) memory.[0..j - 1]

let key () =

    if isp > 0 && offset = inputs.[isp].Length - 1 then// Next input source?

        isp <- isp - 1

        offset <- 0

    if isp > 0 then// Read from a file

        offset <- offset + 1

        int inputs.[isp].[offset]

    else// Read from Console

        let cki = Console.ReadKey(true)

        if cki.Key = ConsoleKey.Backspace then printf "\b "

        int cki.KeyChar

let devices () =

    let getString () =

        let s = pop ()

        let e = Array.FindIndex(memory, s, fun c -> c = 0)

        new String(Array.map char memory.[s .. e - 1])

    let openFile () =

        let handle = Array.findIndex ((=) null) files |> ((+) 1)

        let mode, name = pop (), getString ()

        try

            match mode with

            | 0 -> files.[handle] <- File.Open(name, FileMode.Open)

            | 1 -> files.[handle] <- File.Open(name, FileMode.OpenOrCreate)

       | 2 -> files.[handle] <- File.Open(name, FileMode.Append)

            | 3 ->let f = File.Open(name, FileMode.Open)

                   f.Seek(0L, SeekOrigin.End) |> ignore

                   files.[handle] <- f

            | _ -> failwith "Invalid mode"

            handle

        with _ -> 0

    let readFile h =

        let c = files.[h].ReadByte()

        if c = -1 then 0 else c

    let writeFile h = pop () |> byte |> files.[h].WriteByte; 1

    let closeFile h =

        let f = files.[h]

        if f <> nullthen

            f.Close()

f.Dispose()

            files.[h] <- null

        0

let getFilePos h = int files.[h].Position

    let setFilePos h = files.[h].Seek(pop () |> int64, SeekOrigin.Begin) |> int

    let getFileSize handle = files.[handle].Length |> int

    let deleteFile name = if File.Exists name then File.Delete name; -1 else 0

    if ports.[0] <> 1 then

        ports.[0] <- 1

        if ports.[1] = 1 then ports.[1] <- key () // Read from input source

        if ports.[2] = 1 then

            let x = pop ()

            if x < 0 then Console.Clear()

            else Console.Write(char x)

            ports.[2] <- 0

        if ports.[3] = 1 then ports.[3] <- 0 // Video update

        match ports.[4] with

        | 1 -> saveImage () ; ports.[4] <- 0 // Save Image

        | 2 ->// Add to Input Stack

            isp <- isp + 1

            inputs.[isp] <- System.IO.File.ReadAllText(getString ())

            ports.[4] <- 0

        | -1 -> ports.[4] <- openFile ()

       | -2 -> ports.[4] <- pop () |> readFile

        | -3 -> ports.[4] <- pop () |> writeFile

        | -4 -> ports.[4] <- pop () |> closeFile

| -5 -> ports.[4] <- pop () |> getFilePos

        | -6 -> ports.[4] <- pop () |> setFilePos

| -7 -> ports.[4] <- pop () |> getFileSize

        | -8 -> ports.[4] <- getString () |> deleteFile

        | _ -> ports.[4] <- 0

        ports.[5] <- match ports.[5] with// Capabilities

            | -1 -> MEM_SIZE

            | -5 -> (!data).Length // stack depth

            | -6 -> (!address).Length // address stack depth

            | -8 -> int (DateTime.UtcNow - new DateTime(1970,1,1)).TotalSeconds

            | -9 -> halt <- true; 0

            | -10 ->// Query for environment variable

                let var = getString ()

                let name = ref (pop ())

                Array.iter (fun element ->

                    memory.[name.Value] <- int element

                    name := name.Value + 1)

                    (var |> Environment.GetEnvironmentVariable

                         |> Encoding.ASCII.GetBytes)

                0

            | -11 -> Console.WindowWidth

            | -12 -> Console.WindowHeight

            | _ -> 0

letrec exec () =

    let dyadic fn = let x = pop () in fn (pop ()) x |> push

    let dyadic2 fn = let x, y = fn (pop ()) (pop ()) in push y; push x

    let incIp () = ip <- ip + 1

    let condJump fn =

        let x = pop ()

        if fn (pop ()) x then ip <- memory.[ip + 1] - 1 else incIp ()

    let jump () =

        ip <- memory.[ip] - 1

        if memory.[ip + 1] = 0 then incIp ()

        if memory.[ip + 1] = 0 then incIp ()

    let drop () = pop () |> ignore

    let loop () =

        pop () - 1 |> push

        if tos () > 0 then ip <- memory.[ip + 1] - 1

        else incIp (); drop ()

    if not halt then

        match memory.[ip] with

            | 0 -> () // NOP

            | 1 -> incIp (); memory.[ip] |> push // LIT

            | 2 -> tos () |> push // DUP

    | 3 -> drop () // DROP

            | 4 -> dyadic2 (fun x y -> y, x) // SWAP

            | 5 -> pop () |> pushr // PUSH

            | 6 -> popr () |> push // POP

            | 7 -> loop () // LOOP

            | 8 -> incIp (); jump () // JUMP

            | 9 -> ip <- popr () // RETURN

            | 10 -> condJump (>) // GT_JUMP

            | 11 -> condJump (<) // LT_JUMP

            | 12 -> condJump (<>) // NE_JUMP

            | 13 -> condJump (=) // EQ_JUMP

            | 14 -> memory.[pop ()] |> push // FETCH

            | 15 -> memory.[pop ()] <- pop () // STORE

            | 16 -> dyadic (+) // ADD

            | 17 -> dyadic (-) // SUB

            | 18 -> dyadic (*) // MUL

| 19 -> dyadic2 (fun x y -> y / x, y % x) // DIVMOD

| 20 -> dyadic (&&&) // AND

            | 21 -> dyadic (|||) // OR

            | 22 -> dyadic (^^^) // XOR

            | 23 -> dyadic (<<<) // SHL

            | 24 -> dyadic (>>>) // SHR

            | 25 ->if tos () = 0 then drop (); ip <- popr () // ZERO_EXIT

            | 26 -> pop () + 1 |> push // INC

            | 27 -> pop () - 1 |> push // DEC

            | 28 ->let x = pop () in ports.[x] |> push; ports.[x] <- 0 // IN

            | 29 -> ports.[pop ()] <- pop () // OUT

            | 30 -> devices () // WAIT

            | _ -> pushr ip; jump ()

        ip <- ip + 1

        exec ()

let args = Environment.GetCommandLineArgs()

Array.iteri

    (fun i arg ->

        match arg with

        | "--shrink"-> shrink <- true

        | "--about"-> printfn "Retro Language [VM: F#, .NET]"

        | "--with"->

            isp <- isp + 1

            inputs.[isp] <- File.ReadAllText(args.[i + 1])

        | _ -> ()

    ) args

load ()

exec ()