An Imperative Stream Programming Language Apr. 28, 2016 in Algorithms, Text, University
This is the user manual for the Aqua programming language created as part of Programming Languages and Concepts. Visit the project on Github.
Aqua is a C-like imperative language, for manipulating infinite streams. Statements are somewhat optionally terminated with semicolons, and supports both block ( /* ... */)
and line comments ( // ...)
.Curly brackets are used optionally to extend scope. Example code can be found in the Appendices.
Before continuing, it’s helpful to familiarise yourself with Extended BNF. Special sequences are used to escape.
Usage Instruction
Once the interpreter has been compiled using the make
command, you can choose to run an interactive REPL or a stored program. Executing ./mysplinterpreterwith
no arguments will start in an interactive REPL . You should save your program files as <filename>.spl
and pass the location as the first argument to ./mysqlinterpreter
. As data is read from standard in, you can pipe files in using the <
operator, or pipe programs in using the |
operator, allowing you to create programs that manipulate infinite streams.
- Starting the interactive REPL:
./mysplinterpreter
- Executing a saved program:
./mysplinterpreter <file> [ < <input> ]
- Using infinite streams:
<program> | ./mysplinterpreter <file>
Programs are executed in multiple stages:
- Entire program is loaded into a string (Unix line endings required)
- Program is lexed into Tokens
- Tokens are parsed into an Abstract Syntax Tree (Left associative with the exception of lambdas)
- Types are checked to ensure logical behaviour
- Finally the Abstract Syntax Tree is executed
Types
Our programming language has 4 basic types: unit
, int
, pair
, and lambda
. From these types we can create the complex data types: bools
with a simple comparison to 0, lists
from pairs
, and strings
from lists
. In addition lambda
definitions can be nested to allow both currying and multiple parameters.
Our language is strongly, statically typed. Types are defined using the following Extended BNF grammar:
type = unit | int | pair < <type> , <type> > | <type> > <type>
Values
Complex data types are automatically decomposed into the basic types before they can be used in the program.
unit = ?[]?
int = [ 0 ( b | o | x ) ] { <digit> | _ } (* Bin/Oct/Dec/Hex *)
pair = ?[? <value> . <value> ?]?
lambda = lambda ?(? <type> <identifier> = <value> ?)? <expression>
bool = true | false
list = ?[? <value> { , <value> } ?]?
string = " [ { <character> } ] "
Variables
Variables are used to store dynamic values, with assignment similar to most Clike programming languages. To allow scoping, variables are implemented and type checked using two separate environment trees. As a result, you can access and manipulate variables higher in the tree, and variables that leave scope are lost.
Identifiers must be at least 1 character, start with a letter, and lexed using: [azAZ][azAZ09_]*
bind = <type> <identifier> = <value>
lookup = <identifier>
rebind = <identifier> ( = | += | = | *= | /= | %= | &= | ^= | ?|?= ) <value>
unbind = unbind <identifier>
Math
Like most Clike languages, our language supports all common unary and binary math
operators:
unary = ( ! | + | ) <value>
binary = <value> ( + | | * | / | % | ^ | & | ⊻| ?|? ) <value>
Also, various math
related functions are built in (You can also call the above with math.plus
for example):
math. (random | min | max | abs | sign | sqrt | ln | log | fact | ... ) …
random
takes a number of parameters. Zero = bool, One = between 0 and value, Two = between the values.
Input/Output
Our programming language provides many inbuilt functions for manipulating standard input/output/error.
input = console.read_ ( int | string | bool )
output = console.print [ ln ] _ ( int | string | bool ) <value>
error = console.error [ ln ] _ ( int | string | bool ) <value>
clear = console.clear
The int
, string
, or bool
instructs the interpreter on how to interpret the value it receives. For example:
raw = 65 int = 65 string = "A" bool = true
Conditionals
Conditionals evaluate an expression if another evaluates to true
. In our programming language 0
and unit
evaluate to false
, and every other value evaluates to true
. This allows nice concise comparisons.
if = if <condition> then <consequent> [ else <alternative> ] done
ternary = <condition> ? <consequent> : <alternative>
coalesce = <condition> ?? <default>
compare = <value> ( > | >= | < | <= | == | != ) <value>
Any expression can be used as the condition for a conditional, not just a comparison.
Loops
Our programming language implements the 3 traditional loops
any programmer has come to expect, plus loop
which is equivalent to while true
. Use break
, continue
and return
to manipulate flow.
loop = loop <expression>
while = while <condition> do <expression>
do = do <expression> while <condition>
for = for <bind> ; <condition> ; <action> then <expression>
If you wish to use multiple expressions inside the loop,you can add matching curly braces.
Functions
Once a function has been created using a lambda
expression, you can apply a value to it, to execute the code stored within. The syntax for doing so is very simple: <lambda> ?(? <value> ?)?
.This can be chained if there are nested lambdas.A side effect of this allows you to create a nested lambda
and curry it.
int > int times10 = lambda (int value) (value * 10); times10 (20);
Lists
Our programming language has inbuilt support for Schemelike pairs
and lists
. You can use cons
, head
, and tail
to build and access the pairs.These pairs
can be used to easily create a tree structure: [[1 . 2] . [3 . 4]]
. Alternatively to build lists
, you can use a nice shorthand just like in Scheme:
(cons 1 (cons 2 (cons 3 []))) [1 . [2 . [3 . []]]] [1, 2, 3, []] [1, 2, 3]
In addition, our language provides a number of inbuilt functions allowing you to manipulate lists,functionally.
map = list.map <lambda> <list>
fold = list.fold <lambda> <list> <accumulator>
filter = list.filter <lambda> <list>
limit = list.limit <list> <value>
length = list.length <list>
strings
are simply lists of ints
, therefore you can use any of the above to manipulate them, or string.lower <list>
, and string.upper <list>
to manipulate case and string.rev <list>
to reverse it.
Miscellaneous
You can use assert <value>
and exit
to halt (un)conditionally in your program.
Error Messages
End of File | Our program only accepts files with Unix style LF file endings. Use your text editor to convert. |
Syntax Error | Input cannot be lexed into tokens, the program will give a line and column number. |
Parse Error | Tokens cannot be parsed into an Abstract Syntax Tree, the program will give the incorrect tokens. |
Type Error | Error while typing, the program will give a descriptive message. |
Eval Error | Error while executing, the program will give a descriptive message. |
Appendix
Hello World
console.println_string "Hello, World!";
Factorial
int > int factorial = lambda (int n) {
int result = 1;
for int i = 1; i <= n 1; i += 1 then
result *= i;
result;
};
console.println_int factorial (5);
FizzBuzz
for int i = 0; i <= 99; i += 1 then {
if i % 15 == 0 then
console.println_string "FizzBuzz"
else if i % 3 == 0 then
console.println_string "Fizz"
else if i % 5 == 0 then
console.println_string "Buzz"
else
console.println_int i
done done done
}
Guessing Game
console.println_string "I'm thinking of a number between 1 and 10 (inclusive)";
int number = math.random 1 10;
int guess = console.read_int;
if number == guess then
console.println_string "You guessed correctly!"
else
console.println_string "Better luck next time!"
done;
Functional List Operations
console.println_int list.filter (lambda (int x) x) [1,2,3,0];
console.println_int list.fold (lambda (int x) (lambda(int y) (x+y))) [1,2,3] 1;
console.println_int list.limit [1,2,3] 2;
console.println_int list.map (lambda (int x) (x+1)) [1,2,3];
Experimenting with Microsoft HyperlapseNES Emulator