Declarations

Variables must be declared before they are used.

# Declare one variable.
var a_number = 10

# Multiple variables can be declared at once.
var a_double = 1.23,
    greeting = "Hello",
    numbers = [1, 2, 3]

# A type can be provided as well.
var doubles: List[Double] = []

# A cast can also be used to supply a type.
var strings = [].@(List[String])

Lily uses type inference to determine a var's type when it's declared. In most cases, a type does not have to be given. The type of a variable cannot be changed.

Constants

Constants cannot be modified after they are initialized. Additionally, their initialization must be done with a single literal. A constant must be a Boolean, Integer, Double, String, or ByteString.

constant magic_number = 55

constant minus_five = -5.5

constant greeting = "Hello"

# Invalid, even though the expression only involves literals
# constant count = 2 + 2

Comments

Lily has three kinds of comments. One for a line, one for a block, and one for documentation.

# This is a line comment.
# var w = 10

# This is a block comment.
var w = #[ 10 ]# 20

#[
    Block comments can span multiple lines.
    This is a block comment.
    It can span multiple lines, but doesn't have to.
    These don't support nesting.
]#

### This is a docblock.
### If you use docgen to generate documentation, it will include information in
### docblocks.
### The "docgen" section has more info on how to write docblocks.
define example
{

}

Data types

Lily comes with several data types already defined. A few of those data types have dedicated syntax to make them easier to use.

Boolean

# Boolean can be true or false.

var is_open = true
var is_warm = false

Byte

Byte ranges from 0 to 255 inclusive.

var space = ' '

# Escape codes are allowed.
var tab_byte = '\t'
var byte_127 = 0x7Ft

# You can specify a type to coerce an Integer.
var byte_a: Byte = 12
var byte_b = 12.@(Byte)

ByteString

A series of bytes. Can include \0 and may have invalid utf-8. By default, ByteString literals only span one line.

var bytes = B"123456"

# Use triple quotes to span multiple lines.
# A ByteString can use any escape codes mentioned below.
var multiline_bytes = B"""
    this
    spans
    multiple
    lines
"""

# ByteString allow subscripts and subscript assignment.
print(bytes[0]) # '1'
bytes[0] = '2'
print(bytes) # 223456

# Negative indexes begin from the end.
print(bytes[-1]) # '6'

Double

A very small or very large value.

var small_double = 0.000004
var large_double = 10e1
var negative_double = -1.5

Hash

A key to value mapping. The key must be Integer or String. The value can be any type. The type of a Hash is written as Hash[key, value].

var number_table = [
    "One" => 1,
    "Two" => 2,
    # A trailing comma is allowed on the last value.
    "Three" => 3,
]
var table_value = number_table["Two"]

# New entries can be created by assignment.
number_table["Four"] = 4

# This raises KeyError, because the key does not exist.
# var bad_key = number_table["Twenty"]

# Specifying a type for an empty Hash.
var empty_hash: Hash[Integer, List[String]] = []
var empty_cast_hash = [].@(Hash[Integer, List[String]])

# If a Hash contains duplicates, the right-most key "wins".
# This becomes [1 => "qwerty", 2 => "hjkl"]
var duplicate_hash = [
    1 => "asdf",
    2 => "hjkl",
    1 => "qwerty",
]

Integer

A 64-bit signed value.

var simple_integer = 12345
var negative_int = -67890
var octal = 0c744
var hex = 0xFF
var bin = 0b1010101

List

A container of values with the same type. The type of a List is written as List[element type].

var integer_list = [1, 2, 3]
var trailing_list = [
    4,
    5,
    # Trailing comma is optional here.
    6,
]

# Indexing starts at 0.
var first_number = integer_list[0]

# Negative indexes start from the end.
var last_number = integer_list[-1]

# This raises IndexError, because the index is out of range.
# var bad_index = integer_list[20]

# Indexing by Byte is also possible.
first_number = integer_list[0t]

# Specifying a type to an empty List.
var empty_list: List[String] = []
var empty_cast_list = [].@(List[String])

String

A block of text that is utf-8 valid. Internally, String is also zero terminated.

var message = "Hello"
var multi_line = """\
    This
    spans
    multiple
    lines
"""

# String allows subscripts, but not subscript assignment.
print(message[0]) # 'H'

# Negative indexes begin from the end.
print(message[-1]) # 'o'

Tuple

A List of a fixed size, except it allows each element to be a different type. Empty Tuple values are not allowed.

The type of a Tuple is written as Tuple[type 1, type 2, ...].

var record = <[1, "left", [1]]>

# Tuple subscripts must be literals.
var record_list = record[2]

