Language overview

Variables

Variables in Spearmint are immutable by default and are declared using let.

let x = 10;
let y = 20;
let z = x + y;

Variables can be made mutable by using the let! keyword instead.

let! x = 10;
x = x + 20;

Type annotations

Variables can be annotated in order to tell the compiler what types the variable is allowed to be. Type annotations come after the variable name and before the assignment.

let x: num = 10;

In most cases, the compiler can infer the type of the variable from its usage. However, there may be some cases where an explicit type is required.

Numbers

As we have already seen, the num type is one of the primitive types in Spearmint. There is no distinction between integers and floating point numbers, every number simply takes on the num type.

Strings

The str type is another primitive type and is used to represent a string of characters.

let greeting = 'Hello, World!';

Booleans

The bool type is used to represent values that are either true or false.

let value = true;

Objects

The last primitive type in Spearmint is the object. Objects can be thought of as instances of classes, which will be discussed later. The important thing to remember is that if a variable is not one of the 3 aforementioned primitive types, then it must be an object. This has some implications that are discussed later.

Null (or the lack thereof)

Spearmint does not have a null or undefined data type. To represent values that may not exist, Spearmint uses the Maybe type. This type will be discussed later, but it is important to keep in mind that all variables in Spearmint must have a value.

Control flow

If statements

If statements do not require parentheses around the condition

if x > 10 {
  // do something
}

The body of an if statement must be a block statement. Code such as

if x > 10
  doSomething();

is not valid. This is the case for all other control flow statements as well.

For loops

For loops are declared using the “for … in” style. For loops can be used to iterate over a collection of items such as an array, or through a range of numbers.

Ranges

Ranges are one of the objects in the Spearmint standard library. Because they are so commonly used, there is syntactic sugar to create ranges.

If a range is declared using two dots, the range will be exclusive of the upper bound. That is, a range such as

let range = 0..10;

will include all of the numbers from 0 up to, but not including 10.

An inclusive range can be declared using three dots, like so

let range = 0...10;

A variable that contains a range can be used in a for loop, or the range can just be declared inline.

// these two loops are equivalent.

let range = 0..10;
for x in range {

}

for x in 0..10 {

}

Collections

The same syntax can be used to iterate over the elements in a collection

let items = [1, 2, 3, 4, 5];

for item in items {

}

While loops

Like if statements, while loops do not require parentheses around the condition

let! x = 0;
while x < 10 {
  x += 1;
}

Stop and next

Sometimes it is necessary to skip an iteration of a loop, or break out of the loop completely. In these cases, the stop and next keywords can be used

The stop keyword can be used to break out of the current loop. It can be used in both while and for statements

let! i = 0;

while true {
  i += 1;
  if i == 10 {
    stop;
  }
}

// i is now 10.

next can be used to continue the loop, but stop execution of the current iteration.

for x in 0..1000 {
  if x % 2 != 0 {
    next;
  }
  print('${x} is even');
}

Functions

Functions in Spearmint are declared using the fn keyword.

fn add(a: num, b: num): num {
  return a + b;
}

Several other properties of functions are evident from this code snippet:

Anonymous functions

Anonymous functions are declared using “fat arrow” syntax.

let add = (a: num, b: num) => {
  return a + b;
};

Unlike regular functions, anonymous functions do not need to have a block statement as their body. An anonymous function can simply use an expression after the arrow which then becomes the return value of the function.

let add = (a: num, b: num) => a + b;

An important difference between regular functions and anonymous functions is that regular functions are hoisted. This means that while the following code will work

let x = add(1, 2);

fn add(a: num, b: num) {
  return a + b;
}

The same approach cannot be taken when using anonymous functions

let x = add(1, 2); // error: `add` is undefined

let add = (a: num, b: num) => a + b;

Both regular functions and anonymous functions are first class objects in Spearmint, so they can be assigned to variables, passed as parameters and returned from functions just like other data types.

fn apply(func: (arg: num) => None, value: num) {
  func(value);
}

When

The when expression is similar to the match statement from Rust. It provides a mechanism for executing one of many branches of code depending on the value of something. Like Rust, the cases of the when expression must be exhaustive.

let x = true;

