Pragmas Bern 2.0

File-level directives that tune how Bern behaves - opt into stricter or looser rules, one file at a time.

What is a pragma?

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.

Unknown names are ignored with a warning. Misspell a pragma and Bern prints 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.

PragmaEffect
impure-listslists may hold mixed element types
impure-setsset operations keep duplicates
strict-typesforbid implicit string/number coercion
typedBern 2.1 enable type-annotated variable declarations
strict-arithmeticdivision by zero is an error, not NaN
safe-indexout-of-bounds indexing returns undefined
start-on-oneindexing starts at 1
immutablereassigning an existing variable is an error
no-undefinedreading an unbound variable is an error
no-written-operatorsfree written-word operator names for variables
abort-on-errorcrash on runtime errors instead of error values
partialallow non-exhaustive functions
no-currydisable auto-currying
no-evalbare expressions don't auto-print
show-typesauto-printed values show their type
mainrun main() after the file loads

Collections

{--! impure-lists !--}

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.

{--! impure-sets !--}

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.

Types

{--! strict-types !--}

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.

{--! typed !--} Bern 2.1

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.

Numbers & indexing

{--! strict-arithmetic !--}

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.

{--! safe-index !--}

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.

{--! start-on-one !--}

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.

Variables & names

{--! immutable !--}

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.

{--! no-undefined !--}

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.

{--! no-written-operators !--}

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.

Errors & functions

{--! abort-on-error !--}

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.

{--! partial !--}

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.

{--! no-curry !--}

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.

Output & entry point

{--! no-eval !--}

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.

{--! show-types !--}

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.

{--! main !--}

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.