An interpreted language you can try in my terminal website

protium

protium

Posted on November 17, 2022

An interpreted language you can try in my terminal website

A few weeks ago I started reading Writing An Interpreter In Go and
while reading it and implementing the interpreter a question popped up: what if I can run this
interpreter in the terminal in my website?

To recap, a few months ago I decided to transform my personal website into
a terminal (read more about it here).
In today's post I'll give an overview of the whole implementation, but first a preview:

preview

You can try it by typing simia in the terminal here

The book: a great resource for learning about interpreters

book cover

Although this post is not a book review, I'd like to say that it was a really nice reading.
You can learn from the basics of tokenization to the evaluation of the parsed input.
The author does a great job guiding the reader through all the concepts with concrete examples.
If you want to learn about how interpreters and REPL work this book is definitely a great start!

Extending the Monkey language

In the book, the author introduces us to the Monkey language.
Its syntax is quite simple and that motivated me to add more expressions and syntax, essentially extending
the language to my desire. I wanted to create a language that would combine syntax from go, rust, and
elixir. And that's how simia was born.
I'm still working on it but so far I have added support for:

  • Range expressions
  • for-loop expressions along with the infix operator in. e.g.
for a in 1..10 { 
  log(a); 
}
Enter fullscreen mode Exit fullscreen mode
  • Support for boolean condition in for-loop
for a > 0 { 
  log(a); 
}
Enter fullscreen mode Exit fullscreen mode
  • Parentheses are optional for if and for-loop expressions
  • Support for elixir's pipe operator |>
8 |> factorial() |> add(100)
Enter fullscreen mode Exit fullscreen mode
  • Re-assign variables with = operator
let foo = "bar";
foo = "1234";
Enter fullscreen mode Exit fullscreen mode

And in my TODO list:

  • Support modules
  • Support pub key
  • Add more built-ins for arrays, strings, and maps
  • Support mut key and control mutability

Overall, I want simia to be some sort of modern functional programming language.

Loading the interpreter in the browser: WebAssembly

If you are not familiar with wasm, here a quote from https://webassembly.org/

WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine.
Wasm is designed as a portable compilation target for programming languages, enabling deployment on
the web for client and server applications.

Basically it allows us to convert the simia interpreter (written with golang) into a set of instructions that the browser
can understand and execute.

Doing this in golang was really simple thanks to the syscall/js
package and the arch wasm when compiling the interpreter.
Let's see a simple example:

// main.go

package main

import (
    "syscall/js"
)

func main() {
    done := make(chan struct{}, 0)
    js.Global().Set("wasmHash", js.FuncOf(hash))
    <-done
}

func hello(this js.Value, args []js.Value) any {
    return "hello " + args[0].String()
}
Enter fullscreen mode Exit fullscreen mode

Notice the channel we keep waiting for input. This is necessary because the wasm module it is an application
and not a library. So this application should keep running and not exit as soon as js.Global()... is executed.

Now we compile the app to wasm:

GOOS=js GOARCH=wasm go build -o hello.wasm ./main.go 
Enter fullscreen mode Exit fullscreen mode

In order to load our wasm we need to use the wasm tools go provides

cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" . 
Enter fullscreen mode Exit fullscreen mode

and now the final bit, we load the wasm from js and use our wonderful hello function:

<html>
<head>
    <meta charset="utf-8"/>
    <script src="wasm_exec.js"></script>
    <script>
        const go = new Go();
        WebAssembly.instantiateStreaming(fetch("hello.wasm"), go.importObject).then((result) => {
            go.run(result.instance);
            console.log(window.hello("universe")); // hello universe
        });
    </script>
</head>
<body>
</body>
Enter fullscreen mode Exit fullscreen mode

And that's it, serving the file and opening it in a browser should show the result in the developer console.
I took exactly the same steps to compile and use the simia interpreter in the browser.
The wasm module exports to objects:

  • simia: a function that evaluates simia code and returns the result
  • simia_version: the version of the interpreter

Caveats

All was going beautifully but I had to step back to think about a problem: simia has a built-in function
called log which prints values to the stdout using the fmt package.
When compiled to wasm, the fmt.Println will use console.log as a buffer, so any log instruction will be print
into the browser console.

The solution is to modify the built-in to use a buffer and then pass some sort of buffer from javascript,
so then I can use that buffer to print to the terminal element. I will probably will solve this during the
weekend, but if you have a better idea please let me know in the comments.

Extending my website terminal

When I came up with the idea of adding the REPL into my website, I took a look at the existing code in my website
and identified the requirements needed to make this happen:

  • The wasm module can be loaded on demand, i.e. the first time the user calls simia from the terminal
  • Commands should be able take the std in and out.
  • The REPL will use the wasm module to eval the user input
  • ctrl-c should "kill" the process and return to the shell

In other to implement this requirements I have prepared a few refactors:

  • Improve error handling, e.g. "command missing", "wrong arguments", etc
  • Improve key presses handling and extract common functionality
  • Introduce processId. When a command execution returns a process Id it means that the main function, let's call it shell, should stop processing the input. Once the command exits it should call a callback to restore the main function.

That's it, I can say that now my terminal has a fully working simia REPL.

Outro

I had a lot of fun writing the interpreter and loading it into my terminal website, I definitely learned
a lot of things in the process. I think writing an interpreter is quite a good practice for any developer.

All the changes I made to my website were introduced in this PR

What else would you add to the language or the terminal?
I'm planning to do some sort of treasure hunt in my website, stay tuned!

👽

💖 💪 🙅 🚩
protium
protium

Posted on November 17, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related