lippy.

Syntax reference

The precise rules, for when you want to look something up. If you're just starting, read the Guide first — this is the reference behind it.

Source form

Where a line ends

A statement ends at the end of the line, unless the line is obviously unfinished — and it's unfinished when either:

  1. A bracket is open — a (, [ or { hasn't been closed. This is how list and map literals span lines.
  2. The line ends on something that needs a right-hand side — a + - * /, a word operator (and, or, mod, equals, greater than, …), a comma, is, or a preposition (to, at, of, in).

There's no line-continuation character. To wrap a long line, break it after an operator or comma, where the sentence is audibly unfinished:

total is price + shipping +
    handling

Rule of thumb: end the line where you'd pause reading it aloud.

Names and keywords

Names are letters, digits and _, starting with a letter. They're case-sensitive; lowercase is the convention.

These words are reserved and can't be used as names:

is  and  or  not  mod
equals  greater  less  than  at  least  most
contains  starts  ends  with  matches
of  to  by  from  in  when
if  then  else  end
for  give  loop  stop  skip
fun  return
true  false  nothing

Notice what isn't reserved: say, warn, ask, run, capture, read, write, move, push, split, fetch, connect, query and the rest are ordinary functions, not keywords. say "hi" is just a function call.

Values and literals

Kind Example
number (one numeric type) 42, 3.14, -7
string "hello" — double quotes
bool true, false
nothing nothing
list [1, 2, 3] — trailing comma allowed
map {"gitea": "the git server"} — trailing comma allowed
range 1 to 100 (numbers only)
raw string `\d+` — backticks, taken literally

Strings

Double-quoted strings interpolate"{expr}" evaluates any expression, including at/of chains: "exit code {code of result}". Escapes are \n, \t, \", \\, \{.

A raw string in backticks is taken exactly as written — no escapes, no interpolation — and may span lines. Reach for it when the text has backslashes, braces or quotes:

name matches `^IMG_\d+`
config is from_json(`{"port": 3000}`)

Operators and precedence

Loosest to tightest binding:

Level Operators
1 or
2 and
3 not
4 comparisons: equals, not equals, greater than, less than, at least, at most, contains, starts with, ends with, matches
5 range to
6 + -
7 * / mod
8 unary -
9 access: at, of
10 calls, ( )

Comparisons don't chain — a equals b equals c is an error with a hint. matches is the pattern comparison, the same question contains asks but with a pattern:

if name matches `^IMG_\d+` then …

at and of

There's no dot notation and no [ ] indexing. Two words do all access:

Calls

Calls are paren-free, Ruby-style:

say "hello"
greet name
move old to new
run "rsync -a {src}/ {dst}/"

The rules that keep this unambiguous:

  1. Arguments run to the end of the statement. say a + b means say(a + b).

  2. So a paren-free call is always the last thing in its statement — the whole statement, or the whole right side of is, when, return, or for … in.

  3. Paren-free calls don't nest. An inner call needs parentheses: number(ask "your guess? "), split trim(line). Writing split trim line is an error that says exactly that.

  4. Multiple arguments are separated by commas — greet name, greeting — or by a preposition (below).

  5. A lone name that names a function calls it, but only when it stands alone as a whole statement. In expression position a name is the value itself:

    beep                    # a statement: calls beep()
    b is beep               # an expression: b is now the function
    b()                     # …called later, through the name
    routes is {"/": home}   # functions live in lists and maps like any value
    
  6. Parenthesised calls — trim(line, "%") — are always available and behave identically. Use parens only when nesting forces you to.

Prepositional parameters

Some functions read as sentences because their last parameter is declared with a preposition, which replaces the comma at the call site:

fun move(what, to where)     # called:  move a to b
fun push(item, to list)      # called:  push item to keep
fun write(text, to path)     # called:  write report to "out.txt"

This is ordinary syntax — your own functions can declare prepositional parameters too. The prepositions in use are to and by (as in sort xs by len).

Statements

name is expr                      # assignment (also: target at key is expr)
some_call arg, arg                # a call
if expr then … else if expr then … else … end
for name in expr … end            # lists, ranges, and (with two names) maps
for line in stdin … end           # lines piped in — one at a time
for key, value in expr … end

names is for s in services give name of s        # a for read as one value
big is for f in files give f when len(f) greater than 100

loop … end                        # forever, until stop
stop                              # break     |  stop when expr
skip                              # continue   |  skip when expr

fun name(params) … end
return expr                       # or a bare return

Deliberately not in lippy

Semicolons, ++/--, the ternary ?:, switch, while (use loop + stop when), dot notation, [ ] indexing, single-quoted strings, and more than one statement per line.