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 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
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
{
}
Lily comes with several data types already defined. A few of those data types have dedicated syntax to make them easier to use.
# Boolean can be true or false.
var is_open = true
var is_warm = false
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)
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'
A very small or very large value.
var small_double = 0.000004
var large_double = 10e1
var negative_double = -1.5
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",
]
A 64-bit signed value.
var simple_integer = 12345
var negative_int = -67890
var octal = 0c744
var hex = 0xFF
var bin = 0b1010101
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])
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'
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]
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
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.
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.
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.
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 (== !=
) 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.
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--->