Here’s Another Toy Language

Elliot Chance
4 min readDec 7, 2022

--

I have spent years researching, learning and honing my skills as a language designer to build the many statically-typed, virtual machine powered complex compiled languages. Each with their own expertly written parsers, based on rigorous, well thought out syntax that is immaculately documented to produce the finest programming languages that can be found anywhere…

But sometimes. Just sometimes. I like to throw caution to the wind as I cobble together fragments of late night delirium, mashing together the horribly incompatible ideas of languages long gone with poorly understood foundations that is my programming language segment…

Meanwhile…

Yeah. I did it again. This time I wanted to do something different. Lately, I’ve been thinking about the question: “Can’t you make an ultra simple language that is also useful enough as a general purpose language?”

There are, of course, super simple languages out there such as Brainfuck that take this premise to the extreme. While this is interesting, it’s certainly not useful as a general purpose language, so let’s set some ground rules:

  1. All in one file. A single script that contains the interpreter and library (built in functions). I’ll be using vanilla JavaScript, so all we need to run is a single node command to run a program from a file.
  2. The parser needs to be completely unambiguous. That means that a program of any complexity can be read one token at a time without ever needing to do backtracking or lookahead. This should result is a very fast and simple parser, but I actually don’t care about that because…
  3. We don’t care about execution speed. The goal is to create a simple interpreter and not get bogged down in optimizations.
  4. It has to have all the general purpose stuff. Variables, functions, loops, arrays, closures, etc.

So after a few furious hours how did I do? Well…

🧸 Hello, Toy!

At the time of writing this, toy.js clocks in at less then 400 lines of vanilla JavaScript. That contains everything. It’s worth mentioning that’s not overly clever fifty-things-per-line kind of code.

You can find a bunch of examples in the repository, but here’s the proverbial “Hello, World!” program:

[printLine: "Hello, Toy!"]

// Hello, Toy!

And here is a more complex example using closures:

[intSeq] = {
i = 0
[nextInt] = {
i = (i + 1)
return i
}
return @[nextInt]
}

[nextInt] = [intSeq]

[printLine: [nextInt]]
[printLine: [nextInt]]
[printLine: [nextInt]]

[newInts] = [intSeq]
[printLine: [newInts]]

// 1
// 2
// 3
// 1

General Language Rules

  1. No semicolons. We don’t need them because of the fact the parser is unambiguous. Whitespace is ignored.
  2. Variables are discovered in parent scopes automatically, but created in the inner-most scope.
  3. Traditional function definitions do not exist. Instead, variables are assigned a block that can be called.
  4. Function calls are in the form of [foo:bar baz:qux ...], these can be safely nested. Types are resolved at runtime, so we can overload the same function with different types, such as [printLine:String] vs [printLine:Number].
  5. We’ll support three types: Boolean (true/false), String ("using double quotes" ) and Number (12.34).
  6. Binary expressions need to be wrapped in parenthesis, like (a + b). More complex expressions need to be nested: (a + (b + c)).
  7. I rely heavily on function calls instead of syntax. This means stuff like accessing or setting array elements are done using function calls. For example [on:s set:"b" at:1] instead of s[1] = "b.

More Examples

Functions

[plus:Number b:Number] = {
return (plus + b)
}

[plusPlus:Number b:Number c:Number] = {
return (plusPlus + (b + c))
}

res = [plus:1 b:2]
[print:"1+2 = "]
[printLine:res]

res = [plusPlus:1 b:2 c:3]
[print:"1+2+3 = "]
[printLine:res]

// 1+2 = 3
// 1+2+3 = 6

Loops

i = 1
while (i < 4) {
[printLine: i]
i = (i + 1)
}

while true {
[printLine:"loop"]
break
}

// 1
// 2
// 3
// loop

Arrays

s = [arrayOf:String size:3]
[on:s set:"a" at:0]
[on:s set:"b" at:1]
[on:s set:"c" at:2]

s = [append:"d" onto:s]

[printLine: [len:s]]
[printLine: [on:s at:2]]
[printLine: s]

// 4
// c
// ["a","b","c","d"]

Reading Files

data = [readFile:"toy.js"]
[printLine: data]

f = [openFile:"toy.js"]
b1 = [readBytes:5 from:f]
[printLine: b1]

// const fs = require("fs"); ...
// const

Final Thoughts

I enjoyed this mini-project. It came out better than I expected with less code than I expected.

Also, after playing with the syntax a bit, I kind of like it… Anyway, I’m off to bed now.

Happy coding!

--

--

Elliot Chance

I’m a data nerd and TDD enthusiast originally from Sydney. Currently working for Uber in New York. My thoughts here are my own. 🤓 elliotchance@gmail.com