Checked Exceptions and Coeffects
Nexus separates two concerns in function signatures: coeffects (what capabilities the function needs from its environment) and checked exceptions (what exceptions the function may throw). This distinction is central to the design thesis – every dependency and side effect is declared, not implied.
Function Signature Shape
fn (args...) -> Ret require { Coeffects } throws { Exceptions }
Both clauses are optional. Omitted means empty row (pure function with no requirements).
let pure = fn (x: i64) -> i64 do return x + 1 end
let greet = fn (msg: string) -> unit require { Console } do
Console.println(val: msg)
return ()
end
let risky = fn () -> unit throws { Exn } do
raise RuntimeError(val: "oops")
end
Checked Exceptions
The only builtin throws type is Exn. try/catch discharges Exn from the protected block:
exception NotFound(msg: string)
let search = fn (key: string) -> string throws { Exn } do
raise NotFound(msg: key)
end
let main = fn () -> unit do
try
let _ = search(key: "missing")
catch e ->
match e do
case NotFound(msg: m) -> ()
case _ -> ()
end
end
return ()
end
Exception declarations extend the builtin Exn type:
export exception PermissionDenied(msg: string, code: i64)
raise is an expression that immediately unwinds to the nearest catch. All I/O capabilities use the coeffect system, not throws.
Ports
A port defines a coeffect interface – a set of function signatures that must be provided by the environment:
export port Logger do
fn info(msg: string) -> unit
fn warn(msg: string) -> unit
When a function calls Logger.info(...), it must have Logger in its require row.
Port methods can themselves declare throws and coeffects:
export port Fs do
fn open_read(path: string) -> %Handle throws { Exn }
fn read(handle: %Handle) -> { content: string, handle: %Handle }
fn close(handle: %Handle) -> unit
Handlers
A handler is a value that implements all methods of a port:
let console_logger = handler Logger require { Console } do
fn info(msg: string) -> unit do
Console.println(val: "[INFO] " ++ msg)
return ()
end
fn warn(msg: string) -> unit do
Console.println(val: "[WARN] " ++ msg)
return ()
end
end
let mock_logger = handler Logger do
fn info(msg: string) -> unit do return () end
fn warn(msg: string) -> unit do return () end
end
Handler require { ... } propagates: injecting console_logger adds Console to the caller’s requirements.
The type checker enforces:
- Handler methods must match port signatures exactly
- All port methods must be implemented (exhaustive)
- Handler method bodies inherit the handler’s
requireclause
Inject
inject supplies handler values to a lexical scope, discharging matching require entries:
inject stdio.system_handler do
inject console_logger do
program() // program's Logger + Console requirements satisfied
end
end
Rules:
injectmust reduce requirements – injecting an unused handler is a type error- Multiple handlers can be injected in a single
injectstatement:inject h1, h2 do ... end - Handler requirements propagate to the enclosing scope
Main Constraints
The main function has special restrictions:
- Signature:
() -> unit throwsmust be empty (all exceptions must be handled internally)requiremay contain any subset of runtime permissions:{ PermFs, PermNet, PermConsole, PermRandom, PermClock, PermProc, PermEnv }
let main = fn () -> unit require { PermConsole } do
inject stdio.system_handler do
Console.println(val: "Hello")
end
return ()
end
Permission Mapping
Runtime permissions (PermFs, PermNet, etc.) are special coeffects that map to WASI capabilities. They serve as the bridge between the type system and the runtime sandbox. See WASM and WASI for the complete mapping table.
Row Typing
Throws and coeffect rows are checked by row unification (no subtyping). Open rows use tail variables for polymorphism:
// This function is polymorphic over additional requirements
let log_and_do = fn <R>(f: () -> unit require { Logger | R }) -> unit require { Logger | R } do
Logger.info(msg: "starting")
f()
return ()
end
Compatibility is structural – two rows unify if they contain the same entries (order-independent).