Language guide

The whole language, top to bottom.

Comments & source files

Line comments start with # and run to the end of the line. Source files use the .citril extension (or the short .ctl). Blocks are closed with end — indentation is never significant.

# this is a comment
print("code")   # trailing comment

Values & types

TypeExamplesNotes
nilnilabsence of a value
booltrue, falsedistinct from numbers
number42, 3.14integer or float
string"hi"double- or single-quoted
list[1, 2, 3]mutable, reference semantics
map{ name: "Ada" }insertion-ordered
functionfunction(x) return x endfirst-class, closes over scope

Truthiness: only nil and false are falsy. 0 and "" are truthy.

Variables

Declare with let; reassign with =. Assigning to a name that was never declared is an error, which keeps scoping explicit.

let x = 10
x = x + 1
x += 5          # compound: also -= *= /= //= %= ^=

Operators

Precedence, loosest to tightest:

  1. or
  2. and
  3. == !=
  4. < <= > >=
  5. + -
  6. * / // %
  7. unary -, not
  8. ^ (power, right-associative)
  • + adds numbers, concatenates strings, and joins lists.
  • / is true division (always a float); // floors.
  • and/or short-circuit and return one operand.

Strings & interpolation

Double-quoted strings support interpolation with ${expr}; the expression is converted with str() and spliced in. Single-quoted strings are literal.

let name = "Ada"
print("Hi ${name}, ${2 + 2} things")     # Hi Ada, 4 things
print('literal ${name}')                  # literal ${name}
print("a dollar: \${x}")                  # a dollar: ${x}

Escapes: \n \t \r \0 \\ \" \' (and \$ to escape an interpolation).

Control flow

if cond then
  ...
elif other then
  ...
else
  ...
end

while cond do
  ...
end

for i = 1, 10 do          # numeric: inclusive, optional step
  ...
end
for i = 10, 1, -1 do ... end

for item in collection do  # a list, string, or a map's keys
  ...
end

break and continue work inside any loop.

Functions & closures

function add(a, b)
  return a + b
end

let mul = function(a, b) return a * b end    # anonymous

function make_adder(n)
  return function(x) return x + n end         # a closure
end

let add5 = make_adder(5)
print("${add5(10)}")                          # 15

A function with no return yields nil. Argument count is checked at call time.

Indexing

let xs = [10, 20, 30]
xs[0]        # 10
xs[-1]       # 30  (negative indexes count from the end)
xs[0] = 99

let m = { a: 1 }
m["a"]       # 1
m.a          # 1   (dot is sugar for a string key)
m.b = 2      # insert

Error handling

try
  risky()
  error("something broke")     # raise any value, usually a string
catch e
  print("caught: ${e}")
end

try/catch catches both error(...) throws and built-in runtime errors (the latter bind their message string to e).

Enums & namespaces

Two block forms group related values under one name. Both are frozen — their members are constants, so assigning to one is an error. Each desugars to a frozen map, so members are read with the same . access as any map.

An enum is a group of named constants, auto-numbered from 0; an explicit = value sets a member and unmarked members keep counting after it:

enum Color
  Red        # 0
  Green      # 1
  Blue       # 2
end

enum Status
  Pending = 1
  Active         # 2
  Done           # 3
end

print(Color.Green)   # 1
Color.Red = 9        # error: cannot modify a frozen map

A namespace groups constants and functions. Members reach each other through the namespace name (there is no implicit self):

namespace geometry
  let tau = 6.28318
  function area(r) return 3.14159 * r ^ 2 end
  function circumference(r) return geometry.tau * r end
end

print(geometry.area(5))   # 78.54

For a mutable group, use a plain { } map instead.

Directives

Header lines of the form #pragma <name> [args] configure how the runner treats a file. They are also valid comments, so they never affect parsing.

DirectiveEffect
#pragma version X.Y.ZRefuse to run on an older Citril than required.

See the Standard library for the built-in functions, or the Command line reference.