MonkeyLang
Monkey Programming Language written in Julia.
Table of Contents
- MonkeyLang
- Using Monkey in Julia
- Compile MonkeyLang.jl to a standalone executable
- Start the REPL
- Documentation
- Summary
- Syntax overview
- Variable bindings
- Scopes
- Literals
- Built-in Functions
type(<arg1>): STRING
puts(<arg1>, <arg2>, ...): NULL
len(<arg>): INTEGER
first(<arg: STRING>): STRING | NULL
first(<arg: Array>): any
last(<arg: String>): STRING | NULL
last(<arg: Array>): any
rest(<arg: STRING>): STRING | NULL
rest(<arg: ARRAY>): ARRAY | NULL
push(<arg1: ARRAY>, <arg2>): ARRAY
push(<arg1: HASH>, <arg2>, <arg3>): HASH
- Advanced examples
- Macro System
Using Monkey in Julia
You can start the REPL within Julia:
using MonkeyLang
start_repl()
Or you can evaluate Monkey programs using string macros:
using MonkeyLang
a = 2
monkey_eval"let b = $a; puts(b)"
monkey_vm"let c = [$a, $a]; puts(c)"
monkey_julia"let d = {$a: $a}; puts(d)"
Compile MonkeyLang.jl to a standalone executable
Clone the repo, and run make build
in the root directory.
Caution: The compilation may take up to ~5 minutes.
Start the REPL
You can start the REPL in a Julia script or in the Julia REPL:
import Pkg; Pkg.add("MonkeyLang")
using MonkeyLang
MonkeyLang.start_repl()
MonkeyLang.start_repl(; use_vm = true) # use VM
You can press Ctrl-C
or Ctrl-D
to exit the REPL.
If you have compiled MonkeyLang.jl
locally, then you can directly start the REPL by:
./monkey repl
./monkey repl --vm # use VM
Documentation
I created the document with reference to Writing An Interpreter In Go and rs-monkey-lang.
Summary
- C-like syntax
- variable bindings
- first-class and higher-order functions • closures
- arithmetic expressions
- built-in functions
Syntax overview
An example of Fibonacci function.
let fibonacci = fn(x) {
if (x == 0) {
0;
} else {
if (x == 1) {
1;
} else {
fibonacci(x - 1) + fibonacci(x - 2);
}
}
};
fibonacci(10);
If
It supports the general if
. else
exists, but else if
does not exist.
if (true) {
10;
} else {
5;
}
While
It also supports while
loops.
let x = 5;
while (x > 0) {
puts(x);
x = x - 1;
}
Operators
It supports the general operations.
1 + 2 + (3 * 4) - (10 / 5);
!true;
!false;
+10;
-5;
"Hello" + " " + "World";
Return
It returns the value immediately. No further processing will be executed.
if (true) {
return;
}
let identity = fn(x) {
return x;
};
identity("Monkey");
Variable bindings
Variable bindings, such as those supported by many programming languages, are implemented. Variables can be defined using the let
keyword. Variables cannot be redefined in the same scope, but they can be reassigned.
Format:
let <identifier> = <expression>; # Define
<identifier> = <expression>; # Reassign
Example:
let x = 0;
let y = 10;
let foobar = add(5, 5);
let alias = foobar;
let identity = fn(x) { x };
x = x + 1;
y = x - y;
Scopes
In Monkey, there are types of scopes:
Global Scope
Variables defined at the top level are visible everywhere, and can be modified from anywhere.
let x = 2; # `x` is a global variable
let f = fn() {
let g = fn() {
x = x + 1; # Modifies the global variable `x`
return x;
}
return g;
}
let g = f();
puts(g()); # 3
puts(g()); # 4
let h = f();
puts(h()); # 5
puts(h()); # 6
Local Scope
Variables defined within while loops or functions are of this scope. They can be modified from the same scope, or inner while loops' scopes.
let x = 1;
while (x > 0) {
x = x - 1;
let y = 1; # `y` is a local variable
while (y > 0) {
y = y - 1; # Modifies the local variable `y`
}
puts(y); # 0
}
Closure Scope
A function captures all non-global variables visible to it as its free variables. These variables can be modified from within the function.
let f = fn() {
let x = 2;
let g = fn() {
x = x + 1; # `x` is captured as a free variable
return x;
}
return g;
}
let g = f();
puts(g()); # 3
puts(g()); # 4
let h = f();
puts(h()); # 3, since in function `f`, `x` remains unchanged.
puts(h()); # 4
CurrentClosure Scope
Specially, a named function being defined is of this scope. It cannot be modified from within its body.
let f = fn(x) {
f = 3; # ERROR: cannot reassign the current function being defined: f
}
But redefinition is OK:
let f = fn(x) {
let f = x + x;
puts(f);
}
f(3); # 6
Literals
Five types of literals are implemented.
INTEGER
INTEGER
represents an integer value. Floating point numbers can not be handled.
Format:
[-+]?[1-9][0-9]*;
Example:
10;
1234;
BOOLEAN
BOOLEAN
represents a boolean value.
Format:
true | false;
Example:
true;
false;
let truthy = !false;
let falsy = !true;
NULL
NULL
represents null. When used as a condition, NULL
is evaluated as falsy.
Format:
null;
Example:
if (null) { 2 } else { 3 }; # 3
STRING
STRING
represents a string. Only double quotes can be used.
STRING
s can be concatenated with "+"
.
Format:
"<value>";
Example:
"Monkey Programming Language"; # "Monkey Programming Language";
"Hello" + " " + "World"; # "Hello World"
ARRAY
ARRAY
represents an ordered contiguous element. Each element can contain different data types.
Format:
[<expression>, <expression>, ...];
Example:
[1, 2, 3 + 3, fn(x) { x }, add(2, 2), true];
let arr = [1, true, fn(x) { x }];
arr[0];
arr[1];
arr[2](10);
arr[1 + 1](10);
HASH
HASH
expresses data associating keys with values.
Format:
{ <expression>: <expression>, <expression>: <expression>, ... };
Example:
let hash = {
"name": "Jimmy",
"age": 72,
true: "a boolean",
99: "an integer"
};
hash["name"];
hash["a" + "ge"];
hash[true];
hash[99];
hash[100 - 1];
FUNCTION
FUNCTION
supports functions like those supported by other programming languages.
Format:
fn (<parameter one>, <parameter two>, ...) { <block statement> };
Example:
let add = fn(x, y) {
return x + y;
};
add(10, 20);
let add = fn(x, y) {
x + y;
};
add(10, 20);
If return
does not exist, it returns the result of the last evaluated expression.
let addThree = fn(x) { x + 3 };
let callTwoTimes = fn(x, f) { f(f(x)) };
callTwoTimes(3, addThree);
Passing around functions, higher-order functions and closures will also work.
The evaluation order of function parameters is left to right.
So a memoized Fibonacci function should be implemented like:
let d = {}
let fibonacci = fn(x) {
if (x == 0) {
0
} else {
if (x == 1) {
1;
} else {
if (type(d[x]) == "NULL") {
# You cannot use `d = push(d, x, fibonacci(x - 1) + fibonacci(x - 2))`
# since `d` is evaluated first, which means it will not be updated
# when `fibonacci(x - 1)` and `fibonacci(x - 2)` are called.
let g = fibonacci(x - 1) + fibonacci(x - 2);
d = push(d, x, g);
}
d[x];
}
}
};
fibonacci(35);
Built-in Functions
You can use the following built-in functions
type(<arg1>): STRING
Return the type of arg1
as a STRING
.
type(1); # INTEGER
type("123"); # STRING
type(false); # BOOLEAN
puts(<arg1>, <arg2>, ...): NULL
It outputs the specified value to stdout
. In the case of Playground, it is output to console
.
puts("Hello");
puts("World!");
len(<arg>): INTEGER
- For
STRING
, it returns the number of characters. - For
ARRAY
, it returns the number of elements. - For
HASH
, it returns the number of key-value pairs.
len("Monkey"); # 6
len([0, 1, 2]); # 3
len({1: 2, 2: 3}); # 2
first(<arg: STRING>): STRING | NULL
Returns the character at the beginning of a STRING
. If the STRING
is empty, return NULL
instead.
first("123"); # "1"
first(""); # null
first(<arg: Array>): any
Returns the element at the beginning of an ARRAY
. If the ARRAY
is empty, return NULL
instead.
first([0, 1, 2]); # 0
first([]); # null
last(<arg: String>): STRING | NULL
Returns the element at the last of a STRING
. If the STRING
is empty, return NULL
instead.
last("123"); # "3"
last(""); # null
last(<arg: Array>): any
Returns the element at the last of an ARRAY
. If the ARRAY
is empty, return NULL
instead.
last([0, 1, 2]); # 2
last([]) # null
rest(<arg: STRING>): STRING | NULL
Returns a new STRING
with the first element removed. If the STRING
is empty, return Null
instead.
rest("123"); # "23"
rest(""); # null
rest(<arg: ARRAY>): ARRAY | NULL
Returns a new ARRAY
with the first element removed. If the ARRAY
is empty, return NULL
instead.
rest([0, 1, 2]); # [1, 2]
rest([]); # null
push(<arg1: ARRAY>, <arg2>): ARRAY
Returns a new ARRAY
with the element specified at the end added.
push([0, 1], 2); # [0, 1, 2]
push(<arg1: HASH>, <arg2>, <arg3>): HASH
Returns a new HASH
with arg2: arg3
added. If arg2
already exists, the value will be updated with arg3
.
push({0: 1}, 1, 2); # {1:2, 0:1}
push({0: 1}, 0, 3); # {0:3}
Advanced examples
map
function
A custom let map = fn(arr, f) {
let iter = fn(arr, accumulated) {
if (len(arr) == 0) {
accumulated
} else {
iter(rest(arr), push(accumulated, f(first(arr))));
}
};
iter(arr, []);
};
let a = [1, 2, 3, 4];
let double = fn(x) { x * 2};
map(a, double); # [2, 4, 6, 8]
reduce
function
A custom let reduce = fn(arr, initial, f) {
let iter = fn(arr, result) {
if (len(arr) == 0) {
result
} else {
iter(rest(arr), f(result, first(arr)))
}
}
iter(arr, initial)
}
let sum = fn(arr) {
reduce(arr, 0, fn(initial, el) { initial + el })
}
sum([1, 2, 3, 4, 5]); # 15
Macro System
Now that the Lost Chapter has been implemented, MonkeyLang.jl
provides a powerful macro system.
Here is an example:
let unless = macro(condition, consequence, alternative) {
quote(if (!(unquote(condition))) {
unquote(consequence);
} else {
unquote(alternative);
});
};
unless(10 > 5, puts("not greater"), puts("greater")); # greater
In the REPL, you need to enter all the contents in a single line without
\n
characters.
Enjoy Monkey