The Lit Programming Language
Lit is a simple scripting language.
fn factorial_of { |n|
if n <= 1 do return 1
n * factorial_of(n - 1)
}
if let n = readln().to_i!() {
println("The factorial of {n} is {factorial_of(n)}")
} else {
println("Not a valid number")
}
I build it for fun while reading Crafting Interpreters. This site is more or less my vision for the language. Nothing is set in stone yet, and not all features are implemented.
Getting Started
To use Lit, you’ll have to build it from source. It is build using Crystal, so you’ll need to have that installed first.
- Install Crystal
- Build Lit
- Clone the repository
git clone https://github.com/lit-lang/lit
- Change into the directory
cd lit
- Build the project
tools/build
. This will create an executable atbin/lit
- Clone the repository
- Run a script with
bin/lit hello.lit
orbin/lit
to enter the REPL.
Syntax
Comments
Comments can appear at the end of a line or span multiple lines.
# Single-line comments
#= Multi-line comments
#=
can be nested
=# still a comment
=# "not here"
Literals
Lit has a few basic literal types: integers, floats, strings, booleans, functions, arrays, and maps.
Numbers
1 # integer
1.0 # float
1_000_000 # underscores are allowed
0.000_1 # even on the decimal side
Strings
# double-quoted strings can contain interpolation
"Hello, {"world"}"
# double-quoted strings can contain escape sequences
"I can\tescape\ncharacters"
# single-quoted strings are raw
'Hello, {"world"}'
'I will not escape \n characters'
# short-hand for single-quoted strings
:hey == 'hey' # => true
Strings support these escape codes:
\n
: newline\r
: carriage return\t
: tab\e
: escape- Any other character prefixed with
\
: that character
Booleans
In Lit, only nil
, false
and errors are falsey. Everything else
is truthy.
println(true and 0 and "" and fn {} and [] and {:} and "Truthy") # "Truthy"
println(false or "Falsey") # "Falsey"
Arrays
Arrays are ordered lists of values.
let arr = [1, 2, 3,] # trailing comma is allowed
# You can access elements in an array using the `[]` operator
arr[0] # => 1
# Use `[]=` to set an element in an array:
arr[0] = 4
println(arr) # => [4, 2, 3]
Maps
Maps are key-value pairs (separated by a colon), also known as dictionaries or hashes in other languages.
# Create a map
let user = {
"name" : "Yukihiro Matsumoto",
"role" : "creator", # trailing comma is allowed
}
# Access a value in a map
user["name"] # => "Yukihiro Matsumoto"
# Set a value in a map
user["name"] = "Matz"
println(user) # => {"name" : "Matz", "role" : "creator"}
# An empty map
let empty_map = {:}
{x: 0, y: 0} # same as {"x" : 0, "y" : 0}
Functions
# Function definition
fn greet { |name| println("Hello, {name}") }
# Function call
greet("Matz") # outputs "Hello, Matz"
# Named functions **aren't** closures
let name = "Matz"
fn greet { println("Hello, {name}") } # error: 'name' is not defined
# Anonymous functions are closures
let name = "Matz"
let greet = fn { println("Hello, {name}") } # outputs "Hello, Matz"
# single-expression functions can use the `do` syntax
fn greet do |name| println("Hello, {name}")
# They also support the `it` default parameter:
fn greet do println("Hello, {it}")
greet("Matz") # outputs "Hello, Matz"
# Blocks return their last expression
fn fun {
false
"something"
1
}
fun() # => 1
# you can use `return` to return early
fn fun { |value|
if value {
return "truthy"
}
"falsey"
}
fun(true) # => "truthy"
# you can create anonymous functions:
[1, 2, 3].map(fn { |x| x * 2 }) # => [2, 4, 6]
# use the do syntax for single-expression blocks:
[1, 2, 3].map(fn do it * 2) # => [2, 4, 6]
Variables
You can declare variables using let
or var
. Variables are scoped to the block
they are defined in. You can use let
for immutable bindings and var
for
mutable bindings.
let x = 1
x = 2 # error: cannot assign to immutable variable
var y = 1
y = 2 # ok
Operators
Lit supports a variety of operators for different operations.
-1 # negation
1 + 2 # addition
1 - 2 # subtraction
1 * 2 # multiplication
1 / 2 # division
1 % 2 # modulus
1 > 2 # comparison
1 < 2
1 >= 2
1 <= 2
1 == 2
These operators can be overloaded on custom types. See Operator Overloading for more details.
Augmented Assignment
Lit supports augmented assignment operators for convenience. These operators combine an operation with assignment, making code more concise:
var n = 5
n += 2 # same as n = n + 2
n -= 1 # same as n = n - 1
n *= 3 # same as n = n * 3
n /= 2 # same as n = n / 2
n %= 4 # same as n = n % 4
You can use these with any mutable variable (var
). Augmented assignments work
with any type that support the corresponding operator.
Boolean Operators
Lit provides the and
and or
operators for combining boolean expressions.
These operators short-circuit.
println(true and "yes") # => "yes"
println(false and "no") # => false
println(nil or "default") # => "default"
println("value" or "alt") # => "value"
Blocks & Scoping
Blocks are expressions. The last expression in a block is the return value. Because of that, you can use blocks to change the order of operations.
{1 + 2} * 3
# => 9
For a single-expression body, you can use the do
syntax:
fn debug do println("DEBUG: {it}")
debug("Hello, World!") # outputs "DEBUG: Hello, World!"
Control Flow
if
expressions
Lit uses if
as an expression, meaning it returns a value. You can use if
with or without else
and chain multiple conditions with else if
.
let n = 5
let result = if n > 10 {
"greater than 10"
} else if n > 5 {
"greater than 5"
} else {
"5 or less"
}
println(result) # outputs "5 or less"
The last expression in each block is the value returned by that branch. else
is optional; if omitted and no condition matches, the result is nil
.
You can also use the single-expression do
syntax for concise conditions:
let status = if n % 2 == 0 do "even" else do "odd"
println(status) # outputs "odd"
match
expressions
#todo
while
/ until
expressions
Lit provides while
and until
expressions for looping. Both forms are
expressions, so they return a value: the last expression evaluated in the
loop body or the value of break
if used.
while
repeats as long as the condition is truthy.until
repeats as long as the condition is falsey.
You can use break
to exit the loop early. break
can take an optional value, which becomes the value of the loop expression.
let i = 0
let result = while i < 5 {
if i == 3 {
break "stopped at 3"
}
i += 1
}
println(result) # outputs "stopped at 3"
let j = 0
until j == 2 {
println(j)
j += 1
}
# outputs:
# 0
# 1
Custom Types
Lit supports custom types using the type
keyword. Types are composed of one or
more variant
s. Each variant defines a constructor with fields and optional
methods. Fields and methods are public by default unless their name starts with
an underscore _
.
Defining a Type
To define a type with a single variant, use the following syntax:
type Point { |x, y|
fn to_s { "({x}, {y})" }
}
let origin = Point(0, 0)
println(origin) # outputs "(0, 0)"
Multiple Variants
Types can have multiple variants, each representing a different shape or case. This is useful for enums, tagged unions, or sum types.
type Shape {
variant Circle { |radius|
fn area { 3.14 * radius * radius }
}
variant Square { |side|
fn area { side * side }
}
fn kind {
match self {
Circle then "circle"
Square then "square"
}
}
}
let s1 = Shape.Circle(5)
println(s1.kind()) # outputs "circle"
println(s1.area()) # outputs "78.5"
let s2 = Shape.Square(3)
println(s2.kind()) # outputs "square"
println(s2.area()) # outputs "9"
Field and Method Visibility
Fields are only directly accessible inside the variant block. Fields starting
with _
are private and cannot be matched.
Methods starting with _
are private and cannot be called from outside the type
declaration.
type User { |name, _password|
fn check_password do it == _password # please don't use this in production
fn _debug { println("user={name}") }
}
let user = User("alice", "secret")
match user {
Account { |name, _password| #= body =# } # error: _password is private
}
user.check_password("guess") # => false
user._debug() # error: _debug is private
Operator Overloading
Lit supports operator overloading for custom types. You can define how operators behave for your custom types by implementing the corresponding methods. Here’s a list of the currently supported operators and the methods you need to implement in your type to overload them:
Operator | Method to implement |
---|---|
unary - | neg |
+ | add |
- | sub |
* | mul |
/ | div |
% | mod |
== | eq |
!= | neq |
< | lt |
<= | lte |
> | gt |
>= | gte |
[] | get |
[]= | set |
stringify (i.e. println ) |
to_s |
Modules (#todo)
- How to group related types and functions
Imports
You can now use the import
keyword to import other Lit files. They work
similar to Ruby’s require_relative
. There’s not module system yet, so all the
files imported this way share the same global scope.
import "foo" # imports foo.lit from the same directory
import "../bar" # imports bar.lit from the parent directory
import "baz.lit" # adding the `.lit` extension is optional
Error messages include the file name and line number, to help you find the error source.
Collections & Iteration (#todo)
- Implementing iteration for custom types with
Error Handling
- How to handle dangerous operations