Syntax
This chapter uses Default spelling unless it shows an ML equivalent. Detailed semantics live in the linked chapters.
Flavors
Default (.osp) uses braces, fn, and parenthesized calls. ML (.ospml) uses
layout, whitespace application, and currying by default. Each frontend lowers
to the same osprey_ast::Program before semantic analysis.
Shared AST does not mean every surface form exists in both flavors. Default has
if and structural ternary spellings that ML omits. The
surface/core contract is FLAVOR-BOUNDARY; complete ML
grammar is in ML Flavor Syntax. Multi-file and
cross-flavor project limits are specified in
Modules and Namespaces.
Program Structure
A file contains declarations and expression statements. Declarations include bindings, functions, externs, types, effects, imports, namespaces, modules, and module signatures. Module and import forms are specified in Modules and Namespaces; effect forms are specified in Algebraic Effects.
Bindings
Default uses let for an immutable binding and mut for a mutable cell.
Reassignment uses = and is rejected for an immutable or undeclared name.
mut is not a general-purpose imperative variable. Osprey is
expression-oriented and immutable-first; a mut binding exists only to back
handler-owned state for algebraic effects — a cell an effect handler reads
and writes as it interprets operations
(EFFECTS-HANDLER-STATE). The
checker therefore permits reassignment only in an effect handler arm. A free
procedural x = x + 1 in ordinary statement position is a type error.
let name = "Alice"
// A `mut` cell owned by a handler: it changes only as the effect is performed
// and this arm interprets it — the sanctioned form of mutation.
mut count = 0
let total = handle Counter
tick => { count = (count + 1) ?: count count }
in run()
ML omits let; its reassignment operator is := and follows the same rule.
name = "Alice"
mut count = 0
total = handle Counter
tick => count := (count + 1) ?: count
in run ()
The mutable cell may be declared in an enclosing lexical scope so the handler
can capture it, but every assignment must occur in an arm that interprets an
effect operation. The handled in body is client code, not a handler arm, and
does not gain mutation authority.
A binding may include a type annotation after :. The annotation constrains
inference; it is not required when inference already fixes the type.
Function Declarations
function ::= "fn" identifier typeParameters? "(" parameters? ")"
("->" type)? effectSet? ("=" expression | block)
parameter ::= (identifier | "_") (":" type)?
Default functions have flat parameter lists. Calls are described in Function Calls.
fn double(x) = x * 2
fn add(x, y) = x + y
fn getValue() = 42
Ignored parameters [PARAM-WILDCARD]
_ declares a parameter the body cannot reference. Lowering gives each _ a
distinct unspellable internal name, so repeated ignored parameters do not
collide.
let count = range(0, 10) |> fold(0, |acc, _| => (acc + 1) ?: acc)
A named function can use _ only where its caller supplies arguments
positionally; there is no source name for a named argument to target.
Extern Declarations
An extern declares a C-ABI symbol and has no body. Parameter types are required;
an omitted return type means Unit.
extern ::= "extern" "fn" identifier "(" externParameters? ")" ("->" type)?
extern fn sqlite3_open(filename: string, ppDb: Ptr) -> int
Supported ABI types, callbacks, linking, and the memory-safety boundary are in Foreign Function Interface.
Type Declarations
A type declaration defines a record, a union, or a type alias. Unions may have nullary variants, named-field payloads, or positional payloads.
type Point = { x: int, y: int }
type Shape = Circle { radius: int } | Rectangle { width: int, height: int }
type Color = Red | Green | Blue
Type parameters and variance are defined in Type System.
The Shape union declared above has this shape:
Positional variants [TYPE-UNION-POSITIONAL]
A positional payload is declared, constructed, and matched in slot order:
type Tree = Leaf | Node(Tree, Tree)
let tree = Node(Node(Leaf, Leaf), Leaf)
fn size(tree) -> Result<int, MathError> = match tree {
Leaf => Success { value: 1 }
Node(left, _) => 1 + size(left)
}
Positional constructors require every slot and do not partially apply. Their
slots have no source field names, so named arguments and field access cannot
target them. Nested constructor patterns are not implemented; each positional
pattern slot is a binding or _.
Records
Construction supplies every declared field by name. Construction-site field order does not change which value is assigned to each field.
type Person = { name: string, age: int }
let person = Person { age: 25, name: "Alice" }
let older = person { age: 26 }
Records are immutable; record { field: value } creates a modified copy.
Field typing is specified in Type System. The parsed
where validation suffix is rejected during type checking.
Expressions
Default supports literals, names, calls, lambdas, blocks, field access, indexing, pipes, arithmetic and boolean operators, matches, ternaries, effects, and concurrency expressions.
The relevant precedence, highest to lowest, is:
- Postfix call, field access, and indexing
- Pipe
|> - Unary
!,-,+ - Multiplicative
*,/,% - Additive
+,- - Comparison
==,!=,<,>,<=,>= - Logical AND
&& - Logical OR
|| - Ternary
? :and Result default?:, both right-associative
x |> f(a) lowers to f(x, a). The two Default lambda spellings lower to
Expr::Lambda:
let one = |x| => x + 1
let zero = fn() => 0
The pipe-delimited form requires at least one parameter because || is the
logical-OR token.
Indexing
Postfix indexing is available on lists, maps, and strings. It returns
Result<T, Error> (or Result<string, Error> for a string), so callers can
handle an invalid index or absent key.
match values[0] {
Success { value } => print(value)
Error { message } => print(message)
}
Field Access
value.field reads a record field. A union payload must first be narrowed by a
constructor pattern. Record update uses value { field: replacement }.
Match Expressions
match ::= "match" expression "{" arm+ "}"
arm ::= pattern "=>" expression
Patterns are scalar literals, _, a lower-case binding, union
constructors with named or positional payloads, and list patterns. Pattern
semantics and exhaustive matching are defined in
Pattern Matching.
name: Type is accepted as a typed binding. The backend treats it as
a catch-all binding rather than a runtime type test, so it is valid only when
the scrutinee already has that static type. Standalone structural record
patterns are not implemented; the Default structural ternary is a separate
lowering.
Evaluation order
Statements and positional call arguments evaluate left to right. && and ||
short-circuit. A named call is reordered to parameter declaration order before
its argument expressions are lowered.