Simple procedural programming language, statically typed and compiled to JVM bytecode
A sudoku solver: https://github.com/ValentinAebi/Rattlesnake-sudoku-solver
fn joinWords(words: arr String, endSymbol: Char) -> String {
var joined = "";
for var i = 0; i < #words; i += 1 {
joined += words[i];
if i < #words-1 {
joined += " ";
}
};
return joined + charToString(endSymbol)
}
fn main(arr String){
val msgWords = ["Hello", "world"];
val msg = joinWords(msgWords, '!');
print(msg) // displays "Hello world!"
}
Run help
to see the available commands and options
The language does not support modules. All functions and data structures are top-level and identified by their textual name across the whole program. Functions and types can be referred to from inside the file that defines them or from another file in the exact same way.
Statements are separated with ;
. ;
may be omitted after the last statement.
-
Primitive types:
Int
,Double
,Bool
,Char
Void
: return type of a function that does not return any valueNothing
: return type of a function that terminates the program and thus never returns
-
String
-
Arrays:
arr <element type>
, e.g.arr Int
Creation:
arr <type>[<size>]
, e.g.val array = arr Int[10]
(such an array is always mutable)[<elems>*]
, e.g.val array = [-7, 31, 14, 11]
(orval array = mut [-7, 31, 14, 11]
if the array must be mutable)
Access an element:
<array>[<index>]
, e.g.xs[7]
-
Structures, e.g.
struct Foo { bar: Int }
Fields are unmodifiable by default. Reassignable fields must be marked with
var
, e.g.struct Abc { x: Int, var y: Int }
Creation and field access:
val foo = new Foo { 0 }; val b = foo.bar
Creating a mutable structure:
new mut Abc { 10, 15 }
-
Interfaces, e.g.
interface I { x: Int, var s: String }
The subtyping relation between structures and interfaces must be declared explicitely:
struct A : I { x: Int, y: Double, var s: String }
. The structure must then provide at least the fields declared by the interface. If a field is declared reassignable in an interface (var
keyword), then it must be reassignable in the structure as well. If a structureA
is a subtype of an interfaceI
, then the fields ofA
declared inI
and non-reassignable inI
must declare a type that is a subtype of their type inI
. If a field is reassignable inI
, then it must declare the exact same type as inI
. E.g., assuming the existence of an interfaceZ
and a structU
that is a subtype ofZ
(denotedU <: Z
in the following snippet):// We assume the existence of U and Z such that U <: Z interface I { f: Z, var g: Z } struct A : I { f: Z, var h: String, var g: Z } // valid, extends I with an additional field h struct B : I { f: U, var g: Z } // valid, U <: Z and f is not reassignable struct C : I { f: U, var g: U } // not valid, as U != Z and g is reassignable struct D : I { f: Z, g: Z } // not valid, as g is reassignable in I but not in D
Nothing
is a subtype of all other typesmut X
is a subtype ofX
(but not the other way around)- arrays are covariant iff they are immutable (e.g.
arr mut Foo
is a subtype ofarr Foo
butmut arr mut Foo
is not a subtype ofmut arr Foo
) - the subtyping relation between structures and interfaces is declared explicitely
fn <function name>(<args>*) -> <return type> {
...
}
Return type can be omitted if it is Void
E.g.:
fn bar(i: Int, b: Bool) -> String {
...
return "Hello"
}
A non-void function must contain a return <value>
for each control-flow path in the function. If the function has return type Void
, then return
is not required but may be used (without a value) for early exit.
Parameters may not be named. This is particularly useful for the main
function when the program ignores its arguments (as unused named parameters produce a warning):
fn main(arr String){
...
}
val <name>: <type> = ...
Type may be omitted in almost all cases, except if the right hand-side is a ternary operation involving a complex hierarchy of structures and interfaces. var
s are defined similarly.
E.g.:
val x: Int = 0;
val str = "Rattlesnake";
var
s (but not val
s) can be reassigned: x = <new value>
Constants can only be of primitive types. Their name must be lowercase.
const <name>: <type> = <value>
Type may be omitted, e.g. const anwser = 42
if <cond> {
...
} else if <cond> {
...
} else {
...
}
or without else
branch:
if <cond> {
...
}
while <cond> {
...
}
for <stat>;<cond>;<stat> {
...
}
E.g.:
for var i = 0; i < #array; i += 1 {
...
}
Tail calls are eliminated only if explicitly requested.
This is done by adding !
between the name of the called
function and the parameters list: recurse!(arg1, arg2)
.
-
: opposite of anInt
or aDouble
!
: logical negation of aBool
#
: length operator forString
s and arrays
- Mathematical:
+
,-
,*
,/
,%
(can be combined with=
:+=
,/=
, etc.) - Comparisons (between
Int
s orDouble
s):<
,>
,>=
,<=
- Equality:
==
,!=
(strings are compared by structure,struct
s are compared by reference) - Logical:
&&
,||
(implement lazy evaluation of second operand) - String concatenation:
+
when <cond> then <expr1> else <expr2>
Additionaly to casts, the following conversions can be performed:
Int
<->Char
Int
<->Double
Syntax: <expr> as <type>
, e.g. 10 as Double
, x as Foo
Syntax: <expr> is <type>
E.g.:
interface Foo {}
struct Bar : Foo { x: Int }
...
val f: Foo = ...
val t = when f is Bar then f.x else 0; // smartcast on ternary operator
val condition = f is Bar && f.x == 42; // smartcast on &&
if (f is Bar){ // smartcast on if statement
... = f.x;
}
Terminates the program with an exception:
panic <message>
, e.g.
panic "forbidden argument " + arg
The compiler replaces calls to these functions with special instructions.
print(s: String)
: displayss
on the console?ToString(...)
with?
one ofint
,double
,char
,bool
, and the corresponding parameter type (e.g.intToString(Int) -> String
): conversion to a stringtoCharArray(s: String)
: converts a string into an array of all its characters (the returned array is mutable)
Tests are defined similarly to functions, but fn
is replaced by test
and there is no parameters list. Failures are reported using panic
:
test exampleTest {
val i = 1 + 1;
if i != 2 {
panic "We have a problem!"
}
}
Lexer and parser are inspired from https://github.com/epfl-lara/silex and https://github.com/epfl-lara/scallion, respectively.
Backend uses the ASM bytecode manipulation library: https://asm.ow2.io/