Lily's block syntax is a unique blend with influences from C, Python, Ruby, and Rust. The most important parts are:

  • No semicolons. Lily can work out automatically when one expression stopts and another starts. This allows expressions to span multiple lines.

  • Lily uses curly braces to denote the scope of a block. Unlike most curly brace languages, Lily uses one set of curly braces to cover a whole block. In practice, it looks like this:

Here's an example in C.

int abc, def, ghi, jkl;

if (a == 1) {
    abc = 2;
    def = 3;
}
else: {
    ghi = 4;
    jkl = 5;
}

The same example in Lily.

var a = 1, abc = 0, def = 0, ghi = 0, jkl = 0

if a == 1: {
    abc = 2
    def = 3
else:
    ghi = 4
    jkl = 5
}

Truthiness

Lily allows several types to be checked for truthiness. Here are the different types and what Lily considers false for them.

| Type      | False value |
|-----------|-------------|
| `Boolean` | `false :P`  |
| `Double`  | `0.0`       |
| `Integer` | `0`         |
| `List`    | `[]`        |
| `String`  | `""`        |

if

Conditional execution of code.

define letter_for_grade(grade: Integer): String
{
    if grade >= 90: {
        return "A"
    elif grade >= 80:
        return "B"
    elif grade >= 70:
        return "C"
    elif grade >= 60:
        return "D"
    else:
        return "F"
    }
}

anonymous

Custom scope for a block of code.

{
    var v = 10
    print(v) # 10
}
{
    var v = "hello"
    print(v) # hello
}

do

Execute a block of code as long as a condition is met.

{
    var values: List[Integer] = []
    var i = 0

    do: {
        values.push(i)
    } while i

    print(values) # [0]
}
{
    var values: List[Integer] = []
    var i = 0

    # do supports break and continue

    do: {
        i += 1

        if i % 2: {
            continue
        elif i == 4:
            break
        else:
            values.push(i)
        }
    } while i < 5

    print(values) # [2]
}
{
    # Variables declared in a do cannot be used for the condition.
    #[
        values = []
        i = 0
        do: {
            if 1: {
                continue
            }

            var v = 10
        } while v == 10
    ]#
}

for

A for loop takes either a range, or a List.

The range style looks like this: for index in start...end (by iter). The part in parentheses is not required.

{
    var values: List[Integer] = []

    # Default increment is 1
    for i in 0...5: {
        values.push(i)
    }

    # Syntax error, because i is restricted to the for loop's scope..
    # print(i)
    print(values) # [0, 1, 2, 3, 4, 5]
}
{
    var values: List[Integer] = []

    # An existing local can be used for the increment.
    var index = 0

    # A custom increment can be specified.
    for index in 0...10 by 2: {
        values.push(index)
    }

    print(values) # [0, 2, 4, 6, 8, 10]
    print(index) # 10
}
{
    # For loops support break and continue
    var values: List[Integer] = []

    for i in 0...10: {
        if i % 2: {
            continue
        elif i == 6:
            break
        }

        values.push(i)
    }

    print(values) # [0, 2, 4]
}
{
    var values: List[Integer] = []

    # Modifications to the index and end are overwritten.
    var start = 0
    var end = 5

    for i in start...end: {
        values.push(i)
        end = start
        i = start
    }

    print(values) # [0, 1, 2, 3, 4, 5]
}

Version 2.2 introduces the list style: for (i,) elem in list.

If one variable is given, it is the element. If two are given, the first is the index and the second is the element.

This style allows either using existing variables or creating new ones. If using existing variables, they must be local variables.

{
    var elements: List[String] = []

    for elem in ["a", "b", "c"]: {
        elements.push(elem)
    }

    print(elements) # ["a", "b", "c"]
}
{
    var indexes: List[Integer] = []

    for i, elem in ["a", "b", "c"]: {
        indexes.push(i)
    }

    print(indexes) # [0, 1, 2]
}
{
    define range_fn {
        # Existing variables must be locals
        var i = 0
        var elem = ""

        for i, elem in ["a", "b", "c"]: {}

        print(i) # 3
        print(elem) # c
    }

    range_fn()
}

match

Match takes a subject and compares it to various patterns. The Option enum provides two variants: Some and None. Matching against an Option works as follows:

var v = Some("body")
var message = ""

match v: {
    case Some(s):
        # Unpack the value inside the Some.
        message = s

        # Each branch allows for multiple statements.
        print(s ++ s) # bodybody

    # This must be included, because match must be exhaustive.
    case None:
        # Branches can also be empty.
}

print(message) # body

Variant values can be skipped by using _ instead of a name.

enum Multival {
    One(String, Integer, Double),
    Two,

    define get_one_or_else(fallback: Integer): Integer {
        match self: {
            case One(_, i, _):
                return i
            case Two:
                return fallback
        }
    }
}

var v = One("abc", 123, 1.0)
var vtwo = Two

print(v.get_one_or_else(999)) # 123
print(vtwo.get_one_or_else(999)) # 999

Multiple variants can be tested together, as long as they are empty. else can also be used to group the remaining variants together.

