Aether Stdlib Module Pattern
This is the canonical shape for an Aether standard-library module. Use it when adding a new std/<name>/ module or retrofitting an existing one.
The worked example throughout this document is std/fs/module.ae, read it alongside this doc.
The shape
Every std/<name>/module.ae has two layers:
- Raw externs, thin declarations of the underlying C functions. Fallible externs use a
_rawsuffix and return the old C conventions (ptrfor nullable,intfor 1/0 success flags). - Go-style wrappers, Aether functions that call the raw externs and translate C-style returns into Go-style
(value, err)tuples (or justerrfor operations with no return value).
// std.X - ... header
//
// API shape:
// - Raw externs end in `_raw` and return ptr/int in the old C-style
// convention. They are the escape hatch for advanced callers.
// - Aether-native wrappers (below) use Go-style `(value, err)` tuple
// returns and are the idiomatic way to call X operations.
// ---- raw externs ----
extern X_op_raw(args) -> ptr
extern X_pure_op(args) -> int // infallible, no _raw, no wrapper
// ---- Go-style wrappers ----
// Short docstring stating: what it does, and exactly what shape it
// returns on success vs. failure.
op(args) -> {
r = X_op_raw(args)
if r == null {
return null, "human-readable error"
}
return r, ""
}
Rules
When to add a _raw extern + wrapper
Any operation that can fail at runtime, open a file that doesn't exist, allocate when memory is tight, parse malformed input, look up an absent key, etc. These get:
- A
_raw-suffixed C function (either rename the existing one or add it alongside). - A companion Aether wrapper that turns the C return into either
(value, "")on success or(zero-value, "reason")on failure.
When NOT to wrap
Keep raw externs with no _raw suffix and no wrapper for operations that are:
- Pure total functions that can't fail for any well-typed input (
path_join,path_basename,math_sqrt,math_sin,string_length,list_size), and infallible boolean queries (file_exists,path_is_absolute,fs_is_symlink,json_is_null,map_has). - Sentinel-returning infallibles where the sentinel is part of the contract, not an error. Example:
file_mtimereturns0for missing files or stat failure, callers already handle zero as "no mtime", so wrapping would just add noise. - Intentionally void fire-and-forget operations. Example:
log_writeinstd.logthe module deliberately degrades to stderr if the log file is unopenable, so callers don't need an error from each write. - Structural/DSL builders. Example: the manifest DSL in
std.host,describe,input,event,bindings,java, … are void because they structurally append to a global registry. They're not silent failures, they're a different pattern.
Return shapes
| Operation | Success return | Failure return |
|---|---|---|
| Produces a value | (value, "") | (zero-value, "reason") |
| Produces nothing (setter, writer) | "" | "reason" |
| Lookup that can be absent | (value, "") / (null, "") for absent | (null, "reason") for error |
For the lookup row: distinguish absent from error. An absent key is not an error, the caller asked if X exists. Only return a non-empty error string when something actually went wrong (null receiver, wrong type, allocation failure).
Structured errors (pilot, issue #392)
A small set of newer wrappers extends the (value, err) shape with a third element: an integer "kind" that lets callers programmatically discriminate between failure modes without parsing the human-readable message. The tuple is (value, kind: int, message: string). kind == 0 means success (constant KIND_OK); non-zero means an error of the named class.
// (bytes_copied, kind, message)
// on success: (n, KIND_OK, "")
// on failure: (bytes_so_far, KIND_NOT_FOUND, "open src: ...")
copy(src: string, dst: string) -> (int, int, string) {
return fs_copy_raw(src, dst)
}
Why kinds are returned per-call (not via process-global accessors like vcr.last_kind()): std.fs is called concurrently from actors; a shared mutable slot would race. The triple costs one extra register on every ABI we target (x86-64 SysV, AArch64 AAPCS, MS x64).
The kinds themselves are exported const int values from the module that uses them (e.g. fs.KIND_NOT_FOUND, fs.KIND_PERMISSION_DENIED). Modules that use kinds must also export a matching AETHER_<MODULE>_KIND_* C macro set in their header so the C side and the Aether surface stay in lock-step.
This shape is opt-in and additive. Existing wrappers keep their (value, err) or string-error returns, converting them is a non-breaking but wider change tracked separately. The pilot scope is std.fs.{copy, move, realpath, chmod}. Future modules may adopt the shape after the pilot has shaken out.
| Operation | Success return | Failure return |
|---|---|---|
| Structured-error producer | (value, KIND_OK, "") | (zero-value-or-partial, KIND_*, "reason") |
Docstring
Every wrapper gets a short docstring stating what it does and the exact return shape. Match this voice:
// Open a file. Returns (handle, "") on success, (null, error) on failure.
open(path: string, mode: string) -> {
...
}
Ownership of returned strings
If the underlying C function returns a borrowed const char* (pointer into another object's memory), the wrapper must copy it before returning so the caller's string outlives the source object. The idiomatic copy in Aether is string_concat(borrowed, "") see std.fs's readlink wrapper for the pattern.
Worked example: std.fs
std/fs/module.ae is the reference implementation. Skim it start-to-finish; it demonstrates:
- Raw
_rawexterns grouped at the top with structural comments. - Pure-function externs (the
path_*block) that correctly have no_rawsuffix and no wrapper. - Every fallible operation wrapped in a Go-style function below.
- Consistent
"cannot <verb> <noun>"error phrasing. - An explicit note where a raw extern is left unwrapped intentionally (
file_mtime's sentinel-0 contract).
Modules that follow this pattern
fs, io, http, net, os, string, tcp, dir, file full wrapper coverage. json, collections, list, map completed in the stdlib-consistency pass alongside this doc. log and host, intentionally follow domain-specific variants (fire-and-forget logging; DSL builders) as described above.
Adding a new stdlib module
- Create
std/<name>/withmodule.aeandaether_<name>.c/.h. - Write the C functions first; split into
<op>_raw(fallible) and<op>(infallible) per the rules above. - Mirror each function as an
externinmodule.ae, copying the_rawsuffix for fallible ones. - Add the "API shape" preamble comment.
- Add Go-style wrappers for every fallible extern.
- Add tests exercising both the success and error paths of each wrapper.
- Add an example under
examples/stdlib/<name>-demo.aeshowing idiomatic use of the Go-style API.