Learn

Types, effects, syntax, operators, and the CLI.

Core Types

TypeExampleNotes
Int4264-bit integer
String"hello"UTF-8, interpolation with ${}
Booleantrue, falseNegation with not, not !
Unit()No meaningful value
List<T>[1, 2, 3]Homogeneous, immutable
Option<T>Some(42), NoneReplaces null
Result<T,E>Ok(v), Err(e)Replaces exceptions

Refinement Types

A refinement type is a base type with a constraint the compiler proves at compile time. Define it once, and every function that takes that type can trust the constraint holds.

type Port = Int where self >= 1 && self <= 65535
type Percentage = Int where self >= 0 && self <= 100
type PositiveInt = Int where self > 0

let port: Port = 8080      // OK
let bad: Port = 0         // Compile error

where is only for refinements. You can use >, >=, <, <=, ==, !=, and combine them with &&. No || yet. That's coming in v0.2.

Sum Types

A value can be one of several variants, each optionally carrying data. The compiler makes sure every match handles all of them.

type Shape =
  | Circle(Int)
  | Rect(Int, Int)
  | Point

fn describe(s: Shape) -> String =
  match s
    Circle(r)    -> "circle r=${r}"
    Rect(w, h) -> "rect ${w}x${h}"
    Point        -> "point"

Option<T> and Result<T, E> are built-in sum types. No null, no undefined, no exceptions.

Records

Records give you named fields, dot access, and an update syntax that creates a new record instead of mutating the old one.

let user = { name: "Alice", age: 30 }

// Dot access
user.name  // "Alice"

// Update syntax (creates a new record)
let older = { ..user, age: 31 }

// Named record types
type User = { name: String, age: Int }
let bob = User { name: "Bob", age: 25 }

Tuples

Tuples hold mixed types. Destructure them with let.

let pair = (1, "hello")
let (n, s) = pair

Effects

Side effects are declared in the type signature. The ! suffix marks a function as effectful, and the curly braces say which effects it uses. The compiler enforces this, not a linter.

// Pure: no effects, can be called from anywhere
fn add(a: Int, b: Int) -> Int = a + b

// Effectful: requires {Console}
fn log_add!(a: Int, b: Int) -> {Console} Int =
  Console.print!("${a} + ${b}")
  a + b

// Multiple effects: must declare all of them
fn fetch_and_log!(url: String) -> {Http, Console} String =
  let body = Http.get!(url)
  Console.print!(body)
  body

Built-in Effects

EffectCapabilityNotes
ConsoleTerminal I/Oprint, read
HttpNetwork requests and servers
FsFilesystem read/write
RandomRandom number generationMust declare
EnvEnvironment variables
SqliteSqlite databaseRequires server prelude
PostgresPostgres databaseRequires server prelude
MysqlMysql databaseRequires server prelude
LogStructured loggingAmbient (no declaration needed)
TimeClock and timingAmbient (no declaration needed)

Log and Time are ambient: you don't need to declare them. Everything else must appear in the effect set.

Effect Inference

If you leave off the effect annotation, the compiler infers it from the function body. Only explicit annotations get checked against.

// No annotation: effects inferred as {Console}
fn helper!(msg: String) =
  Console.print!(msg)

// Explicit annotation: compiler checks it
fn main!() -> {Console} Unit =
  helper!("hello")

In practice: skip annotations on internal helpers, add them at public boundaries.

Transitive Effects

Effects bubble up. If foo! calls bar!, then foo must declare everything bar uses too.

fn bar!() -> {Http} String =
  Http.get!("/data")

// Must include {Http} because bar! requires it
fn foo!() -> {Http, Console} Unit =
  let data = bar!()
  Console.print!(data)

AI Sandboxing

You can use effects to sandbox AI-generated code. Grant {Console} for output but withhold {Fs} and {Http}. If the generated code tries to use an effect you didn't grant, the compiler rejects it before anything runs.

Syntax Summary

// Functions
fn name(x: Type) -> ReturnType = body
fn effectful!(x: Type) -> {Effect} ReturnType = body

// Variables
let x = 42
let y: String = "hello"

// Control flow
if condition then a else b
match value
  Pattern -> result

// Types
type Name = Int where self > 0
type Sum = | A | B(Int)

// Pipes
x |> f |> g

// Lambdas
|x| x + 1
|x, y| x * y

// Error propagation
let value = may_fail()?

// Tests
@test
test "name" = expr == expected

// Modules
@prelude(script)
@module MyModule
import OtherModule

Operators

+ - * / %Arithmetic
== !=Equality
< <= > >=Ordering
&& ||Short-circuit logical
notNegation (keyword, not !)
|>Pipe
?Error propagation
..Range (1..10)

Modules

Functions live in modules. You call them with Module.function(args). There's no value.method().

// Module.function(value) is the only call style
String.to_upper("hello")   // "HELLO"
List.length([1, 2, 3])   // 3

// Prelude levels control which modules are available
@prelude(script)   // Console, String, List, etc.
@prelude(server)   // + Router, Server, Sqlite, etc.

Tooling

# Run a program
blc run app.bl

# Type check with structured output
blc check app.bl --json

# Run tests with structured output
blc test app.bl --json

# Look up API docs
blc docs List.map
blc docs --search "filter" --json

# Verification levels
blc check app.bl --json --level types        # Fast (~ms)
blc check app.bl --json --level refinements  # Default (~100ms)
blc check app.bl --json --level full         # SMT (~seconds)

Not Supported

These don't exist in Baseline. The compiler rejects them and tells you what to use instead.

  • No class / extends / implements: use records + sum types + effects
  • No try / catch / throw: use Result<T, E> with ?
  • No null / undefined / nil: use Option<T>
  • No async / await: effects handle this
  • No + for string concatenation: use "${a}${b}"
  • No ! for boolean negation: use not
  • No value.method(): use Module.method(value)
  • No mutable variables, no return keyword, no semicolons

Resources