LLuce

Statements and declarations

File structure#

A file holds, in any order: import lines, top-level let constants, struct declarations, and func declarations. There is no top-level executable code and no top-level var.

Entry#

A program is a script and requires exactly one func main():. It may declare -> !, in which case an uncaught error ends the run and is reported by the host.

An evaluator mode also exists — func evaluate(input: Input, output: Output): against a port schema — which is machinery kept for a deferred part of the system and not something a script uses.

func#

func name(param: Type, ...) -> Type:
func name(param: Type, ...) -> Type!:
func name(param: Type, ...) -> !:
func name(param: Type, ...):

A parameter may be prefixed give to take ownership of an object. Every path through a function that declares a value return type must return; the compiler checks it.

There are no default values, no named arguments, no variadics, no receivers and no first-class functions.

struct#

struct Name:
    field: Type
    ...

    func member(...):
        ...

Fields and namespaced functions, in one indented block. Construction names every field.

let and var#

let name = expr and let name: Type = expr bind once. var allows reassignment. var name: Type with no initializer declares the binding and leaves the slot at its type's zero value.

Neither freezes what the name points at: let xs = [1, 2] still permits xs.append(3), and refuses xs = [9]. This is JavaScript's const, not Swift's let.

No name may shadow one from an enclosing scope. A loop variable is immutable inside its body.

Assignment#

The target is a place: a name, a field, or an index, nested freely.

name = expr
name.field = expr
name.inner.field = expr
xs[i] = expr
grid[r, c] = expr
cells[0].value = expr

The place is read once — every subscript evaluated exactly once — and then rebuilt: value structs update functionally up to their root binding, and the innermost container element is written in place.

A nested place assigns a value (a number, a String, or a plain struct). To restock an object field use the single-level form: bag.items = [1, 2].

Compound assignment is += -= *= /= %=, value-only arithmetic — the place is a number, or a String for += — and the place is evaluated once, so counts[key] += 1 looks the key up a single time.

main.luc
func main():
    var counts = new Map(String, Int)
    counts["a"] = 0
    counts["a"] += 5
    var text = "x"
    text += "y"
    print(f"{counts["a"]} {text}")
Output
5 xy

if / elif / else#

if condition:
    ...
elif condition:
    ...
else:
    ...

The condition is Bool. There is no ternary operator and no switch.

while#

while condition:
    ...

for#

for name in range(low, high):     Int, excluding high
for name in sequence:             list or rank-1 array elements, in order
for name in map:                  map keys, in insertion order
for index, name in sequence:      position and element
for key, value in map:            key and value

The loop variable is immutable inside the body. Do not grow, shrink or free a collection while iterating it: bounds stay checked per step, but which elements are visited is the program's problem.

break, continue, return, try#

All four leave one or more scopes, and all four free whatever those scopes still own. return additionally moves the returned binding to the caller.

Expression statements#

A call may stand alone as a statement. A fallible call standing alone must be tryed or catched; ignoring it is luce.sema.fallible.

An ignored returned object is a statement temporary and is freed at the end of the statement.

File-scope constants#

A top-level let declares a compile-time constant.

main.luc
let width = 80
let margin = 4
let usable = width - margin * 2
let title = "loom"
let banner = title + " console"

func main():
    print(f"{banner}: {usable} columns")
Output
loom console: 72 columns

Initializers fold at compile time. What may be folded is: literals, other constants (including module.constant through an import), arithmetic, comparisons, and/or, string concatenation, Int()/Float(), and value-struct construction.

Calls are not constant. Heap objects are not constant either, and the reason is ownership rather than an arbitrary limit: a top-level binding has no scope to die at, so it cannot own an object, so new, list literals, slices and indexing are all refused there, as is a struct whose layout carries objects.

Constants may reference each other in any order but never in a cycle. Every use site inlines the folded value, so an unused constant costs nothing to ship, decode or compile.

Scope#

One scope per file (constants, structs, functions), per struct (its namespaced functions), and per function — where parameters and every indented block get one, so if, while and for bodies open nested scopes.

Nothing is visible without an import. There is no shadowing anywhere.

Dead code#

Functions unreachable from the entry point never reach the compiled module, so an unused import std.math costs nothing to ship, decode or compile.