Syntax

This chapter defines the syntactic forms that make up an Osprey program. Semantics for individual constructs are in their dedicated chapters; cross-references are noted inline.

Program Structure

program   ::= statement* EOF
statement ::= importStmt
            | letDecl
            | fnDecl
            | externDecl
            | typeDecl
            | moduleDecl
            | exprStmt

Imports

importStmt ::= "import" ID ("." ID)*
import std
import std.io
import graphics.canvas

Let Declarations

letDecl ::= ("let" | "mut") ID (":" type)? "=" expr
let x       = 42
let name    = "Alice"
mut counter = 0
let result  = calculateValue(input: data)

let binds immutably; mut binds mutably. Type annotations are optional.

Function Declarations

fnDecl    ::= docComment? "fn" ID "(" paramList? ")" ("->" type)? effectSet?
              ("=" expr | "{" blockBody "}")
paramList ::= param ("," param)*
param     ::= ID (":" type)?
fn double(x)   = x * 2
fn add(x, y)   = x + y
fn greet(name) = "Hello " + name
fn getValue()  = 42

Effect sets (!E) are described in Algebraic Effects. Functions of two or more parameters require named arguments at call sites; see Function Calls.

Extern Declarations

extern declares an interface to a foreign function (Rust, C, or any C-ABI library). It has no body.

externDecl      ::= docComment? "extern" "fn" ID "(" externParamList? ")" ("->" type)?
externParamList ::= externParam ("," externParam)*
externParam     ::= ID ":" type

Parameter types are required. Calls use named arguments (single-parameter functions may use positional).

extern fn rust_add(a: int, b: int) -> int
extern fn rust_is_prime(n: int) -> bool

let sum     = rust_add(a: 15, b: 25)
let isPrime = rust_is_prime(17)

ABI mapping:

Osprey Rust C
int i64 int64_t
bool bool bool
string *const c_char char *

The foreign function must use the C ABI (extern "C" and #[no_mangle] in Rust) and be linked at compile time.

Type Declarations

typeDecl          ::= docComment? "type" ID ("<" typeParamList ">")? "=" (unionType | recordType)
unionType         ::= variant ("|" variant)*
recordType        ::= "{" fieldDeclarations "}"
variant           ::= ID ("{" fieldDeclarations "}")?
fieldDeclarations ::= fieldDeclaration ("," fieldDeclaration)*
fieldDeclaration  ::= ID ":" type constraint?
constraint        ::= "where" function_name
type Color = Red | Green | Blue

type Shape = Circle    { radius: int }
           | Rectangle { width: int, height: int }

Records

A record type names a fixed set of fields. Construction uses TypeName { field: value, ... }; field order at the call site is irrelevant.

type Point  = { x: int, y: int }
type Person = { name: string, age: int } where validatePerson

let point  = Point { x: 10, y: 20 }
let person = Person { name: "Alice", age: 25 }

Validation, non-destructive update (record { field: value }), and full field-access semantics are in Type System.

Expressions

expression          ::= logicalOrExpression
logicalOrExpression ::= logicalAndExpression ("||" logicalAndExpression)*
logicalAndExpression::= comparisonExpression ("&&" comparisonExpression)*
comparisonExpression::= additiveExpression (("==" | "!=" | "<" | ">" | "<=" | ">=") additiveExpression)*
additiveExpression  ::= multiplicativeExpression (("+" | "-") multiplicativeExpression)*
multiplicativeExpression ::= unaryExpression (("*" | "/" | "%") unaryExpression)*
unaryExpression     ::= ("+" | "-" | "!")? pipeExpression
pipeExpression      ::= callExpression ("|>" callExpression)*
callExpression      ::= primaryExpression (
                          "." ID "(" argumentList? ")"
                        | "(" argumentList? ")"
                        | "[" expression "]"
                        | "." ID
                      )*
primaryExpression   ::= literal | ID | "(" expression ")"
                      | lambdaExpression | blockExpression | matchExpression

argumentList        ::= namedArgument ("," namedArgument)+
                      | expression ("," expression)*
namedArgument       ::= ID ":" expression

Precedence, highest to lowest:

  1. Unary !, -, +
  2. Multiplicative *, /, %
  3. Additive +, -
  4. Comparison ==, !=, <, >, <=, >=
  5. Logical AND &&
  6. Logical OR ||

Block expressions and their scoping are defined in Block Expressions. Pattern-matching for booleans (the only conditional construct) is in Boolean Operations.

List Access

listAccess ::= expression "[" expression "]"

Indexing returns Result<T, IndexError>:

let numbers = [1, 2, 3, 4]

match numbers[0] {
    Success { value }   => print("first: ${value}")
    Error   { message } => print("index error: ${message}")
}

Field Access

fieldAccess ::= expression "." ID

Fields are accessible directly only on record values:

type User = { id: int, name: string }
let user  = User { id: 1, name: "Alice" }
let n     = user.name

Field access on any, Result, or any union type requires a match to narrow the value first. See Type System for the full rules.

Records are immutable. Use the non-destructive update form to produce a modified copy:

let p2 = point { x: 15 }   // y carried over

Match Expressions

matchExpr   ::= "match" expr "{" matchArm+ "}"
matchArm    ::= pattern "=>" expr
pattern     ::= unaryExpr                              (* literals incl. -1, +42 *)
              | ID ("{" fieldPattern "}")?             (* constructor / destructure *)
              | ID "(" pattern ("," pattern)* ")"      (* positional constructor *)
              | ID ":" type                            (* type annotation *)
              | ID ":" "{" fieldPattern "}"            (* named structural *)
              | "{" fieldPattern "}"                   (* anonymous structural *)
              | "_"                                    (* wildcard *)
fieldPattern::= ID ("," ID)*
type Status = Ready | Running | Done { code: int }

let label = match status {
    Ready          => "ready"
    Running        => "running"
    Done { code }  => "done (${code})"
}

Pattern semantics, exhaustiveness, and the ternary shorthand are in Pattern Matching.

Variable Binding