A comprehensive guide to the Bern programming language and its standard libraries
In Bern, literals are automatically interpreted and printed. You can write values directly and they'll be evaluated immediately:
"Hello, World!" 23 3.14 true
Variables are defined using the simple name = value syntax. Bern supports various data types including integers, doubles, strings, and booleans:
integer = 2 double = 3.14 helloworld = "Hello, World!" isActive = true
Like literals, variables can be called directly and will be properly evaluated. Simply write the variable name to see its value:
helloworld -- Output: "Hello, World!" integer -- Output: 2
Bern provides two special operators for introspection:
:: (typeof) - Returns the type of a value as a string:> (length) - Returns the length of a list, set, or stringtypeof_integer = :: 2 -- Output: "Int" typeof_string = :: "hello" -- Output: "String" length_list = :> [1,2,3,4] -- Output: 4 length_string = :> "hello" -- Output: 5
Bern supports conditional execution through if-then-else statements. These allow your code to make decisions based on conditions.
The basic conditional structure lets you execute different code based on whether a condition is true or false:
age = 18
if age >= 18 then
"You are an adult."
else
"You are a minor."
end
-- Output: "You are an adult."
For multiple conditions, use else if to check additional cases in sequence:
score = 85
if score >= 90 then
"You got an A."
else if score >= 80 then
"You got a B."
else if score >= 70 then
"You got a C."
else
"You need to improve."
end
-- Output: "You got a B."
A classic programming challenge demonstrating nested conditionals:
numbers = [1..100]
for n : numbers do
if n % 15 == 0 then
"FizzBuzz"
else if n % 3 == 0 then
"Fizz"
else if n % 5 == 0 then
"Buzz"
else
n
end
end
Bern functions are powerful and flexible, supporting pattern matching, inline arrow syntax, and block bodies with explicit returns.
For one-line functions, use the arrow syntax def name(params) -> expr:
def add(x, y) -> x + y add(2, 3) -- Output: 5 def square(x) -> x * x square(4) -- Output: 16
Define multiple clauses for a function. Bern will use the first matching pattern:
def sign(0) -> "zero" def sign(n) -> "positive" sign(0) -- Output: "zero" sign(42) -- Output: "positive"
Use _ to ignore parameters, or match specific literal values:
def greet("Alice") -> "Hi, Alice!"
def greet(_) -> "Hello, someone else!"
greet("Alice")
-- Output: "Hi, Alice!"
greet("Bob")
-- Output: "Hello, someone else!"
For complex functions, use do ... end blocks with explicit return statements:
def sumList(xs) do
total = 0
for n : xs do
total = total + n
end
return total
end
sumList([1,2,3])
-- Output: 6
Anonymous functions use backslash syntax and can be assigned to variables or passed as arguments:
inc = \x -> x + 1 pairSwap = \a, b -> [b, a] inc(10) -- Output: 11 pairSwap(1, 2) -- Output: [2, 1]
Functions can accept other functions as parameters:
def applyTwice(f, x) -> f(f(x)) applyTwice(\n -> n * 2, 5) -- Output: 20 (5 * 2 * 2)
Bern uses Odin-inspired loop syntax where for is the only loop keyword. The behavior changes based on how you use it.
Execute a block a specific number of times:
for 3 do
"Hello, Loops!"
end
-- Output: "Hello, Loops!" (three times)
Repeat while a condition is true (like a while loop):
counter = 2
for counter > 0 do
"Hello, Loops!"
counter = counter - 1
end
-- Output: "Hello, Loops!" (twice)
Create an infinite loop with for true:
for true do
"This runs forever!"
end
Iterate over elements in lists, sets, or strings:
text = "Bern"
for char : text do
"Current char is: " + char
end
-- Output:
-- "Current char is: B"
-- "Current char is: e"
-- "Current char is: r"
-- "Current char is: n"
Get both the element and its index during iteration:
text = "Bern"
for char, index : text do
"Char: " + char + " at index: " + index
end
-- Output:
-- "Char: B at index: 0"
-- "Char: e at index: 1"
-- "Char: r at index: 2"
-- "Char: n at index: 3"
A list is an ordered collection of elements of the same type. Lists allow duplicates and preserve insertion order. In Bern, strings are also treated as lists of characters.
numbers_list = [1,2,3,4,5] numbers_list -- Output: [1,2,3,4,5]
Create lists using Haskell-style range notation:
range_list = [5..10] -- Output: [5,6,7,8,9,10] small_range = [1..5] -- Output: [1,2,3,4,5]
Access specific elements using zero-based indexing:
range_list = [5..10] range_list[3] -- Output: 8 (fourth element)
Arithmetic operations with a scalar apply to every element:
[1,2] + 2 -- Output: [3,4] [10,20,30] * 2 -- Output: [20,40,60]
Operations between two lists require equal lengths:
[1,2,3] + [4,5,6] -- Output: [5,7,9] [10,20] - [5,3] -- Output: [5,17]
Lists support set-like operations while preserving order and allowing duplicates:
<> - Union (concatenation)</> - Difference (elements in first but not second)|> - Intersection (common elements)<| - Symmetric difference (elements in either but not both)[1,2,3] <> [3,4,5] -- Output: [1,2,3,3,4,5] [1,2,3] > [2,3] -- Output: [1] [1,2,3] |> [2,3] -- Output: [2,3] [1,2,3] <| [2,3] -- Output: [1]
result = ([1,2,3,4] <> [3..5]) <| ([2,3,4] |> [4,5]) > [1] -- Combines concatenation, symmetric difference, -- intersection, and regular difference
A set is an unordered collection of unique elements that can be of different types. Sets automatically remove duplicates and don't preserve order.
numbers_set = {1,2,3,4,5}
numbers_set
-- Output: {1,2,3,4,5}
mixed_set = {1, "hello", 3.14}
-- Sets can contain different types
While sets are unordered, you can still access elements by index:
numbers_set = {1,2,3,4,5}
numbers_set[2]
-- Output: (some element from the set)
Adding to a set inserts elements (if not already present):
{1,2} + 3
-- Output: {1,2,3}
{1,2} + {3,4}
-- Output: {1,2,3,4}
{1,2,3} + 2
-- Output: {1,2,3} (2 already exists)
Subtraction removes elements:
{1,2,3} - 2
-- Output: {1,3}
Mathematical set operations with automatic duplicate removal:
{1,2,3} <> {3,4,5}
-- Union: {1,2,3,4,5}
{1,2,'a',3.5} <> {3,4,'b',1}
-- Union with mixed types: {1,2,'a',3.5,3,4,'b'}
{1,2,3} |> {2,3,4}
-- Intersection: {2,3}
{1,2,3} > {2,3,4}
-- Difference: {1}
{1,2,3} <| {2,3}
-- Symmetric difference: {1}
Bern supports objects (hashmaps/hashtables) using the #{...}# notation for key-value pairs.
obj = #{
key: "value",
hello: "world"
}#
obj
-- Output: #{key: "value", hello: "world"}#
Reassign values using bracket notation:
obj["key"] = "new_value"
obj
-- Output: #{key: "new_value", hello: "world"}#
Add new key-value pairs by assigning to a new key:
obj["new_key"] = "added_value"
obj
-- Output: #{key: "new_value", hello: "world", new_key: "added_value"}#
Objects can contain other objects. Access nested values with multiple indexing:
obj["test"] = #{
nested_key: "nested_value"
}#
obj["test"]["nested_key"]
-- Output: "nested_value"
person = #{
name: "Alice",
age: 25,
address: #{
city: "Boston",
zip: "02101"
}#
}#
person["address"]["city"]
-- Output: "Boston"
ADTs allow you to define custom data types with multiple constructors, similar to enums or tagged unions in other languages.
Use the adt keyword followed by type name and constructors:
adt Shape = Circle Double | Rectangle Double Double
Each constructor can have zero or more fields of specific types.
Call constructors like functions to create instances:
c = Circle(5.0) r = Rectangle(3.0, 4.0)
Destructure ADTs in function definitions to access their fields:
def area(Circle(r)) -> 3.14159 * r * r def area(Rectangle(w, h)) -> w * h area(c) -- Output: 78.53975 (π * 5²) area(r) -- Output: 12.0 (3 * 4)
A common ADT pattern for representing optional values:
adt Maybe = Just Int | None def safeDivide(n, 0) -> None() def safeDivide(n, m) -> Just(n / m) safeDivide(10, 3) -- Output: Just(3) safeDivide(5, 0) -- Output: None()
Bern comes with several standard libraries and bundled vendor modules. Import standard libraries with import library_name and vendor modules with full paths like import vendor/csv.
The foundational library providing list operations, functional programming utilities, and type conversions.
import core
Apply a function to each element of a list.
map([1,2,3], \x -> x * 2) -- Output: [2,4,6]
Keep only elements where predicate returns true.
filter([1,2,3,4,5], \x -> x % 2 == 0) -- Output: [2,4]
Reduce a list from left to right.
foldl([1,2,3], 0, \acc, x -> acc + x) -- Output: 6
Reduce a list from right to left.
Sum all elements in a list.
sum([1,2,3,4]) -- Output: 10
Multiply all elements in a list.
Find the minimum value in a list.
Find the maximum value in a list.
Reverse a list or set. Automatically detects type.
reverse([1,2,3]) -- Output: [3,2,1]
Take first n elements from a list.
Drop first n elements from a list.
Get the first element of a list.
Get all elements except the first.
Check if a list is empty.
Check if any element satisfies the predicate.
Check if all elements satisfy the predicate.
Return first element matching predicate, or false if none found.
Combine two lists into pairs.
zip([1,2,3], ['a','b','c']) -- Output: [[1,'a'], [2,'b'], [3,'c']]
Combine two lists using a function.
Get the type of a value (abstraction for ::).
Get length of string, list, or set (abstraction for :>).
Find the index of an element in a list or set. Returns -1 if not found.
Convert a string to an integer.
to_int("42")
-- Output: 42
Convert an integer to a double.
Comprehensive math library inspired by C's math.h, providing constants, basic operations, and transcendental functions.
import math
pi = 3.141592653589793tau = 6.283185307179586 (2π)e = 2.718281828459045Absolute value of a number.
Alias for abs (mirrors C's fabs).
Return the smaller of two numbers.
Return the larger of two numbers.
Constrain x to the range [lo, hi].
Floating-point remainder (C's fmod).
Raise base to the power of exponent. Uses integer exponentiation by squaring.
pow(2, 8) -- Output: 256
Square root using Newton-Raphson method.
Hypotenuse length: √(a² + b²)
Exponential function e^x using Taylor series.
Natural logarithm (base e) using Newton's method.
Base-10 logarithm.
Sine of angle in radians.
Cosine of angle in radians.
Tangent of angle in radians.
Inverse tangent.
Two-argument arctangent respecting quadrants.
Convert degrees to radians.
Convert radians to degrees.
Comprehensive string processing utilities for common text operations.
import strings
Extract a portion of a string from start to end index.
substring("Hello", 1, 4)
-- Output: "ell"
Get character at specific index.
Split a string by delimiter into a list of strings.
split("a,b,c", ",")
-- Output: ["a", "b", "c"]
Convert string to uppercase.
Convert string to lowercase.
Convert single character to uppercase.
Convert single character to lowercase.
Remove leading and trailing whitespace, newlines, and tabs.
trim(" hello ")
-- Output: "hello"
Replace all occurrences of old substring with new substring.
replace("hello world", "world", "Bern")
-- Output: "hello Bern"
Check if string contains substring.
Check if string starts with prefix.
Check if string ends with suffix.
Cross-platform random number generation using foreign function interface to system libraries.
import random
Generate a random integer.
get_random_int() -- Output: (random integer)
Generate a random double between 0 and 1.
Generate either a random int or random double (randomly chosen).
Return a random element from a list.
random_choice(["apple", "banana", "cherry"]) -- Output: (random fruit)
Generate a random alphanumeric string of specified length.
get_random_string(10) -- Output: "aB3xK9mP2q" (example)
Simple assertion functions for writing tests in Bern.
import assert
Assert that two values are equal. Returns "PASS" or "FAIL" with test name.
assert_equals("Addition test", 2 + 2, 4)
-- Output: "Addition test PASS"
assert_equals("Wrong test", 2 + 2, 5)
-- Output: "Wrong test FAIL"
Assert that two numbers are approximately equal within a tolerance.
assert_approx("Pi test", 3.14159, 3.14, 0.01)
-- Output: "Pi test PASS"
Check if two numbers are approximately equal. Helper function for assert_approx.
Utilities for joining, normalizing, and inspecting filesystem paths.
import path
Join two path segments with a single separator.
path_join("a/", "/b")
-- Output: "a/b"
Join a list of path segments in order.
path_join_many(["a", "b", "c"]) -- Output: "a/b/c"
Check if a path is absolute (supports Unix and drive-prefixed paths).
Return the last segment of a path.
Return the parent directory path.
Return extension including leading dot, or empty string.
Return basename without extension.
Resolve redundant separators and dot segments like . and ...
path_normalize("a/./b/../c")
-- Output: "a/c"
path_normalize("/a//b/../c")
-- Output: "/a/c"
Helpers for URL encoding, decoding, query processing, and full URL parsing/building.
import url
Percent-encode a URL component.
url_encode_component("a b&c")
-- Output: "a%20b%26c"
Decode a percent-encoded URL component.
Build query text from key/value pairs.
build_query([["a", "1"], ["name", "bern lang"]]) -- Output: "a=1&name=bern%20lang"
Parse query string into an object of decoded key/value pairs.
Parse URL into scheme, host, port, path, query, and fragment.
parsed = parse_url("http://example.com:8080/docs/api?x=1#top")
parsed["host"]
-- Output: "example.com"
Build a URL from components (default ports are omitted when applicable).
build_url("https", "example.com", 443, "/hello", "a=1", "frag")
-- Output: "https://example.com/hello?a=1#frag"
Pure-Bern helpers for token generation, checksums, and UUID-like identifiers.
import crypto
Generate a random token string with the given length.
Generate random hex text (2 characters per requested byte).
crypto_random_hex(8) -- Output: (16 hex chars)
Compute a 32-bit checksum for text.
Checksum represented as an 8-character hexadecimal string.
Compare two strings in fixed-length iteration style to reduce timing leakage.
Create a random UUID v4-like identifier string.
uuid = crypto_uuid_v4_like() -- Output: "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
Bundled module for reading and writing CSV data from strings or files.
import vendor/csv
Parse CSV text. With headers enabled, returns objects keyed by header names.
csv = "name,age\nAna,30\nBob,25" rows = parse_csv(csv) rows["0"]["name"] -- Output: "Ana"
Read a CSV file and parse it in one step.
Parse raw rows without converting to header-keyed objects.
Write a list-of-lists dataset to CSV.
Write an indexed object of rows to CSV using specified header order.
FFI-backed HTTP server module for static route responses from Bern.
lib/vendor/http (for example linux/libbern_http.so or windows/bern_http.dll).
import vendor/http/http
Start a server bound to 0.0.0.0.
Start a server on a specific host address.
Stop a running server.
Check whether server is still running.
Set fallback response for unmatched routes.
Add a simple GET route with plain-text response.
Add a GET route with HTML response.
Add a GET route with JSON response.
Run one poll tick; useful for controlled event loops.
Keep polling while the server is running.
server = http_server_start(8080)
http_server_get(server, "/", "hello from bern http")
http_server_add_route_json(server, "/health", "{\"ok\":true}")
http_server_serve_forever(server, 50)
Bern allows importing libraries to extend functionality. Use the import keyword to load standard libraries or custom modules.
import core
Once imported, you can use functions like map, filter, and type conversions:
map([1,2,3,4,5], \x -> x + 2) -- Output: [3,4,5,6,7] def isEven(x) -> x % 2 == 0 filter([1,2,3,4,5,6,7,8,9,10], isEven) -- Output: [2,4,6,8,10]
The core library provides conversion functions for handling input:
import core
age_text = input("Type your age: ")
age = to_int(age_text)
"Your age in 5 years: " + (age + 5)
Capture user input with the input() function, similar to Python.
name = input("Type your name: ")
name
-- Displays whatever the user typed
By default, input() returns text. Convert to other types using core library functions:
import core
age_text = input("Type your age: ")
age = to_int(age_text)
"You'll be " + (age + 10) + " in 10 years"
Bern provides built-in functions for file operations and system utilities.
read_file(path) - Read file contents as a stringwrite_file(path, content) - Write content to a fileget_current_dir() - Get the current working directorycontent = read_file("myfile.txt")
write_file("output.txt", "Hello, Bern!")
write_file("data.txt", content + "\nNew line")
Returns the system the user is running. Useful for bindings.
os = get_host_machine() -- Returns OS/system information
Applies a function to each element of a collection. Used too in Algebraic Data Types (ADTs) to modify contained values without breaking functorial context.
ast Maybe = Just Int | None fmap(Just(5), \x -> x * 2) -- Output: Just(10)
You can call C functions directly from Bern using the foreign function interface (FFI). This allows you to leverage existing C libraries and system calls.
For this, you need to declare the C function signature using the foreign keyword, specifying the library, its function name, argument types and return type.
foreign example("lib.so", "int") -> "void"
Recommendation: Use get_host_machine() inside an auxiliary function to get the correct library path for multiple operating systems.
def get_path() do
os = get_host_machine()
if os == "mingw64" || os == "mingw32" then
return "windows_lib.dll"
else
return "linux_lib.so.6"
end
end