Flint compiles source directly to bytecode. Statements end at a newline, semicolon, closing brace, or end of file. Braces are required for control flow bodies.
let count = 0
while count < 3 {
print(count)
count += 1
}
Output:
0
1
2
Values are numbers, strings, booleans, nil, lists, tables, and functions.
Numbers are doubles. Strings are bytes. Lists hold any values; table keys are
strings.
let xs = [1, "two", nil]
const host = "localhost"
let cfg = {host: host, port: 8080}
print(cfg.host)
let without an initializer gives nil. const requires an initializer and
rejects assignment. A local duplicate in the same scope is a compile error;
globals may be redeclared, subject to the const rule.
Either form can destructure. let {host, port} = cfg binds names from table
fields, and let [first, second] = xs binds them by position, from a list or
a string. Names are flat, with no nesting, defaults, or renaming. const
destructures the same way. A missing table key reads nil; a list name past
the end raises the ordinary index error.
let [first, second] = [10, 20]
print(first + second) # 30
let [a, b] = [1, 2, 3] # 3. extra elements are ignored.
Only nil and false are falsy. 0, "", and empty containers are true.
This is where a C programmer's first if (count) port usually goes wrong.
Operators, from loose to tight:
= += -= *= /= ??=
??
or
and
== != in
< <= > >=
..
+ -
* / %
as
! not -
call, index, field, ?.
and and or short-circuit and return an operand. + adds two numbers or
joins two strings. There is no implicit conversion. Comparisons do not chain:
1 < 2 < 3 compares the boolean result of 1 < 2 with 3 and errors.
a ?? b is b when a is nil, and a otherwise; only nil triggers, so
false, 0 and "" stay. a ??= b assigns b to a only when a is
nil, and runs b only then. ?. short-circuits on nil: n?.a?.b
is nil when n is nil, n?.f(1/0) never evaluates its arguments, and
assignment through ?. is a compile error. x in y is membership: a substring for
strings, an element for lists, a key for tables, and a runtime error for
anything else.
as checks a value's type and leaves the value alone:
print(4 as number)
print(type("x"))
Output:
4
string
Valid names are number, string, bool, nil, list, table, and
function. A bad type name is a compile error; a value of the wrong type is
a runtime error.
Lists and strings accept zero-based and negative indices. Negative indices count back from the end. Indices must be finite whole numbers; assignment does not grow a list.
let xs = [10, 20, 30]
print(xs[-1])
xs[0] = 5
Output:
30
len accepts strings and lists. push appends and returns the item; pop
removes and returns the last item. An empty pop is an error. Table fields
use t.name or t["name"]; a missing field reads as nil.
Functions are declared with fn. They return nil unless a return runs.
Closures capture locals by reference, so a write through a closure changes
the captured binding.
fn make_counter() {
let n = 0
fn next() { n += 1; return n }
return next
}
let next = make_counter()
print(next())
Output:
1
while repeats while its condition is truthy. for x in list visits values.
for n in start..end visits numbers from start up to, but not including,
end; start..end..step steps by step, and a negative step runs
backwards. break leaves the innermost loop; continue starts its next
iteration. Ranges exist only in this for form. They are not values.
import "path.fl" runs a file in the same VM. Relative paths use the
importing file's directory. Every module has its own namespace: export
marks the names an importer can reach, and unexported names stay private.
Successful imports run once per VM.
input() reads a line and returns a string, or nil at end of file.
input(prompt) writes the prompt without a newline first. Empty input is "";
end of file is nil.
For file, environment, string, and process functions, see library.md. For errors and exit codes, see errors.md.