Functions
Use fn for named functions. The keyword function is accepted as a synonym.
Basic Function
Section titled “Basic Function”fn double(x: int) -> int { x * 2}
print(double(21))Parameters and Return Types
Section titled “Parameters and Return Types”fn add(a: int, b: int) -> int { a + b}
print(add(2, 3))Tail Expression vs ;
Section titled “Tail Expression vs ;”The last expression returns automatically. Adding a trailing semicolon turns
the expression into a statement and the function returns ().
fn a() -> int { 1}
fn b() -> () { 1;}
print(a())Default Parameter Values
Section titled “Default Parameter Values”A parameter declares a default with =:
fn greet(name: string = "world") -> string { f"hello {name}"}
print(greet())print(greet("shape"))Positional default-filling works for trailing parameters:
fn rect(width: int = 10, height: int = 20) -> int { width * height}
print(rect()) // 200print(rect(5)) // 100 (height defaults to 20)print(rect(5, 6)) // 30Lambdas
Section titled “Lambdas”Lambdas use |params| body syntax. A single-parameter lambda whose result is
consumed at the call site infers its parameter type:
let inc = |x| x + 1print(inc(10))Type-annotations are not allowed inside the pipes. Lambdas rely on bidirectional inference from the surrounding context (see Closure Inference from Context below).
// Bare two-parameter lambdas in a `let` binding currently fail// inference at HEAD — the engine cannot recover the parameter types// from `x + y` alone. Pass the lambda directly into a method whose// signature pins the element type instead (see Closure Inference below).let add = |x, y| x + yprint(add(2, 3))A block body is allowed when the lambda needs multiple statements:
let nums: Vec<int> = [1, 2, 3]let doubled = nums.map(|x| { let bumped = x + 1 bumped * 2})print(doubled)Generics
Section titled “Generics”A type parameter is introduced in angle brackets after the function name:
// Generic type parameters parse and check, but generic-fn instantiation// returning `T` from a `Vec<T>` element access is on the// `stress_generics::generic_identity_*` residual cluster (see// CLAUDE.md "Known Constraints" — pre-existing shape-test failure// class (a); follow-up tracked, runtime materialization gap).fn first<T>(items: Vec<T>) -> T { items[0]}
print(first([1, 2, 3]))For now, prefer a concrete element type when you need the value at the call site:
fn first_int(items: Vec<int>) -> int { items[0]}
print(first_int([1, 2, 3]))Trait Bounds
Section titled “Trait Bounds”Bounds attach to a type parameter with :. Single bound:
// Type-illustration: `Display` is a user-defined trait in this snippet.// To run, define `trait Display { fn display(self) -> string; }` and// `impl Display for ...` for the chosen element type.fn render_all<T: Display>(items: Vec<T>) -> Vec<string> { items.map(|x| x.display())}Multiple bounds on the same parameter use +:
// Type-illustration. Provide impls of both traits to compile.fn save<T: Display + Serializable>(item: T) -> string { item.display()}where clauses
Section titled “where clauses”When bounds become noisy in the angle-bracket list, move them to a where
clause after the return type:
// Type-illustration: trait-bound surface only. The where-clause grammar// is `where Param: Bound (, Param: Bound)*` per shape.pest:338-344.fn combine<T, U>(a: T, b: U) -> string where T: Display, U: Display{ f"{a.display()}-{b.display()}"}Named Arguments
Section titled “Named Arguments”Calls accept named arguments to clarify intent at the call site. The
grammar accepts name: value at any positional slot:
// Parses + dispatches at HEAD; default-fill for non-trailing names is// `W10-followup-named-args-default-value` per close-summary §5.1 entry 7// — calls like `sma(threshold: 0.02)` silently use the `period` default// (no override propagation). Document below until the JIT verifier// and call-lowering catch up.fn sma(period: int = 14, threshold: number = 0.01) -> number { threshold * (period as number)}
let v1 = sma(period: 20, threshold: 0.05) // both named — workslet v2 = sma(threshold: 0.02) // partial — HEAD bug, see Aside abovelet v3 = sma(20, threshold: 0.02) // positional + named mix — worksThe supported call shapes at HEAD:
- All-positional:
f(a, b, c). - All-named:
f(a: 1, b: 2, c: 3)(order-independent when every name is supplied). - Positional-then-named:
f(1, b: 2, c: 3)— names must follow positions.
Reference Parameters
Section titled “Reference Parameters”A parameter prefixed with & accepts a reference to the caller’s binding.
Writing to the parameter mutates the caller’s value in place:
// VM-correct (`VM=6`) but currently diverges under JIT (`JIT=5`) per// the W14.2-G4-derefstore-drift fix at commit 005b5170 — the producer-// side ref-chain stamp landed on the VM compile path; the JIT// mir_compiler's equivalent stamp gap is tracked separately and// remains a real HEAD divergence. Until the JIT side catches up, run// ref-param code under `--mode vm` for correct behavior.fn bump(&x: int) { x = x + 1}
let mut n = 5bump(&mut n)print(n)The pass mode (& vs &mut) is inferred from how the body uses x. The
LSP shows the inferred pass mode as an inlay hint so the calling convention
is never hidden. See References and Borrowing
for the full borrow-check model and error codes.
const may be combined with a parameter declaration to declare immutability
explicitly:
fn no_mutate(const x: int) -> int { // `x = x + 1` would be a compile error here. x * 2}
print(no_mutate(21))Destructuring Parameters
Section titled “Destructuring Parameters”A parameter pattern may destructure a struct-shaped argument when the argument’s type is known by annotation:
// Bare `{x, y}` destructure-param without a type annotation cannot// recover the field types at HEAD. Annotate the parameter with the// record type to enable destructuring.fn magnitude2({x, y}: {x: int, y: int}) -> int { x * x + y * y}
print(magnitude2({x: 3, y: 4}))A nominal type alias keeps the call site tidy:
// Same shape with a type alias. The pre-W17.3 destructure-param// inference path on `{x, y}` does not project the alias's field// kinds at this site; `p.x` / `p.y` field access on the typed// parameter works without destructuring.type Point = { x: int, y: int }
fn magnitude2(p: Point) -> int { p.x * p.x + p.y * p.y}
print(magnitude2({x: 3, y: 4}))Closure Inference from Context
Section titled “Closure Inference from Context”Closure parameter types are inferred from the surrounding method or function
signature. For example, in Vec<int>.filter(|x| ...), x is known to be
int without explicit annotation:
let nums: Vec<int> = [1, 2, 3, 4]let evens = nums.filter(|x| x % 2 == 0) // x: int (inferred)let big = nums.map(|x| x * 10) // x: int (inferred)print(evens)print(big)This is the recommended way to type a lambda at HEAD — let the method signature pin the element type so you don’t need an annotation inside the pipes.
Higher-Order Functions
Section titled “Higher-Order Functions”A function whose parameter is itself a function can take a lambda directly. The single-callee call shape works today:
fn apply(f, x) { f(x) }
let double = |x| x * 2print(apply(double, 21)) // 42Returning a callable from a function, and calling the result inline, is on the closure-return follow-up list:
// Inline-chained calls on closure-returning functions// (`compose(f, g)(3)`) currently surface// `call_value_immediate_nb: callee must be NativeKind::Ptr(...)`.// Bind the intermediate result to a `let` and call it explicitly// when you need this shape today.fn compose(f, g) { |x| f(g(x)) }
let double = |x| x * 2let inc = |x| x + 1print(compose(double, inc)(3)) // 8Returning a callable and binding it to a let works:
fn adder(a) { |b| a + b }
let plus10 = adder(10)print(plus10(5)) // 15Closures and Capture
Section titled “Closures and Capture”Lambdas capture variables from their enclosing scope. Read-only capture works today:
let count = 10let bump_by_count = |x| x + countprint(bump_by_count(5)) // 15print(bump_by_count(7)) // 17Function Modifiers
Section titled “Function Modifiers”Function declarations support several optional modifiers before fn:
async fn
Section titled “async fn”async fn fetch_value(n: int) -> int { n * 2}
let result = await fetch_value(21)print(result)See Async for full semantics: async scope,
for await, join blocks.
comptime fn
Section titled “comptime fn”Bodies of comptime fn are evaluated at compile time. Call them from a
comptime { ... } block or assign the call to a const:
// `const` initializer requires a literal (or unary `-` / `!` on a literal) at// HEAD per R8 W8 Cluster A — the comptime evaluator does not yet fold function// calls into the const-init slot. Tracked as v0.4-concurrency-design-pass// territory per docs/v0.3-close-summary.md §5.15. Until that lands, use a plain// literal for the const, or evaluate `comptime { build_tag() }` outside the// const initializer and re-bind with `let`.comptime fn build_tag() -> string { "v0.3"}
const TAG = comptime { build_tag() }print(TAG)See Comptime for the full surface (comptime for,
comptime builtins, type_info, build_config, warning, error).
fn python / fn typescript — polyglot functions
Section titled “fn python / fn typescript — polyglot functions”A polyglot function declares its body in another language. The body is captured verbatim and executed by the language extension runtime:
// Requires the Python extension to be installed and loaded// (`shape ext install python` + extension `extensions = ["python"]`// frontmatter). See `tooling/polyglot.mdx` for the runtime contract.fn python std_dev(values: Vec<number>) -> number { import statistics return statistics.stdev(values)}See Polyglot Functions and
Python Extension for the install + load
steps, the type-marshaling table, and the Result<T> error model.
extern "C" fn — native libraries
Section titled “extern "C" fn — native libraries”Declares a function loaded from a shared library at runtime:
// Requires the named shared library to be present on the load path.// The `from "libm.so.6"` clause names the library; the optional// `as "cos"` clause renames the imported symbol.extern "C" fn cos(x: number) -> number from "libm.so.6"See Native C Interop for the FFI rules,
the out keyword on pointer parameters, and the Permission gating that
extern functions inherit (NetConnect, FsRead, etc. as declared in the
extension’s LanguageRuntimeVTable).
- Prefer explicit parameter and return types for public functions.
fnis the canonical function declaration syntax in this book.functionis accepted as a synonym.- Untyped simple parameters participate in reference inference for
heap-like values; the LSP shows the inferred pass mode (
&/&mut) so hidden behavior is explicit. - See References and Borrowing for the full borrow-check model and error codes.