enum RGB {
    Red,
    Blue,
    Green,

    define is_green: Boolean {
        match self: {
            case Green:
                return true
            case Red, Blue:
                return false
        }
    }

    define is_blue: Boolean {
        match self: {
            case Blue:
                return true
            else:
                return false
        }
    }
}

print(RGB.is_blue(Blue)) # true
print(RGB.is_green(Red)) # false

Match treats scoped and non-scoped enums the same.

scoped enum RGB {
    Red,
    Green,
    Blue,
}

define is_red(v: RGB): Boolean
{
    match v: {
        case Red:
            return true
        else:
            return false
    }
}

print(is_red(RGB.Red)) # true

Value enums and value variants are matched the same as enums and variants.

enum VRGB < Integer
{
    Red,
    Green = 2,
    Blue,

    define is_blue: Boolean {
        match self: {
            case Blue:
                return true
            else:
                return false
        }
    }
}

print(Green) # 2
print(Blue.is_blue()) # true

Match statements can also have a class as their subject. When matching to a user-defined class, branches must be classes that inherit from that class. Each branch will receive a single value. Matching against a class is only considered exhaustive if an else is present, even if all current possible child classes have been accounted for.

Match statements can also have a class as their subject. When matching against a class, each branch receives one value (the class instance). Match against a class is only considered exhaustive if an else case is present at the end.

Classes that have generics cannot be matched, because the interpreter does not store generic information at runtime.

class Point(public var @x: Integer, public var @y: Integer)
{
    public define add_Point(other: Point) {
        @x += other.x
        @y += other.y
    }
}

class Point3(x: Integer, y: Integer, public var @z: Integer) < Point(x, y)
{
    public define add_Point3(other: Point3) {
        add_Point(other)
        @z += other.z
    }

    public define all: List[Integer] { return [@x, @y, @z] }
}

define add_to_point3(source: Point3, other: Point)
{
    match other: {
        case Point(p):
            source.add_Point(p)
        case Point3(p):
            source.add_Point3(p)
        else:
            raise Exception("oh no")
    }
}

var p1 = Point(10, 20)
var p2 = Point3(100, 200, 300)

add_to_point3(p2, p1)
print(p2.all()) # [110, 220, 300]

Match against a class only works for an exact class type.

var v: Exception = ValueError("test")

match v: {
    case Exception(e):
        # This is unreachable!
        0/0
    case ValueError(e):
        print(e.message) # test
    else:
        # The else can be empty.
}

The above examples demonstrate various uses for match statements. Match can also be used within an expression. However, match expressions are limited. They must be used to either initialize a variable, or return a value. In a match expression, each branch yields the value of a single expression (empty branches are not allowed).

define return_some_or(a: Option[Integer], fallback: Integer): Integer
{
    return match a: {
        case Some(s):
            s
        case None:
            fallback
    }
}

define is_empty(o: Option[String]): Boolean
{
    var result = match o: {
        case Some(s):
            false
        case None:
            true
    }

    return result
}

print(return_some_or(Some(10), 200)) # 10
print(return_some_or(None, 200)) # 200

print(is_empty(Some("1"))) # false
print(is_empty(None)) # true

try

{
    var caught = false

    try: {
        1 / 0
    except IndexError:
        # This branch is ignored.
    except DivisionByZeroError:
        caught = true
    }

    print(caught) # true
}
{
    # A parent class of the raised exception can also be given.
    # Use "as" to capture the exception and use it.
    var str = "a1"
    var error_message = ""

    try: {
        str.parse_i().unwrap()
    except Exception as e:
        error_message = e.message
    }

    print(error_message) # unwrap called on None.
}

class MyValueError(message: String) < ValueError(message)
{  }

{
    # Errors can be raised through raise.
    # Subclass matching is allowed.
    var message = ""

    try: {
        raise MyValueError("Hello")
    except ValueError as e:
        message = e.message
    except DivisionByZeroError as e:
        0 / 0
    }

    print(message) # Hello
}

# Custom errors are also possible.
class MyError(message: String, code: Integer) < Exception(message)
{
    public var @code = code
}

{
    var code = 1

    try: {
        raise MyError("Oh no", 100)
    except MyError as e:
        code = e.code
    }

    print(code) # 100
}

while

var values: List[Integer] = []
var i = 0

while i != 5: {
    values.push(i)
    i += 1
}

print(values) # [0, 1, 2, 3, 4]
values = []
i = 0

# While loops support break and continue.

while i != 5: {
    if i % 2: {
        i += 1
        continue
    elif i == 4:
        break
    else:
        values.push(i)
        i += 1
    }
}

print(values) # [0, 2]

with

Match against a specific case.

var v = Some(1)

# Using match to extract a specific case.
match v: {
    case Some(s):
        print(s) # 1
    case None:
}

# Using "with" instead.
with v as Some(s): {
    print(s) # 1
}

# With allows else.
with v as None: {
else:
    print(v) # Some(1)
}

When used against a class, an exact class instance must be used.

var error: Exception = IndexError("")

with error as Exception(e): {
    # This will not be executed.
    0 / 0
}