Language Reference
Complete Synoema reference — syntax, types, stdlib, concurrency
Contents
- §1 Mental Model
- §2 Comments
- §3 Functions and bindings
- §4 Data types
- §5 Operators
- §6 Conditionals and control flow
- §7 Lists
- §8 Records
- §9 ADT (Algebraic types)
- §10 Modules and imports
- §11 Traits and implementations
- §12 Input/Output (IO)
- §13 Strings
- §14 Math builtins
- §15 Concurrency
- §16 Async/Await
- §17 Verification contracts
- §18 Testing
- §19 Error handling
- §20 Common compiler errors
- §21 CLI commands
- §23 Binary Metadata & Extraction
§1 Mental Model
If you know Python or Haskell, unlearn these habits:
| Instead of (Python/Haskell) | Write in Synoema | Why |
|---|---|---|
def f(x): | f x = body | No def keyword |
if c then x else y | ? c -> x : y | 3 tokens instead of keywords |
return x | (just an expression) | Last expression = result |
[1, 2, 3] | [1 2 3] | Space-separated, no commas |
let x = e in body | Indented x = e, then body | Offside rule |
data T = A | B | T = A | B | No keyword — name = constructors |
class / instance | trait / impl | |
import M | use M (f g) | Selective import |
s1 + s2 (strings) | s1 ++ s2 | + is numbers only |
f"x={x}" | "x=${x}" | Interpolation via ${} |
Key principles:
- No
def, noreturn—name arguments = body; last expression = result - Offside rule — indentation defines blocks (like Python)
- Everything is immutable — all bindings are immutable
- Strict evaluation — eager left-to-right evaluation
- Types are inferred — annotations are optional
- Define before use — functions must be declared before they are used
- Multi-equation functions at top level only
§3 Functions and Bindings
-- Function: name arguments = body (last expression = result)
double x = x * 2
-- Multiple equations = pattern matching (first match wins)
-- Top-level module only!
fac 0 = 1
fac n = n * fac (n - 1)
-- Wildcard _ matches anything
describe 0 = "zero"
describe _ = "other"
-- Local bindings via indentation (offside rule)
circleArea r =
pi_val = 3.14159 -- local variable
r2 = r * r
pi_val * r2 -- result
-- Lambda: \arguments -> body
square = \x -> x * x
add = \x y -> x + y
-- Pipe |>: x |> f = f x (reads as pipeline)
result = [1..10]
|> filter (\x -> x % 2 == 0)
|> map (\x -> x * x)
|> sum
-- result = 220
-- Currying (partial application)
addFive = add 5 -- function Int -> Int
results = map (add 10) [1 2 3] -- [11 12 13]
§4 Data Types
Hindley-Milner type inference — annotations are optional but useful as documentation.
Primitive types
| Type | Examples | Notes |
|---|---|---|
Int | 42, -7, 0 | 63-bit signed integer |
Float | 3.14, -0.5 | 64-bit IEEE 754 |
Bool | true, false | |
String | "hello", "x=${x}" | UTF-8, interpolation via ${} |
() | () | Unit — no meaningful result |
Nat | 5, 0n | Non-negative integer |
Rational | 1/2, 3/4 | Exact fraction without float rounding |
Char | 'A', '\n' | Unicode character |
Bytes | b"hello" | Binary data |
-- Float and Int do not mix -- explicit conversion required
area = pi * to_float 5 * to_float 5 -- Float * Float * Float
-- Rational: exact fraction arithmetic
sum_thirds = 1/3 + 2/3 -- exactly 1 (not 0.9999...)
-- String interpolation
name = "World"
s = "Hello, ${name}!" -- "Hello, World!"
n = "2 + 2 = ${2 + 2}" -- "2 + 2 = 4"
Composite types
| Type | Example | Notes |
|---|---|---|
List a | [1 2 3], [] | Homogeneous, space-separated |
| Record | {x=3, y=4} | Named fields |
| ADT | Color = Red | Green | Blue | Algebraic types — no keyword |
Result a e | Ok 42, Err "fail" | From prelude |
Maybe a | Just 42, None | From prelude |
§5 Operators
| Operator | Meaning | Example |
|---|---|---|
<- | IO bind | line <- readline |
|> | pipe: x |> f = f x | [1 2 3] |> sum |
|| | logical OR | a || b |
&& | logical AND | a && b |
== != | equality / inequality | x == 0 |
< > <= >= | comparison | x > 0 |
++ | string and list concatenation | "a" ++ "b" |
+ - | addition / subtraction | x + 1 |
* / % | multiply / divide / remainder | x * 2, 10 % 3 |
** | Float exponentiation | 2.0 ** 0.5 |
>> | function composition | double >> show |
! - | NOT / unary minus | !flag, -x |
. | record field access | point.x |
f x | function application | sqrt 2.0 |
Every operator is exactly 1 BPE token (cl100k_base).
-- Int / Int = Int (integer division)
quot = 10 / 3 -- 3 (not 3.333...)
rem = 10 % 3 -- 1 (remainder)
-- Cons operator : (prepend to list)
lst = 1 : [2 3] -- [1 2 3]
-- In patterns requires parens: (x:xs)
§6 Conditionals and Control Flow
-- Conditional expression: ? condition -> then : else
-- This is an EXPRESSION, not a statement -- it returns a value
abs x = ? x >= 0 -> x : -x
-- Chained conditions (elif equivalent)
classify x =
? x < 0 -> "negative"
: ? x == 0 -> "zero"
: "positive"
-- Pattern matching via multiple equations (idiomatic)
fac 0 = 1
fac n = n * fac (n - 1)
-- Patterns: literal, wildcard _, cons (x:xs), constructor
head (x:_) = x -- first element
tail (_:xs) = xs -- tail
-- Cons patterns must be in parens!
-- CORRECT: head (x:_) = x
-- WRONG: head x:_ = x ← parse error
§7 Lists
-- Creation
empty = []
nums = [1 2 3 4 5] -- space-separated, no commas!
strs = ["a" "b" "c"]
ranges = [1..10] -- [1 2 3 4 5 6 7 8 9 10]
-- Cons: prepend element
lst = 0 : [1 2 3] -- [0 1 2 3]
-- Pattern matching (patterns in parens!)
myHead (x:_) = x
myTail (_:xs) = xs
myLen [] = 0
myLen (_:xs) = 1 + myLen xs
-- Standard library
map (\x -> x * 2) [1..5] -- [2 4 6 8 10]
filter (\x -> x > 3) [1..5] -- [4 5]
foldl (+) 0 [1..100] -- 5050
take 3 [1..10] -- [1 2 3]
drop 3 [1..10] -- [4 5 6 7 8 9 10]
reverse [1 2 3] -- [3 2 1]
sort [3 1 2] -- [1 2 3]
zip [1 2 3] ["a" "b" "c"] -- [(1,"a") (2,"b") (3,"c")]
concat [[1 2] [3 4]] -- [1 2 3 4]
any (\x -> x > 5) [1..10] -- true
all (\x -> x > 0) [1..5] -- true
length [1 2 3] -- 3
join ", " ["a" "b" "c"] -- "a, b, c"
elem 3 [1 2 3 4] -- true
§8 Records
-- Type definition
type Point = {x: Int, y: Int}
-- Creation
p = {x=10, y=20}
-- Field access
px = p.x -- 10
py = p.y -- 20
-- Update (immutable -- creates a new record)
p2 = {p | x=15} -- {x=15, y=20}
p3 = {p | x=15, y=25} -- {x=15, y=25}
-- Pattern matching on records
showPoint {x=px, y=py} = "Point(${show px}, ${show py})"
-- Nested records
type Rect = {topLeft: Point, bottomRight: Point}
r = {topLeft={x=0, y=0}, bottomRight={x=100, y=100}}
width = r.bottomRight.x - r.topLeft.x -- 100
§9 ADT — Algebraic Data Types
ADTs are defined as a union of constructors. No keyword — just Name = Constructor | Constructor. The type keyword is for type aliases only (see §4).
-- Definition: no data, no type keyword
Color = Red | Green | Blue
Shape
= Circle Float -- one argument
| Rectangle Float Float -- two arguments
| Point -- no arguments
-- Parameterized types
Maybe a = Just a | None -- None, not Nothing! (from prelude)
Result a e = Ok a | Err e
-- Usage
c = Circle 5.0
r = Rectangle 10.0 20.0
-- Pattern matching in function heads
area (Circle r) = 3.14159 * r * r
area (Rectangle w h) = w * h
area Point = 0.0
-- Maybe: use None, not Nothing
fromMaybe def None = def
fromMaybe _ (Just x) = x
-- Result
safe_div _ 0 = Err "division by zero"
safe_div a b = Ok (a / b)
-- Derive typeclasses automatically
Status = Active | Inactive | Pending derive (Show, Eq, Ord)
-- Tree (recursive polymorphic ADT)
Tree a = Leaf | Node (Tree a) a (Tree a)
tree_insert x Leaf = Node Leaf x Leaf
tree_insert x (Node l v r) =
? x < v -> Node (tree_insert x l) v r
: ? x > v -> Node l v (tree_insert x r)
: Node l v r
Important gotchas:
- Constructors start with uppercase:
Just,None,Circle,Ok,Err - Variables start with lowercase:
x,r,result - The prelude defines
Maybe a = Just a | None— useNone, neverNothing - Recursive ADTs like
Treework without annotations — HM infers everything - Cons patterns require parentheses:
(x:xs)notx:xs
§10 Modules and Imports
-- Import specific functions
use Math (sqrt pow sin cos)
-- Import everything from a module
use List (*)
-- File math_utils.sno automatically creates module MathUtils
-- IMPORTANT: define before use -- functions must be declared before use
-- No forward declarations
§11 Traits and Implementations
-- Trait definition (typeclass / interface)
trait Displayable a =
display : a -> String
-- Implementation for a concrete type
impl Displayable Int =
display x = "Int(${show x})"
impl Displayable Bool =
display true = "yes"
display false = "no"
-- Usage with constraints
printAny : Displayable a => a -> ()
printAny x = print (display x)
§12 Input/Output (IO)
-- Output
print "Hello" -- print + newline
-- File operations
content = file_read "data.txt" -- read entire file as String
file_write "out.txt" "content" -- write to file
exists = file_exists "data.txt" -- Bool
-- Subprocess (run a command)
pfd = fd_popen "ls -la" -- open process for reading
line = fd_readline pfd -- read a line
fd_close pfd -- close
-- Environment
val = env_or "MY_VAR" "default" -- env var or default
-- Network (TCP)
listener = tcp_listen 3000 -- open port
client = tcp_accept listener -- wait for connection
req = fd_readline client -- read request
fd_write client "response" -- send response
fd_close client
§13 Strings — Standard Library
-- Concatenation and interpolation
s = "Hello" ++ ", " ++ "World" -- "Hello, World"
n = 42
s2 = "n = ${n}" -- "n = 42"
str_len "Hello" -- 5
str_slice "Hello World" 6 11 -- "World"
str_find "Hello World" "World" 0 -- 6 (-1 if not found)
str_starts_with "Hello" "He" -- true
str_ends_with "Hello" "lo" -- true
replace "Hello World" "World" "Synoema" -- "Hello Synoema"
str_split "a,b,c" "," -- ["a" "b" "c"]
join ", " ["a" "b" "c"] -- "a, b, c"
str_upper "hello" -- "HELLO"
str_lower "HELLO" -- "hello"
str_trim " hello " -- "hello"
show 42 -- "42" (any type to String)
to_int "42" -- Just 42 / None
§14 Math Builtins
15 built-in math functions. All operate on Float.
| Function | Description | Example |
|---|---|---|
sin x | Sine (radians) | sin 0.0 → 0.0 |
cos x | Cosine (radians) | cos 0.0 → 1.0 |
tan x | Tangent | tan (pi/4.0) → 1.0 |
asin x | Arcsine | asin 1.0 → π/2 |
acos x | Arccosine | acos 0.0 → π/2 |
atan x | Arctangent | atan 1.0 → π/4 |
atan2 y x | 2D arctangent (sign-aware) | atan2 1.0 1.0 → π/4 |
exp x | e^x | exp 1.0 → 2.718... |
ln x | Natural logarithm | ln e → 1.0 |
log10 x | Base-10 logarithm | log10 100.0 → 2.0 |
log2 x | Base-2 logarithm | log2 8.0 → 3.0 |
pi | π = 3.14159... | circleArea r = pi * r * r |
e | e = 2.71828... | compound = e ** (rate * time) |
mean xs | Arithmetic mean | mean [1.0 2.0 3.0] → 2.0 |
stddev xs | Sample std. dev. (n−1) | stddev [1.0 2.0 3.0] → 1.0 |
-- Degrees to radians
toRad deg = deg * pi / 180.0
-- sin(90deg) = 1.0
val = sin (toRad 90.0) -- 1.0
-- Circle area
circleArea r = pi * r * r
-- Distance between two points
distance x1 y1 x2 y2 =
dx = x2 - x1
dy = y2 - y1
sqrt (dx*dx + dy*dy)
-- Statistics
data_pts = [1.0 2.0 3.0 4.0 5.0]
avg = mean data_pts -- 3.0
dev = stddev data_pts -- 1.581...
§15 Concurrency
Structured parallelism with data isolation. Threads do not share mutable state.
-- scope: waits for ALL spawned threads inside
-- spawn: run in a separate OS thread
main =
scope
spawn (print "Worker 1") -- parallel
spawn (print "Worker 2") -- parallel
-- both have finished here
-- Channels: pass data between threads
main2 =
ch = chan
scope
spawn (send ch 42) -- producer
val = recv ch -- consumer (= 42)
print "Got: ${show val}"
-- Bounded channel (backpressure)
ch = bounded_chan 10 -- 10-element buffer
-- Non-blocking
sent = try_send ch 42 -- Bool
val = try_recv ch -- Maybe a
-- Timeout
result = recv_timeout ch 500 -- Maybe a (500 ms)
-- select: first ready from multiple channels
pair = select [ch1 ch2 ch3] -- (idx, value)
idx = fst pair
value = snd pair
-- pmap: parallel map (CPU-bound)
results = pmap heavy_fn [1..10000] -- all CPU cores
§16 Async/Await
Synoema has native async/await support (Phase D). Async functions return a Task a — a deferred computation. The JIT compiles async functions into stackless state machines. The interpreter uses OS threads.
-- Declare: async fn name = body
async fn fetch_data url =
content = await (async_read url)
upper content
-- Call: await e where e : Task a
main =
result = await fetch_data "/tmp/data.txt"
print result
-- Multiple awaits (JIT: stackless state machine)
async fn pipeline src dst =
raw = await (async_read src)
_ = await (async_write dst (upper raw))
done = await (async_read dst)
done
Async builtins
| Builtin | Type | Description |
|---|---|---|
async_sleep ms | Int → Task Unit | Non-blocking sleep (milliseconds) |
async_read path | String → Task String | Async file read |
async_write path s | String → String → Task Int | Async file write; returns bytes written |
async_tcp_connect host port | String → Int → Task Int | Connect to TCP host; returns socket fd |
async_tcp_read fd | Int → Task String | Async TCP read |
async_tcp_write fd s | Int → String → Task Int | Async TCP write |
async_tcp_listen port | Int → Task Int | Listen on port; returns listener fd |
async_tcp_accept fd | Int → Task Int | Accept connection; returns client fd |
async_tcp_close fd | Int → Task Unit | Close socket |
Error handling and cancellation (Phase E)
-- scope_result: capture panic/error as Result
async fn risky =
await (async_sleep 10)
error "something went wrong"
main =
r = scope_result (\() -> await risky)
? is_ok r -> print "ok"
: print ("failed: " ++ show (unwrap_err r))
-- Cancellation
token = new_cancel_token ()
-- ... in another thread: cancel token
result = with_cancel token some_task -- Maybe a (None if cancelled)
-- Timeout
result2 = await_with_timeout 500 some_task -- Maybe a (None if timed out)
-- try_await: await without propagating panics
safe = try_await risky_task -- Result a Error
Task combinators
-- race: first task to complete wins
winner = race [task1 task2 task3]
-- gather: run all in parallel, wait for all
results = gather [fetch_a fetch_b fetch_c] -- List a (ordered)
Reactor (Phase G): The async runtime uses mio (epoll/kqueue) for socket I/O and a timer wheel for sleep. A bounded file-I/O pool handles async_read/async_write (4 workers, configurable via SNO_FILE_IO_THREADS).
§17 Verification Contracts
-- requires: precondition (checked BEFORE execution)
-- ensure: postcondition (checked AFTER, result = returned value)
safe_div : Int -> Int -> Result Int String
requires b != 0 -- b must not be zero
ensure is_ok result -- result is always Ok
safe_div a b = ? b == 0 -> Err "division by zero" : Ok (a / b)
-- Multiple contracts
clamp : Int -> Int -> Int -> Int
requires lo <= hi
ensure lo <= result
ensure result <= hi
clamp lo hi x =
? x < lo -> lo
: ? x > hi -> hi
: x
-- Check without running
-- sno check file.sno
-- Check + run with timeout + JSON output
-- sno verify file.sno
-- Contract documentation
-- sno doc --contracts file.sno
§18 Testing
--- Doctests in doc comments (triple dash)
--- > square 4
--- 16
--- > square 0
--- 0
square x = x * x
-- Unit tests
test "fac base" = fac 0 == 1
test "fac five" = fac 5 == 120
test "sort works" = sort [3 1 2] == [1 2 3]
-- Property-based tests
test "reverse" = prop xs -> reverse (reverse xs) == xs
test "sort idem" = prop xs -> sort (sort xs) == sort xs
sno test file.sno -- all tests in file
sno test examples/ -- recursively in directory
sno test --doctest f.sno -- doctests only
§19 Error Handling
-- Result: for recoverable errors
parse_int : String -> Result Int String
parse_int s =
n = to_int s
? is_just n -> Ok (unwrap n)
: Err "Not a valid integer: ${s}"
-- Chain with and_then
result = parse_int "42" |> and_then (\n -> Ok (n * 2)) -- Ok 84
-- error: for unrecoverable errors (halts the program)
assertPositive n =
? n > 0 -> n
: error "Expected positive, got ${show n}"
-- JSON errors for LLM tools
-- sno --errors json run file.sno
§20 Common Compiler Errors
| Message | Cause and fix |
|---|---|
parse error: unexpected token | Syntax broken. Check parens around cons patterns: (x:xs), not x:xs |
type error: expected T but got U | Type mismatch. Check argument types. Int and Float do not mix — use to_float |
unbound variable: name | Variable not declared OR declared AFTER use (define-before-use) |
non-exhaustive patterns | Pattern matching does not cover all cases. Add _ or all ADT constructors |
occurs check: infinite type | Recursive type without annotation. Add an explicit type annotation |
requires violation | Contract precondition violated. Check arguments before calling |
parse error: multi-equation def in let | Multi-equation functions at top level only, not inside let/where |
Int literal in Float context | Use to_float n or write the literal with a dot: 5.0 |
§22 Package Manager
Synoema packages are source-only — only .sno files, verified at publish time. No native binaries, no .so or .dll files are allowed in the registry.
- Browse packages — search the registry by keyword or capability
- Install commands —
sno pkg add,sno pkg login - Publish a package — developer guide: sno.toml format, quality gates, CLI workflow
Quick start
sno pkg login # authenticate via GitHub or Google
sno pkg add sno-http # install a package
sno pkg search http # search registry
import "sno-http"
use Http (http_serve, http_router)
main = http_serve 8080 my_router
Import layers
| Layer | Examples | Requires |
|---|---|---|
| Syntax | operators, let, fn, trait | nothing |
| Builtins | error, show, sin, tcp_listen | nothing |
| Prelude | Result, Maybe, Map, Ok, Err | nothing (auto-loaded) |
| Packages | http_serve, http_router, http_json | import "sno-http" + sno pkg add sno-http |
Only Layer 4 requires an explicit import statement and sno pkg add. Layers 1–3 are always available.
§23 Binary Metadata & Documentation Extraction
Compiled .wasm and .bc artifacts embed documentation metadata by default — types, contracts, and doc comments — accessible without the source file.
Controlling embedding
# Default: embed docs in output
sno build examples/quicksort.sno
sno wasm examples/factorial.sno
# Production / size-sensitive: skip embedding (~5–15% smaller)
sno build --no-docs examples/quicksort.sno
sno wasm --no-docs examples/factorial.sno
Extracting from a compiled binary
sno doc-from-binary ./quicksort.wasm # JSON (default)
sno doc-from-binary ./quicksort.wasm --format md # Markdown
sno doc-from-binary ./quicksort.sno.bc # from bytecode
JSON schema
{ "version": "0.1",
"types": [{ "name": "Shape", "kind": "adt", "signature": "Shape a", "doc": "..." }],
"contracts": [{ "function": "safe_div", "requires": "b != 0", "ensures": null }],
"docs": [{ "name": "map", "signature": "a -> b -> [a] -> [b]", "doc": "..." }] }
| Format | Storage location |
|---|---|
.wasm | WASM custom section .sno.metadata |
.bc bytecode | Text block [.sno.metadata] appended at end |
| Native ELF/Mach-O | Planned for v0.2 |
§21 CLI Commands
| Command | Description |
|---|---|
sno run file.sno | Run via interpreter (all features) |
sno jit file.sno | Run via Cranelift JIT (~3× faster than Python) |
sno eval "expr" | Evaluate expression, print result |
sno check file.sno | Parse + typecheck without running |
sno verify file.sno | Check + run with timeout, JSON output |
sno test dir/ | Run doctests, unit, and property tests |
sno fmt file.sno | Format file in-place |
sno doc file.sno | Generate documentation from --- doc-comments |
sno doc examples/ | Generate docs for all .sno files in a directory |
sno doc-from-binary ./app.wasm | Extract embedded documentation from a compiled binary |
sno wasm file.sno | Compile to WebAssembly (.wasm, embeds docs by default) |
sno wasm --no-docs file.sno | WASM without embedded documentation |
sno build --native file.sno | AOT native binary (macOS/Linux x86_64 or aarch64) |
sno build --no-docs file.sno | Bytecode without embedded documentation |
sno new myapp | Create a new project |
sno watch run file.sno | Re-run on every file save |
sno doctor | Check installation (PATH, GBNF, version) |
sno setup claude | Configure MCP integration for Claude |
sno --errors json run file.sno | Errors in JSON format (for LLM tools) |
More on installation: Start · User Guide · IoT Platform
§2 Comments