Language quick reference
New to Sprig? Start with the beginner tutorial. This page is a quick reference to the language as the stage-0 compiler actually implements it. Every snippet on this page is a real file under website/snippets/ in the repository and is executed by scripts/internal/verify-doc-snippets.py during documentation checks.
The normative documents are the language spec (design contract) and the implemented feature status. Where the design kit proposes more than the compiler does, this page follows the compiler.
Layout and comments
Blocks are indentation-based. The first code line starts at column 1, one indent level is any consistent number of spaces, and tabs are rejected with SPR-LEX-TAB. Newlines inside (), [] and {} are ignored, so calls and literals may span lines. # starts a comment.
Bindings
# let binds once; var may be reassigned.
let name = "Ada"
var visits = 0
visits += 1
# Local bindings infer their type from the initializer.
let greeting = "Hello, " + name
print(greeting)
print(visits)let binds once and cannot be reassigned; var can. Local bindings infer their type from the initializer, while class fields always need an explicit type. There is no implicit truthiness: conditions must be Bool.
Functions
# Named functions declare parameter and return types, including -> Unit.
func add(a: Int, b: Int) -> Int:
return a + b
func factorial(n: Int) -> Int:
if n <= 1:
return 1
return n * factorial(n - 1)
print(add(2, 3))
print(factorial(5))Every named function and method declares parameter types and a return type, including -> Unit. Calls use positional arguments. A function that can fail adds throws ErrorType (see errors below).
Classes
class Hero:
let name: String
var health: Int = 100
func heal(amount: Int) -> Unit:
health += amount
func describe() -> String:
return name + " (" + health.toString() + " hp)"
# Class construction uses named fields only.
let hero = Hero(name="Ada", health=80)
hero.heal(15)
print(hero.describe())A class declares let (immutable) and var (mutable) fields with optional defaults. Construction is always named: Hero(name="Ada", health=80). Missing, unknown or duplicate fields are compile errors. Methods access the current instance's fields without a prefix; parameters and locals may not shadow a field.
Enums, variants and match
variant Expr:
Literal(value: Int)
Add(left: Expr, right: Expr)
enum Mode:
Fast
Careful
# match must cover every case; there is no default branch.
func eval(expr: Expr) -> Int:
match expr:
case Expr.Literal as node:
return node.value
case Expr.Add as node:
return eval(node.left) + eval(node.right)
func label(mode: Mode) -> String:
match mode:
case Mode.Fast:
return "fast"
case Mode.Careful:
return "careful"
let tree = Expr.Add(
left=Expr.Literal(value=20),
right=Expr.Literal(value=22)
)
print(eval(tree))
print(label(Mode.Careful))enum cases carry no payload. variant declares a sealed sum type whose cases have immutable named fields. Statement match allows multi-statement suites; expression match produces a value with exactly one expression per branch. Each case names one enum or variant case, optionally binding the payload with as node. Missing, duplicate, wrong-type and unreachable branches are compile errors; there is no default or wildcard, and no fallthrough. Because the match is exhaustive by construction, adding a case to a variant forces every visitor to handle it — the property the compiler itself relies on in tests/visitor/ast_visitor.spr, a multi-visitor AST interpreter written in Sprig and run by the test suite.
Collections
let immutable: List[Int] = [3, 1, 2]
let mutable: MutableList[Int] = immutable.toMutableList()
mutable.append(4)
mutable.sort()
print(immutable.contains(2))
print(2 in immutable)
print(mutable)
print(mutable[0])
print(immutable) # toMutableList returns a new outer collection
let ages: Map[String, Int] = {"ada": 36, "bob": 41}
print(ages.get("ada"))
print(ages["missing"])
print("bob" in ages)List[T] and Map[K,V] are read-only; MutableList[T] and MutableMap[K,V] are mutable. toMutableList(), toList(), toMutableMap() and toMap() produce new outer collections. Mutation through an immutable type is rejected with SPR-COLLECTION-IMMUTABLE. Indexing, in, get, set, append, sort and the higher-order map/filter/forEach methods are implemented. Floating-point map keys are rejected because IEEE equality and hashing disagree for NaN and signed zero.
Nullability
# T? marks expected absence. A checked value narrows to non-null inside the branch.
func find(words: List[String], target: String) -> String?:
for word in words:
if word == target:
return word
return null
let found = find(["alpha", "beta"], "beta")
if found != null:
print("found " + found)
else:
print("not found")
let missing = find(["alpha"], "gamma")
print(missing == null)null is only assignable to T?. A value narrows to non-null inside a proven != null branch; using a possibly-null value where non-null is required is SPR-TYPE-NULLABLE. Mutable fields are not narrowed across calls. Java reference results are conservatively nullable (see JVM interoperability).
Errors
# Recoverable failures use typed errors: func ... throws T, throw, try/catch.
func checkout(quantity: Int) -> Int throws Error:
if quantity <= 0:
throw Error("quantity must be positive")
return quantity * 3
try:
print(checkout(4))
catch problem: Error:
print("rejected: " + problem.message)
try:
print(checkout(0))
catch problem: Error:
print("rejected: " + problem.message)A function declares the error types it can raise with throws. Callers must either handle them with try/catch (plus optional finally) or declare the same effect; SPR-FLOW-THROWS is reported otherwise. Error values expose a message field. Java checked exceptions can be caught as the imported Java exception class.
Explicit generics
# v0.8 generics: one or more parameters per block, explicit [Type] at every use.
generic T:
class Box:
let value: T
generic T:
func identity(value: T) -> T:
return value
let box = Box[Int](value=41)
print(identity[Int](box.value) + 1)User-defined classes, variants and functions support generic T: or generic K, V: blocks. Every use writes explicit type arguments, such as Box[Int](value=42); parameters are invariant and there is no inference. Equality on a parameter requires requires T: Equatable. See the generics guide for the implemented contract.
Lambdas
# Lambdas are expressions: fn(params) => expression. Arities 0 through 3.
let doubled = [1, 2, 3].map(fn(x: Int) => x * 2)
print(doubled)
let evens = [1, 2, 3, 4].filter(fn(x: Int) => x % 2 == 0)
print(evens)
let collected: MutableList[Int] = []
[1, 2, 3].forEach(fn(x: Int) => collected.append(x * 10))
print(collected)Lambdas are expressions: fn(x: Int) => expression. Arities 0 through 3 are supported, bodies are single expressions, and a lambda cannot declare throws. A lambda that captures a var local is rejected (SPR-TYPE-CAPTURE); copy it into a let binding first.
JSON object lookup
import "@std/json.spr" as json
let document = json.parse("{\"name\":null}")
match json.find_member(document, "name"):
case json.Lookup.Missing:
print("missing")
case json.Lookup.Found as member:
print(json.stringify(member.value)) # prints null
case json.Lookup.NotObject:
print("expected an object")json.find_member distinguishes Missing, Found(value: json.Value) and NotObject. Present JSON null, false, zero and empty strings remain found values. Duplicate object keys still raise Error; the object member order is preserved. The standard-layer contract explains parsing, lookup and serialization boundaries.
Function types
A function type spells parameter types and result types:
let inc: fn(Int) -> Int = fn(x: Int) => x + 1
print(inc(4))
let optional: (fn(Int) -> Int)? = nullfn(Int) -> Int is a type; fn(x: Int) => x + 1 is a value expression. Arity is 0–3. Parameters and results are invariant: there is no function subtyping, implicit conversion or untyped fallback. Use parentheses for outer nullability, (fn(Int) -> Int)?; fn(Int) -> Int? has a nullable result. Ordinary null narrowing applies before invoking a nullable function value. Function types may annotate locals, fields, parameters and results, or occur in explicit generic arguments. Function types cannot declare throws; checked errors must be handled inside a non-throwing callable.
The JVM bridge accepts corresponding Sprig-owned sprig.runtime.Fn0–Fn3 formal signatures with supported concrete type arguments. It does not convert functions to arbitrary Java Function, Consumer, Runnable or interfaces. Ask sprig api <Class> --json about the actual formal signature.
Modules
import "./math_module.spr" as math
print(math.square(7))
print(math.PI_APPROX)func square(x: Int) -> Int:
return x * x
let PI_APPROX: Float = 3.14159import "./file.spr" as alias imports another Sprig file. Imports must appear before any declaration or statement. The imported module initializes once; import cycles are rejected with SPR-NAME-IMPORT-CYCLE. Importing Java classes uses the same syntax with a qualified class name: import java.time.LocalDate as LocalDate.
What is not in the language
Generic inference, variance, inheritance and interfaces, %=, tuples/destructuring and string interpolation are not implemented. Source array syntax, varargs and wildcard shapes are outside the JVM interop profile (arrays still cross as opaque foreign values; see JVM interoperability). See Known limitations for the full list, and the stage-1 roadmap for what comes next.
Conservative ergonomics
Use explicit declaration facades, value-producing matches and canonical comment-preserving formatting. These add no wildcard exports, block expressions or formatter configuration.
