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

Execute a block of code a set number of times.

{
    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]
}

foreach

Simplified for that automatically unpacks List elements.

var v = [1, 2, 3, 4]
var result: List[Integer] = []

foreach element in v: {
    result.push(element * element)
}

print(result) # [1, 4, 9, 16]

match

Exhaustive selection on an enum or class.

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

    match v: {
        # Decomposition creates 's' in this scope.
        # All fields in the variant must be decomposed.
        case Some(s):
            # Decomposition creates 's' in this scope.
            message = s
        case None:
    }

    print(message) # body

    #[
        # Syntax error, because decomposition must name all vars.

        match v: {
            case Some(s):
        }
    ]#
}

enum Color {
    Red,
    Blue,
    Green,
    RGB(Integer, Integer, Integer)
}

{
    var color = RGB(0xFF, 0xCC, 0xDD)

    # Use _ to skip decomposition.
    match color: {
        case RGB(_, blue, _):
            var f = "Custom color (blue: {}).".format(blue)

            print(f) # Custom color (blue: 204).
        case Red:
        case Blue:
        case Green:
    }

    # Empty variants can be multi-matched.
    match color: {
        case RGB(_, blue, _):
        case Red, Blue, Green:
    }
}

scoped enum ScopedColor {
    Red,
    Blue,
    Green,
    RGB(Integer, Integer, Integer)
}

{
    # When matching against a scoped enum, the scope must be provided.
    var color = ScopedColor.RGB(0xff, 0xcc, 0xdd)

    match color: {
        case ScopedColor.RGB(_, _, _):
        case ScopedColor.Red,
             ScopedColor.Blue,
             ScopedColor.Green:
    }
}

class MyValueError(message: String, code: Integer) < ValueError(message)
{
    public var @code = code
}

{
    var v: Exception = MyValueError("asdf", 1234)
    var code = 0

    # Match against a class always decomposes to one var.
    # Unlike try, match only works against the exact class.
    match v: {
        case ValueError(e):
            code = 1
        case MyValueError(e):
            code = e.code
        # An else case is required, but can be empty.
        else:
    }

    print(code) # 1234
}

class GenericError[A](message: String) < Exception(message)
{}

{
    #[
        # Cannot match against a generic class.
        var v: Exception = GenericError("")

        match v: {
            case GenericError(e):
            else:
        }
    ]#
}

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
}