Learn
Types, effects, syntax, operators, and the CLI.
Core Types
| Type | Example | Notes |
|---|---|---|
Int | 42 | 64-bit integer |
String | "hello" | UTF-8, interpolation with ${} |
Boolean | true, false | Negation with not, not ! |
Unit | () | No meaningful value |
List<T> | [1, 2, 3] | Homogeneous, immutable |
Option<T> | Some(42), None | Replaces 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) = pairEffects
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
| Effect | Capability | Notes |
|---|---|---|
Console | Terminal I/O | print, read |
Http | Network requests and servers | |
Fs | Filesystem read/write | |
Random | Random number generation | Must declare |
Env | Environment variables | |
Sqlite | Sqlite database | Requires server prelude |
Postgres | Postgres database | Requires server prelude |
Mysql | Mysql database | Requires server prelude |
Log | Structured logging | Ambient (no declaration needed) |
Time | Clock and timing | Ambient (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 OtherModuleOperators
+ - * / % | Arithmetic |
== != | Equality |
< <= > >= | Ordering |
&& || | Short-circuit logical |
not | Negation (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: useResult<T, E>with? - No
null/undefined/nil: useOption<T> - No
async/await: effects handle this - No
+for string concatenation: use"${a}${b}" - No
!for boolean negation: usenot - No
value.method(): useModule.method(value) - No mutable variables, no
returnkeyword, no semicolons
Resources
- Source code: GitHub
- llms.txt: compact reference for AI agent context windows
- Quickstart: from install to web server
- API Reference: every module and function