Language Basics
Overview
Expression-Based Language
Snail is an expression-based language. Most constructs in snail are
expressions, and every expression has a value and an associated type. This can
be a little confusing for programmers with familiarity of languages with
statements (such as C, Java, or Python) where certain control structures do
not have values. For example, an if
expression in snail produces a value and
can thus be used as part of an assignment expression (akin to the ternary
operator in other languages).
// name will either have the value of preDefinedName
// or prompt the user for a name
let name = if (predefined) {
preDefinedName;
} else {
print_string("Enter your name: ");
read_string();
};
Implicit Returns
There is no return
keyword in snail. The last expression in a block
or method definition is the returned value.
let fourteen = {
let x = 7;
let y = 2;
// x * y is the implicit return of this block
x * y;
};
Dynamic Typing
Snail programs are dynamically typed. This means that the type of any variable in a snail program is the type of the value most recently assigned to it. This can make for more rapid prototyping, but it can also lead to unusual errors when programming. For example, attempting to add an integer and string will result in a runtime error:
let foo = 3 + "hello";
// runtime error
Classes
All values in snail are objects that are defined by some class. Snail programs
may contain multiple classes, but all names must be unique. Create a class with
the class
keyword:
class Person {
let first_name = "";
let last_name = "";
init(first, last) {
first_name = first;
last_name = last;
self;
};
get_first() {
first_name;
};
get_last() {
last_name;
};
};
Values of the Person
type may now be constructed in other places in the code:
{
let p = new Person;
p.init("Jane", "Doe");
}
By convention, class names begin with a capital letter, but any valid identifier may be used. Note that classes may not have the same name as a built-in type.
The body of a class definition consists of a list of member variable and method definitions. Member variables and methods may share the same names; snail differentiates between these two features of a class.
Note that there is no initializer method that is called automatically when an
object is constructed. Only the member variables are initialized (either to
default values or to the value of the expression in the declaration). In the
example above, the init
method must be called explicitly. Note that this
method returns a reference to itself thereby allowing an object to be created,
initialized, and assigned in one line of code:
let p = (new Person).init("Dorothy", "Vaughan");
Member Variables
Member variables are prefixed by the let
keyword. They may be
given an initial value (using an assignment expression) or may be left
uninitialized. Uninitialized variables have a value of void
and generally
cannot be used.
An object may refer to itself using the self
identifier.
Methods
A method is a function that has access to the member variables of an instance of the class (i.e., a particular object). Methods consist of a name, a list of parameters, and a body. Method names must be unique within a class.
Given an value p of type Person (from the example above), we can set values for
the first- and last-names by calling the init
method of p
:
p.init("Jane", "Doe")
This is an object-oriented dispatch on object p
. There may be different
implementations of init
in various classes of the snail program. Snail looks
up the class of object p
and executes the version most closely associated with
this class.
In this particular example, the values of "Jane"
and "Doe"
and passed to the
method and are bound to variables first
and last
, respectively. Methods are
called by value in snail, meaning a shallow copy of each value is made and
provided to the method.
Inheritance
Classes may form a hierarchy in snail. To inherit all of the member variables and methods from another class in the program, add the parent class’s name following a colon after the class definition:
class Student : Person {
let id = 0;
init(first, last, s_id) {
self@Person.init(first, last);
id = s_id;
};
get_id() {
id;
};
};
In this example, Student
values will have all of the data and methods of a
Person
. In addition, they store information about an id
. If a member
variable or method in a deriving class shares the same name as feature in the
parent class, then the feature in the derived class takes precedence.
If a class does not specify its parent, then that class inherits from Object
,
which is a special class (that has no parent). A class may only inherit from a
single class. The parent-child relationship forms a graph. Program behavior is
undefined if this graph contains cycles.
Main Class
All snail programs must contain a Main
class that defines a main()
method.
The main
method may be inherited from a parent of Main
, but this is not
common.
Program execution begins by evaluating (new Main).main()
.
Built-In Classes
Snail comes with several built-in classes to provide basic functionality. Programs may not redefine these classes. For detailed information about provided classes, see Built-In Classes.
Basic Expression Types
Snail supports many of the expression types found in modern programming languages. All expression types are described below.
Arithmetic
Arithmetic is supported on values of type Int
(64-bit integers) in snail.
First exp1
is evaluated and then exp2
. These two values are then combined
using the standard arithmetic operation and this result is the result of the
expression. Arithmetic produces a value of type Int
. Note that snail only has
integer division.
exp1 + exp2 // addition
exp1 - exp2 // subtraction
exp1 * exp2 // multiplication
exp1 / exp2 // division
Comparisons
Snail has three comparison operations: <
, <=
, and ==
. These comparisons
apply to sub-expressions of any types using the following rules:
- If both sub-expressions are
Int
, then standard arithmetic comparison is used - If both sub-expressions are
String
, then lexicographic comparison is used - If both sub-expressions are
Bool
, thenfalse < true
- If both sub-expressions are
void
, they are equal
On all other types, equality is decided by pointer value. If two values share the same space in memory, they are equal.
Comparisons produce Bool
values.
exp1 < exp2
exp1 <= exp2
exp1 == exp2
Unary Expressions
To negate a number in snail, the ~
operator is used. This may only be used
on a value of type Int
.
To negate a value of type Bool
use the !
operator. This will produce a
Bool
of the opposite value.
{
// Integer negation
print_int(~12);
// Boolean negation
if (!false) {
print_string("this will print");
} else {
abort();
}
}
Local Variables
The let
keyword is used to declare variables in snail. If let
is used in
the body of a method (or on the right side of a member variable declarations),
it creates a local variable. The value of a let
expression is the value of
the right hand side.
{
let salutation = "hello";
let points = 10;
}
A local variable is in scope from its declaration to the end of the scope. Scopes in snail are created using blocks (denoted by curly braces). Scopes can nest, meaning that an inner block has access to all of the local variables of the outer block.
Variables may also be redefined within an inner scope. As soon as control leaves this scope, however, the previous value is restored. This is known as variable shadowing.
{
let x = 10;
let y = 2;
{
let y = 34;
print_int(x + y);
};
print_int(x + y);
// will output: 4412
}
Variable Assignment
Once a variable has been declared (either as a member variable or locally), its value may be updated using an assignment expression. The right hand side of the expression is evaluated and replaces the original value for the variable. It is an error to assign to a variable that has not previously been declared. Because snail is dynamically typed, the variable will have the type of the most recent assignment.
{
let x = 3;
x = "foo";
}
Constructing Objects
The new
keyword constructs a value of the specified type. This expression
returns the newly-constructed value. Snail separates out constructor method
from the initial creation of an object, so only the default values for members
variables are set initially.
new Person
Checking if an Object is void
Programs may use an isvoid
expression to determine if an value is void
. The
expression evaluates to true
if the contained expression is void
and evaluates
to false
otherwise.
isvoid(exp)
Blocks
Sequences of expressions may be grouped together with a block expression. These
are also used for the body of methods, conditionals, and loops. Expressions are
evaluated from top-to-bottom (left-to-right). The value of a block is the value
of the last expression in the block. Each expression in a block is terminated
by a semicolon. Note that this means that if
and while
expressions are also
terminated by semicolons.
{
exp1;
exp2;
exp3;
}
Conditionals
The semantics of conditional expressions is standard. The predicate is
evaluated first. If the predicate is true
, then the then
branch is
evaluated, otherwise the else
branch is evaluated. The value produced by the
conditional is the value of the evaluated branch. Both branches of the if
expression are treated as blocks of code (and thus each expression contained
within the braces needs a semicolon).
if (condition predicate) {
a;
} else {
b;
}
Loops
Snail supports while
-style loops. The loop guard (predicate) is evaluated
before each iteration of the loop. If the predicate is ever false
, the loop
terminates and a void
value is produced. If the predicate is true
, the body
of the loop is evaluated and the process repeats. The body of the while
expression is treated as a block of code (and thus each expression needs to be
terminated with a semicolon).
while (guard) {
a;
}
Dispatch
Dispatch, or method calls, have three supported formats. Dynamic dispatch calls a method on another value. The number of arguments in a dispatch must match the number of parameters in the method definition.
First, each of the arguments is evaluated from left to right. These values are then stored in the parameters of the target value’s method. Then, the value of the target object is determined. Inheritance rules are used to determine the method that is selected. Finally, the body of this method is evaluated, producing the value of this dispatch.
If the reference implementation detects an inheritance cycle during a dispatch, it will exit with an error.
exp.method(arg1, ..., argn);
Static dispatch allows the programmer to select a method from a specific class
in a value’s inheritance chain. Class B
must be in the inheritance graph of
exp
.
// Assuming exp inherits from type B,
// call method that is defined in class B
exp@B.method(arg1, ..., argn);
Self dispatch is a shortcut to allow calling a method on the current self object.
// Call method on the current object
method(arg1, ..., argn);
Arrays
Snail also supports an array data type for storing contiguous blocks of values in memory. Arrays are fixed-size—the size must be specified on instantiation and cannot be changed. To change the size of an array, a new array must be created.
It is recommended that Arrays only store one type of data, but the type system will allow for any type of data to be stored.
Constructing Arrays
Arrays are constructed using a variation of the new
expression, which includes
the size of the array in square brackets. The following creates an array that
stores ten (10) values. Note that this syntax is only valid for the Array
class. Initially, an array is constructed with all void values.
let myArray = new[10] Array;
Accessing Values
Values are stored contiguously in an array and can be accessed by placing an integer index value inside of square brackets after an identifier. Arrays in snail are zero-indexed meaning that the valid indices for an array of length \(n\) are \(0 \leqslant i < n\).
{
myArray[0] = 10;
myArray[9] = 9;
print_int(myArray[0]);
}
Unicode Support
The snail language supports unicode characters in string and a subset of unicode for identifiers. The reference implementation supports source files with UTF-8 encoding.
Snail supports “XID” identifiers per the unicode identifier specification.
class Main : IO {
let π = "3.14159";
main() {
print_string("The value of π is: ");
print_string(π);
};
};