File-level directives that tune how Bern behaves - opt into stricter or looser rules, one file at a time.
A pragma is a magic comment that flips the interpreter into a stricter or looser mode. Write it near the top of a file, one per line:
{--! strict-types !--}
{--! immutable !--}
Pragmas are file-scoped, not global. The pragma set always belongs to the file whose code is currently running: every function remembers the pragmas of the file that defined it, so a library that declares {--! impure-lists !--} gets impure lists inside its own code without forcing them on the program that imports it - and vice-versa.
Warning: unknown pragma '…' and carries on, so a typo never silently changes behaviour.
The whole set is summarised in the table below; the sections after it explain each one with an example and when to reach for it.
| Pragma | Effect |
|---|---|
impure-lists | lists may hold mixed element types |
impure-sets | set operations keep duplicates |
strict-types | forbid implicit string/number coercion |
typed | Bern 2.1 enable type-annotated variable declarations |
strict-arithmetic | division by zero is an error, not NaN |
safe-index | out-of-bounds indexing returns undefined |
start-on-one | indexing starts at 1 |
immutable | reassigning an existing variable is an error |
no-undefined | reading an unbound variable is an error |
no-written-operators | free written-word operator names for variables |
abort-on-error | crash on runtime errors instead of error values |
partial | allow non-exhaustive functions |
no-curry | disable auto-currying |
no-eval | bare expressions don't auto-print |
show-types | auto-printed values show their type |
main | run main() after the file loads |
By default a list is homogeneous: every element must share a type, and mixing them is an error. This pragma lifts that rule so a list can hold values of different types.
-- Without the pragma:
[1, "two", 3.0]
-- Error: list elements must have the same type
-- With {--! impure-lists !--}:
mixed = [1, "two", 3.0]
:> mixed -- 3
mixed[0] -- 1
Use it when you are modelling rows of mixed data - a CSV record, a JSON-like tuple, or a heterogeneous payload - and the one-type rule gets in the way.
Set operations normally remove duplicates. This pragma makes union keep every occurrence, turning sets into multisets for the duration of the file.
-- Without the pragma:
{1, 2, 3} <| {2, 3, 4} -- {1, 2, 3, 4} (4 elements)
-- With {--! impure-sets !--}:
combined = {1, 2, 3} <| {2, 3, 4}
:> combined -- 6 (duplicates kept)
Use it when you actually want bag/multiset semantics - counting occurrences or concatenating without the automatic dedup.
Bern is happy to coerce across types - adding a number to a string stringifies the number, for instance. This pragma forbids that implicit coercion, so a mix of types is an error you have to resolve explicitly.
-- Without the pragma:
"answer: " + 42 -- "answer: 42"
-- With {--! strict-types !--}:
"answer: " + 42 -- Error: the strict-types pragma forbids implicit coercion
"answer: " + to_str(42) -- "answer: 42" (convert on purpose)
Use it when you want accidental string/number mixing to surface as an error instead of a silently stringified value.
Enables type-annotated variable declarations with name :: Type = value. The value is checked against the declared type when the binding runs; a mismatch is a hard error. Without the pragma the syntax is inactive and :: keeps its usual meaning as the typeof operator.
{--! typed !--}
x :: Integer = 10
a :: Character = 'a'
name :: String = "bern"
bad :: Integer = 'a'
-- Error: type mismatch for 'bad': declared Integer but value is Character
The declared type accepts the canonical typeof names (Integer, Double, Boolean, Character, String, List, Set, Object), the aliases Int, Float, Char, Bool, Text, any ADT type name, and Auto / Any for "accept whatever type is passed".
Use it when a few key variables carry an invariant worth documenting and enforcing, while the rest of the file stays dynamically typed. See the Types section for the full reference.
Division by zero normally yields NaN ("not a number"), which then quietly poisons later arithmetic. This pragma turns it into an immediate error instead.
-- Without the pragma:
1 / 0 -- NaN
-- With {--! strict-arithmetic !--}:
1 / 0 -- Error: division by zero
Use it when a stray division by zero should stop the program at the source rather than spreading a NaN through the results.
Indexing past the end of a list or string is normally an error. With this pragma an out-of-bounds index returns undefined instead, so a lookup never fails.
-- Without the pragma:
[1, 2, 3][99] -- Error (an error value)
-- With {--! safe-index !--}:
[1, 2, 3][99] -- undefined
Use it when you want forgiving lookups - probing optional positions and treating "missing" as undefined rather than handling an error.
Bern indexes from 0 by default. This pragma shifts indexing to start at 1, so the first element is at position 1.
-- Without the pragma:
[10, 20, 30][0] -- 10
-- With {--! start-on-one !--}:
[10, 20, 30][1] -- 10
Use it when you are porting 1-based code or working in a domain (matrices, line numbers, spreadsheets) where 1-based indexing reads more naturally.
By default a variable can be reassigned freely. This pragma makes reassigning an existing name an error - every binding is write-once.
{--! immutable !--}
x = 1
x = 2
-- Error: cannot reassign 'x' (the immutable pragma forbids it)
Use it when you want constants to stay constant and to catch accidental shadowing or re-use of a name.
Reading a name that was never assigned normally produces undefined. This pragma turns that into an error, so an unbound variable is caught immediately.
-- Without the pragma:
print(typo_name) -- undefined
-- With {--! no-undefined !--}:
print(typo_name) -- Error: 'typo_name' is not defined
Use it when you want misspelled or forgotten variables to fail loudly instead of slipping through as undefined.
Bern accepts written-word operators (plus, minus, and, or, length, not, …) as aliases for the symbolic ones, which reserves those names. This pragma switches the word operators off, freeing the names for use as ordinary variables.
-- Without the pragma:
length = 5 -- Error: 'length' is a written-word operator
-- With {--! no-written-operators !--}:
length = 5 -- fine; use :> for size instead of the word `length`
plus = 10 -- also fine
Use it when a name like length, and, or plus is the natural variable name for your data and you don't need the English-word operator spellings.
By default a runtime error becomes a value you can inspect with is_error, and the program keeps running (errors-as-values). This pragma restores the classic behaviour: an error crashes the program on the spot.
-- Without the pragma:
result = head([])
is_error(result) -- true; the program continues
-- With {--! abort-on-error !--}:
head([]) -- crashes immediately with the error
Use it when you would rather fail fast - a script where an error should stop everything, not quietly flow on as a value.
A single-argument function whose clauses don't cover every case is normally rejected as non-exhaustive. This pragma allows such partial functions; calling an uncovered case is still an error at runtime.
-- Without the pragma:
def describe(Num(n)) -> "number"
describe(Bool(true))
-- Error: function 'describe' is not exhaustive
-- (add a catch-all clause, or use the `partial` pragma)
-- With {--! partial !--}: the definition is accepted as-is.
Use it when you are prototyping, or a function is intentionally defined only for some shapes and you accept the runtime risk.
Bern auto-curries: calling a function with fewer arguments than it expects returns a new function waiting for the rest. This pragma disables that, so every call must pass all the arguments.
def add(x, y) -> x + y
-- Without the pragma:
add5 = add(5) -- a function expecting one more argument
add5(3) -- 8
-- With {--! no-curry !--}:
add(5) -- Error: 'add' expects 2 arguments
Use it when you'd rather a missing argument be reported as a mistake than silently produce a partially-applied function.
At the top level, a bare expression auto-prints its result. This pragma suppresses that, so only explicit print(...) calls produce output.
-- Without the pragma:
2 + 2 -- prints 4
-- With {--! no-eval !--}:
2 + 2 -- prints nothing
print(2 + 2) -- prints 4
Use it when a file is a module or script that shouldn't spray intermediate values to the console - you control output through print.
When an auto-printed value is shown, this pragma appends its type, in the form value : Type.
-- Without the pragma:
42 -- 42
-- With {--! show-types !--}:
42 -- 42 : Integer
"hi" -- hi : List
Use it when you are exploring in the REPL or debugging and want to see at a glance what type each result is.
By default a file runs top to bottom. This pragma additionally calls main() once the file has finished loading, giving the program a conventional entry point.
{--! main !--}
def greet(name) -> "Hello, " + name
def main() do
print(greet("Bern"))
end
-- After loading, main() runs and prints: Hello, Bern
Use it when you want an explicit entry point - defining helpers and a main up front, and letting the pragma kick it off.