Lightning Script Examples
Complete, runnable programs covering the core features of Lightning Script. Open any example in the playground or copy it for local use.
01 Hello 01_hello.li
LIGHTNING
const language = "Lightning Script"
const version = 1
print(`Hello from {language}!`)
print(`This is documentation example {version}.`)
print("2 + 3 =", 2 + 3)
02 Fibonacci 02_fibonacci.li
LIGHTNING
fn fibonacci(limit) {
let values = []
let previous = 0
let current = 1
for index in 0..limit {
values::push(previous)
const next = previous + current
previous = current
current = next
}
values
}
const sequence = fibonacci(12)
print("First twelve Fibonacci numbers:")
for index, value in sequence {
print(`fib({index}) = {value}`)
}
03 Strings 03_strings.li
LIGHTNING
const single = 'S'
const double = "double-quoted string with a newline\non the second line"
const raw = [=[C:\projects\lightning\{literal}.li]=]
const name = "Mira"
const completed = 4
const total = 7
const message = `Hello, {name}: {completed + 1} of {total} tasks complete`
print("Single-quoted literal:", single)
print(double)
print(`Raw path: {raw}`)
print(message)
print(`Literal braces look like {{this}}.`)
04 Control Flow 04_control_flow.li
LIGHTNING
fn first_even(values) {
defer { print("finished searching") }
for index, value in values {
if value < 0 {
continue
}
if value % 2 == 0 {
return {index: index, value: value}
}
}
nil
}
let countdown = 3
while countdown > 0 {
print(`launch in {countdown}`)
countdown -= 1
}
try {
const result = first_even([-5, 3, 7, 12, 15])
if result {
print(`first nonnegative even value: {result.value} at index {result.index}`)
} else {
throw "no nonnegative even value found"
}
} catch error {
print(`search failed: {error}`)
}
for value in 0..10 {
if value == 6 {
break
}
if value % 2 != 0 {
continue
}
print(`range value: {value}`)
}
05 Closures 05_closures.li
LIGHTNING
fn make_counter(start, step?) {
const increment = step ?? 1
let current = start
|| {
current += increment
current
}
}
let label = "captured before reassignment"
const read_label = || label
label = "outer binding changed"
const by_one = make_counter(0)
const by_five = make_counter(10, 5)
print(read_label())
print("by one:", by_one(), by_one(), by_one())
print("by five:", by_five(), by_five())
fn describe(prefix?, values...) {
const heading = prefix ?? "values"
print(`{heading} ({values::len()} items)`)
for index in 0..values::len() {
print(` {index}: {values[index]}`)
}
}
describe("counter snapshots", by_one(), by_five(), 42::str())
06 Structs Classes 06_structs_classes.li
LIGHTNING
# Struct values copy at strict boundaries; class values share identity.
struct Point {
x: number = 0
y: number = 0
new!(x: number, y: number) {
self.x = x
self.y = y
}
add!(other: Point) -> Point {
let result = struct_copy(self)
result.x += other.x
result.y += other.y
result
}
get length_squared() -> number {
self.x * self.x + self.y * self.y
}
}
[[strict]] fn translated(point: Point, dx: number, dy: number) -> Point {
point.x += dx
point.y += dy
point
}
let start = Point(3, 4)
let moved = translated(start, 5, -2)
assert(start.x == 3 && start.y == 4)
assert(moved.x == 8 && moved.y == 2)
assert(start.length_squared == 25)
let total = start + Point(1, 2)
assert(total.x == 4 && total.y == 6)
# Dynamic struct boxes can be copied explicitly.
let boxed_copy = struct_copy(start)
boxed_copy.x = 30
assert(start.x == 3 && boxed_copy.x == 30)
class Account {
owner: string = ""
_balance: number = 0
new!(owner: string, opening_balance: number) {
self.owner = owner
self.balance = opening_balance
}
get balance() -> number { self._balance }
set balance(value: number) {
if value < 0 {
throw "balance cannot be negative"
}
self._balance = value
}
deposit(amount: number) -> number {
self.balance = self.balance + amount
self.balance
}
add!(amount: number) -> number {
self.balance + amount
}
at!(key) {
if key == "summary" {
return `{self.owner}: {self.balance}`
}
nil
}
}
let primary = Account("Ada", 125)
let alias = primary
assert(alias is Account)
assert(alias.deposit(25) == 150)
assert(primary.balance == 150)
assert(primary + 50 == 200)
assert(primary.summary == "Ada: 150")
print("point", moved.x, moved.y)
print("account", primary.owner, primary.balance)
07 Collections 07_collections.li
LIGHTNING
import "array" as arrays
import collections
import "table" as tables
# Tables provide hashed keys and arrays provide contiguous positional storage.
let inventory = {apples: 4, pears: 2}
tables.reserve(inventory, 32)
inventory["oranges"] = 6
inventory.apples += 1
assert(inventory.apples == 5)
assert(inventory["oranges"] == 6)
let queue = ["parse", "compile"]
arrays.reserve(queue, 16)
queue::push("execute")
assert(queue::len() == 3)
arrays.resize(queue, 5)
assert(queue[3] == nil && queue[4] == nil)
arrays.fill(queue, "pending", 3, 5)
assert(queue[3] == "pending" && queue[4] == "pending")
arrays.resize(queue, 3)
const scores = [12, 7, 19, 10, 7]
const doubled = collections.map(scores, |value, index| value * 2)
assert(doubled[0] == 24 && doubled[2] == 38)
const passing = collections.filter(scores, |value, index| value >= 10)
assert(passing::len() == 3)
const total = collections.reduce(scores, 0,
|sum, value, index| sum + value)
assert(total == 55)
let indexed = {}
collections.each(scores, |value, index| {
indexed[index] = value
})
assert(indexed[2] == 19)
assert(collections.any(scores, |value, index| value > 15))
assert(collections.all(scores, |value, index| value >= 0))
assert(collections.find(scores, |value, index| value % 2 == 1) == 7)
const records = [
{name: "Ada", score: 12},
{name: "Grace", score: 7},
{name: "Lin", score: 12}
]
const ordered = collections.sort(records,
|left, right| left.score < right.score)
assert(ordered[0].name == "Grace")
assert(ordered[1].name == "Ada")
assert(ordered[2].name == "Lin")
assert(records[0].name == "Ada")
const names = collections.keys(inventory)
const counts = collections.values(inventory)
assert(names::len() == 3 && counts::len() == 3)
const assignments = collections.zip(
["Ada", "Grace", "Lin"],
["parser", "runtime"]
)
assert(assignments::len() == 2)
assert(assignments[0][0] == "Ada")
assert(assignments[0][1] == "parser")
print("score total", total)
print("passing", passing::len())
print("assignments", assignments::len())
08 Typed Arrays 08_typed_arrays.li
LIGHTNING
import math
import typed
# A byte buffer stores values densely and validates every write.
let packet = u8[]([76, 73, 1, 0])
assert(typed.len(packet) == 4)
assert(packet::len() == 4)
assert(typed.capacity(packet) == 4)
assert(typed.element_size(packet) == 1)
assert(typed.kind(packet) == "u8")
assert(typed.get(packet, 0) == 76)
typed.set(packet, 3, 7)
assert(packet[3] == 7)
typed.reserve(packet, 32)
assert(packet::len() == 4)
assert(typed.capacity(packet) >= 32)
typed.resize(packet, 8)
assert(packet::len() == 8)
assert(packet[4] == 0 && packet[7] == 0)
let packet_copy = typed.dup(packet)
packet_copy[0] = 0
assert(packet[0] == 76)
assert(packet_copy[0] == 0)
# Integer arrays never wrap or truncate their inputs.
let signed_samples = i16[]([-32768, -10, 0, 32767])
let counters = u32[]([0, 1000, 4294967295])
assert(signed_samples[0] == -32768)
assert(counters[2] == 4294967295)
let safe_signed = i64[]([-9007199254740991, 9007199254740991])
let safe_unsigned = u64[]([0, 9007199254740991])
assert(safe_signed[1] == 9007199254740991)
assert(safe_unsigned[1] == 9007199254740991)
# f32 stores round once to binary32; f64 preserves binary64 values.
let positions = f32[]([0.1, 16777217, -0.1])
assert(positions[1] == 16777216)
let measurements = f64[]([math.pi, math.inf, math.nan])
assert(measurements[0] == math.pi)
assert(measurements[1] == math.inf)
assert(math.isnan(measurements[2]))
let gains = f32[](4)
typed.fill(gains, 0.5)
assert(gains[0] == 0.5 && gains[3] == 0.5)
# All packed numeric element kinds are available through T[] constructors.
assert(i8[]([-128, 127])::len() == 2)
assert(u16[]([0, 65535])[1] == 65535)
assert(i32[]([-2147483648, 2147483647])[1] == 2147483647)
print("packet bytes", packet::len(), typed.capacity(packet))
print("f32 rounded", positions[1])
09 Coroutines 09_coroutines.li
LIGHTNING
import coroutine
// A resume value is returned from the suspended yield expression.
const conversation = coroutine.create(|| {
const first_reply = coroutine.yield("ready")
const second_reply = coroutine.yield(`first reply: {first_reply}`)
return `second reply: {second_reply}`
})
assert(coroutine.status(conversation) == "created")
assert(coroutine.resume(conversation) == "ready")
assert(coroutine.status(conversation) == "suspended")
assert(coroutine.resume(conversation, "hello") == "first reply: hello")
assert(coroutine.resume(conversation, "goodbye") == "second reply: goodbye")
assert(coroutine.status(conversation) == "dead")
// Closing a suspended coroutine unwinds it and executes deferred cleanup.
let cleanup = []
const worker = coroutine.create(|| {
defer { cleanup::push("outer") }
defer { cleanup::push("inner") }
coroutine.yield(1)
cleanup::push("unreachable")
})
assert(coroutine.resume(worker) == 1)
coroutine.close(worker)
assert(coroutine.status(worker) == "dead")
assert(cleanup::len() == 2)
assert(cleanup[0] == "inner")
assert(cleanup[1] == "outer")
print("coroutines: ok")
10 Math Vectors 10_math_vectors.li
LIGHTNING
import math
import vec3
fn close(actual, expected) {
math.abs(actual - expected) < 0.000001
}
# Constants and elementary functions use IEEE 754 binary64 numbers.
assert(math.pi > 3.14 && math.pi < 3.15)
assert(math.e > 2.71 && math.e < 2.72)
assert(math.inf > math.huge)
assert(math.nan != math.nan)
assert(math.epsilon > 0)
assert(math.small > 0)
assert(close(math.sin(math.pi / 2), 1))
assert(close(math.cos(0), 1))
assert(close(math.tan(math.pi / 4), 1))
assert(close(math.asin(1), math.pi / 2))
assert(close(math.acos(0), math.pi / 2))
assert(close(math.atan(1), math.pi / 4))
assert(close(math.atan2(1, 1), math.pi / 4))
assert(math.abs(-9) == 9)
assert(math.sqrt(81) == 9)
assert(math.ceil(2.1) == 3)
assert(math.floor(2.9) == 2)
assert(math.trunc(-2.9) == -2)
assert(math.min(4, 7) == 4)
assert(math.max(4, 7) == 7)
assert(math.clamp(14, 0, 10) == 10)
assert(math.lerp(10, 20, 0.25) == 12.5)
assert(math.sign(-3) == -1)
assert(math.gcd(84, 30) == 6)
assert(math.lcm(12, 18) == 36)
assert(math.isnan(math.nan))
assert(math.isfinite(math.pi))
assert(math.isinteger(42))
assert(!math.isinteger(42.5))
const pseudorandom = math.random()
const system_random = math.srandom(5, 8)
assert(pseudorandom >= 0 && pseudorandom <= 1)
assert(system_random >= 5 && system_random <= 8)
# Native vec3 values store f32 components and support vector arithmetic.
const position = vec3::new(1, 2, 3)
const velocity = vec3::new(4, -1, 2)
const next = position + velocity * 0.5
assert(next.x == 3 && next.y == 1.5 && next.z == 4)
assert(position.dot(velocity) == 8)
const normal = vec3::new(1, 0, 0).cross(vec3::new(0, 1, 0))
assert(normal == vec3::new(0, 0, 1))
const direction = vec3::new(3, 4, 0).normalize()
assert(close(direction.x, 0.6))
assert(close(direction.y, 0.8))
assert(close(direction.length(), 1))
const midpoint = position.lerp(velocity, 0.5)
assert(midpoint == vec3::new(2.5, 0.5, 2.5))
print("next position", next::str())
print("unit direction", direction::str())
11 Defer Errors 11_defer_errors.li
LIGHTNING
// Deferred blocks run on normal return and exceptional unwind.
let events = []
fn divide(numerator, denominator) {
defer { events::push("leave divide") }
if denominator == 0 {
throw "division by zero"
}
return numerator / denominator
}
assert(divide(12, 3) == 4)
assert(events::len() == 1)
assert(events[0] == "leave divide")
let caught = nil
try {
divide(12, 0)
} catch error {
caught = error
events::push("caught error")
}
assert(caught == "division by zero")
assert(events::len() == 3)
assert(events[1] == "leave divide")
assert(events[2] == "caught error")
// Multiple deferred blocks in one scope run in last-in, first-out order.
fn nested_cleanup() {
defer { events::push("first defer") }
defer { events::push("second defer") }
events::push("body")
return 42
}
assert(nested_cleanup() == 42)
assert(events[3] == "body")
assert(events[4] == "second defer")
assert(events[5] == "first defer")
print("defer and errors: ok")
12 Shared Concurrency 12_shared_concurrency.li
LIGHTNING
import coroutine
import shared
// Shared values live in the process-wide shared heap.
const state = shared {completed: 0, checksum: 0}
assert(shared.is_shared(state))
// A lock block protects a compound update and unlocks on every exit path.
fn record(value) {
lock state {
state.completed += 1
state.checksum += value
}
}
record(10)
assert(state.completed == 1)
assert(state.checksum == 10)
// atomic_add performs one numeric field update and returns the new value.
assert(shared.atomic_add(state, "completed", 1) == 2)
// An atomic block commits related numeric updates together.
atomic {
state.completed += 3
state.checksum = state.checksum * 2
}
assert(state.completed == 5)
assert(state.checksum == 20)
// Coroutines are cooperative rather than parallel, but the same update
// functions are safe when shared values are published to host worker threads.
fn make_worker(base) {
coroutine.create(|| {
for offset in 0..3 {
record(base + offset)
coroutine.yield(state.completed)
}
return state.checksum
})
}
const left = make_worker(1)
const right = make_worker(100)
assert(coroutine.resume(left) == 6)
assert(coroutine.resume(right) == 7)
assert(coroutine.resume(left) == 8)
assert(coroutine.resume(right) == 9)
assert(coroutine.resume(left) == 10)
assert(coroutine.resume(right) == 11)
const expected_checksum = 20 + 1 + 100 + 2 + 101 + 3 + 102
assert(coroutine.resume(left) == expected_checksum)
assert(coroutine.resume(right) == expected_checksum)
assert(coroutine.status(left) == "dead")
assert(coroutine.status(right) == "dead")
assert(state.completed == 11)
assert(state.checksum == expected_checksum)
print("shared concurrency: ok")