Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

User-defined Functions

Define a function with #name(params) syntax followed by a block in braces:

#greet() {
  write("hello")
}

greet()

Parameters and Return Values

Functions take parameters and return the last expression’s value. There’s no return keyword.

Python:

def add(a, b):
    return a + b

lil:

#add(a, b) {
  a + b
}

result = add(3, 4)
write(result)

Practical Examples

Convert Fahrenheit to Celsius:

#fahr_to_celsius(f) {
  (f - 32) * 5 / 9
}

temp = fahr_to_celsius(100)
write(temp)

Roll a die using a library function inside a user function:

#roll_dice() {
  randint@math(1, 6)
}

roll = roll_dice()
write("you rolled", roll)

Check if a number is even:

#is_even(n) {
  n % 2 == 0
}

x = is_even(10)
write(x)

Scope

Each function call gets its own scope. Parameters and local variables are isolated to that call and don’t leak:

#a(x) {
  x = x + 10
  x
}

#b(x) {
  x = x * 2
  x
}

write(a(5))    : 15
write(b(5))    : 10
write(a(100))  : 110

Two functions with the same parameter name don’t interfere with each other:

#a(x) {
  write(x)
}

#b(x) {
  a(x + 1)
  write(x)
}

b(100)

Variables created inside a function are local to that call and cleaned up when the function returns:

#outer() {
  x = 99
  inner()
  write(x)
}

#inner() {
  x = 42
}

outer()

Variables inside a function create new bindings in the function’s scope. They don’t modify outer-scope variables of the same name:

x = 10

#set_x(val) {
  x = val
}

set_x(42)
write(x)

Include

Functions defined in other files can be loaded with include:

include dice

roll = roll_dice()
write(roll)

This loads and runs dice.lil, making its functions available. For example, dice.lil might contain:

#roll_dice() {
  randint@math(1, 6)
}

Then your main script includes it and uses roll_dice like any local function.

Selective import: specify which functions to import by appending / and function names:

include dice.lil/roll_dice flip_coin

Only roll_dice and flip_coin are registered from dice.lil. Other functions are discarded. The file runs in an isolated variable scope.

Recursion

Functions can call themselves:

#fib(n) {
  if n <= 2 {
    1
  } else {
    fib(n - 1) + fib(n - 2)
  }
}

write(fib(10))

lil’s functions work in both interpreted and compiled (AOT) mode.

Anonymous Functions (Closures)

Functions without a name can be created inline with #(params) { body }:

double = #(x) { x * 2 }
write(double(5))

The real power is passing them to higher-order library functions:

include list

nums = new@list
push@list(nums, 1)
push@list(nums, 2)
push@list(nums, 3)

doubled = map@list(nums, #(x) { x * 2 })
write(doubled)

Anonymous functions see variables from the scope where they’re defined:

prefix = "> "
#add_prefix(s) { prefix + s }
write(add_prefix("hello"))

The anonymous function creates an internal name, registers in the function table, and returns the name as a string. map@list and filter@list resolve it automatically.