# It is a syntax error if a Tuple index is not an integer, or out of range.
# var bad_tuple_index = record[5]

Unit

The return type of a Function that doesn't specify aFunctions that don't specify a return type. The only instance of Unit is unit.

var my_unit = unit

Magic constants

The following are "magic" keywords. When they are invoked, they are replaced with a literal.

__dir__ is a directory relative to the first file imported.

__file__ is the name of the current file.

__function__ is the name of the current function. If outside of a definition, this returns __main__ for the first import, or __module__ for subsequent imports.

__line__ is the current line number.

Escape codes

When writing a Byte, ByteString, or String, the following escape sequences are allowed. In the case of String, sequences that specify a value over 127 or 0 are not allowed.

\a: Terminal bell (\007)

\b: Terminal backspace (\008)

\t: Tab (\009)

\n: A newline (\010)

\r: A carriage return (\013)

\": The " character.

\': The ' character.

\\: The \ character.

\/: \ on Windows, / elsewhere.

\nnn: Base 10, 0 to 255 inclusive. \0 is equivalent to \000.

\xNN: Hex, \x00 to \xFF inclusive. \x0 is equivalent to \x00.

\<newline>: Bytestring and String only. The newline of the current line and leading whitespace ( or \t) will be omitted.

Precedence

Lily's precedence table, from highest to lowest. Assignments share the same priority, but are spread apart for readability.

| Operator              | Description                       |
|-----------------------|-----------------------------------|
| / % *                 | Divide, modulo, multiply          |
| - +                   | Minus, plus                       |
| << >>                 | Left / right shift                |
| & | ^                 | Bitwise and / or / xor            |
| |>                    | Function pipe                     |
| ++                    | Concatenate                       |
| >= > < <= == !=       | Comparison and equality           |
| &&                    | Logical and                       |
| ||                    | Logical or                        |
|=======================|===================================|
| *= /= %=              | Multiply / divide / modulo assign |
| -= +=                 | Plus / minus assign               |
| <<= >>=               | Left / right shift assign         |
| &= |= ^=              | Bitwise and / or / xor assign     |
| =                     | Simple assignment                 |

Operations such as x.y member lookup and subscripts take over either the entire current value, or the right side of the current binary operation.

Operations

The following operations are supported:

| Operation              | Types => Result                   |
|------------------------|-----------------------------------|
| Math (+ - * % /)       | Byte,    Byte    => Integer       |
|                        | Integer, Byte    => Integer       |
|                        | Integer, Integer => Integer       |
|                        | Integer, Double  => Double        |
|                        | Double,  Double  => Double        |
|========================|===================================|
| Shift (<< >>)          | Integer, Byte    => Integer       |
|                        | Integer, Integer => Integer       |
|========================|===================================|
| Relational (<= < > >=) | (The result is always Boolean)    |
|                        | Byte,    Byte                     |
|                        | Byte,    Integer                  |
|                        | Integer, Integer                  |
|                        | Integer, Double                   |
|                        | Double,  Double                   |
|                        | String,  String                   |
|========================|===================================|

Value variants (see enum documentation) are treated as Integer values.

Equality

Equality (== !=) is only allowed when both types are equivalent to each other. The only exception is Integer, which can be compared to both Byte and Double.

For primitive numeric values, equality works as expected.

Hash, List, and Tuple, values are equal if they're structurally equal to each other. [1] == [1] is true.

Variants such as None and Some also use structural equality. Some(1) == Some(1) is true.

All other predefined data types and user-defined classes use exact equality. Exception("") == Exception("") is false. They're only equivalent to each other if they're the same instance.

Value variants (see enum documentation) are treated as Integer values here too.

Interpolation

The ++ operator, String.format, and print all make use of built-in interpolation. Every kind of a value can be interpolated, and has a set way of being interpolated.

print(true)                    # true
print(' ')                     # ' '

# ByteString values have escape codes written out.
print(B"test\r\n")             # test\r\n
print(1.5)                     # 1.5

print(stdout)                  # <open file at 0x--->
print(print)                   # <built-in function print>

# Hash order is not guaranteed.
print([1 => "2"])              # [1 => "2"]
print(12)                      # 12
print([1, 2, 3])               # [1, 2, 3]
print("asdf")                  # asdf
print("Hello")                 # Hello
print(<[1, "2"]>)              # <[1, "2"]>
print(unit)                    # unit

# Variants print how they're written.
# If Option was scoped, this would say "Option.Some(1)".
print(Some(1))                 # Some(1)

# None doesn't have a full type, so a cast is necessary.
# The type doesn't matter.
print(None.@(Option[Integer])) # None

# Classes always print their address.
print(Exception(""))           # <Exception at 0x--->