The Nevla Programming Language Specification
Version 1 (v1). This document is the normative reference for the nevla
language: it states what a conforming implementation must do. Rationale and
design history live in the design document at
docs/specs/2026-07-01-mongoose-v1-design.md; where that document and this
one could disagree, this one governs the language, and the golden tests under
tests/golden/ are its executable companion (see the final chapter).
Source files use the .nv extension.
Table of contents
- Introduction
- Notation
- Source code representation
- Lexical elements
- Types
- Declarations and scope
- Expressions
- Statements
- Flow narrowing
- Errors and error handling
- Value semantics and equality
- Runtime faults
- The Python bridge
- Builtin functions
- Standard library
- Modules and multi-file programs
- Program execution
- Implementation limits
- Conformance and maintenance
1. Introduction
Nevla is a statically typed, interpreted language with Go’s copy model
(scalars and structs copy; lists, maps, and functions are references),
errors as values, option types instead of nil, and an embedded Python bridge.
A nevla program is checked in full before any of it runs; no crash can
originate in nevla. The worst outcomes available to a running program are
returning an error from main and a runtime fault, both of which terminate
the program in a controlled way (chapter 12, chapter 17), and every Python
exception crossing the bridge arrives as an error value (chapter 13). The
guarantee has one documented boundary: the bridge embeds real CPython in
this process, so native code inside a C extension that itself segfaults or
aborts takes the process with it, as it would take any host that loaded it.
Throughout this document, “must” states a requirement on conforming implementations or on valid programs, “may” states a permission, and “unspecified in v1” marks behavior a program must not rely on.
2. Notation
The syntax is specified using Extended Backus-Naur Form (EBNF) in the style of the Go specification:
Syntax = { Production } .
Production = production_name "=" Expression "." .
Expression = Term { "|" Term } .
Term = Factor { Factor } .
Factor = production_name | token | Group | Option | Repetition .
Group = "(" Expression ")" .
Option = "[" Expression "]" .
Repetition = "{" Expression "}" .
Productions are expressions built from terms and the following operators, in increasing precedence:
| alternation
() grouping
[] option (0 or 1 time)
{} repetition (0 to n times)
Lowercase production names denote lexical tokens. Non-terminals are in
CamelCase. Terminal symbols appear in double quotes "".
The token newline denotes the line-terminator token produced by the lexer
(section 4.2). Where a production does not mention newline, line breaks are
significant unless section 4.2 permits them.
3. Source code representation
Source code is Unicode text encoded in UTF-8. A source file is a sequence of Unicode code points; the lexer processes them directly, without any normalization.
3.1 Shebang line
If the first two characters of a source file are #!, the entire first line
up to and including nothing beyond the first line feed is ignored by the
lexer. The line feed itself is retained for line counting, so diagnostics on
subsequent lines report true line numbers. This permits executable scripts:
#!/usr/bin/env nv
fn main() {
print("hello")
}
The shebang form is recognized only at the very start of the file. A #
anywhere else in a program is a lexical error.
3.2 Characters
Identifiers and keywords are restricted to ASCII (section 4.3). Arbitrary
Unicode may appear in string literals and comments. String indexing, slicing,
and len operate on characters, not bytes (sections 7.5, 7.6, 14.4).
4. Lexical elements
4.1 Comments
A comment starts with // and runs to the end of the line. There is no block
comment form. Comments do not produce tokens and do not suppress the newline
that ends their line.
x := 1 // this is a comment
4.2 Tokens and line terminators
Tokens are identifiers, keywords, operators and punctuation, and literals. Space (U+0020), horizontal tab, and carriage return are white space and are ignored except as token separators.
A line feed produces a newline token, with these rules:
- Consecutive line feeds produce a single
newlinetoken. - Line feeds before the first token of the file produce no token.
- The
newlinetoken acts as the statement terminator inside blocks (section 8.1) and is otherwise skipped only where the grammar permits.
There are no semicolons; ; is not a token and its appearance is a lexical
error.
Line breaks are permitted, and consumed without producing a statement break, in the following positions:
- after a binary operator (
x := 1 +may be continued on the next line); - after the opening delimiter of a parenthesized expression, list literal, call argument list, parameter list, parenthesized return-type list, map literal body, or struct literal body;
- after a comma in any of those constructs;
- before the closing delimiter of those constructs.
A line break is not permitted before a binary operator, before a . selector,
before an else (which must follow its } on the same line; section 8.6),
or in the middle of any other production. In particular, a method chain must
keep each . on the same line as the expression it follows.
ok := 1 +
2
// invalid: line break before the operator
// bad := 1
// + 2
4.3 Identifiers
identifier = letter { letter | ascii_digit } .
letter = "a" ... "z" | "A" ... "Z" | "_" .
Identifiers name variables, functions, structs, fields, parameters, and modules. They consist of ASCII letters, ASCII digits, and underscore, and must not start with a digit. Identifiers are case sensitive.
The identifier _ is the blank identifier (section 6.6).
4.4 Keywords
The following identifiers are reserved and must not be used as names:
break check continue else false fn
for if import none py range
return struct true with
py is a keyword; it also serves as the name of the py type (section 5.8)
and as the marker in import py declarations (section 6.4).
The names int, byte, float, bool, str, error, and map are not
keywords. They are predeclared type names, resolved contextually; int,
byte, float, str, and bool followed immediately by ( in expression
position always denote a conversion (section 7.7), and map [ in expression
position always begins a map literal. Slice types and slice conversions are written
with the [] prefix ([]int, []float(x); sections 5.9, 7.7) and involve
no reserved name. Builtin function names (print, printf, sprintf,
len, append, clone, charcode, char) are also not
reserved; a variable or
function declaration with the same name shadows the builtin.
4.5 Operators and punctuation
+ - * / %
== != < <= > >=
&& || !
= := ? :
( ) [ ] { }
, .
A single & or a single | is a lexical error. All other characters not
covered by this chapter are lexical errors.
4.6 Integer literals
int_lit = ascii_digit { ascii_digit } .
Integer literals are decimal only. There is no sign in the literal itself;
negative values are formed with the unary - operator. An integer literal
must fit in the range of int values, 0 through 9223372036854775807
(2^63 - 1); a larger literal is a lexical error. Consequently the minimum
int value, -9223372036854775808, is not writable as a negated literal; it
can only be produced by arithmetic.
4.7 Float literals
float_lit = int_lit "." ascii_digit { ascii_digit } .
A float literal is a digit sequence, a period, and at least one further digit.
There is no exponent form and no leading or trailing period form. A period
not followed by a digit is not part of a numeric literal, so 1.abs lexes as
the integer 1, ., and the identifier abs.
2.5 // float
3.0 // float
1 // int
4.8 String literals
string_lit = `"` { unicode_char | escape } `"` .
escape = `\` ( "n" | "t" | `"` | `\` ) .
String literals are delimited by double quotes and must not span multiple lines; a line feed inside a string literal is a lexical error, as is an unterminated string. Exactly four escape sequences are recognized:
| Escape | Meaning |
|---|---|
\n | line feed (U+000A) |
\t | horizontal tab (U+0009) |
\" | double quote |
\\ | backslash |
Any other character after a backslash is a lexical error. All other characters, including arbitrary Unicode, stand for themselves.
5. Types
The complete set of v1 types is:
int float bool str
[]T map[K]V T?
fn(...) ... struct types error py
Two types are identical if and only if they have the same structure: the same kind, and identical element, key, value, parameter, and result types. Struct types are identical when they have the same name.
5.1 Boolean, numeric, and string types
boolhas exactly the valuestrueandfalse. There is no truthiness: no other type converts implicitly tobool, and conditions must have typebool(section 8.6).intis a 64-bit signed integer. Overflow is a runtime fault (chapter 12), never a silent wrap.byteholds the integers 0 through 255, zero value0. It supports only comparison (==,!=,<,<=,>,>=, section 7.9.2); there are no arithmetic operators onbytein this version (section 7.9.1). Widen withint(b)to compute.byte(n)fromintnarrows and is a runtime fault whennis outside 0..255 (section 7.7); this is a deliberate deviation from Go, whereuint8wraps silently (ADR 0021).floatis an IEEE 754 64-bit binary floating point number. Float arithmetic follows IEEE 754: division by zero yields an infinity,0.0/0.0yields NaN, and no float arithmetic faults.stris an immutable sequence of characters. A character is one Unicode code point; “character” means exactly that everywhere in this specification.
There are no implicit conversions between any of these types, including
between int and float (section 7.9.1).
5.2 List types
[]T is an ordered sequence of values of element type T. Lists are
reference types (chapter 11): assignment, argument passing, and capture
alias the one underlying list. clone(xs) makes an explicit copy.
[]byte is an ordinary list type over byte: every rule above applies
verbatim. Its runtime representation is a compact contiguous buffer rather
than a boxed sequence of values (an implementation detail, section 11.1);
the language-visible behavior is identical to any other []T.
5.3 Map types
map[K]V maps keys of type K to values of type V. The key type K
must be int, byte, str, or bool; any other key type is a
compile-time error.
Maps preserve insertion order: iteration, keys(), and values() visit
entries in the order the keys were first inserted, and delete preserves the
relative order of the remaining entries. Maps are reference types
(chapter 11), like lists.
Reading m[k] yields V?: a missing key reads as none (section 7.5).
Writing m[k] = v inserts or updates (section 8.3).
5.4 Option types
For any non-option type T, the option type T? holds either a value of
type T or the absent value none. There is no nil; absence exists only
inside option types.
noneis the untyped empty-option literal. It is assignable to every option type and compares only against operands of option type (section 7.9.3).- A value of type
Tis assignable whereT?is expected (widening, section 5.10). The reverse never holds implicitly. - A value of option type must be narrowed (chapter 9) before its fields or
methods can be used, and it does not support the operators of
T. Using an unchecked option is a compile-time error:
fn find() User? { ... }
u := find()
print(u.name) // compile error: value might be none
if u != none {
print(u.name) // ok: u is User here
}
Option types do not nest syntactically: T?? is not writable.
5.5 Function types
fn(P1, ..., Pn) R and fn(P1, ..., Pn) (R1, ..., Rm) are the types of
function values with the given parameter and result types. A function type
with no results is written fn(P1, ..., Pn). Functions are first class:
declared functions and function literals are values of function type and may
be stored, passed, and returned.
In a function-type result position, an unparenthesized result must begin with
an identifier or py; a result that is itself a function type must be
parenthesized: fn() (fn() int).
5.6 Struct types
A struct type is declared at the top level (section 6.3) and consists of an
ordered list of named, typed fields. Structs are nominal: two structs with
the same fields but different names are distinct types. Structs are value
types (chapter 11); copies are shallow in Go’s sense, so reference-typed
fields alias. Fields are accessed with . and assigned through assignment
statements. User struct types have no methods in v1.
Recursive structs are restricted: a struct must not contain itself by value, directly or through a chain of struct-typed fields. Such a value could never be constructed, and the declaration is a compile-time error. The cycle must be broken with an option, list, or map along the way:
struct Node {
val int
next Node? // ok; `next Node` would be a compile error
}
5.7 The error type
error is the type of error values (chapter 10). Every error value exposes
the fields:
| Field | Type | Meaning |
|---|---|---|
msg | str | human-readable message |
cause | error? | wrapped inner error, or none |
pytype | str | Python exception type name; "" for non-bridge errors |
traceback | str | Python traceback text; "" for non-bridge errors |
origin | str | file:line of the statement where the error was born (error.new, error.wrap, a test helper, or a py exception entering value space); "" when unknown |
Error values are constructed with error.new and error.wrap
(section 15.1) and by the Python bridge (chapter 13). Error values do not
support == (section 7.9.3); test for presence with err != none on an
error? and inspect .msg.
5.8 The py type
py is the type of references to live Python objects (chapter 13). It is
the single dynamic type in the language and the documented exception to value
semantics: assigning a py value copies the reference, not the object.
The zero value of py is a handle to Python’s None; operations on it
produce ordinary Python error values (for example AttributeError), never a
fault.
5.9 Type syntax
Type = BaseType [ "?" ] .
BaseType = TypeName | SliceType | MapType | FnType | "py" .
TypeName = identifier [ "." identifier ] .
SliceType = "[" "]" Type .
MapType = "map" "[" Type "]" Type .
FnType = "fn" "(" [ TypeList ] ")" [ FnResult ] .
FnResult = "(" [ TypeList ] ")" | Type .
TypeList = Type { "," Type } .
A TypeName is one of the predeclared names int, byte, float, bool,
str, error, a struct name, or a dotted name module.Struct referring to a
struct declared in an imported file module (chapter 16). Any other name is a
compile-time error (“unknown type”). The unparenthesized FnResult
alternative must begin with an identifier or py (section 5.5).
5.10 Assignability
A value of type V is assignable to a location (variable, parameter, field,
element, return slot) of type T when:
TandVare identical; orTisT0?andVis assignable toT0, orVisV0?andV0is assignable toT0(widening into options, applied recursively); orTis[]A,Vis[]B, andBis assignable toA; orTismap[AK]AV,Vismap[BK]BV, andBK,BVare assignable toAK,AVrespectively; or- either side’s type could not be determined because of a prior compile error (assignability then does not produce a second error).
The literal none is assignable to every option type. An empty list literal
[] is assignable to every list type except []byte, and only in a context
that supplies the list type; the empty []byte is spelled []byte{}
(section 7.2.1).
An integer literal in the range 0 to 255 is additionally assignable to
byte: []byte{137, 80}, b = 255, x == 137 all work bare. An
out-of-range integer literal in a byte position is a compile-time error
(“cannot use n as byte”). This is the only implicit conversion in the
language; a variable of type int is never assignable to byte without an
explicit byte(...) conversion (section 7.7). The implicit applies only at
a position already known to be byte (a scalar slot, or an element of a
[]byte{...} typed literal); it does not reach into a bare list literal’s
own element-type inference, so []int (including a bare [1, 2]) is never
assignable to []byte, and a bare list literal never has type []byte at
all (section 7.2.1).
5.11 Zero values
Every type has a zero value, used to fill the non-error result slots when a
check expression propagates an error (section 7.8) and as the success slot
of failed conversions and stdlib calls:
| Type | Zero value |
|---|---|
int | 0 |
byte | 0 |
float | 0.0 |
bool | false |
str | "" |
[]T | [] |
map[K]V | empty map |
T? | none |
| struct | struct with every field set to its zero value, recursively |
error (bare, non-option) | none |
py | a handle to Python None |
fn(...) ... | the zero function value; calling it is a runtime fault |
6. Declarations and scope
6.1 Program structure
SourceFile = { Declaration } .
Declaration = ImportDecl | StructDecl | FunctionDecl .
A source file consists solely of import, struct, and function declarations, separated by any number of line breaks. There are no top-level variables and no constants. Top-level declarations are visible throughout the program regardless of order; a function may call a function declared later in the file.
Declaring two functions with the same name, or two structs with the same name, is a compile-time error.
A complete program must declare fn main with an entry-point signature
(section 17.1).
6.2 Function declarations
FunctionDecl = "fn" identifier Parameters [ Result ] Block .
Parameters = "(" [ ParameterList [ "," ] ] ")" .
ParameterList = Parameter { "," Parameter } .
Parameter = identifier [ Type ] .
Result = Type | "(" [ TypeList [ "," ] ] ")" .
Every parameter of a top-level function must have a declared type; omitting
one is a compile-time error. (The grammar shares Parameter with function
literals, where types may be inferred; section 7.3.)
A function may declare zero, one, or several result types. A function with a
nonempty result list must end in a statement that diverges (section 8.1.1)
on every path; otherwise “missing return” is a compile-time error. A function
with no results returns implicitly at the end of its body, and return with
no values is permitted inside it.
fn fetch(url str) (str, error?) {
return "", none
}
6.3 Struct declarations
StructDecl = "struct" identifier "{" { newline } [ FieldDeclList ] "}" .
FieldDeclList = FieldDecl { ( "," | newline ) { newline } FieldDecl } [ "," | newline ] { newline } .
FieldDecl = identifier Type .
Whether a struct or field is visible to other file modules follows its name’s capitalization (section 16.3). Fields are separated by commas or line breaks; both of these are valid:
struct User { name str, age int }
struct User {
name str
age int
}
The recursive-struct restriction of section 5.6 applies.
6.4 Import declarations
ImportDecl = "import" ( ImportSpec | "(" { newline } [ ImportSpec { newline { newline } ImportSpec } ] { newline } ")" ) .
ImportSpec = [ "py" ] string_lit .
Three forms exist, distinguished by the py marker and the path string:
import "name"wherenameis one of the standard library modulesmath,error,file,ctx,gpu,http,test,time,os,regex,flag,procimports that module (chapter 15).import "path.nv"where the path ends in.nvimports another nevla source file as a module (chapter 16).import py "modname"imports a Python module through the bridge (chapter 13). Dotted module paths (import py "os.path") are permitted.
An import path that is none of these is a compile-time error (“unknown module”).
The factored form groups any number of specs, one per line, each
optionally py-marked, and means exactly the same sequence of single
imports:
import (
"ctx"
"os"
"time"
py "torch"
)
fn main() {
print(len(os.args()))
}
The one true style (17.6) renders two or more imports as one factored
block in source order; nevla imports additionally manages the set
itself, adding missing standard library imports, removing unused
imports, and sorting (plain paths first, then py, alphabetical
within).
6.5 Blocks and scope
Block = "{" { newline } [ StatementList ] "}" .
Each block introduces a new scope. A short variable declaration (section 8.2)
declares its names in the innermost enclosing scope; declaring a name twice
in the same scope is a compile-time error, while an inner scope may shadow an
outer name. Function parameters are declared in the function’s outermost
scope. for x := range xs declares its iteration variables in a scope
enclosing the loop body, fresh on each iteration.
Name resolution inside a function proceeds from the innermost scope outward, then to top-level functions, then to imported module names, then to builtins. A local variable therefore shadows a top-level function of the same name, which shadows a module name, which shadows a builtin.
6.6 The blank identifier
The blank identifier _ discards a value. It may appear:
- as a name in a short variable declaration:
_, err := f()or_ := f(); - as an iteration variable:
for _, v := range m { ... }.
_ is never declared: it cannot be read, and it is not a valid assignment
target (_ = f() is a compile-time error, “undefined: _”). Binding an error
slot to _ counts as handling it (section 10.2): v, _ := f() is legal and
deliberately drops the error.
7. Expressions
7.1 Operands
PrimaryExpr = int_lit | float_lit | string_lit
| "true" | "false" | "none"
| identifier
| "(" Expression ")"
| ListLit | MapLit | StructLit
| FunctionLit
| Conversion .
An identifier denotes, in resolution order, a variable in scope, a top-level function (as a function value), or an imported module. An identifier that resolves to nothing is a compile-time error (“undefined”).
A module name is not a first-class value: it may only appear as the receiver
of a selector (math.pi, util.double(2)).
7.2 Composite literals
7.2.1 List literals
ListLit = "[" { newline } [ ExpressionList [ "," ] ] { newline } "]"
| "[" "]" Type "{" { newline } [ ExpressionList [ "," ] ] { newline } "}" .
ExpressionList = Expression { "," { newline } Expression } .
A nonempty list literal has type []E where E is the element type
supplied by context, or, absent context, the type of the first element; each
subsequent element must be assignable to E.
An empty list literal [] requires a context that supplies its list type: an
argument position, return position, field value, map value, or assignment to
a location of known list type. A bare [] with no such context is a
compile-time error (“cannot infer element type of []”).
The typed form []T{e1, ..., en} carries its element type and needs no
context: every element must be assignable to T, and []T{} is the empty
list of type []T. In expression position []T followed by { is always a
list literal; followed by ( it is a conversion (section 7.7).
fn total(xs []int) int { return len(xs) }
print(total([])) // ok: parameter supplies []int
// xs := [] // compile error
xs := []int{} // ok: empty, typed
ys := []str{"a", "b"}
[]byte{e1, ..., en} is an ordinary typed list literal with E = byte:
each element must be assignable to byte, so an integer literal in
0..255 works bare (the literal rule, section 5.10) and any other element
needs an explicit byte(...) conversion.
b := []byte{137, 80, 78, 71} // ok: literals assignable to byte
A bare list literal never has type []byte; only the []byte{...} form
does. The literal rule applies only to the typed form: a bare list
literal’s elements type themselves first (an integer literal types int),
so a bare [1, 2] is []int regardless of a surrounding []byte context,
and []int is not assignable to []byte (section 5.10). A bare empty []
does not take []byte from context either ([]byte is the one list type
excluded from the empty-literal context rule above; write []byte{}), and
a bare literal whose first element is byte-typed ([byte(1), byte(2)])
is a compile-time error rather than a []byte.
fn takesBytes(b []byte) bool { return len(b) > 0 }
takesBytes([]byte{1, 2}) // ok
takesBytes([]byte{}) // ok
// takesBytes([1, 2]) // compile error: expected []byte, got []int
// takesBytes([]) // compile error: expected []byte, got []?
// xs := [byte(1)] // compile error: bare list literal cannot be []byte
7.2.2 Map literals
MapLit = "map" "[" Type "]" Type "{" { newline } [ MapEntryList [ "," ] ] { newline } "}" .
MapEntryList = MapEntry { "," { newline } MapEntry } .
MapEntry = Expression ":" Expression .
A map literal names its key and value types explicitly. Each key must be
assignable to K and each value to V; K must satisfy the map key
restriction (section 5.3). Entries are inserted left to right; a repeated key
overwrites, keeping the original insertion position.
m := map[str]int{"a": 1, "b": 2}
7.2.3 Struct literals
StructLit = identifier [ "." identifier ] "{" { newline } [ FieldValueList [ "," ] ] { newline } "}" .
FieldValueList = FieldValue { "," { newline } FieldValue } .
FieldValue = identifier ":" Expression .
A struct literal must name a declared struct type, either bare (Pair{...})
or dotted for a struct of an imported file module (util.Pair{...},
chapter 16; the struct and, because every field is supplied, all of its
fields must be exported there, section 16.3), and must supply every
declared field exactly once, each value
assignable to the field’s type.
Missing fields and unknown fields are compile-time errors. Field order in the
literal is free; the constructed value’s field order follows the declaration.
Struct literals are suppressed in control-flow headers: in the condition of
an if or else if, in the header expression of a for statement
(both the condition form and the operand of range), and in the operand of
a with statement, an identifier
followed by { is not parsed as a struct literal, so the { opens the
statement’s block. To use a struct literal in a header, parenthesize it.
Struct literals remain available inside any parenthesized or bracketed
subexpression of a header.
The struct type Ctx (section 15.4) is opaque and cannot be constructed by
a struct literal; Ctx{...} is a compile-time error.
7.3 Function literals
FunctionLit = "fn" Parameters [ Result ] Block .
A function literal (lambda) evaluates to a function value. Parameter types
may be omitted when the context supplies an expected function type, from
which they are inferred positionally; otherwise omitting a parameter type is
a compile-time error (“lambda parameter needs a type here”). Contexts that
supply a function type include arguments to list methods (map, filter,
each, sorted_by), arguments to parameters of function type, and
assignment to a location of known function type.
Result typing:
- If the literal declares result types, its body must diverge on every path,
exactly as for declared functions, and
checkmay be used inside it subject to section 7.8. - If the literal declares no result types and its body is a single
expression statement, the literal is an expression-bodied function: its
result type is the expression’s type (no result if the expression has no
value), and calling it returns the expression’s value. Exception: an
expression-bodied function whose inferred result type is
byteis a compile-time error (“lambda returning byte needs a declared return type”); abyteresult must be written down (fn(x byte) byte { return x }). - Otherwise the literal has no results.
nums := [1, 2, 3, 4]
big := nums.map(fn(x) { x * 2 }).filter(fn(x) { x > 2 }).sum() // 18
f := fn(x int) int { return x * 2 }
Function literals capture their free variables by reference, as in Go: the closure and the enclosing scope share the variable, and reads and writes flow both ways. Capture is per-variable (the names the body actually uses), so a closure keeps alive exactly what it references.
n := 1
f := fn() int { return n }
n = 2
print(f()) // 2: f shares n, it did not snapshot it
total := 0
add := fn(x int) { total = total + x }
add(1)
add(2)
print(total) // 3: the accumulator idiom works directly
Loop iteration variables are per-iteration bindings (Go 1.22 semantics): closures created in different rounds capture different variables.
Top-level functions and imported modules are not captured; they resolve normally at call time.
7.4 Selectors, calls, and method calls
PostfixExpr = PrimaryExpr { Selector | Arguments | Index | Slice } .
Selector = "." identifier .
Arguments = "(" { newline } [ ArgumentList [ "," ] ] { newline } ")" .
ArgumentList = Argument { "," { newline } Argument } .
Argument = Expression | identifier ":" Expression .
x.f selects, depending on the type of x:
- a struct field, whose type is the field’s declared type;
- an error field (
msg,cause,pytype,traceback; section 5.7); - a member of a module: a stdlib constant (
math.pi,math.e) reads as its value; a module function member (stdlib or file module) must be called directly (math.sqrt(4.0),util.double(3)). Module functions are not first class in v1:f := math.sqrtis a compile-time error; - a Python attribute, when
xhas typepy(chapter 13).
Selecting through an option type is a compile-time error; narrow first (chapter 9).
f(a1, ..., an) calls a function value. The argument count must equal the
parameter count and each argument must be assignable to the corresponding
parameter type. A call of a non-function is a compile-time error
(“not callable”).
x.m(a1, ..., an) is a method call. Methods exist only on strings, slices,
and maps (section 14.9), on Ctx (section 15.4), on modules
(where mod.f(...) calls the module function), on error as the receiver of
the builtin constructors error.new and error.wrap (section 15.1), and on
py values (chapter 13). User struct types have no methods in v1.
A named argument (identifier ":" Expression) binds the value to the named
Python parameter and is permitted only in calls whose callee is a py value
(chapter 13); a named argument in any other call is a compile-time error, as
is a positional argument following a named one. Nevla functions are
positional only.
Arguments are evaluated left to right, after the callee (or receiver) expression. Calls with multiple results are covered in section 7.10.
7.5 Index expressions
Index = "[" { newline } Expression { newline } "]" .
For a[i]:
- If
ahas type[]T,imust beintand the result isT. Indices run from 0. An index outside0 <= i < len(a)is a runtime fault. Negative indices are not supported.[]byteindexes tobyteunder this same rule; its compact runtime representation (section 11.1) does not change the semantics. - If
ahas typestr,imust beintand the result is astrholding the single character at positioni(positions count characters). Out of range is a runtime fault. - If
ahas typemap[K]V,imust be assignable toKand the result isV?; a missing key yieldsnone. Reading a map never faults. - If
ahas typepy, indexing is a Python subscript operation (chapter 13).
Indexing any other type is a compile-time error.
m := map[str]int{"k": 1}
v := m["missing"] // v: int?, none here
if v != none {
print(v + 1)
}
7.6 Slice expressions
Slice = "[" { newline } Expression ":" Expression "]" .
a[lo:hi] slices a []T (yielding []T) or a str (yielding
str, positions in characters). Both bounds are required and must be int.
The bounds must satisfy 0 <= lo <= hi <= len(a); anything else is a runtime
fault. The result is a copy of the half-open range [lo, hi); a[n:n] is
empty. Slicing any other type, including py, is a compile-time error.
b[lo:hi] on a []byte yields a fresh []byte, copying the range out of
the compact buffer; the source buffer is unaffected.
7.7 Conversions
Conversion = ( "int" | "float" | "str" | "bool" | "byte" | "py" ) "(" Expression ")"
| SliceType "(" Expression ")" .
Conversions are the explicit casts of the language. Fallibility follows the
operand type: a conversion from str (a parse) or from py (a bridge
extraction, section 13.5) has the multi-value type (T, error?) and must be
consumed accordingly (sections 7.10, 10.2). One conversion is fallible by its
operand rather than its target: str(x) is fallible when x is []byte (a
UTF-8 decode) — the sole non-py fallible str(x). Every other permitted
pair, including every other str(x), cannot fail, is single-valued, and is
used inline:
n, err := int("42") // 42, none: str source is a parse
m, err2 := int("x") // 0, error("cannot parse \"x\" as int")
i := int(3.9) // 3: numeric conversions are single-valued
s := str(123) + "!" // "123!": str(x) never fails
buf := []byte("hi") // UTF-8 encode: []byte(s) never fails
t, err3 := str(buf) // "hi", none: []byte source is a UTF-8 decode
Permitted operand types and behavior, for non-py operands:
| Conversion | Operand types | Behavior |
|---|---|---|
int(x) | int | identity |
byte | widening, exact | |
float | truncation toward zero; values outside the int range saturate to the nearest bound, NaN yields 0 | |
str | decimal parse after trimming leading and trailing white space; failure is an error value | |
byte(x) | byte | identity |
int | narrowing; a runtime fault (“byte conversion out of range: n”) when the operand is outside 0..255, never a silent truncation. Bounds-check first for data-driven narrowing. str is not a permitted operand; parse with int(s) and narrow | |
[]byte(x) | []byte | identity, per the []T(x) pass-through rule below |
str | UTF-8 encode; never fails | |
float(x) | float | identity |
int | exact or nearest representable value | |
str | float parse after trimming; failure is an error value | |
bool(x) | bool | identity |
str | after trimming, exactly "true" or "false"; anything else is an error value | |
str(x) | any type except []byte | the canonical rendering of section 14.1; never fails |
[]byte | UTF-8 decode; failure (“invalid UTF-8 in byte conversion to str”) is an error value. The sole non-py fallible str(x); every other operand renders and cannot fail | |
py(x) | int, float, bool, str, the literal none | the inbound bridge conversion (section 13.5) as a py handle; never fails. py(none) is the zero value of py (section 5.11). Containers, structs, functions, and option-typed values are compile-time errors (“cannot convert … to py”); pass them to py calls directly |
[]T(x) | []U | yields the operand list unchanged; element types are not validated in v1 (see below). Excluded when T and U disagree on byteness at any depth: []byte(xs) on a []int, []int(b) on a []byte, and the nested pairs ([][]byte(xss) on a [][]int, and its mirror) are compile-time errors (“cannot convert”); convert element-wise with a for loop |
Any other operand type is a compile-time error (“cannot convert”).
[]T applied to a nevla list performs no per-element checking in v1:
the list value passes through single-valued, and the expression’s static
type becomes []T. The pass-through never crosses the byte boundary at
any depth (the exclusion in the table above, applied recursively through
nested list types); []byte([]byte) and [][]byte([][]byte) identity are
pass-throughs like any other. If the actual elements do not match T, later operations on them
fault at runtime. Programs must not rely on this as a checked cast; its
intended use is extraction from py values, where elements are genuinely
converted (section 13.5).
Conversions applied to py operands are the outbound bridge conversions and
are specified in section 13.5. A conversion applied to a py chain absorbs the
chain’s fallibility: if the chain raised, the conversion yields
(zero value of T, the error).
7.8 check expressions
UnaryExpr = PostfixExpr | ( "check" | "!" | "-" ) UnaryExpr .
check is a prefix operator over a unary-postfix chain: it binds the whole
selector/call/index chain to its right, but nothing past a binary operator.
check f() + 1 means (check f()) + 1; check torch.randn([2, 3]) applies
to the whole call.
check e requires:
- The enclosing function’s declared result list must end in
error?; otherwise “check requires enclosing function to return error?” is a compile-time error. This applies per function; acheckinside a function literal looks at the literal’s declared results. emust be fallible: its type must be(T1, ..., Tn, error?)forn >= 0(including a loneerror?, and including a py chain, which is fallible as a unit; section 7.11). Otherwise “check needs a fallible expression” is a compile-time error.
Semantics: evaluate e. If its error slot is none, the check expression
yields the remaining values: no value for n = 0, the single value for
n = 1, the multi-value for n > 1. If the error slot holds an error, the
enclosing function returns immediately: its final result slot is the error,
and every preceding result slot is filled with the zero value of its declared
type (section 5.11).
fn boom() (int, str, error?) {
return 0, "", error.new("bad")
}
fn run() (int, str, error?) {
a, b := check boom() // on error: run returns 0, "", the error
return a, b, none
}
Applied to a py chain, check yields the chain’s py value on success and
propagates the converted Python exception on failure (section 13.3).
7.9 Operators
Binary operators, in increasing precedence:
| Precedence | Operators |
|---|---|
| 1 | || |
| 2 | && |
| 3 | == != |
| 4 | < <= > >= |
| 5 | + - |
| 6 | * / % @ |
All binary operators are left associative. Unary operators (!, -,
check) bind tighter than any binary operator.
@ is matrix multiplication. It is defined only when at least one operand
is py (section 13.2) and is a compile-time error otherwise (“@ needs py
operands”). There is no native matrix type.
Expression = UnaryExpr | Expression binary_op Expression .
binary_op = "||" | "&&" | "==" | "!=" | "<" | "<=" | ">" | ">="
| "+" | "-" | "*" | "/" | "%" .
7.9.1 Arithmetic operators
| Operator | Operand types | Result |
|---|---|---|
+ - * / % | int, int | int |
+ - * / | float, float | float |
+ | str, str | str (concatenation) |
+ | []A, []B | list concatenation, see below |
int and float never mix: 1 + 2.5 is a compile-time error
(“int and float do not mix”). byte has no arithmetic operators in this
version (section 5.1): widen with int(b), compute, and narrow with
byte(n). % is defined on int only. Integer / and
% fault on a zero divisor; integer +, -, *, /, %, and unary -
fault on overflow (chapter 12). Integer division truncates toward zero.
Float arithmetic never faults (section 5.1).
List concatenation requires one element type to be assignable to the other; the result takes the wider element type, so concatenation widens toward options and never narrows:
xs := [1] // []int
ys := [maybe()] // []int?
zs := xs + ys // []int?
If neither element type accepts the other, concatenation is a compile-time
error. When either operand of any of these operators has type py, the
operation is a bridge operation instead (section 13.2).
Unary - requires int or float; unary ! requires bool.
7.9.2 Comparison operators
<, <=, >, >= are defined on int with int, byte with byte,
float with float, and str with str (lexicographic by character
number). The result is bool. byte and int never mix, except that an
integer literal in 0 to 255 compares directly against a byte operand
(section 5.10); otherwise widen with int(b) to compare across the two.
All other operand combinations are compile-time errors. Because comparisons
are left associative and yield bool, a chained comparison such as
a < b < c parses but is rejected by the type checker.
7.9.3 Equality operators
== and != yield bool and are defined in exactly two shapes:
- Scalar equality: both operands have the same type, which must be
int,byte,float,bool, orstr— except that abyteoperand compares directly against an integer literal in 0 to 255 (section 5.10). Float equality follows IEEE 754 (NaN is not equal to itself). - None comparison: one operand is the literal
noneand the other has an option type.x == noneis true iffxis absent. Comparingnoneagainst a non-option operand is a compile-time error (“none only compares to option types”).none == noneis true.
Everything else, including list, map, struct, error, fn, and option-to-option
comparison, is a compile-time error (“cannot compare”). Structural equality
is available through the contains method (section 11.2). When either
operand is py, equality is a bridge operation yielding py
(section 13.2).
7.9.4 Logical operators
&& and || require bool operands and yield bool. They short-circuit:
a && b does not evaluate b when a is false; a || b does not evaluate
b when a is true. py operands are not permitted.
7.10 Multiple values
A call of a function whose type declares n >= 2 results, a conversion
(always (T, error?)), and a py chain at its point of consumption
((py, error?)) produce a multi-value. Multi-values are not first class:
they cannot be stored, nested, or passed on. A multi-value may be consumed
only:
- by a short variable declaration whose name count equals the value count:
a, b := f(); - by a
checkexpression (which strips the error slot; section 7.8).
In particular, a multi-value cannot be forwarded by a return statement as
a unit; each result is returned by listing expressions (section 8.5). When
a return holds a single multi-value whose count matches the function’s
result list, the diagnostic is “a multi-value cannot be returned as a unit”
and names the binding idiom.
Using a multi-value in a single-value context is a compile-time error
(“multiple values in single-value context”), and binding it to the wrong
number of names is a compile-time error, with the special diagnostic
“error result must be handled” when exactly the trailing error? was left
unbound (section 10.2).
7.11 py chains
An expression is a py chain when it applies an operation to a value of type
py: attribute selection, call, method call, indexing, or a binary operator
with a py operand. Reading a py-typed struct field, or writing py(x)
(section 7.7), yields a plain py value and starts no chain. Within further postfix or binary operations the chain
continues to act as py, so consecutive Python steps need no per-step
handling. A chain appearing as an argument (positional or named) to an
enclosing py call is absorbed into that chain: f(g(x)) on py values is one
fallible unit, not f(check g(x)). At its point of consumption the chain as
a whole has type (py, error?):
import py "json"
fn main() (error?) {
a := check json.loads("40") // whole call is one fallible unit
b := check (a + json.loads("2")) // operators extend the chain
print(check int(b)) // 42
return none
}
The first Python exception raised anywhere in the chain, including inside an absorbed argument, aborts the rest of the chain and becomes the chain’s error value (section 13.4).
A py chain must be consumed by one of: a two-name destructure
(v, err := chain), a check, or a conversion (which absorbs the
fallibility; section 7.7). Binding a chain to a single name, or evaluating it
as a bare expression statement, is a compile-time error (“error result must
be handled”). Assigning into a py target (obj.attr = x or
obj[i] = x) is a statement, not a chain consumption; its semantics are
given in section 13.2.
When a destructured chain fails, the value slot receives the zero value of
py (a Python None handle) and the error slot the error; on success the
error slot is none.
Comparisons on py operands yield py (Python semantics), not bool, so a
py expression cannot appear directly in a condition; extract with
check bool(x) first. && and || reject py operands outright.
7.12 Evaluation order
Expressions evaluate left to right:
- binary operands: left then right (subject to short-circuit, 7.9.4);
- calls: callee (or method receiver), then arguments left to right;
- index and slice: the indexed expression, then the index or bounds;
- list literals: elements left to right; map literals: for each entry in order, key then value; struct literals: field values in the order written.
In an assignment statement the right-hand side is evaluated first; index expressions inside the target are then evaluated from the outermost (rightmost) index inward (section 8.3).
8. Statements
Statement = ShortVarDecl | Assignment | ExpressionStmt
| ReturnStmt | BreakStmt | ContinueStmt
| IfStmt | ForStmt | WithStmt .
8.1 Statement lists and terminators
StatementList = Statement { newline { newline } Statement } [ newline { newline } ] .
Inside a block, statements are separated by one or more line breaks. Every
statement except the last before the closing } must be followed by a line
break; two statements on one line are a syntax error. The line-continuation
positions of section 4.2 do not terminate a statement.
8.1.1 Divergence and unreachable code
A statement diverges when control cannot flow past it: return, break,
continue, a for with no condition whose body contains no break
lexically at its own level, a with whose body diverges, and an if
statement with an else block in
which the then block, every else if block, and the else block all
diverge. An if without an else never diverges. A statement following a
diverging statement in the same block is a compile-time error
(“unreachable code”).
8.2 Short variable declarations
ShortVarDecl = IdentifierList ":=" Expression .
IdentifierList = identifier { "," identifier } .
x := e declares x in the innermost scope with the type of e and binds a
copy of e’s value. e must produce a value; x := f() where f has no
results is a compile-time error.
With multiple names, e must be a multi-value (or py chain) of matching
arity; each name is declared with the corresponding component type. The
blank identifier discards its component. Redeclaring a name already declared
in the same scope is a compile-time error. There is no way to declare a
variable with an explicit type; the type is always the initializer’s.
8.3 Assignments
Assignment = AssignTarget "=" Expression .
AssignTarget = identifier
| PostfixExpr Index
| PostfixExpr Selector .
The target must be a variable, an element access, or a field access rooted at
a variable, for example x, xs[i], u.name, ps[0].y, m["k"]. Any
other expression as target is a syntax or compile-time error. The assigned
value must be assignable to the target’s type. Assignment to a bare variable
invalidates any flow narrowing of that variable (chapter 9).
Element and field targets mutate in place through the path:
xs[i] = von a list requires0 <= i < len(xs); out of range faults.m[k] = von a map inserts or updates; the static type of the assigned value must beV(notV?). A map access may appear only as the final operation of a target path: because a map read has typeV?, indexing or selecting through it (m["a"][0] = v) is a compile-time error; narrow the value out first.s[i] = von a string type-checks but is a runtime fault (“cannot assign into a string”); strings are immutable.
The right-hand side is evaluated before the target path; the target’s index expressions are evaluated outermost first (section 7.12).
8.4 Expression statements
ExpressionStmt = Expression .
An expression may stand alone as a statement. Its value, if any, is
discarded, subject to the mandatory error handling rule: an expression
statement whose type is error?, ends in error?, or is a py chain is a
compile-time error (“error result must be handled”), except that a check
expression handles the error itself and may stand alone:
fn main() (error?) {
check boom() // ok: check consumes the error slot
// boom() // compile error
return none
}
8.5 Return statements
ReturnStmt = "return" [ Expression { "," Expression } ] .
return e1, ..., en returns from the enclosing function (or function
literal). The number of expressions must equal the number of declared
results, and each expression must be assignable to the corresponding result
type. In a function with no declared results, return takes no expressions.
A bare return in a function with declared results is a compile-time error.
8.6 If statements
IfStmt = "if" Condition Block { "else" "if" Condition Block } [ "else" Block ] .
Condition = Expression .
A Condition is an ordinary expression, parsed with struct literals
suppressed (section 7.2.3); the same production supplies the header
expressions of for statements (section 8.7).
Each condition must have type bool; any other type, including py, is a
compile-time error (“condition must be bool”). else and else if must
appear on the same line as the closing } of the preceding block. Struct
literals are suppressed in conditions (section 7.2.3). Conditions may narrow
option-typed variables inside the branches (chapter 9).
8.7 For statements
ForStmt = "for" Block
| "for" [ IdentifierList ":=" ] "range" Condition Block
| "for" Condition Block .
Nevla has one loop keyword with three forms, as in Go:
-
for { ... }loops forever; onlybreakorreturnleaves it. -
for cond { ... }evaluatescond(abool) before each iteration and stops when it is false. -
for x := range e { ... }ranges overe. The range operand and the bindings follow Go:operand type one variable two variables intni: int, values0throughn-1compile-time error []Tindex i: intindex i: int, elementv: Tmap[K]Vkey k: Kkey k: K, valuev: Vstrindex i: intindex i: int, characterc: strpyiteration index i: intindex i: int, itemx: pyAn
intoperand ofn <= 0runs zero iterations. Lists range in element order, maps in insertion order; a[]byteoperand bindsv: byteunder the same[]Trow. Strings range by character: the index is the character index (the same indexs[i]uses, section 7.5) and the value is a one-characterstr. Apyoperand may be a py chain (ranging absorbs it); iteration calls Python’siter()once, then__next__per round, and StopIteration ends the loop silently. Any other exception, including fromiter()itself, faults (“py range: …”): a loop is a statement with no error slot, and a data source raising mid-iteration is not a per-round condition a program handles. The variables and:=may be omitted (for range e { ... }) to run the body once per element. Ranging over any other type, or with more variables than the operand admits, is a compile-time error. The iteration variables are fresh bindings each round, copied by their kinds (chapter 11): rebinding or mutating a value-typed variable does not affect the container; a reference-typed variable aliases the element. A list operand’s length is fixed at loop entry (element writes during iteration are visible; growth is a new list anyway, since append is pure); a map operand’s keys are snapshotted at entry, entries deleted mid-iteration are skipped and additions are not visited.
Struct literals are suppressed in the loop header (section 7.2.3).
8.8 Break and continue
BreakStmt = "break" .
ContinueStmt = "continue" .
break terminates the innermost enclosing loop; continue begins its next
iteration. Either outside any loop is a compile-time error. Both diverge for
the purposes of section 8.1.1: statements after them in the same block are
unreachable.
8.9 With statements
WithStmt = "with" Expression Block .
A with statement runs its block under a Python context manager. The
operand must be py: a py chain (the statement absorbs it, like range;
section 7.11) or a py-typed value. Any other operand type is a compile-time
error (“with needs a py value”).
The operand is evaluated to a Python object and its __enter__ method is
called; the result is discarded. The block then runs as an ordinary block
scope, and __exit__ runs on every nevla-level exit from it:
- On normal completion, on
breakorcontinue(which bind to an enclosing loop as usual), and on a return whose finalerror?result isnone(or whose function declares noerror?result), the call is__exit__(None, None, None)and its return value is ignored. Control then continues where it was headed. - On a return whose final
error?result holds an error, whether written explicitly or produced bycheckpropagation,__exit__receives a synthesized exception: an instance of a dedicated exception class (a subclass of Python’sException) carrying the error’s rendered text, passed as__exit__(type, instance, None). A context manager whose exit branches on exception state (a transaction commit/rollback, for example) therefore sees the error path as Python would. If__exit__returns a truthy value, which in Python suppresses the exception, the program faults (“py with:__exit__cannot suppress a nevla error”): nevla control flow cannot be resumed by a py call. On a falsy return the return propagates unchanged.
The statement has no error slot. A Python exception raised while
evaluating the operand, by __enter__, or by __exit__ faults
(“py with: …”), the same rule as py assignment and py iteration
(chapter 12). Fallible acquisition belongs before the statement:
import py "torch"
fn eval_loss(model py, batch py) (float, error?) {
with torch.no_grad() {
out, err := model(batch)
if err != none {
return 0.0, err // __exit__ sees the error as an exception
}
return check float(out.item()), none
}
}
A fault raised inside the block terminates the program without calling
__exit__: faults are not catchable and no construct observes one
(chapter 12). A with whose body diverges diverges itself (section
8.1.1). There is no binding form and no multi-operand form in v1; nest
statements for multiple managers, and reach state through the manager
object bound before the statement.
9. Flow narrowing
Flow narrowing (flow typing) is the mechanism that unwraps option types. The
checker refines the type of a variable within regions where a condition
proves it is not none.
9.1 Narrowing conditions
Exactly two condition forms narrow, and only when the whole condition is that form:
x != none(ornone != x), wherexis a variable of option typeT?: narrowsxtoTwhere the condition holds.x == none(ornone == x): narrowsxtoTwhere the condition fails.
No other form narrows. In particular, a compound condition such as
x != none && y != none narrows neither variable, a narrowing comparison on
a field or element (u.next != none) narrows nothing (bind it to a variable
first), and a function call returning bool never narrows.
u := find() // User?
if u != none {
print(u.name) // u: User here
}
9.2 Branch scope
For if cond with a narrowing condition:
- the positive narrowing applies within the
thenblock; - if there are no
else ifarms, the negative narrowing applies within theelseblock:
if x == none {
// x: int? here
} else {
print(x + 1) // x: int here
}
Each else if arm applies its own condition’s positive narrowing within its
own block. Narrowings do not combine across arms.
9.3 Terminal narrowing
For an if statement with a narrowing condition and no else if arms,
narrowing extends past the statement in exactly two cases:
- the
thenblock diverges (section 8.1.1) and there is noelse: the negative narrowing applies after the statement; - there is an
elseblock that diverges while thethenblock does not: the positive narrowing applies after the statement.
x := maybe() // int?
if x == none {
return
}
print(x + 1) // x: int from here on
9.4 Invalidation
Narrowing is erased wherever it can no longer be proven:
- An assignment
x = eanywhere, including in a nested block, erases every active narrowing ofxfrom that point on (in all scopes; losing a narrowing is always sound):
x := maybe()
if x != none {
if true {
x = none
}
y := x + 1 // compile error: x is int? again
}
- A loop body runs more than once, so an assignment to
xanywhere in a loop body erases the narrowing ofxfor the entire body, including statements before the assignment:
x := maybe()
if x != none {
for i := range 2 {
y := x + 1 // compile error: x assigned below in this body
x = none
}
}
Rebinding with := creates a new variable and does not affect the outer
one’s narrowing.
10. Errors and error handling
10.1 Error values
Errors are ordinary values of type error (section 5.7). There are no
exceptions and no user-visible panic. Fallible functions return their error
in a trailing error? result:
fn fetch(url str) (str, error?) { ... }
fn cleanup() (error?) { ... }
By convention, and as required by check, the error slot is the last result.
10.2 Mandatory handling
Dropping an error is a compile-time error. Specifically, the diagnostic “error result must be handled” is issued when:
- an expression statement’s value is
error?or ends inerror?, or is a py chain (section 8.4), unless the expression is acheck; - a short variable declaration binds one fewer name than the value count and
the unbound trailing component is
error?(n := f()wherefreturns(int, error?)); - a py chain is bound to a single name or otherwise consumed as one value (section 7.11).
Handling means one of: binding the error slot to a name (which may be _,
an explicit and legal way to discard it), or propagating with check.
10.3 Recovery
Recovery is the two-value form plus a none-check, with flow narrowing
unwrapping the error?:
v, err := run()
if err != none {
print("failed: " + err.msg)
return
}
print(v)
10.4 Propagation and wrapping
check (section 7.8) propagates: on error, the enclosing function returns
the error in its final slot with all other slots zero-filled. error.wrap
adds context while preserving the cause chain:
fn parse_age(s str) (int, error?) {
n, err := int(s)
if err != none {
return 0, error.wrap(err, "bad age")
}
return n, none
}
main may itself declare (error?); returning a non-none error from main
terminates the program with a nonzero exit status (section 17.2).
11. The copy model and equality
11.1 Value and reference types
Nevla splits its types the way Go does. Value types copy on
assignment, argument passing, returning, iteration binding, and placement
in a container: int, float, bool, str (immutable), error,
structs, and tuples. A struct copy is shallow in Go’s sense: fields of
reference type copy the reference, so the copy’s containers alias the
original’s.
Reference types copy the reference; there is one underlying object:
[]T, map[K]V, fn, py (section 5.8), and Ctx (section 15.4).
[]byte is a []T like any other (reference type, aliasing, clone); its
compact contiguous-buffer representation is an implementation detail with
no language-visible effect (design 2026-07-13).
a := [1, 2, 3]
b := a
b[0] = 99
print(a[0]) // 99: a and b are the same list
fn mutate(xs []int) { xs[0] = 42 }
mutate(a)
print(a[0]) // 42
The zero value of a list or map is a fresh empty container, immediately usable; nevla has no nil and no nil-map write fault.
clone(x) returns a one-level copy of a list or map: the container is
new, its elements are copied by their own kinds (values copy, references
alias), matching Go’s slices.Clone and maps.Clone. clone of a value
type is a compile-time error; those copy already.
Mutation happens through element and field assignment (xs[i] = v,
m[k] = v, s.f = v) and through m.delete(k), which removes in place.
append(xs, v) stays pure, Go’s contract: it returns a fresh list, and
growth becomes visible to other names only by rebinding
(xs = append(xs, v)). xs.sorted(), xs.map(f), and the other list
methods also return fresh lists.
Aliasing makes cyclic values constructible (a struct field list can reach its own container through a chain of references). Every deep walk over values is bounded: structural comparison and bridge conversion fault with “value too deep or cyclic” past depth 256, rendering truncates to “…”, and an assignment path that reaches the same container twice faults (“assignment path aliases itself”). A cyclic value can exist; it cannot hang or crash the interpreter.
11.2 Equality
The == operator is scalar-only (section 7.9.3). Structural equality is
provided by list.contains, which compares its argument against each element
recursively: lists element-wise, structs by name and field values,
maps by key set and per-key values, scalars and none by value. py, fn,
Ctx, and module values never compare equal to anything under structural
equality.
ps := [Point{x: 1, y: 2}]
print(ps.contains(Point{x: 1, y: 2})) // true
12. Runtime faults
A fault is a runtime error that terminates the program. Faults are not catchable in v1: no language construct observes or recovers one. A fault must terminate the program with a nonzero exit status and a diagnostic including a nevla call-stack trace, and must never crash the hosting process, raise a foreign exception, or trigger undefined behavior. An interpreter panic is an implementation bug, never specified program behavior. (Native code inside a C extension crashing below the bridge is outside this chapter’s reach; section 1 documents that boundary.)
The complete set of fault conditions reachable from checked programs:
- integer division or remainder by zero;
- integer overflow in
+,-,*,/,%, or unary-onint(including-9223372036854775808 / -1), insumover[]int, or inmath.absof-9223372036854775808; - list or string index out of bounds;
- slice bounds out of range (
lo < 0,hi < lo, orhi > len); - assignment into a string index (
s[i] = v); repeatwhose result would exceed the implementation’s string size limit;- calling the zero value of a function type (section 5.11);
- exceeding the call-depth limit (chapter 18), diagnostic “recursion limit exceeded”;
printf/sprintfwith a non-literal format string whose verbs do not match the arguments at runtime (wrong count, wrong type, unknown verb, or a format ending inside a verb; section 14.3);printf/sprintfwith a non-literal format string whose width or precision exceeds the implementation’s pad limit (section 14.2);- operations on list elements whose actual type does not match the list’s
static element type after an unchecked
[]Tconversion (section 7.7); - structural comparison or bridge conversion of a value deeper than the implementation’s depth limit (a cyclic value; section 11.1), and an assignment whose path reaches the same container twice;
__exit__of awithstatement returning truthy against a propagating nevla error (section 8.9).
Float arithmetic never faults (section 5.1). Reading a map never faults.
Python exceptions are not faults; they become error values (chapter 13) –
except in assignment into a py target, in for range over a py
iterable, and in the operand, __enter__, or __exit__ of a with
statement (all statements with no error slot), where an exception faults
(sections 13.2, 8.7, and 8.9).
13. The Python bridge
13.1 import py
import py "modname" binds the named Python module as a value of type py.
Module resolution follows Python’s own import rules in the embedded
interpreter. A dotted path imports the submodule and, as in Python, binds
the top-level segment: import py "os.path" loads os.path and binds
os, so os.path.join(...) is an ordinary chain. Several imports sharing
a top segment are one binding. If an import fails at
program start (for example the module does not exist), the program terminates
with a runtime error before main runs, carrying the Python exception text.
Inside a project, py imports are validated against the manifest at compile time (section 17.5).
13.2 Operations on py values
A py value supports:
- attribute selection
x.attr, yieldingpy; - calls
x(args...)and method callsx.m(args...), yieldingpy. Named arguments (f(x, lr: 0.001)) pass as Python keyword arguments; - subscript
x[i], yieldingpy; - assignment into attributes and subscripts (
x.attr = v,x[i] = v, including at the end of a longer chain): the value converts per the inbound table of section 13.5, and the referent mutates in place.pyis a reference type; this is the mutation that references exist for. Assignment is a statement with no error slot, so a Python exception (or an unconvertible value) here faults (“py assignment: …”), unlike the expression operations below. The assigned value may itself be a py chain; the assignment absorbs it, and a chain exception faults the same way; - the binary operators
+ - * / % @ == != < <= > >=when either operand ispy, dispatched to the corresponding Python operation and yieldingpy(comparisons included: the result is a Python bool as apyvalue, not a nevlabool).@dispatches to Python matrix multiplication (__matmul__/__rmatmul__); unlike the arithmetic operators it has no meaning on native operands (section 7.9).
&& || reject py operands; unary ! and - are not defined on py;
py values can be ranged over with for (section 8.7) and run as context
managers with with (section 8.9), but cannot be sliced or used as
conditions.
13.3 Fallibility
Every operation of section 13.2 may raise a Python exception. As specified in
section 7.11, a chain of such operations is fallible as a single unit typed
(py, error?) at consumption. The first exception in a chain converts to a
nevla error value and aborts the remainder of the chain.
13.4 Exception conversion
A Python exception becomes an error value with:
pytype: the exception type’s name (for example"JSONDecodeError","ModuleNotFoundError");msg:"<pytype>: <str(exception)>";traceback: the formatted Python traceback, or""when unavailable;cause:none.
13.5 Conversions across the bridge
Data crosses the bridge by one rule per kind:
- Value types (scalars,
str, structs): convert, i.e. copy. A value has no identity to preserve, so the copy is lossless. - Contiguous primitive buffers (
[]byte; the compact numeric list types[]float64/[]int64inherit this design if they arrive): cross by reference, zero-copy, through CPython’s buffer protocol. This is the data plane. - Structured containers (lists other than
[]byte, maps): copy, per element, into a fresh Pythonlist/dict. CPython cannot view foreign memory as alist. pyhandles: already references; the referenced object crosses unchanged.
Inbound (nevla value passed as an argument or index to a Python operation): conversion is automatic, per this table, applied recursively to list elements and map entries:
| Nevla | Python |
|---|---|
int | int |
byte | int |
float | float |
bool | bool |
str | str |
none | None |
[]byte | a buffer-protocol view (nevla.bytesview), zero-copy; see below |
[]T (T ≠ byte) | list (a per-element copy) |
map[K]V | dict |
py | the referenced object itself |
A []byte argument crosses as nevla.bytesview, a bridge-defined object
exposing the buffer’s memory (pointer and length) through CPython’s buffer
protocol. memoryview, hashlib.update, numpy.frombuffer,
torch.frombuffer, and file.write consume it with no copy at any size,
with no size threshold and no opt-in. The view shares the buffer’s memory,
so mutations are visible both ways: a nevla b[i] = x changes what a
frombuffer tensor sees, and a Python in-place write changes what nevla
reads. nevla is single-threaded and the GIL serializes Python, so there is
no torn access. The view is not a Python bytes; code that requires a true
bytes (an isinstance(x, bytes) check, a dict key, .decode()) calls
bytes(x) py-side, the one explicit copy, paid at the call site that
demands it.
Passing any other type (a struct, function, error, Ctx, or module) is not a conversion error at compile time but produces an error value at runtime (“cannot pass … to python”), flowing through the chain’s error slot like any Python exception.
Outbound (Python object to nevla value): always explicit and fallible,
via the conversions of section 7.7 applied to a py operand. Each yields
(T, error?):
| Conversion | Succeeds when | Notes |
|---|---|---|
int(x) | the object is a Python integer (extractable as a 64-bit int) | a Python float is an error |
float(x) | the object supports extraction as a Python float | Python ints convert |
bool(x) | (practically) always | Python truthiness of the object |
str(x) | (practically) always | Python str() of the object |
[]T(x) | the object is iterable and every element extracts as T | T may be int, float, bool, str, or py; py keeps elements as handles |
[]byte(x) | the object supports the buffer protocol (bytes, bytearray, memoryview, including a nevla.bytesview round-tripped through Python) | one copy out of the buffer, into a fresh compact []byte; a non-buffer object is an error |
Failure produces (zero value of T, error) with the Python exception
converted per section 13.4.
13.6 Rendering
print, %v, and str() render a py value as Python’s str() of the
object.
14. Builtin functions
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).
14.1 print
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 |
14.2 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).
14.3 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).
14.4 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.
14.5 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.
14.6 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.
14.7 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.
14.8 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.
14.9 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]
}
15. Standard library
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.
15.1 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.
15.2 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
}
15.3 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
}
15.4 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
}
15.6 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
}
}
15.5 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
"".
15.7 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
}
15.8 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).
15.9 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.
15.10 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
}
15.11 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
}
15.12 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.
16. Modules and multi-file programs
16.1 File imports
import "util.nv" imports another nevla source file. The path is
resolved relative to the directory of the importing file. The imported
file’s exported top-level functions and structs (section 16.3) become
visible under a namespace
equal to the file’s stem (the file name without .nv):
// util.nv
struct Pair { A int, B int }
fn Double(x int) int { return twice(x) }
fn twice(x int) int { return x * 2 } // private to util.nv
fn Make(a int, b int) Pair { return Pair{A: a, B: b} }
// main.nv
import "util.nv"
fn sum(p util.Pair) int { return p.A + p.B }
fn main() {
print(util.Double(21)) // 42
p := util.Make(1, 2)
print(sum(p)) // 3
}
Struct types of a file module are named with the dotted form util.Pair,
in type positions (section 5.9) and in struct literals
(util.Pair{A: 1, B: 2}, section 7.2.3).
16.2 Semantics
- Imports are transitive: an imported file may import further files, each resolved relative to its own directory.
- A file imported through multiple paths is loaded once (diamond imports are fine).
- An import cycle is a compile-time error naming the cycle.
- An unreadable or missing import path is a compile-time error.
- The root file (the one being run) is not namespaced. Namespacing respects local shadowing inside the imported module: a local variable that shadows a module-level name refers to the local.
- Modules are namespaces only; they are not first-class values.
16.3 Visibility
A module’s top-level name is exported when its first character is an ASCII capital letter; otherwise it is private to its file. There are no visibility keywords. The rule binds at module boundaries only: inside the defining file every name is reachable, and the root file’s own names are unaffected.
- Calling an unexported function through a module
(
util.twice(...)) is a compile-time error (“twice is not exported by util”). - A foreign struct literal must name an exported struct (“pair is not exported by util”), and every field of the struct must be exported: because literals supply every field (section 7.2.3), a struct with any unexported field cannot be constructed outside its module at all (“util.Pair has unexported fields (secret); construct it inside util”), Go’s constructor pattern.
- Reading or assigning an unexported field of a foreign struct is a compile-time error (“field secret of util.Pair is not exported”).
An exported function may mention unexported types: its results flow, and their exported fields read fine; the importer just cannot write the type in a literal or touch its unexported fields.
One file is inside another’s boundary: a file whose stem is the other’s
plus _test (util_test.nv for util.nv) may use the paired module’s
unexported names through the ordinary qualified syntax. Test files are
same-module code, as in Go. Standard library modules
(chapter 15) are exempt — their members are defined by this specification
— as are py values, whose access rules are Python’s. Visibility is a
compile-time rule; the REPL is unchecked (section 17.6).
17. Program execution
17.1 Entry point
Program execution begins at fn main. main must be declared, must take no
parameters, and must declare either no results or the single result
(error?). Any other signature, or a missing main, is a compile-time
error. The exception is a test file run by nevla test (section 17.7),
whose entry points are its test functions and which needs no main.
Before main runs, all import py modules are imported; a failing Python
import terminates the program as a runtime error.
Program arguments follow the file on the runner command line and are exposed
through os.args() (section 15.9).
17.2 Termination and exit status
A program run terminates in one of four ways:
| Outcome | Exit status | Diagnostics |
|---|---|---|
main returns (no error) | 0 | none |
| compile error (lex, parse, or typecheck) | nonzero | each diagnostic as line:col: message on standard error; the program does not run at all |
main returns a non-none error | nonzero | the error’s msg on standard error |
| runtime fault (chapter 12) | nonzero | the fault message and a nevla stack trace on standard error |
Program output written by print/printf up to the point of termination is
delivered to standard output in all cases. A program that typechecks must
never terminate by crashing the host process.
17.3 The two binaries
nevlais the toolchain:nevla run [file]typechecks and runs (defaulting to the enclosing project’ssrc/main.nv);nevla check [file]typechecks only and never runs code or provisions an environment;nevla new <name>scaffolds a project;nevla py add <pkg>declares a Python dependency and syncs the environment;nevla fmt [paths]rewrites source in the canonical style (--checkreports instead);nevla imports [paths]organizes imports (add missing stdlib, drop unused, sort) and then formats;nevla test [paths]runs test functions (section 17.7);nevla replstarts the REPL.nvis the runner:nv file.nvtypechecks and runs the file; barenvstarts the REPL.
Bare nevla run and nevla check outside any project fail with a
diagnostic. nevla check on a valid program produces no output and exits
0.
17.4 Projects
A project is a directory tree rooted at a nevla.toml manifest, found by
walking upward from the file being operated on (or the working directory).
The layout:
nevla.toml: project name, Python version pin, and declared Python dependencies ([py-deps]).nevla.lock: exact resolved Python package versions. Manifest and lock together fully determine the Python environment..nevla/: the generated virtual environment and sync markers. Disposable; deleting it is always safe, it regenerates on the next run.src/main.nv: the default entry point for barenevla run.
A project’s nevla.toml may carry nevla = "x.y.z", the version the
project was built against; nevla new writes it. Running or checking
the project under a different nevla prints a warning naming both
versions, so a language change surfaces as “built against 0.1.5” rather
than a mystifying compile error. The pin never blocks execution.
17.5 The manifest rule for py imports
When the compiled file lies inside a project, every import py "m" is
validated at compile time: the top-level segment of m (the part before the
first .) must either be declared under [py-deps] in nevla.toml or be
a module of the Python standard library. Declared names match import names
case-insensitively with - and _ interchangeable, mirroring PyPI name
normalization (sentence-transformers satisfies
import py "sentence_transformers"). An undeclared import is a compile-time
error directing the user to nevla py add.
A [py-deps] entry is either a version string or a table with optional
version (default "*") and optional module naming the import the
package satisfies when the two differ:
[py-deps]
torch = "*"
mlflow-skinny = { module = "mlflow" }
A module override replaces the package-name match: mlflow-skinny
above satisfies import py "mlflow" and no longer satisfies
import py "mlflow_skinny". Only the package name and version reach the
dependency resolver. nevla py add <pkg> writes the string form;
nevla py add <pkg> --module <name> writes the table form, and
re-adding an existing package preserves its table entry.
Hand-editing [py-deps] is first-class: the lock’s first line records a
fingerprint of the resolution inputs (the python pin and the requirement
lines), and provisioning with a drifted manifest re-resolves the lock
automatically before syncing the environment. A lock is never trusted
past its manifest. A changed lock rebuilds .nevla/venv from scratch
rather than syncing in place: packages that own overlapping directories
(mlflow and mlflow-skinny both own mlflow/) corrupt in-place removals,
and the venv is disposable by design.
The manifest’s python pin must match the interpreter embedded in the
running nevla (major.minor); a mismatch is a compile-time error naming
both versions. When the pin is omitted it defaults to the embedded version.
nevla new scaffolds with the embedded version.
Inside a project, sys.executable in the embedded interpreter refers to the
project venv’s python, so Python libraries that spawn worker interpreters
(multiprocessing and similar) function normally.
Outside a project there is no manifest to check; py imports resolve at
program start against the embedded interpreter, and a missing module is a
runtime error (section 13.1). Running a project with declared Python
dependencies provisions the environment automatically before execution;
nevla check never provisions.
17.6 The REPL
Bare nv (or nevla repl) starts an interactive session. The v1 REPL is
unchecked: input goes to the evaluator without typechecking, and faults are
reported and survived rather than ending the session. A line whose first
word is fn, struct, or import is treated as a declaration and
registered; any other line is executed as a statement, and an expression
statement’s value, if any, is printed in canonical rendering. Bindings and
declarations persist for the session. REPL behavior beyond this paragraph is
unspecified in v1.
17.7 nevla test
nevla test discovers *_test.nv files (the whole project’s src/ when
bare, or the given files and directories) and runs their test functions.
A test function’s name starts with Test, takes no parameters, and
returns (error?); any other Test-prefixed shape in a test file is an
error. Test files need no main (section 17.1) and may use the
unexported names of the module their stem pairs with (section 16.3).
Each test function runs in a fresh interpreter instance; tests run in
parallel (-j N bounds the workers, -j 1 serializes). A test passes
when it returns none, is reported skipped when it returns test.skip’s
sentinel, and fails otherwise; a runtime fault fails its test with the
nevla stack trace and the run continues. Failure reports lead with the
error’s origin (section 5.7). print output is captured per test and
shown only for failures. The exit status is nonzero when any test fails.
The embedded CPython and the filesystem are shared across tests; python module state persists and parallel tests race on shared paths exactly as any two programs would.
18. Implementation limits
Limits a program may rely on, stated as minimum guarantees:
- Syntactic nesting: an implementation must accept at least 256 levels of combined expression, type, and block nesting. Input exceeding the implementation’s limit must be rejected with a compile-time diagnostic (“expression too deeply nested”), never a crash. The reference implementation’s limit is exactly 256.
- Call depth: an implementation must support at least 1000 simultaneously active nevla function calls. Exceeding the implementation’s limit is a runtime fault (“recursion limit exceeded”) carrying a (possibly truncated) stack trace, never a host stack overflow. The reference implementation’s limit is exactly 1000.
intis exactly 64-bit two’s complement (section 5.1); this is not implementation-defined.
19. Conformance and maintenance
The golden tests under tests/golden/ are the executable companion to this
specification: each .nv file paired with an .out (expected stdout of a
successful run) or .err (required substrings of the compile or runtime
diagnostic) fixes observable behavior. A conforming implementation must pass
them. Where this document and a golden test disagree, the golden test is
taken as the intended behavior and this document must be corrected.
Any change to language semantics must land together with a matching edit to this document, in the same commit, alongside the golden tests that prove the new behavior.