anglais is a small programming language with a bytecode VM. (see below) It runs in
your browser with the magic of WebAssembly
Output
What is this?
anglais was my first time learning how programming languages that are evaluated using virtual machines are built and how they work.
I started working on this project after the summmer of 2024. The previous 8 months i had been
working on and off on a different programming language, and it was the first time i dipped my toes
into that sort of meta-work. My first language, radion, was evaluated using a tree-walking
interpreter. The project was loosely based on Robert Nystrom's Crafting Interpreters, but while it was an amazing guide and resource, what i both preferred and found most
interesting was experimenting and trying out my own ideas.
When i began writing the code for anglais, i had only a rough idea of what i wanted. Most
of the project was made up along the way. At the beginning, i had decided to have a full parser as
the step before compiling, as it allowed for more complex grammar, and more flexibility. The
"algorithm" i used for the parser was recursive descent. I
wanted to try something new, like an LR-parser, but the lack of good learning resources made me
decide to stick with something i already knew how worked.
> The abstract structure of the program.
anglais runs considerably slower in the browser than on a computer. Running in my browser,
the basel-problem-pi program with 100 000 terms takes on average
~110 ms, while running it on my own computer gives an average of 19.5 ms ± 1.4 ms. With 1 000
000 terms, the browser (WASM) version takes ~850 ms, while running it on my computer takes 165.5
ms ± 2.7 ms.
What's the syntax?
anglais is loosely inspired by several languages, namely go (which it was implemented in), and strikingly so. Some go code should work as anglais code. The main
difference is that go is structured with functions on the top level, while anglais is a scripting language like Python and JavaScript. For
example syntax and (working) example programs in the language, see above.
Strings ("Hello world"), numbers (1, 3.141), and booleans (true, false) are written identical to almost every other language. In anglais, the null/nil value is nil. Functions are called
using parentheses, with arguments seperated by commas, for example sum(1, 2). There is also support for lists. They look identical to
lists in python and JavaScript: with square brackets opening and closing. (e.g. [1, nil, 2, "Hello", true]). Lists can have any size, and any value
can be in a list; lists are very dynamic.
The "print" function in anglais is write. It takes a single
argument, and prints it to the standard output. The following is a simple hello-world program in anglais.
write("Hello world!")
The output from this program would be
Hello world!
Assigning and declaring
In anglais, assigning and declaring are two different things. When you want to update the
value of a variable that already exists, you assign it a new value. When you want to make a
new variable with an initial value, you declare it. Syntax-wise, they are pretty similar,
but with one important difference: declaring uses := instead of = (exactly like in
go).
a := 1
b := 2
a = b + 2
You can redeclare a variable in a new scope (inside braces/blocks, e.g. functions, loops):
a := 10
write(a)
{
# the variable is redeclared
a := 7
write(a)
# and updated
a = 4
write(a)
}
# and the original value is resumed
write(a)
The previous code would produce the output:
10
7
4
10
Loops
The only loop currently implemented is the while-loop. It executes
as long as the condition is true:
while <condition> {
...
}
Using this, a for-like-loop can be made by also declaring a counter i which is incremented each iteration:
i := 0
while i < 100 {
...
i = i + 1
}
Functions
Functions are declared with the func keyword. This is for both
inline functions and function declarations. A function declaration has the same effect as
assigning an inline function to a variable:
func sum(a, b) {
return a + b
}
# or
sum := func(a, b) {
return a + b
}
Functions are first-class citizens. This means that you can pass them as function
parameters, but also assign them to variables, since they are treated as values. If you (for some
reason) want to rename the write function to print, you could
to the following:
print := write
The new print function is the same as write, meaning that it takes the same parameters, yields the same
value, and has the same body.
print := write
# Both of these do the same thing
print("Hello world!")
write("Hello world!")
Importing
Importing is done with an import statement, which has the following form:
import "<file>"
Where the name of the file is inside a string. Importing has the same effect as taking the source
of the library of program and replacing the import statement with it. It is very simple.
This means that there is no way of just importing one or two functions; importing always imports
all available functions. When importing all statements that are not functions are also included.
Standard library
The two functions for outputting are write and print. write appends a newline
behind the output, while print only outputs the string version of
its argument.
Here, in this web-demo, other examples can be imported using the id shown in the dropdown to
import them. The small standard math library is available, and i would recommend going through the
source if you're looking for documentation. For example, if you would like to import the math
library:
import "math"
Some values have properties which are standard functions. Currently, only lists and strings have
these. Lists have list.append(n), which allows you to add an
element to a list. The function returns a new list with the previous elements first, and the
appended element after ([1, 2].append(3) == [1, 2, 3]). This means
that you have to reassign the list when you append a value:
list := [1, 2]
write(list)
# append a value
list = list.append(3)
write(list)
The previous code would produce the following output:
[1, 2]
[1, 2, 3]
Lists also have a list.length() function, which simply returns the
length of the list (the amount of elements). For example:
Lists also have a list.map(fn) and a list.reduce(fn, s) function. These functions take in a function as
their first parameter, and list.reduce(fn, s) takes a starting
value as a second parameter. The list.map(fn) function applies the
function on each element in the list, and returns a new list, same length, but with the return
value from the function instead of each element. For example: