Skip to content

Functions

Use fn for named functions. The keyword function is accepted as a synonym.

fn double(x: int) -> int {
x * 2
}
print(double(21))
fn add(a: int, b: int) -> int {
a + b
}
print(add(2, 3))

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())

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()) // 200
print(rect(5)) // 100 (height defaults to 20)
print(rect(5, 6)) // 30

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 + 1
print(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 + y
print(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)

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]))

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()
}

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()}"
}

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 — works
let v2 = sma(threshold: 0.02) // partial — HEAD bug, see Aside above
let v3 = sma(20, threshold: 0.02) // positional + named mix — works

The 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.

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 = 5
bump(&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))

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 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.

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 * 2
print(apply(double, 21)) // 42

Returning 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 * 2
let inc = |x| x + 1
print(compose(double, inc)(3)) // 8

Returning a callable and binding it to a let works:

fn adder(a) { |b| a + b }
let plus10 = adder(10)
print(plus10(5)) // 15

Lambdas capture variables from their enclosing scope. Read-only capture works today:

let count = 10
let bump_by_count = |x| x + count
print(bump_by_count(5)) // 15
print(bump_by_count(7)) // 17

Function declarations support several optional modifiers before 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.

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.

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.
  • fn is the canonical function declaration syntax in this book. function is 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.