when x {
  true => print('x is true'),
  false => print('x is false'),
}

For types where it would be impossible to manually cover all cases (such as numbers), a named branch can be used to cover any remaining cases.

let x = 10;

when x {
  1 => print('x is 1'),
  2 => print('x is 2'),
  other => print('x is something else (${other})'),
}

Because when expressions are exhaustive, they can be used as an expression in cases where all of the branches return a value.

let x = 10;

let y = when x {
  10 => 10,
  other => {
    let z = other * 10;
    return z + 3;
  }
}

If the when expression has been used where a value is expected (such as in a variable assignment), but some of the branches do not return values, errors will be raised for those branches.

Classes

Classes are declared using the class keyword followed by the name of the class.

class Point {

}

Members

Members can be added to a class by declaring the name of the member, followed by its type.

class Point {
  x: num;
  y: num;
}

By default, class members are public and immutable. Class members can be made private by using the private keyword.

class Point {
  x: num;
  y: num;

  private id: num;
}

Members can be made mutable by using the mut keyword.

class Point {
  mut x: num;
  mut y: num;

  private id: num;
}

Methods

Methods can be added to a class by declaring a function, but without the fn keyword.

class Point {
  // ...

  scale(factor: num) {
    this.x *= factor;
    this.y *= factor;
  }
}

Again, the default is that methods are public, but they can be hidden by using private.

Constructors

To construct an instance of a class, Spearmint uses a syntax similar to JSON. Each member of the class must be given a value when the class is constructed.

let p = Point {
  x: 10,
  y: 20,
  id: 0,
};

If a variable has the same name as one of the properties of the class, it can be used without specifying the name of the field.

let x = 10;
let y = 20;

let p = Point {
  x, y,
  id: 0,
};

Instances of classes can always be created like this. There is no way to make the constructor of a class private.

Static methods

Methods can be declared as static by adding the static keyword to the beginning of the declaration.

class Point {
  // ...

  static new(x: num, y: num) {
    return Point {
      x, y,
      id: 0,
    };
  }
}

let p = Point.new(10, 20);

Static methods do not have a this variable in their scope and thus cannot access any instance members of the class they belong to.

Pre assigned members

In some cases it can be desirable to always assign the same value to a class member when that class is constructed. To do this, we can simply assign a value to it when it is declared.

class MyClass {
  x: num;
  y: num;
  private id = 0;
}

If a member is pre assigned, it does not have to be included when constructing an instance of the class. For example,

// this is fine because `id` is pre assigned
let p = Point {
  x: 10,
  y: 20,
};

// this is still valid and will override the pre assigned value
let p2 = Point {
  x: 20,
  y: 10,
  id: 1,
};

Note that this cannot be used in pre assigned values.

Inheritance

Spearmint supports single inheritance. A class can be extended by using the extends keyword.

class Point3D extends Point {

}

Although we have extended the class correctly, this code will raise an error, because the Point class is not an “open class”.

Open classes

By default, all classes are “closed” which means they cannot be inherited from. To allow a class to be inherited, it must explicitly be declared as open.

open class Point {
  // ...
}

With this change, our Point3D class no longer raises an error.

Subclasses inherit all of the fields from their parent class, so the constructor for the subclass will include those fields. For example, given the following structure:

open class A {
  a: num;
  b: num;
}

class B {
  c: num;
}

An instance of B would be constructed like this

let b = B {
  a: 0,
  b: 1,
  c: 2,
};

Abstract classes

Abstract classes are designed to be inherited from, and cannot be instantiated. A class can be marked as abstract using the abstract keyword

abstract class Node {

}

Abstract classes are open by default. There is no way to create a closed abstract class.

Abstract members and methods

Abstract classes can contain members and methods in the same way that normal classes can.

abstract class Node {
  kind: NodeKind;

  isKind(kind: NodeKind) {
    return this.kind === kind;
  }
}

However, methods in abstract classes can be marked as abstract. If they are marked as abstract, they do not require an implementation.

abstract class Node {
  kind: NodeKind;

  abstract isKind(kind: NodeKind): bool;
}

Because the return type of an abstract method can never be inferred, a type annotation will always be required.

Interfaces

TODO

Enums

TODO