Skip to content

Language Design

The name, much like the language, is a work in progress.

Import statements

Import statements import the whole module and assign it to a variable. Specific things from the module cannot be imported, such as JavaScript's import { function } from pkg or Python's from pkg import function. Wildcard imports like Python's from pkg import * are also not allowed.

import math // assigned to variable `math`

float phi = (math.sqrt(5) + 1) / 2

This is beneficial because different modules can export functions with the same name, and it'll always be clear where each function came from, at the cost of typing slightly more.

Comments

Comments start with // and can be in the middle or end of a line.

// This is a comment
int a = 2 // This is a comment after a statement

Multi-line comments can be written with /* */ syntax, similar to C.

/*
  This is a multi-line comment.

  You can even nest comments!
  /*
    Like this!
  */
*/

Doc comments can be written with /// or /** */ for single- and multi-line comments, respectively:

import math

/// The value of pi
decimal pi = 3.141592653589793

/**
 * A function to find the square root of an array of integers
 * @param The array of integers to square root
 * @returns An array of square roots
 */
fn sqrtAll(int[] nums) -> float[] {
  mut float[] rootVals = []
  for i in nums {
    rootVals[i] = math.sqrt(nums[i])
  }

  return rootVals
}

These behave the same as normal comments since it's only the // and /* */ that matter, all the other / and * are optional. However, the LSP will only recognise /// and /** */ as doc comments.

Variable declaration

[type] [name] = [value]

All variables are constant and immutable by default

int a = 1
a = 2 // Error

To make a variable mutable, you can add the mut keyword

mut int b = 2
b = 3 // Works!

Functions

fn [name]( [type] [arg], [type] [arg], ... ) -> [return type] {
  // Arguments are immutable
  // Any variables declared inside are scoped to the function
}

Example function:

fn hello(int a, int b) -> string {
  a = 2 // Error
  mut int b = b // Creates new scoped variable `b`, let's call it `bscope`.
  b = 2 // This will edit `bscope`, not the argument value `b`

  return "Hello, world!"
}
// `bscope` is not available here

string hello = hello(a, b) // pass in `int a` and `mut int b` from above
print(hello)

Currying

fn add(int a, int b) -> int {
  return a + b
}

fn addA = add(1) // Curried function. `addA` now has the `int a` parameter of `add` prefilled, and only takes one parameter `int b`

int c = addA(2) // 3

Overloading

Functions can be overloaded with different implementations of the same function. This is usually helpful when you want to have multiple parameter types, for example. However, because the language is strictly typed, each overload must have its own implementation.

fn sqrt(int n) -> float {
  // implementation for int
}

fn sqrt(float n) -> float {
  // implementation for float
}

fn sqrt(decimal n) -> float {
  // implementation for decimal
}

However, overloads can only differ by their parameters. So if you'd like to create two functions that take the same input and return different outputs, they must have different names.

fn sqrt(int n) -> float {
  // returns float
}

// This overload is not allowed as there's no way to know during compile time which function was used.
fn sqrt(int n) -> decimal {
  // returns decimal
}

// This is allowed because the function name is different, so it's not an overload.
fn sqrtDec(int n) -> decimal {
  // returns decimal
}

Loops

For loops:

for i in 1:20 {
  print(i) // Prints 1, 2, 3, ..., 20
}

for j in 1:2:20 { // You can increment/decrement by other values
  print(j) // Prints 1, 3, 5, ..., 19
}

While loops:

mut int j = 2
while j > 0 {
  j--
}
// `j` is available here since it was declared outside

Conditions

If-else statement:

float pi = 3.14
float euler = 2.71

if pi == 3.14 {
  print("Pi moment")
} else if euler == 2.71 {
  print("Euler moment")
} else {
  print("Not cool")
}

Pattern matching:

fn checkIfAnimal(string animal, string check) -> boolean {
  return animal == check
}

