Introduction
nevla exists because training and tuning language models means living in Python, and Python kept core dumping mid-run. Everything good in ML is a python library; everything painful about those runs — the crash twenty minutes in, the typo that survives until the epoch ends, the exception that ate the metrics — is the language around the libraries. nevla is a new language around the same libraries: Go’s discipline, CPython’s ecosystem.
import py "torch"
// check propagates: the caller decides
fn logits(n int) (str, error?) {
w := check torch.randn([784, 10], requires_grad: true)
x := check torch.randn([n, 784])
y := check (x @ w)
return check str(y.shape), none
}
// main can act, so it handles
fn main() {
shape, err := logits(32)
if err != none {
print("torch failed: " + err.msg)
return
}
print("logits: " + shape)
}
That is real torch, one import away, with Python’s exceptions arriving as typed error values instead of tracebacks.
The tenets
These are the principles the language is built on, each with its reason. They are recorded as decisions in the repo’s ADRs; this is the reader’s digest.
The whole program is checked before any of it runs, and no crash
originates in nevla. The worst outcomes are an error returned from
main or a controlled runtime fault with a nevla stack trace, and
every Python exception arrives as an error value. The one documented
boundary: a C extension that itself segfaults below the bridge takes
the process with it, as it would take any host. The reason for the
tenet is the twenty-minute crash: a training run should die at
nevla check, in milliseconds, or not at all.
Errors are values, and handling them is mandatory. Dropping an error
is a compile error; check propagates, v, err := handles. There is no
exception control flow to forget about. Handle errors at the layer that
can act; propagate only when the caller owns the decision.
Everything is data. Errors carry inspectable fields (including the
file:line where they were born). A test is a fallible function whose
outcome is an error value. A test table is a list of structs. Faults are
the single deliberate exception — process death refuses to be a value,
which is exactly what makes the no-crash guarantee provable.
Follow the Go way unless there is a compelling reason not to. The
copy model, capitalization visibility, _test files seeing their
module’s internals, one true format with no configuration — all Go,
adopted wholesale. Deviations exist (option types instead of nil, a
lowercase stdlib) and each one has its reason written down.
Python is the ecosystem, not the runtime. One file in the interpreter speaks to CPython. A chain of Python operations is one fallible unit. Dependencies are declared in the manifest or the program does not compile — a missing package is a compile error, not a stack trace at hour two.
Names are descriptive. A longer clear name beats a shorter cryptic
one: file.modified, never mtime. Domain-standard terms that
practitioners actually say (http, ctx, regex, min) stay short;
Unix fossils and ad hoc abbreviations do not get in.
Break early. Until the language has users to protect, fundamental improvements ship now, not after they calcify. The version stamp in every project says what it was built against, and that is the promise: honesty about change, not absence of change.
Nothing is remembered. Anything derived is generated or enforced by a test: these docs’ reference chapters render from the spec, every example in this book compiles in CI, the dependency lock fingerprints its manifest, releases flow from a git tag to PyPI and Homebrew without hands. If keeping two things in sync requires a human to remember, that’s a bug.
nevla is built primarily for its author and the agents that write most of its code — and it would be worth building even if it never gains another user. That freedom is why the tenets above can be held without compromise.
Getting started
Try it in the playground without installing anything, or install it:
uv tool install nevla # or: brew install guygrigsby/tap/nevla
nevla new hello && cd hello
nevla run
You don’t need python installed first: uv fetches a managed CPython for
the install, and the wheels carry their own libpython. The py bridge
does need a matching CPython standard library at runtime, which the uv
and brew installs both guarantee; a hand-rolled setup missing one gets a
warning naming the fix (uv python install <version>).
Two binaries, split like uv and python: nevla does setup (new,
py add, check, fmt, test, run), nv runs code (nv file.nv;
bare nv is the repl).
This book is the guide. The normative reference is the language spec, and every behavior in it is pinned by golden tests.
A tour of nevla
Each example links to the playground, where the URL carries the program itself; edit and rerun as you go.
Hello
fn main() {
print("hello, nevla")
}
fn main() is the entry point. print renders anything. Four-space
indents and nevla fmt settles every other style question.
Errors are values
fn half(n int) (int, error?) {
if n % 2 != 0 {
return 0, error.new("odd number")
}
return n / 2, none
}
fn main() {
v, err := half(42)
if err != none {
print("error: " + err.msg)
} else {
print(v)
}
}
A fallible function ends its result list with error?. Dropping an error
is a compile error; you either bind it (v, err :=) and decide, or
propagate it with check, which requires your own function to return
error?:
fn quarter(n int) (int, error?) {
h := check half(n) // on error: return it, zero values elsewhere
return check half(h), none
}
A multi-value never travels as a unit, so Go’s return half(n) is a
compile error here. Propagate early with return check half(n), none,
or bind and decide: v, err := half(n); return v, err. The error slot
stays visible at every hop.
Handle errors at the layer that can do something about them; propagate only when the caller owns the decision.
Options, no nil
fn main() {
m := map[str]int{"a": 1}
v := m["a"] // a map read is int?: present or none
if v != none {
print(v + 1) // narrowed to int inside the branch
}
}
There is no nil. Absence is an option type (int?), and the checker
makes you look before you touch: using v unnarrowed is a compile
error.
The copy model
struct User {
Name str
Age int
}
fn main() {
u := User{Name: "nevla", Age: 1}
v := u
v.Age = 99
print(u.Age) // 1: structs copy
xs := [1, 2, 3]
ys := xs
ys[0] = 99
print(xs[0]) // 99: lists are references
}
Go’s split: scalars, strings, and structs copy on assignment; lists, maps, functions, and py values are references. Closures capture by reference.
Modules and visibility
import "util.nv" binds a sibling file as module util. Capitalized
top-level names are exported; lowercase is private to its file, fields
included. The Go rule, no keywords.
The py bridge
import py "torch"
fn main() (error?) {
w := check torch.randn([784, 10], requires_grad: true)
x := check torch.randn([32, 784])
logits := check (x @ w)
print(check str(logits.shape))
return none
}
A chain of Python operations is one fallible unit: any exception anywhere
in model(x).loss.item() becomes one nevla error at the point of
consumption. Keyword arguments pass through, @ is matrix
multiplication, and for range iterates any Python iterable. Inside a
project, every import py must be declared (nevla py add torch), so a
missing dependency is a compile error rather than a crash twenty minutes
into a run.
with: Python context managers
import py "torch"
fn main() (error?) {
x := check torch.randn([4, 4])
with torch.no_grad() {
y := check (x * 2)
print(check str(y.shape))
}
return none
}
with expr { } runs the block under a Python context manager: __enter__
before, __exit__ on every exit from the block. A return that carries an
error reaches __exit__ as an exception, so a manager that branches on
exception state (a transaction’s commit/rollback) sees the error path
exactly as Python would. The statement itself has no error slot —
fallible acquisition belongs before it (db := check connect(...),
then with db.transaction() { }), and an exception raised by the manager
itself is a fault. One manager per statement; nest for more.
Functions and structs
Declaring functions
fn declares a function. Every parameter of a top-level function has a
declared type, results come after the parameter list, and a function
with results must return on every path; the checker rejects a missing
return at compile time, not twenty minutes into a run.
fn double(n int) int {
return n * 2
}
fn divmod(a int, b int) (int, int) {
return a / b, a % b
}
fn main() {
print(double(21)) // 42
q, r := divmod(7, 3)
print(q, r) // 2 1
}
A function may declare several results. A call with multiple results is
a multi-value: bind every component (q, r := divmod(7, 3)) or, when
the last result is error?, propagate with check. Multi-values are
not first class; they cannot be stored, nested, or returned as a unit
(the errors chapter covers the idioms).
A function with no results returns implicitly at the end of its body; a
bare return inside one exits early.
Function values and literals
Functions are first class. A declared function is a value; a function
literal (fn(x int) int { return x * 2 }) is an expression. In a
context that supplies the function type, parameter types can be
omitted, and a literal whose body is a single expression returns that
expression’s value:
fn main() {
nums := [1, 2, 3, 4]
big := nums.map(fn(x) { x * 2 }).filter(fn(x) { x > 2 }).sum()
print(big) // 18
}
Closures capture by reference, as in Go: the closure and the enclosing scope share the variable, and writes flow both ways.
fn main() {
total := 0
add := fn(x int) { total = total + x }
add(1)
add(2)
print(total) // 3
}
Loop variables are fresh per iteration (Go 1.22 semantics), so closures made in different rounds capture different variables.
Structs
struct declares a nominal record type. Fields are separated by commas
or line breaks. A struct literal names the type and supplies every
field exactly once, in any order:
struct User {
Name str
Age int
}
fn main() {
u := User{Age: 1, Name: "nevla"}
u.Age = 2
print(u.Name, u.Age) // nevla 2
}
Structs are value types: assignment and argument passing copy (the copy model chapter has the full split). Capitalization controls visibility across modules, Go’s rule (modules). User structs have no methods in v1; write functions that take the struct.
A struct must not contain itself by value; such a value could never be constructed. Break the cycle with an option:
struct Node {
val int
next Node? // `next Node` would be a compile error
}
fn main() {
n := Node{val: 1, next: none}
print(n.val) // 1
}
Values and the copy model
The types
int float bool str
[]T map[K]V T?
fn(...) ... struct types error py
int is 64-bit signed and overflow is a fault, never a silent wrap.
float is IEEE 754 double. str is an immutable sequence of Unicode
characters (“character” means code point everywhere in nevla). bool
has exactly true and false; there is no truthiness, and conditions
must be bool.
int and float never mix: 1 + 2.5 is a compile error. Convert
explicitly.
Declaring and assigning
x := e declares x with the type of e; there is no way to declare
a variable with an explicit type, and no top-level variables or
constants. x = e assigns to an existing variable, element, or field.
Shadowing in an inner scope is fine; redeclaring in the same scope is a
compile error.
Conversions
int(x), float(x), bool(x), str(x), and []T(x) are the casts.
Fallibility follows the operand: converting from str is a parse and
returns (T, error?); numeric conversions cannot fail and are used
inline.
fn main() (error?) {
n, err := int("42") // a parse can fail
if err != none {
return err
}
print(n) // 42
i := int(3.9) // 3: truncation toward zero, single-valued
s := str(123) + "!"
print(i, s) // 3 123!
return none
}
Coming from Go: conversion is spelled like a call on the type name,
and the fallible cases return an error value instead of silently
producing a zero. int("x") gives you (0, error); nothing panics and
nothing guesses.
Value types and reference types
Nevla splits its types the way Go does. Scalars, strings, structs, and
errors are value types: assignment, argument passing, and iteration
bind copies. Lists, maps, functions, and py values are reference
types: one underlying object, however many names point at it.
struct User {
Name str
Age int
}
fn main() {
u := User{Name: "nevla", Age: 1}
v := u
v.Age = 99
print(u.Age) // 1: structs copy
xs := [1, 2, 3]
ys := xs
ys[0] = 99
print(xs[0]) // 99: lists are references
}
A struct copy is shallow in Go’s sense: a reference-typed field copies the reference, so the copy’s containers alias the original’s.
The zero value of a list or map is a fresh empty container, immediately usable. There is no nil and no nil-map write crash.
clone(xs) makes an explicit one-level copy of a list or map, matching
Go’s slices.Clone. append(xs, v) is pure: it returns a fresh list
and never mutates in place, so growth is visible only by rebinding
(xs = append(xs, v)). The list methods (map, filter, sorted)
also return fresh lists.
Equality
== and != work on scalars (int, float, bool, str) and for
comparing an option against none. Nothing else compares with ==;
lists, maps, structs, and errors are compile errors. Structural
equality goes through contains or an explicit walk, and tests use
test.eq, which compares structurally and reports the difference.
<, <=, >, >= order int, float, and str (lexicographic by
character). Chained comparisons (a < b < c) are rejected by the
checker.
Control flow
if and else
Conditions are plain bool expressions, no parentheses, braces always.
else and else if sit on the same line as the closing brace of the
previous block:
fn describe(n int) str {
if n < 0 {
return "negative"
} else if n == 0 {
return "zero"
} else {
return "positive"
}
}
fn main() {
print(describe(-3), describe(0), describe(7)) // negative zero positive
}
Conditions narrow option types inside the branches; that mechanism has its own chapter.
for: one loop keyword, three forms
fn main() {
// forever: only break or return leaves it
n := 0
for {
n = n + 1
if n == 3 {
break
}
}
// while: condition checked before each round
for n > 0 {
n = n - 1
}
// range: over an int, list, map, str, or py value
total := 0
for i := range 5 {
total = total + i
}
print(total) // 10
}
range follows Go, including ranging over an integer:
| operand | one variable | two variables |
|---|---|---|
int n | i from 0 through n-1 | compile error |
[]T | index | index, element |
map[K]V | key | key, value |
str | index | index, character (a one-char str) |
py | iteration index | index, item |
The variables can be dropped entirely (for range 3 { ... } runs the
body three times), and _ discards one position
(for _, v := range xs). Maps range in insertion order, always.
Iteration variables are fresh bindings each round, so closures created
in different rounds capture different variables.
fn main() {
for _, c := range "abc" {
print(c.to_upper())
}
// a map ranges in insertion order
m := map[str]int{"one": 1, "two": 2}
for k, v := range m {
print(k, v)
}
}
break and continue
break leaves the innermost loop; continue starts its next round.
Either outside a loop is a compile error. Both count as divergence: the
checker rejects unreachable statements after them, the same way it
rejects code after return.
Coming from Go: there are no labels, no goto, no switch, and no
three-clause for. for i := range n covers counting loops, and an
if/else if chain covers the rest.
Options, not nil
This chapter is the biggest departure from Go. There is no nil, no nil pointer, no nil map, no “invalid memory address” at hour two. Absence is a type, and the checker makes you look before you touch.
The option type
For any type T, the option type T? holds either a T or the
absent value none. Anywhere a value can be missing, the type says so:
a map read is V?, a function that might not find its answer returns
T?, a struct field that starts empty is declared T?.
struct Profile {
Name str
Nick str? // may be absent
}
fn main() {
p := Profile{Name: "nevla", Nick: none}
n := p.Nick
if n != none {
print(n)
} else {
print(p.Name) // nevla
}
}
A plain T widens into T? on assignment; the reverse never happens
implicitly. none is assignable to every option type and compares only
against options; none == none is true.
Narrowing
An option must be narrowed before use. Using it unnarrowed is a compile error, not a runtime surprise:
fn main() {
m := map[str]int{"a": 1}
v := m["a"] // v: int?
// print(v + 1) // compile error: v might be none
if v != none {
print(v + 1) // 2: v is int inside this branch
}
}
The checker follows the control flow (flow typing). A comparison
against none narrows in both directions: if v != none gives you v
as T in the then-branch, and if v == none gives it to you in the
else-branch.
Early exits narrow the rest of the function. When one side of the
branch always diverges (return, break, continue), the narrowing
survives past the statement:
fn first(xs []int) int {
if len(xs) == 0 {
return 0
}
return xs[0]
}
fn main() {
m := map[str]int{"a": 1}
v := m["a"]
if v == none {
return
}
print(v + 1) // 2: v is int from here on
print(first([7])) // 7
}
Invalidation
Narrowing is erased wherever it can no longer be proven. Assigning to
the variable erases it; a loop body erases it when the body assigns the
variable anywhere, because the body runs more than once. Rebinding with
:= makes a new variable and leaves the outer narrowing alone. Losing
a narrowing is always sound; the fix is to re-check or bind the value
out to a new name.
Coming from Go: the v, ok := m[k] two-value read does not exist.
A map read is one value of type V?, and the ok check became a type
the compiler enforces. You cannot forget it: code that compiles has
looked.
Errors and check
Errors are values, and dropping one is a compile error. Those two sentences are most of the design; the rest is vocabulary.
Error values
error is an ordinary type. Every error exposes msg, cause (the
wrapped inner error, or none), origin (the file:line where it was
born), and, for errors that crossed the Python bridge, pytype and
traceback. Construct with error.new and error.wrap:
fn main() {
e := error.new("boom")
w := error.wrap(e, "while testing")
print(w.msg) // while testing
c := w.cause
if c != none {
print(c.msg) // boom: the chain stays structured
}
}
A fallible function ends its result list with error?:
fn fetch(url str) (str, error?) { ... }
fn cleanup() (error?) { ... }
Handling is mandatory
The checker rejects any path where an error is silently dropped. Calling a fallible function as a bare statement, or binding one fewer name than the value count, is the compile error “error result must be handled”. Handling means one of three things:
- bind it and decide:
v, err := f(), then branch onerr != none; - propagate it:
check f(); - discard it explicitly:
v, _ := f(). The underscore is legal and visible in review, which is the point.
Recovery
Recovery is the two-value form plus a none-check; flow narrowing
unwraps the error? exactly like any other option:
fn half(n int) (int, error?) {
if n % 2 != 0 {
return 0, error.new("odd number")
}
return n / 2, none
}
fn main() {
v, err := half(42)
if err != none {
print("failed: " + err.msg)
return
}
print(v) // 21
}
check propagates
check e evaluates a fallible expression. On success it yields the
value(s); on error the enclosing function returns immediately, the
error in its final slot and every other slot zero-filled. The enclosing
function must itself end in error?, so propagation is visible in
every signature it passes through.
fn half(n int) (int, error?) {
if n % 2 != 0 {
return 0, error.new("odd number")
}
return n / 2, none
}
fn quarter(n int) (int, error?) {
h := check half(n) // on error: return it, zeros elsewhere
return check half(h), none
}
fn main() {
v, err := quarter(44)
if err != none {
print(err.msg)
return
}
print(v) // 11
}
A multi-value never travels as a unit: return half(n) is a compile
error even when the signatures line up. Bind and return
(v, err := half(n); return v, err) or propagate early
(return check half(n), none). The error slot stays visible at every
hop.
main may declare (error?); returning an error from main prints it
and exits nonzero.
Coming from Go: check is the if err != nil { return err } you
were going to write anyway, reduced to one keyword and checked by the
compiler. The differences that matter: you cannot forget (dropping an
error does not compile), and wrapping keeps a typed cause chain instead
of one flattened string.
Faults are not errors
Some failures do not return: index out of range, integer overflow, division by zero. These are faults; they print a nevla stack trace and terminate with a nonzero exit. Faults are deliberately not catchable, and no user program can crash the process any other way (native code inside a C extension is the one documented boundary; see the intro). If a failure is something a caller could reasonably handle, it is an error value; if it is a bug in the program, it is a fault.
The py bridge
Python is the ecosystem, not the runtime. import py "torch" gives you
the real torch, and everything that crosses the boundary arrives as
typed values with typed errors. Nothing in this chapter exists in Go;
it is the half of nevla that Go could never give you.
Importing and calling
import py "math"
fn main() (error?) {
v := check float(math.sqrt(2))
print(v > 1.41 && v < 1.42) // true
return none
}
import py "modname" imports a Python module through the bridge
(dotted paths like "os.path" work). Inside a project every py import
must be declared in the manifest (nevla py add torch), so a missing
dependency is a compile error, not a crash after the first epoch.
Values of type py are references to live Python objects. They are the
one dynamic type in the language: attribute access, calls, indexing,
and operators on them dispatch to Python at runtime.
Chains: one fallible unit
A sequence of Python operations is one fallible unit. Any exception
anywhere in model(x).loss.item() becomes one nevla error at the point
where the chain is consumed; you do not handle each step.
import py "json"
fn main() (error?) {
parsed := check json.loads("{\"a\": 1}")
n := check int(parsed["a"])
print(n) // 1
return none
}
Consume a chain with check (propagate), with v, err := (bind), or
with a conversion, which absorbs the chain’s fallibility. Letting a
chain’s error drop on the floor is the usual compile error.
Python exceptions become error values with the full story attached:
.msg is the rendered exception, .pytype names the exception class,
.traceback carries the Python traceback text.
Crossing the boundary
Nevla scalars pass into Python calls directly, as do lists and maps
(converted recursively). Named arguments pass through to Python
keywords: torch.randn([784, 10], requires_grad: true). @ is matrix
multiplication, defined when an operand is py.
Coming back is explicit: a conversion extracts a typed value from a
py, and the parse-like forms are fallible.
w := check float(logits.item()) // py to float, fallible
xs := check []float(tensor.tolist())
for x := range e iterates any Python iterable. with runs Python
context managers; a nevla error return inside the block reaches
__exit__ as an exception, so transaction-shaped managers see the
error path exactly as Python would (see
the tour).
What stays out
py values do not leak into the rest of the type system: you cannot
put one in a condition, compare one with == and get a bool, or
slice one. Every branch decision needs an extracted, typed value. That
line is what keeps a nevla program checkable while half of it lives in
CPython.
Modules
File imports
import "util.nv" binds a sibling source file as module util. The
path is relative to the importing file, the module’s name is the file
stem, and its members are reached with the dot: util.Double(3),
util.Pair{...}. Diamond imports load once; an import cycle is a
compile error naming the cycle.
// util.nv
fn Double(n int) int {
return n * 2
}
// main.nv
import "util.nv"
fn main() {
print(util.Double(21)) // 42
}
Modules are namespaces, not values: u := util does not compile, and
module functions are called directly.
Visibility: the capital letter
A module’s top-level name is exported when it starts with a capital letter; otherwise it is private to its file. No keywords, Go’s rule, and it applies to struct fields too:
- calling an unexported function through a module is a compile error;
- a struct with any unexported field cannot be constructed outside its module at all, because literals must supply every field. That is the constructor pattern: keep one field lowercase and exports control creation.
Inside the defining file, everything is reachable; the rule binds at module boundaries only.
Test files are inside the boundary
A file named util_test.nv sees util.nv’s unexported names through
the ordinary qualified syntax, exactly like Go’s same-package tests.
The testing chapter covers the rest.
The three imports
import "file" // standard library (math, error, file, ctx, gpu, http, test)
import "util.nv" // another nevla file, namespaced by stem
import py "torch" // a Python module through the bridge
Standard library modules are documented in
the reference. Py imports are governed by the project
manifest: inside a project, import py of an undeclared package is a
compile error (the py bridge).
Testing
This chapter was written first as the design document for
nevla testand the implementation follows it. Anything that behaves differently is a bug in one of the two.
A test is a fallible function. There is no test framework type, no
assertion keyword, and no new control flow: a test fails by returning an
error, which means everything you already know about errors — check,
error.new, wrapping — is the testing vocabulary too.
Writing tests
Tests live in *_test.nv files beside the code they test, in functions
whose names start with Test and whose only result is error?:
// util_test.nv
import "test"
import "util.nv"
fn TestDouble() (error?) {
check test.eq(util.Double(21), 42)
return none
}
fn TestParseRejectsGarbage() (error?) {
_, err := util.Parse("nope")
if err == none {
return error.new("Parse accepted garbage")
}
return none
}
check is the assertion propagator: the first failing test.eq returns
its error, and that error is the failure report. Anything else in a
_test.nv file (helpers, lowercase functions, structs) is ordinary code.
Run them:
nevla test # every *_test.nv under the project
nevla test src/util_test.nv
ok util_test.nv TestDouble
ok util_test.nv TestParseRejectsGarbage
FAIL util_test.nv TestHalf
util_test.nv:14: expected 21, got 20
2 passed, 1 failed
The runner exits nonzero when anything fails.
The test module
import "test" provides helpers that return error?, built to sit
behind check:
| Helper | Behavior |
|---|---|
test.eq(got, want) | structural equality over any two values (the deep comparison == deliberately does not offer); error describes both sides |
test.neq(got, unwanted) | structural inequality |
test.err(err) | fails when err is none: asserts an error happened |
test.skip(reason) | returns a sentinel the runner reports as skipped, not failed |
That is the whole v1 surface. test.eq covers most assertions because
its error message carries both values; when it doesn’t fit, build the
error yourself — return error.new(...) is always available, and a
custom helper is just a function returning error?.
Table tests
Go’s table-driven idiom is a stance, not a framework feature: a table is
a list of structs and the runner is a for loop, so nevla has table
tests by construction:
struct Case {
In int
Want int
}
fn TestDouble() (error?) {
cases := [
Case{In: 1, Want: 2},
Case{In: 0, Want: 0},
Case{In: -3, Want: -6},
]
for _, c := range cases {
err := test.eq(util.double(c.In), c.Want)
if err != none {
return error.wrap(err, sprintf("double(%v)", c.In))
}
}
return none
}
The error.wrap names the failing case in the report. Tables are also
exactly the workload that will eventually justify the two deferred
features below: test.run for named cases, soft failures for reporting
every bad row instead of the first.
Failure locations
An error value records its origin: the file and line where error.new
created it (or where a py exception crossed the bridge). The runner
prints the origin with the failure, so test.eq’s errors point at the
check test.eq(...) line that produced them. Origins ride the error
through check propagation and error.wrap, so a failure deep in a
helper still names the source line that started it. (Origins are a
property of all nevla errors, not a test feature; production error
reports carry them too.)
Isolation and parallelism
Every test function runs in a fresh interpreter instance: globals,
imports, and module state are rebuilt per test, and the test module’s
bookkeeping hangs off that instance — per-test identity is injected by
the runtime, not threaded through your code as a handle. Because tests
share nothing on the nevla side, the runner executes them in parallel by
default (-j 1 to serialize).
Two things per-test isolation cannot isolate, both properties of the world rather than the runner:
- CPython is one interpreter per process. Python module state (imports, caches, monkeypatching) is shared across tests, and the GIL serializes py-heavy tests. The same is true of pytest in one process.
- The filesystem is shared. Two parallel tests writing the same path
race, exactly as in Go with
t.Parallel.test.tmpdir()is the planned escape hatch; until then, derive per-test paths.
A fault in a test (integer overflow, index out of range) fails that test with its nevla stack trace and the run continues; faults cannot cross test boundaries because nothing else lives in that interpreter.
Output discipline: print output is captured per test and shown only
for failures. A passing test is silent.
Deliberate omissions
- Soft failures (Go’s
t.Error, “record and continue”): v1 is fail-fast per test. The design composes — a recordingtest.fail(msg)can be added without changing any existing test — but it waits for the first table test that genuinely hurts. - Subtests (
t.Run): same posture.test.run(name, fn () (error?))fits the model when named table cases earn it. - In-test concurrency: nevla has no concurrency yet. When it lands,
spawned work inside a test will carry identity the way all nevla code
carries cross-cutting context — through
ctx— not through a test-only handle. This is a standing requirement on the concurrency design. - Cleanup: nevla has no
defer, so a test that creates external state and fails viacheckskips its own teardown.withcovers py resources; native cleanup is an open language question that testing will keep pressure on. - Benchmarks and fuzzing: out of scope for v1; the
Testname prefix leaves room for siblings.
White box, Go’s way
util_test.nv sits inside util.nv’s trust boundary: the _test stem
pairing lets the test file touch util’s unexported names — functions,
struct fields, literals — through the ordinary qualified syntax
(util.helper(...) just compiles there). This is Go’s same-package
testing translated to file modules: decomposed internals are unit-testable
without exporting them, while every other file still sees only the API.
The pairing follows the file name, so it holds anywhere a _test.nv
file appears, and nothing else about visibility changes.
Builtins
Generated from the language spec, which is normative.
Builtins are available without import. They are resolved only when the name is not bound by a variable in scope or a declared function; such a binding shadows the builtin entirely (a shadowed builtin is not callable through any other path).
print(v1, ..., vn)
Takes zero or more arguments of any single-value types, renders each
canonically, joins them with single spaces, and writes the result followed by
a line feed to standard output. Canonical rendering (shared by print, the
%v verb, and str()):
| Type | Rendering |
|---|---|
int | decimal |
byte | decimal (byte(7) renders as 7) |
float | shortest decimal that round-trips; integral values render without a fractional part (3.0 renders as 3); infinities as inf/-inf, NaN as NaN |
bool | true / false |
str | the string itself, unquoted |
list | [e1, e2, ...], elements rendered recursively |
map | {k1: v1, k2: v2} in insertion order |
| struct | Name{f1: v1, f2: v2} in declaration order |
none | none |
error | error(<msg>) |
py | Python str() of the object |
fn | fn |
printf and sprintf
printf(format, a1, ..., an) // writes to standard output, no implicit newline
sprintf(format, a1, ..., an) str // returns the formatted string
The format string uses Go-style verbs:
| Verb | Argument type | Output |
|---|---|---|
%v | any | canonical rendering (section 14.1); renders byte as decimal |
%d | int | decimal; byte is not accepted, widen with int(b) |
%s | str | the string |
%t | bool | true / false |
%q | str | double-quoted, backslash-escaped |
%f | float | fixed-point, default 6 fractional digits |
%% | none | literal % |
A verb may carry a minimum width (%5s) and a precision (%.2f), both
decimal digit sequences, in the form %[width][.precision]verb. Width pads
on the left with spaces to the given count of characters (not bytes).
Precision is honored by %f; on other verbs it is accepted and ignored.
There is no left-align or zero-pad flag. A width or precision exceeding the
implementation’s pad limit (2^20 in the reference implementation) is
rejected: a compile-time diagnostic when the format string is a literal
(section 14.3), a fault otherwise (chapter 12).
Static and dynamic format checking
When the format argument is a string literal, the format is checked at compile time: verb count must equal argument count, each argument’s type must match its verb, verbs must be from the table, and the format must not end inside a verb. Violations are compile-time errors.
When the format is not a literal, the same checks happen at runtime and a violation is a fault (chapter 12).
len
len(x) int
For str, the number of characters; for list (including []byte), the
element count; for map, the entry count. Any other argument type is a
compile-time error.
charcode
charcode(c str) int
The Unicode code point (the character’s number) of c, which must be
exactly one character; any other argument value is a runtime fault.
char
char(n int) str
The one-character string for the Unicode code point n. A value that is not a valid
Unicode scalar (negative, greater than 0x10FFFF, or a surrogate) is a
runtime fault. char(charcode(c)) == c for every one-character c.
Program arguments and standard input live in the os module
(section 15.9); nevla has no args or input builtin.
append
append(xs []T, v1 T, ..., vn T) []T
A fresh list: xs with the values appended, as in Go’s idiom
xs = append(xs, v). The first argument must be a list; every following
value must be assignable to its element type. Zero values yield a plain
copy. The original list is never modified; other names bound to it see
growth only through rebinding (chapter 11).
append on []byte observes the same contract: every call returns a fresh
buffer, and xs is never modified in place from the caller’s perspective.
An earlier reference-count-based in-place growth optimization was removed
(design 2026-07-14) after it broke exactly this contract: no refcount
threshold can distinguish xs = append(xs, v) (safe to reuse storage, since
xs immediately rebinds to the result) from ys := append(xs, v) (xs
must stay unchanged) without move semantics or escape analysis, which the
reference implementation has neither of. []byte append now always copies,
exactly like every other []T.
clone
clone(x []T) []T
clone(x map[K]V) map[K]V
A one-level copy of a list or map (chapter 11): the container is new, its
elements copy by their kinds, exactly Go’s slices.Clone/maps.Clone.
Applying clone to a value type is a compile-time error; value types
already copy. clone on []byte copies the underlying buffer.
Methods on builtin types
All receivers are unchanged; results are new values.
String methods
Receiver str. Positions and counts are in characters.
split(sep str) []str— split on the separator.trim() str— strip leading and trailing white space.upper() str,lower() str— case conversion.contains(sub str) bool,starts_with(prefix str) bool,ends_with(suffix str) bool— substring, prefix, and suffix tests.replace(from str, to str) str— replace all occurrences.find(sub str) int?— character index of the first occurrence,noneif absent.fields() []str— split on runs of white space; no empty fields.lines() []str— split on line feeds; a trailing line feed adds no empty line.trim_prefix(p str) str,trim_suffix(p str) str— remove a leading or trailingpif present, else unchanged.chars() []str— the characters as one-character strings.repeat(n int) str— the string tiledntimes; negativenfaults, as does a result exceeding the implementation’s size limit (2^30 bytes in the reference implementation).
fn main() {
s := " the nevla book "
t := s.trim()
print(t.to_upper()) // THE NEVLA BOOK
print(t.split(" ").join("-")) // the-nevla-book
print(t.replace("book", "spec")) // the nevla spec
i := t.index("nevla")
if i != none {
print(i) // 4
}
print("na".repeat(2) + " batman") // nana batman
print(len("héllo")) // 5: characters, not bytes
}
List methods
Receiver []T.
map(f fn(T) U) []U— applyfto each element.filter(f fn(T) bool) []T— keep elements wherefis true.each(f fn(T))— callfon each element; no result.sum() T—Tmust beintorfloat; the sum of the elements. Integer overflow faults (chapter 12). Summing an empty[]intyields 0; the result of summing an empty[]floatis unspecified in v1 (the reference implementation yields a value that faults on later float use).sorted() []T—Tmust beint,byte,float, orstr; a fresh ascending list.sorted_by(before fn(T, T) bool) []T— a sorted copy per the comparator; the sort is stable.contains(v T) bool— structural membership (section 11.2).join(sep str) str—Tmust bestr; concatenation with the separator.
[]byte (T = byte) has every method above except sum and join,
which byte does not satisfy (byte is not int/float, and is not
str); sorted, sorted_by, contains, filter, each, and map
all apply, per the general rules above.
fn main() {
xs := [3, 1, 4, 1, 5]
print(xs.sorted()) // [1, 1, 3, 4, 5]
print(xs.map(fn(x) { x * 10 }).sum()) // 140
print(xs.filter(fn(x) { x > 2 })) // [3, 4, 5]
print(xs.contains(4)) // true
print(xs.sorted_by(fn(a, b) { a > b })) // [5, 4, 3, 1, 1]
}
Map methods
Receiver map[K]V. Iteration order is insertion order (section 5.3).
keys() []K— the keys, in insertion order.values() []V— the values, in insertion order.has(k K) bool— key presence.delete(k K)— removeskin place, Go’s delete; the remaining order is preserved.
keys() on a map[byte]V and values() on a map[K]byte are
compile-time errors: the result would need to be a compact []byte, but
the two methods build their result generically across every K/V,
independently of byteness, and have no compact []byte repack. Iterate
with a for k, v := range m instead (section 8.7), which binds the byte
key or value directly with no repack in the way.
fn main() {
m := map[str]int{"b": 2, "a": 1}
print(m.keys()) // [b, a]: insertion order, not sorted
print(m.has("a")) // true
m.delete("b")
print(m.values()) // [1]
}
Standard library
Generated from the language spec, which is normative.
Standard library modules are imported by bare name: import "math". The
module name then acts as a namespace: math.sqrt(2.0), math.pi. The v1
modules are math, error, file, ctx, and http.
Where a stdlib signature below ends in error? or (T, error?), failures
are ordinary error values subject to chapter 10; stdlib functions do not
fault on I/O failure.
error
The constructors error.new and error.wrap require no import; they are
part of the core language. import "error" remains legal and adds nothing.
error.new(msg str) error— a new error with the message; emptypytype,traceback, no cause, andoriginset to the call site (section 5.7).error.wrap(cause error, msg str) error— a new error with the message and the given cause;originis the wrap site.
fn fetch() (error?) {
return error.new("connection refused")
}
fn main() {
err := fetch()
if err != none {
wrapped := error.wrap(err, "startup failed")
print(wrapped.msg) // startup failed
cause := wrapped.cause
if cause != none {
print(cause.msg) // connection refused
}
}
}
Error fields are specified in section 5.7.
math
math.abs(int) intor(float) float— absolute value, polymorphic over the two numeric types;abs(-9223372036854775808)faults with integer overflow.math.min(int, int) int/(float, float) float,math.max(...)likewise — both arguments the same numeric type.math.sqrt(float) float— square root.math.cos(float) float,math.sin(float) float,math.tan(float) float— trigonometry, radians.math.pow(base float, pow float) float— exponentiation.math.exp(pow float) float— e raised to the argument.math.ln(num float) float— natural logarithm.math.log(base float, num float) float— logarithm ofnuminbase.math.floor(float) int— round down.math.ceil(float) int— round up.math.round(float) int— round half away from zero (not banker’s rounding).math.pi,math.e—floatconstants.
import "math"
fn main() {
print(math.max(2, 40) + math.abs(-2)) // 42
print(math.pow(2.0, 10.0)) // 1024
printf("%.4f\n", math.log(2.0, 1024.0)) // 10.0000
print(math.round(2.5)) // 3
printf("%.5f\n", math.pi) // 3.14159
}
file
Paths are str. File contents are UTF-8 str or raw bytes []byte.
file.read(path str) (str, error?)— whole-file read; the value slot is""on error.file.write(path str, s str) error?— create or truncate, then write.file.append(path str, s str) error?— create if missing, append.file.readbytes(path str) ([]byte, error?)— whole-file read; the value slot is an empty[]byteon error.file.writebytes(path str, b []byte) error?— create or truncate, then write binary data.file.exists(path str) bool— existence test; never errors.file.list(dir str) ([]str, error?)— entry names, sorted lexicographically.file.remove(path str) error?— remove a file or an empty directory.file.mkdir(path str) error?— create the directory and any missing parents.file.glob(pattern str) ([]str, error?)— the paths matching a shell-style pattern, sorted.*and?stay within one path segment and do not match a leading dot;**crosses directories;[...]matches a character class. Relative patterns resolve against the process working directory. Zero matches is an empty list; only a malformed pattern is an error. Unreadable directories along the way are skipped, as in the shell.file.modified(path str) (int, error?)— the file’s last modification time, epoch nanoseconds (the one time currency, section 15.8).
Importing "file" also declares the opaque struct type File, a
handle to an open file, with reference semantics (section 11.1);
file.open and file.create make them. There is no seek and no
append-mode handle in v1.
file.open(path str) (File, error?)— open an existing file for reading.file.create(path str) (File, error?)— create or truncate, open for writing.f.read(n int) ([]byte, error?)— read up tonbytes from the handle’s current position; negativenis treated as 0 (an empty read, no error). EOF is([]byte{}, none), not an error value. Reading a closed handle is an error value, not a fault.f.write(b []byte) error?— writebto the handle. Writing a closed handle is an error value, not a fault.f.close() error?— close the handle. Idempotent: closing an already-closed handle succeeds.
import "file"
fn main() (error?) {
path := "/tmp/nevla-book-handle-example.bin"
w := check file.create(path)
check w.write([]byte{1, 2, 3})
check w.close()
r := check file.open(path)
chunk := check r.read(2)
print(chunk) // [1, 2]
check r.close()
check file.remove(path)
return none
}
import "file"
fn main() (error?) {
path := "/tmp/nevla-book-example.txt"
check file.write(path, "one\n")
check file.append(path, "two\n")
body := check file.read(path)
print(body.lines()) // [one, two]
print(file.exists(path)) // true
check file.remove(path)
return none
}
ctx
Importing "ctx" also brings the opaque struct type Ctx into scope. A
Ctx is a cancellation handle: a deadline plus an interrupt flag. Ctx
values are handles with reference semantics (section 11.1) and cannot be
constructed with a struct literal (section 7.2.3).
ctx.background() Ctx— never done.ctx.timeout(parent Ctx, d int) Ctx— deadlinednanoseconds from now (the one time currency, section 15.8;30 * time.second), clamped so a child deadline never exceeds its parent’s; negativedis treated as 0.ctx.interrupt(parent Ctx) Ctx— additionally becomes done when the process receives SIGINT.
Methods on Ctx:
done() bool— whether the deadline has passed or the interrupt fired.err() error?—nonewhile live;"deadline exceeded"or"interrupted"when done.
import "ctx"
fn main() {
c := ctx.timeout(ctx.background(), 0) // already expired
print(c.done()) // true
e := c.err()
if e != none {
print(e.msg) // deadline exceeded
}
print(ctx.background().done()) // false
}
test
Importing "test" provides the helpers nevla test is built around
(section 17.7); each returns error? so it composes with check, and
each failure carries an origin (section 5.7).
test.eq(got, want) error?—nonewhen the two values are structurally equal (the comparison of section 11.2’scontains); otherwise an error naming both sides. Comparing values deeper than the implementation limit faults.test.neq(got, unwanted) error?— the negation.test.err(e error?) error?—nonewhen given an error; an error when givennone. Asserts that something failed.test.skip(reason str) error?— an error the test runner reports as skipped rather than failed.
import "test"
fn main() {
print(test.eq([1, 2], [1, 2]) == none) // true: structural
bad := test.eq(2, 3)
if bad != none {
print(bad.msg) // expected 3, got 2
}
}
http
Importing "http" also declares two struct types:
struct Request { method str, url str, body str, headers map[str]str }
struct Response { status int, body str, headers map[str]str }
http.get(c Ctx, url str) (Response, error?)— GET.http.post(c Ctx, url str, body str) (Response, error?)— POST with the given body.http.request(c Ctx, req Request) (Response, error?)— any method, with headers.http.stream(c Ctx, url str, body str, f fn(str)) (Response, error?)— POST, invokingfper response line as it arrives.
import "ctx"
import "http"
import "time"
fn main() (error?) {
c := ctx.timeout(ctx.background(), 5 * time.second)
resp, err := http.get(c, "http://localhost:9/unreachable")
if err != none {
print("transport error, as expected here")
} else {
print(resp.status)
}
return none
}
Behavior:
- If the ctx is already done, the call returns an error before any network I/O.
- A live ctx deadline bounds the whole request; without a deadline, an implementation-defined default timeout applies (30 seconds in the reference implementation).
- A completed HTTP exchange is a success regardless of status code: a 404 is
a
Responsewithstatus404 and anoneerror. Only transport-level failures (connection refused, timeout, invalid request) are error values, with the zeroResponsein the value slot. - Redirects are followed automatically.
- For
http.request, an empty body on a GET request sends no body. http.streamPOSTsbodyand invokesfonce per response line as lines arrive, before the response completes (server-sent events are consumed this way). The returnedResponse.bodyholds the accumulated lines, newline terminated, so the program can reparse the full payload afterward; closures historically could not accumulate it themselves (pre-ADR-0010 closures captured by value); kept for compatibility. Its default deadline, absent a ctx deadline, is 300 seconds rather than 30.- Response header names are as received; values that are not valid strings
read as
"".
gpu
GPU sharing. The module speaks the gputex lock protocol (an advisory
flock plus a holder registry under $GPUTEX_DIR, default ~/.gputex;
the contract is documented in the gputex repository), so a nevla program
coordinates with every other job on the host — wrapped in the gputex CLI
or not — without an external wrapper.
Every function takes the card id first ("default" on single-card
hosts; multi-card hosts name their cards, e.g. "cuda0" — the host’s
convention is whatever gputex status lists); label names
the job for status displays. A card id that is empty or contains a path
separator is an error value (“bad card id”): ids become file names in
the shared state directory.
gpu.lock(card str, label str) error?— take the card exclusively. Preemptible holders (shared acquirers, and any registry entry markedpreemptible) on this host are first asked to leave with SIGTERM, given about ten seconds, then removed with SIGKILL; holders on other hosts are never signaled, dead registry entries are pruned, and a non-preemptible holder is simply waited out, blocking in the kernel until the card is free. The evict-then-take sequence retries a bounded number of times (flock has no fairness; a new shared holder can slip in) before settling into the blocking wait. Errors if this program already holds that card.gpu.trylock(card str, label str) (bool, error?)— non-blocking probe:trueand hold the card if it was free,falseif it is busy (including when held by this program). Busy is data, not an error; the error slot is for real failures (an unwritable state directory).gpu.shared(card str, label str) error?— take the card as a shared, lowest-priority holder: many coexist, all yield to an exclusive acquirer, which may terminate them (the gputex--lowsemantics).gpu.unlock(card str) error?— release. Errors if that card is not held.
Behavior:
- A program may hold several cards at once (training on one while embedding on another), one hold per card; a second acquire of a held card is an error.
- A hold lasts until
gpu.unlock(card)or process exit — any exit. The kernel releases the flock when the process dies, so a fault, kill, or crash never strands a card. - Acquiring also injects the managed environment (
$GPUTEX_ENV_FILEif set, else/etc/gputex/env;KEY=VALUElines) into the process environment, existing values winning: taking the card and getting the metrics contract (MLFLOW_TRACKING_URI) are one step, as with the CLI. - Two environment variables configure the module, mirroring gputex:
GPUTEX_DIRrelocates the state directory (tests, sandboxes) andGPUTEX_ENV_FILErelocates the managed environment file. - On non-unix builds (the playground) every
gpufunction faults (“gpu.lock is not available in this build”).
import "gpu"
fn main() (error?) {
check gpu.lock("default", "tinyllama eval")
// the card is ours until unlock or exit
check gpu.unlock("default")
ok, err := gpu.trylock("default", "opportunistic sweep")
if err != none {
return err
}
if !ok {
print("card busy; skipping")
return none
}
check gpu.unlock("default")
return none
}
time
Clocks, sleeping, and civil time. The single time currency is int
nanoseconds: every duration and every instant in the standard library
is an integer count of nanoseconds (exact in int until the year
2262), and durations are written with the constants below.
Importing "time" also declares:
struct Parts { year int, month int, day int, hour int, minute int, second int }
Constants (all int):
| Constant | Value |
|---|---|
time.nanosecond | 1 |
time.microsecond | 1000 |
time.millisecond | 1000000 |
time.second | 1000000000 |
time.minute | 60 · 10⁹ |
time.hour | 3600 · 10⁹ |
Functions:
time.now() int— the wall clock, nanoseconds since the Unix epoch.time.clock() int— a monotonic clock, nanoseconds since an arbitrary origin; only differences are meaningful. Unaffected by wall-clock adjustment.time.sleep(c Ctx, d int) error?— block fordnanoseconds, thennone. A non-positivedreturnsnonewithout blocking. If the ctx is done, or becomes done while sleeping, the sleep ends promptly and returns the ctx error ("deadline exceeded"or"interrupted"). Wake-up latency after the ctx ends is implementation-defined but bounded (the reference implementation checks at least every 50ms).time.parts(epoch int) Parts— the local civil time for an epoch instant, split into fields. Nanoseconds within the second truncate. An epoch outside the representable civil range faults.
import "ctx"
import "time"
fn main() {
t0 := time.clock()
e := time.sleep(ctx.background(), 250 * time.millisecond)
if e != none {
print("interrupted early: " + e.msg)
}
elapsed_ms := (time.clock() - t0) / time.millisecond
p := time.parts(time.now())
printf("%d:%d:%d slept about %dms\n", p.hour, p.minute, p.second, elapsed_ms)
}
In contexts with no usable clock (the browser playground) time.now,
time.clock, time.sleep, and time.parts report their absence as a
fault naming the build, the same contract as ctx.timeout (15.4).
os
The process’s own surroundings. Absence is an option or an error value, never a sentinel.
os.workdir() (str, error?)— the current working directory as an absolute path.os.env(name str) str?— the environment variable’s value, which may be the empty string, ornonewhen unset (or set to bytes that are not valid unicode). There is no get-with-default; narrowing is the mechanism:
import "os"
fn main() {
bin := "./a.out"
v := os.env("BIN")
if v != none {
bin = v
}
print(bin)
}
os.args() []str— the program’s arguments: everything after the source file on the command line (nv prog.nv a bandnevla run prog.nv a bboth yield["a", "b"]). In contexts with no command line (tests, embedding) the list is empty.os.readline() (str, error?)— read one line from standard input. The returned string excludes the line terminator. End of input and read failures are error values, not faults (eofon end of input). A prompt is the caller’s ownprintf; when a program runs through the CLI runner its output is streamed unbuffered, so a prompt written beforeos.readlineis visible before the read blocks.
In contexts with no operating system to speak of (the browser
playground) every os function reports its absence as a fault naming
the build.
regex
Pattern matching. Importing "regex" also declares:
struct Match { text str, start int, end int, groups []str }
and the opaque struct type Re, a compiled pattern. Re values are
handles with reference semantics (section 11.1) and cannot be
constructed with a struct literal; regex.compile makes them.
regex.compile(pattern str) (Re, error?)— compile a pattern. A malformed pattern is an error naming the problem.
Methods on Re:
matches(s str) bool— whether the pattern matches anywhere ins.find(s str) Match?— the leftmost match, ornone.find_all(s str) []Match— every non-overlapping match, left to right; an empty list when there are none.replace(s str, repl str) str—swith every match replaced.$1,$2, and$nameinreplsubstitute capture groups;$$is a literal dollar.
A Match is plain data: text is the matched text, start and end
are character indices into the subject (half-open, so
s[m.start:m.end] == m.text, section 7.6), and groups holds
captures 1 through n in order, with a group that did not participate
reading as "".
The flavor is the RE2 family (the Rust regex crate): matching runs in
time linear in the input, and backreferences and lookaround do not
exist; a pattern that wants them is a compile error naming the missing
feature. Case-insensitivity and other flags are written inline
((?i), (?m), (?s)). Full backtracking semantics remain available
through the bridge (import py "re").
import "regex"
fn main() (error?) {
re := check regex.compile("(?i)(\\w+)=(\\d+)")
for _, m := range re.find_all("A=1 b=22 c=x") {
printf("%s is %s\n", m.groups[0], m.groups[1])
}
return none
}
flag
Command-line flags, data-shaped: no registry, no global state, no
output. Importing "flag" also declares:
struct Flag { name str, short str, fallback str, usage str, toggle bool }
struct Parsed { values map[str]str, rest []str }
flag.value(name str, short str, fallback str, usage str) Flag— a flag that takes a value.flag.toggle(name str, short str, usage str) Flag— a presence flag; parses to"true", fallback"false".flag.parse(argv []str, flags []Flag) (Parsed, error?)— parse argv (typicallyos.args()). Pure: same inputs, same outputs.flag.get(p Parsed, name str) str— read a parsed value. Parse fills every declared flag with its fallback first, so a lookup by declared name always answers; an undeclared name reads"". Plain map reads onp.valueswork too and narrow as options (7.6).
Grammar, Go-shaped:
--name value,--name=value,-s value,-s=value.- A toggle takes no value;
--name=valueon a toggle is an error. - Parsing stops at
--(consumed) or at the first argument that does not begin with-; everything from there lands inrestverbatim. A bare-is an argument, not a flag. -hand--helpare synthesized: parse returns an error whose message is the usage text, one line per flag, value flags showing their fallback. Declaring a flag namedhelpor with shorthis itself an error. The module never writes output and never exits; main decides what an error means (usually print it and return it).- An unknown flag, a toggle given a value, and a value flag at the end of argv with nothing to take are errors carrying the usage text.
- Values are strings;
int(x)andfloat(x)conversions are the typed layer, already mandatory-checked.
import "flag"
import "os"
fn main() (error?) {
p := check flag.parse(os.args(), [
flag.value("addr", "a", ":8080", "listen address"),
flag.toggle("verbose", "v", "log more"),
])
if flag.get(p, "verbose") == "true" {
print("listening on " + flag.get(p, "addr"))
}
return none
}
proc
Subprocesses. Importing "proc" also declares:
struct Cmd { argv []str, dir str, env map[str]str, stdin str, log str }
struct Result { status int, stdout str, stderr str }
and the opaque struct type Proc, a handle to a started child, with
reference semantics (section 11.1); proc.start makes them.
proc.run(c Ctx, argv []str) (Result, error?)— run to completion and capture output. Shorthand forproc.execwith an emptyCmdaroundargv.proc.exec(c Ctx, cmd Cmd) (Result, error?)— the full form.cmd.dirsets the working directory (""inherits);cmd.enventries are ADDED to the inherited environment, overriding on collision (an empty map inherits unchanged; there is no way to drop the inherited environment in v1); a non-emptycmd.stdinis written to the child and closed.proc.attach(c Ctx, argv []str) (int, error?)— run a child that OWNS the terminal: stdin, stdout, and stderr are inherited, nothing is captured, and the call blocks until the child exits, returning its status withrun’s exit semantics. For editors, REPLs, and anything else interactive.proc.start(cmd Cmd) (Proc, error?)— start a long-running child. Its stderr merges into stdout as one stream, interleaved at line granularity in arrival order. A non-emptycmd.logappends the stream to that file instead;readlineon a logged child returns an error naming the file.
Exit semantics for run/exec: a child that ran and exited zero is a
Result with a none error. A nonzero exit fills Result AND sets
the error (exit status 3): handling stays mandatory, the output stays
data. A child terminated by a signal reports status -1 and the error
names the signal. Failure to spawn at all (missing binary, bad
directory) returns the zero Result and an error. If the ctx is
already done, nothing spawns. If the ctx ends while the child runs,
the child is terminated (then killed after a short grace) and the call
returns the ctx error with output captured so far and status -1.
Methods on Proc:
pid() int— the child’s process id.running() bool— whether the child is still alive.readline(c Ctx) (str, error?)— the next line of the merged stream, blocking until one arrives, the stream ends (eof, theos.readlinecontract), or the ctx ends (the ctx error; the child is left alone).wait(c Ctx) (int, error?)— block until the child exits and return its status, or return the ctx error when the ctx ends first (the handle stays valid; waiting again is fine).stop(grace int) error?— terminate politely, waitgracenanoseconds, kill. Idempotent; stopping an exited child isnone.
The runtime owns the pipes: child output is moved into buffers (or the log file) by the implementation the moment it exists, so a child that fills one pipe while the program reads another cannot deadlock. No part of a program observes the threads this implies.
import "ctx"
import "proc"
import "time"
fn main() (error?) {
c := ctx.timeout(ctx.background(), 30 * time.second)
r := check proc.run(c, ["git", "status", "--short"])
printf("%s", r.stdout)
p := check proc.start(Cmd{
argv: ["make", "serve"],
dir: "", env: map[str]str{}, stdin: "", log: "/tmp/serve.log",
})
printf("serving as pid %d\n", p.pid())
check p.stop(2 * time.second)
return none
}
In contexts with no processes (the browser playground) every proc
function reports its absence as a fault naming the build.
Nevli the mongoose
Nevli is the mascot; nevla is the language. Both are the mongoose in Hindi: नेवला (nevlā) is the noun, and नेवली (nevlī) is a female mongoose, which she is.
The artwork lives in
art/ (CC BY 4.0;
the code is MIT and stays that way).