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")