string animal = "Dog"
match animal {
  "Dog" {
    // Equivalent to `if animal == "Dog"`
    print("Woof")
  }

  "Cat" {
    print("Meow")
  }

  "Mouse" {
    print("Squeek")
  }

  checkIfAnimal("Bird") {
    // Can call functions that return a boolean value
    // The first value is prefilled with the value being checked, `animal` in this case
    // Equivalent to `if checkIfAnimal(animal, "Bird")`

    print("Chirp")
  }

  .startsWith("B") {
    // Can have methods of the type, for example String.startsWith()
    // Equivalent to `if animal.startsWith("B")`

    print("Starts with a B")
  }

  default {
    // Required to have a default
    print("Not a recognised pet")
  }
}

Operators

addition

1 + 2
// Also used for string and list concatenation

substraction

2 - 1

multiplication

3 * 2

division

5 / 2

exponent

2 ** 6

modulo

10 % 4

move bit left

1 << 4

move bit right

6 >> 1

equality

1 == 1 // always strict equality

less than

1 < 2

greater than

1 > 0

less than or equal to

5 <= 6
5 <= 5

greater than or equal to

7 >= 4
7 >= 7

Pipes

Similar to functional programming, we can use a |> pipe operator to pass values from one function to the next. This will automatically fill in the first value of the receiving function with the value being passed into it. For example, something like this:

fn square(int[] numbers) -> int[] {
  mut int[] numbers = numbers

  for i in numbers {
    numbers[i] = numbers[i] ** 2
  }

  return numbers
}

fn addToAll(int[] numbers, int adding) -> int[] {
  mut int[] numbers = numbers

  for i in numbers {
    numbers[i] += adding
  }

  return numbers
}

mut int[] numbers = [1, 2, 3, 4, 5]
numbers = square(numbers) // [1, 4, 9, 16, 25]
numbers = addToAll(numbers, 5) // [6, 10, 15, 21, 30]

can be turned into a pipeline, like so:

// Same function definitions as above

int[] numbers = [1, 2, 3, 4, 5]
                  |> square()
                  |> addToAll(5)

While this may seem pointless for a simple case like this, it becomes incredibly helpful for situations where a piece of data needs to be passed around a lot. For example:

// Some data to be transformed
DataStruct[] newData = data
                         |> map(someFn)
                         |> filter(someFilter)
                         |> sort(someSort)
                         |> take(10) // Take the first 10 values

Fractions and Decimals

Programming languages have had an issue with how to accurately represent decimal values. Most of them have followed the IEEE 754 standard for floating point values, and that works fine, but it comes with some limitations. I'm sure you've seen 0.1 + 0.2 == 0.3 resolving to false in most languages, including Python, C++, JavaScript, etc. I won't get into the reason why that is here, but what if we instead looked at math to handle that for us?

In math, any rational decimal value can be represented as a fraction. For example, 0.4 can be written as 4/10, and 0.333... can be written as 1/3. We can use this knowledge to create a Fraction data structure that looks like this:

struct Fraction {
  int numerator
  int denominator
}

Given that all numbers have to be rational in traditional programming languages anyway, we can get rid of the issues that irrational numbers like π can cause, simply by rounding them to a sane value. This will also allow values like 1/3 to be represented with full accuracy, and operations like multiplication and division can be massively simplified by multiplying or dividing the numerator or denominator. However, this will slightly complicate the implementations of addition and substraction, as an algorithm for the least common multiple of two denominators will need to be found.

Additionally, a struct will generally be slower to work with than a simple floating point value, but I believe the performance cost may be worth it for the accuracy it will bring. While the float data type will be consistent with IEEE 754 standards, the decimal data type (which will be slower but with higher accuracy) will be represented with this Fraction structure. However, whenever the value needs to be displayed, such as when it is printed to the screen, it will be converted to the float type by default, and can optionally be displayed as a fraction.

Released under the 0BSD license.