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
}
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` | `""` |
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"
}
}
Custom scope for a block of code.
{
var v = 10
print(v) # 10
}
{
var v = "hello"
print(v) # hello
}
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
]#
}
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 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
{
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
}
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]
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
}