A large domesticated ox with shaggy hair.
As a programming language, it functions as an interpreted, stack-based, postfix notation language created in go.
If you don't have a go workspace set up but would like to: here is a good article detailing go workspaces.
If you have a go workspace set up with your $GOPATH
set and $GOBIN
in your path then it will be very easy.
Simply use get
to grab this repo and install
to create
the binary. Then you can just run the program which takes
the name of the file you wish to parse as an argument.
For example, if you wanted to run one of the example programs included in this repo:
$ go get github.com/llewellyn-kevin/yak
$ go install yak
$ cp $GOPATH/src/github.com/llewellyn-kevin/yak/examples/recursion.yak .
$ yak recursion.yak
If you are not a go developer and do not plan to be you won't have the whole workspace set up. You will still have to have go installed, after you do you will have to manually compile from this repo:
$ git clone https://github.com/llewellyn-kevin/yak.git
$ cd yak
$ go build yak
This will create an executable binary called yak you can put anywhere you like (or just leave it).
You can use this executable like normal:
$ ./yak examples/recursion.yak
And yak will interpret the recursion yak program.
Or you can add yak to your path to run it from anywhere on your system.
$ export PATH=$PATH:$PWD/yak
$ cd examples
$ yak recursion.yak
You can add the export
line with $PWD
replaced with the
path to the yak binary to ~/.bash_profile
so the yak
command will work on terminal startup.
Go works in windows. So download the code, compile it, and run the executable. I don't use Windows, so I wouldn't even know how. But you do, so I hope you can.
The interpreter is still in active development, when it is closer to an official release I will add binaries that can be downloaded directly without having to compile using go.
The output of the program will be the stack that results from running the code.
Every value the interpreter finds in a program gets placed on the stack. Each operation interacts with the values on the stack, in most cases popping some off and, and always placing the result on the top.
For example: the +
binary operator, takes the two top
values from the stack, adds them, and pushes the result to
the stack.
So a program that reads:
1| 5 10
will result in a stack:
0: 10
1: 5
But a program that reads:
1| 5 10 +
will result in a stack:
0: 15
The lexer seperates all tokens by whitespace. This means all inputs and operations must be seperated by spaces, linebreaks, or tabs. Which is used is irrelavent. That means:
1| 1
2| 2
3| +
4| 3
5| *
and
1| 1 2
2| + 3 *
and even
1| 1 2 + 3 *
are all valid yak, and do the same thing. But:
1| 1 2+ 3*
is invalid.
The binary operators all take two values and return one,
and are: +, -, *, /, %, ==
+
adds the two values and returns the result
-
subtracts the top value from the second value and
returns the result
*
multiplies the two values and returns the result
/
divides the second value by the top value and returns
the result
%
divides the second value by the top value and returns
the remainder (expects integers)
==
compares the two values and returns 1 if they are the
same, otherwise 0
The unary operators take one value and return one, the only
unary operator currently is: .
.
The duplication operator. This is necessary because
in most cases interacting with the stack observes values,
and in most cases, observing the thing also destroys the
thing. This operator copies the top value from the stack,
and adds the duplicate to the stack.
There are currently 3 control statements: a function definition, an if statement, and an if not statement.
After each control statement, yak requires a block of code.
A block is defined by an open bracket, {
, a set of
instructions, and then a close bracket, }
.
A function definition takes the form: n#identifier
, where
n is the number of arguments, and the identifier is the name
of the function.
A function is called simply by placing the function name
in a block of code when there are at least n
arguments on
the stack.
When a function is called the interpreter creates a new
stack outside the main one named after the function.
it then pops n
values off the main stack, and pushes them
onto the new function stack. After running the code in the
function block, the top value from the function stack will
returned to the main stack.
Example:
1| 1#increment {
2| 1 +
3| }
This increment function takes one argument, adds one, and returns the result.
An if statement is simply the ternary operator from
other languages: ?
. When the interpreter sees the
conditional operator, it pops off the top of the stack
and checks its value. If it is 1, it executes the following
code block. If it is not 1, it does not.
Example:
1| 10
2| 5 5 == ? { 1 + }
This will push 10, 5, and 5 onto the stack. Then the ==
will compare the top two values, see they are the same and
replace them with 1
. The ?
will pop the 1
off the
stack and execute the code block. This code block pushes
a 1
onto the stack and adds it to the remaining value,
ultimately returning 11.
Example 2:
1| 10
2| 6 5 == ? { 1 + }
Ultimately this is the same example, but 6 5 ==
returns 0
instead of 1
. This means the block after
?
never executes and 10 is ultimately returned.
The if not statement is simply a !
. It functions the same
as the conditional operator, except the block of code
executes if the value observed on the stack is not 1.
Example 3:
1| 10
2| 6 5 == ! { 1 + }
In this example, the value 0 will be checked, which is not 1, so the block will execute. This would also work if 10 was checked.
Example 4:
1| 10 . ! { 1 + }
Here we duplicate the 10 and check its value. Because it
is not 1, it is false, and the block of code executes.
The duplicated 10 was popped of the stack by the !
,
but we preserved the origional 10 because of the .
.
This means we get 10 1 +
, or the expected value: 11.