lippy.

Types and behaviour

What lippy's values are and how they behave. A companion to the Syntax reference; start with the Guide if you're new.

The types

Seven of them — the whole list fits in your head:

Type Literals Notes
number 42, 3.14 one numeric type, no int/float split
string "hello, {name}", `raw text` immutable; interpolation built in
bool true, false
nothing nothing the absence of a value
list [1, 2, 3] mutable, ordered, 1-based
map {"key": "value"} mutable, string keys, keeps insertion order
function fun greet(who) … end defined, not written inline; a value like any other

A range (1 to 100) is expression syntax for loops and functions like random — not a type you store. reply "not found", 404 is similar: a value serve understands, made and handed straight back. It takes an optional third argument for the kind of answer — reply page, 200, "html" serves a rendered page as HTML.

Functions are values

A function has no literal form — you get one by defining it with fun and naming it. The name is an ordinary lookup, so a function can be assigned, stored, passed and returned:

fun greet(who)
    return "hello, {who}"
end

g is greet                 # the function itself, not a call
say g                      # prints: fun greet
say g("world")             # prints: hello, world

routes is {"/": greet}     # functions live in lists and maps like anything else

The one rule: a lone name calls the function when it stands alone as a whole statement (beep), because there's nothing else it could mean there. Everywhere else a name is just a lookup, so b is beep stores the function.

Built-in functions are values too — sort names by len, f is upper, and passing len to your own function all work. The catch is the same as for your functions: say now prints fun now, not the time; calling still needs the parens, now().

Numbers

One numeric type: a 64-bit float, like Lua and JavaScript. Whole numbers stay exact up to about ±9 quadrillion — more than any script will meet.

Truthiness

if x, not x, and, or, stop when x all use one table. Empty things are false:

Falsy Truthy
false true
nothing any other number (including negatives)
0 any non-empty string (including "false"!)
"" any non-empty list or map
[], {} functions, results, everything else

This is what shell scripts want: if not folder then catches both "no argument given" (nothing) and "empty argument given" ("") in one line.

and/or short-circuit and return the deciding value, so name is args at 1 or "default" works — though the style to reach for is if and truthiness.

Equality

equals never coerces — the JavaScript "1" == 1 trap doesn't exist here:

Ordering

greater than, less than, at least, at most:

Reaching into strings

s at n is the nth character, slice(s, from, to) a substring, index_of(s, part) the position of a part (nothing if absent), repeat(s, n) the string n times. All 1-based, inclusive, and counted in characters, not bytes"café" at 4 is "é", and len agrees.

Strings don't add

+ is for numbers only. Building strings has one obvious way — interpolation:

say "found {count} files in {folder}"     # yes
greeting is "hello, " + name              # error: + is for numbers
                                          # hint: try "hello, {name}"

The place interpolation gets in the way is text that contains braces or backslashes — that's what raw strings are for.

Mutability and copying

nothing, missing things, and when lippy stops you

The rule: absence of data is normal; a wrong program is not.

Situation Result Why
services at "nope" (missing map key) nothing absence is data
args at 9 (past the end of a list) nothing scripts probe arguments
first of [] nothing same reason
using an undefined variable error + did-you-mean that's a typo, not data
number("forty") error naming the value garbage in should be loud
5 less than "hat" error showing both types there's no right answer
arithmetic on non-numbers error + hint + is for numbers

To tell "missing" from "set to nothing", ask with contains: if services contains "gitea" then (on maps contains asks about keys; on strings and lists, about contents).

attempt — when a stop should be a result

attempt catches failures caused by the world — a missing file, bad JSON, a command that failed — and hands back a result instead of stopping. It never catches mistakes in the program (a typo, a wrong argument count); those stay loud, which is the point.

r is attempt read(path)              # the file might not be there
r is attempt from_json(read(path))   # the JSON might be rubbish
r is attempt capture("systemctl is-active gitea")

if not ok of r then
    say "carrying on without it: {why of r}"
end

It gives back {ok, value, why} — did it work, what did it give, and if not, what lippy would have said. When the other end answered with a number — a command's exit code, an HTTP status — there's also a code:

r is attempt fetch(url)
if not ok of r and code of r equals 404 then …

Read the result in two halves: ok and value describe the success; why and code describe the failure. A success has no failure to describe, so it carries no code.

attempt needs parens around the call it guards — attempt capture("date"), not attempt capture "date" — because a paren-free call can't contain another one.

Maps keep their order

for name, what in services visits entries in the order they were added — every run, on every machine. Deterministic order means a script behaves the same on your laptop and your server, and it's one less idea to learn.