Skip to content

Functions

do add(a i64, b i64) -> i64 {
return a + b
}
do greet(name string = "World") -> string {
return "Hello, ${name}!"
}
do process() {
// Void function (no return type)
}
do main() {
}

fn is an alias for do. Both are valid, user’s choice.

fn add(a i64, b i64) -> i64 {
return a + b
}
fn main() {
println(add(1, 2))
}

fn and do are interchangeable; use whichever reads more naturally and stay consistent within a file.

By default, parameters are passed by value and cannot modify the caller’s variables:

do double(x i64) -> i64 {
return x * 2
}
do main() {
}

The & prefix on a parameter name declares it as mutable (pass-by-reference). The function can modify the caller’s variable through the parameter:

do increment(&x i64) {
x = x + 1
}
do main() {
mut val i64 = 5
increment(val) // no & at the call site
println(val) // 6
}

Rules:

  • & goes before the parameter name in the function signature: do f(&x i64).
  • At the call site, pass the variable directly — no & prefix: f(val).
  • Only mut variables can be passed to & parameters. Passing a const variable is a compile-time error (E3027).
  • & parameters also accept struct fields (increment(point.x)), array elements (increment(arr[0])), and map values (increment(map["key"])).

Multiple parameters of the same type can be grouped:

do add(a, b i64) -> i64 {
return a + b
}
do swap(&a, &b i64) {
mut t i64 = a
a = b
b = t
}
do main() {
}

Parameters can have default values. Default values are supported for all primitive types (integer and float types, string, bool, char):

do greet(name string = "World") -> string {
return "Hello, ${name}!"
}
do main() {
println(greet()) // "Hello, World!"
println(greet("Alice")) // "Hello, Alice!"
}

Multiple parameters may have defaults. When calling, arguments fill left-to-right and any remaining parameters use their defaults:

do connect(host string, port i64 = 8080, verbose bool = false) {
if verbose {
println("Connecting to ${host}:${port}")
}
}
do main() {
connect("localhost") // port=8080, verbose=false
connect("localhost", 3000) // port=3000, verbose=false
connect("localhost", 3000, true) // port=3000, verbose=true
}

Default parameters must appear after non-default parameters. A required parameter cannot follow a parameter with a default value:

do bar(a i64, b i64 = 10) {} // OK: required first, then default
// do foo(a i64 = 10, b i64) {} // error: required parameter cannot follow a default parameter
do main() {
bar(1)
}

When calling a function, arguments can be passed by name using name: value syntax. This lets callers provide arguments in any order and skip over defaulted parameters to target specific ones:

do connect(host string, port i64 = 8080, verbose bool = false) {
if verbose {
println("Connecting to ${host}:${port}")
}
}
do main() {
connect(host: "localhost", verbose: true) // port uses default 8080
connect(verbose: true, host: "localhost") // same — order doesn't matter
connect("localhost", verbose: true) // positional + named mix
}

Rules:

  • Positional arguments must come before named arguments. Once a named argument appears, all remaining arguments must also be named:
do add(a i64, b i64) -> i64 { return a + b }
do main() {
println(add(1, b: 2)) // OK: positional first, then named
// add(a: 1, 2) // error: positional argument after named argument
}
  • Named arguments must match a parameter name in the function signature exactly. Unknown names are rejected:
do add(a i64, b i64) -> i64 { return a + b }
do main() {
println(add(a: 1, b: 2))
// add(a: 1, c: 2) // error: unknown parameter name 'c' in call to 'add'
}
  • A parameter cannot be provided both positionally and by name:
do add(a i64, b i64) -> i64 { return a + b }
do main() {
println(add(1, b: 2))
// add(1, a: 2) // error: parameter 'a' is already provided positionally
}
  • Named arguments work with struct functions. For instance dispatch, the self parameter is implicit — only name the non-self parameters:
const Vec struct {
x i64
y i64
do scale(self Vec, factor i64) -> Vec {
return Vec{x: self.x * factor, y: self.y * factor}
}
}
do main() {
mut v = Vec{x: 2, y: 3}
mut scaled = v.scale(factor: 5) // instance dispatch: name non-self params
mut also = Vec.scale(self: v, factor: 5) // static dispatch: name all params
}
  • Named arguments are not supported for built-in functions (println, len, cast, etc.) or standard library module functions (strings.to_upper, math.sqrt, etc.):
import @strings
do main() {
println("hello")
println(strings.to_upper("hello"))
// println(value: "hello") // error: named arguments not supported
// strings.to_upper(s: "hello") // error: named arguments not supported
}
do square(x i64) -> i64 {
return x * x
}
do main() {
}

A function that returns ^Type can be dereferenced at the call site with ^, which gives the value directly without storing the pointer:

const Foo struct {
n i64
}
do something() -> ^Foo {
mut f = new(Foo)
return f
}
do main() {
mut f = something()^ // dereference at call site, f is Foo, not ^Foo
println(f)
}
do divide(a, b i64) -> (i64, i64) {
return a / b, a % b
}
do main() {
mut quotient, remainder = divide(17, 5)
}
do parse(s string) -> (i64, Error) {
if s == "" {
return 0, error(.Unknown, "empty string")
}
return 42, nil
}
do main() {
mut value, err = parse("test")
if err != nil {
// Handle error
}
}

Return values can be given names to document what each position in the return tuple represents. Naming a return value does not implicitly declare a variable — the programmer must still explicitly declare a variable with that exact name in the function body:

do divide(a, b i64) -> (quotient i64, remainder i64) {
mut quotient i64 = a / b
mut remainder i64 = a % b
return quotient, remainder
}
do main() {
mut q, r = divide(17, 5) // q=3, r=2
}

Named returns support grouped types (multiple names sharing one type):

do get_info() -> (name, city string, age i64) {
mut name string = "Alice"
mut city string = "NYC"
mut age i64 = 30
return name, city, age
}
do main() {
}

Named return values must be enclosed in parentheses.

The return statement must reference the named variable itself, not merely an equal or same-typed expression. Once a return position is named, return in that position accepts only the variable declared under that exact name — assigning an equivalent value to a differently-named variable and returning that instead is a compile-time error (E3080):

do square(x i64) -> (result i64) {
mut result i64 = x * x
return result
// mut other i64 = result
// return other // error[E3080]: function must return named variable 'result', not a different expression
}
do main() {
println(square(4))
}

So the names are not purely cosmetic documentation: they constrain what a return in that position may name, in addition to documenting the position for callers and tooling (e.g., gray doc).

Restriction: Wildcard types (?) cannot be used in named return positions. Since ? resolves to a different concrete type at each call site, the name adds no useful documentation. Use an unnamed return instead:

do first(arr [?]) -> ? { // OK: unnamed wildcard return
return arr[0]
}
// do first(arr [?]) -> (result ?) { ... } // Error: wildcard type '?' cannot be named
do main() {
println(first({1, 2, 3}))
}

Functions without a return type return no value:

do print_greeting() {
println("Hello!")
}
do main() {
}

By default, every top-level declaration is public. The private keyword restricts access to the declaring file, and applies to functions, constants, structs, enums, type aliases, and struct functions:

private const MAX_ITERATIONS i64 = 1000
private const Counter struct {
n i64
}
private const Mode enum {
FAST
SLOW
}
private do validate(n i64) -> bool {
return n > 0
}
do factorial(n i64) -> i64 {
// Can call private members within the same module
if !validate(n) { return 1 }
mut result i64 = 1
for i in range(1, n + 1) { result = result * i }
return result
}
do main() {
println(factorial(5))
}

Private members cannot be accessed from other modules:

// import "./mathlib"
// mathlib.factorial(5) // OK - public
// mathlib.validate(5) // error: private function
// mathlib.MAX_ITERATIONS // error: private constant
// mathlib.Counter{n: 1} // error: private struct
// mathlib.Mode.FAST // error: private enum
do main() {}

import and use does not change this: a private declaration is not published under its bare name either.

Attributes are annotations prefixed with # that modify declaration behavior. Attributes are placed on the line(s) immediately before a declaration, either stacked one per line or grouped into a single-line #[...] list:

import @json
#doc("A person with a name and age")
#json
const Person struct {
name string
age i64
}
// Equivalent, using the single-line container form:
#[doc("A person with a name and age"), json]
const Employee struct {
name string
age i64
}
do main() {}

Rules:

  • Attributes may be stacked one per line, or written as a single-line #[a, b, c] list (see #[…] Attribute Lists). The stacked and container forms are equivalent and may be mixed on the same declaration. Same-line stacking without the container (#doc("x") #json) is not supported.
  • Order is irrelevant. #doc then #json and #json then #doc produce identical results.
  • A given attribute may appear at most once per declaration; a repeat is rejected (E2090).
  • Blank lines between attributes and the declaration are allowed.
  • Each attribute applies to the immediately following declaration only. It does not skip ahead to find a compatible declaration further down the file.
  • Misapplied attributes are rejected. For example, #json on a function produces an error; #json can only be applied to struct declarations.
Attribute Applies To Description
#doc("...") functions, structs, enums, file-scope variables Documentation metadata, used by gray doc
#json structs Enables JSON serialization for the struct
#flags enums Marks enum as a bitflag set (values are powers of 2)
#error_code enums Contributes the enum’s variants to the open ErrorCode set (see ErrorCode)
#strict when blocks Requires all enum variants to be handled
#discard functions Allows callers to ignore the return value without triggering E5011
#deprecated / #deprecated("...") functions, structs, enums Warns (W3007) at every reference to the item, with an optional replacement message
#test functions Marks a test function, run by gray test and stripped from normal builds

The #doc attribute adds documentation metadata to functions, structs, enums, and file-scope variables. Used by the gray doc command to generate documentation.

#doc("Adds two integers and returns the sum")
do add(a i64, b i64) -> i64 {
return a + b
}
#doc("Represents a 2D point")
const Point struct {
x i64
y i64
}
#doc("Maximum number of retries before giving up")
do main() {
const MAX_RETRIES i64 = 5
}

The #json attribute marks a struct for JSON serialization and deserialization. The compiler generates all marshaling and unmarshaling code automatically, with no manual encoding/decoding calls and no error juggling at every step. Just annotate the struct and use json.parse() / json.stringify().

import @json
#json
const User struct {
name string
age i64
active bool
}
do main() {
// Parse a JSON string directly into a typed struct
mut u User = json.parse("{\"name\": \"Alice\", \"age\": 25, \"active\": true}")
println(u.name) // Alice
// Serialize back to JSON, fields are mapped automatically
println(json.stringify(u)) // {"name": "Alice", "age": 25, "active": true}
}

json.parse() returns a fully typed struct (or array of structs), and json.stringify() accepts any #json struct and returns a string. The compiler knows the struct layout at compile time, so it generates field-by-field serialization code directly with no reflection, no runtime schema lookup, and no intermediate map step.

By default, a field’s JSON key is its Grayscale name. A field can serialize under a different key with a trailing tag, the same backtick-string spelling Go and Odin use:

import @json
#json
const User struct {
name string `json:"Name"`
age i64 `json:"Age"`
}
do main() {}

json.stringify()/json.parse() then use "Name"/"Age" as the JSON keys instead of name/age.

A field of any sized number type (i8 through i256, u8 through u256, f32, f64) is encoded and decoded at that type. json.parse() panics on a number field whose JSON value is not a number of that type or does not fit it.

An enum field is serialized by the enum’s backing type. An integer-backed enum (the default) becomes a JSON number — the variant’s underlying value; a string-backed enum becomes a JSON string — the variant’s string value. json.parse() reverses the mapping:

import @json
const Priority enum {
LOW // 0
HIGH // 1
}
const Role enum {
ADMIN = "admin"
USER = "user"
}
#json
const Task struct {
name string
priority Priority
owner Role
}
do main() {
mut t Task = Task{name: "deploy", priority: Priority.HIGH, owner: Role.ADMIN}
println(json.stringify(t)) // {"name": "deploy", "priority": 1, "owner": "admin"}
mut back Task = json.parse(json.stringify(t))
println(back.priority == Priority.HIGH) // true
}

A JSON value that names no variant of the field’s enum is a json.parse() failure (P0129), the same as any other malformed field value. A tagged enum (variants with payloads) has no flat JSON representation and is rejected on a #json struct at compile time (E3173).

Rules:

  • Without a tag, a field’s JSON key must match the struct field name exactly.
  • A tag is written `json:"Name"` immediately after the field’s type, before any default value. The key can be any non-empty text but cannot contain a " or a backslash.
  • A tag cannot be shared across a comma-grouped field list (x, y i64 \json:“V”`` is rejected — E2095); give each field its own line and its own tag.
  • A #json struct’s fields are either all tagged or all untagged — mixing the two within one struct is rejected (E3171). This is scoped per struct, not per file: a file that aggregates many structs is free to tag some and leave others untagged, as long as each struct is internally consistent.
  • Two fields of the same #json struct cannot serialize under the same key (E3172).
  • A #json struct requires import @json in the same file; the generated serializer helpers depend on the json module (E6012).
  • Without #json, the struct has no serialization machinery and json.parse() / json.stringify() will fail.
  • Supported field types: i64, u64, f64, string, bool, and non-tagged enums (serialized by backing type).
  • json.parse() into an array of a #json struct ([Task]) parses each element independently, so an enum field works there with no extra handling.

The #discard attribute marks a function whose return value may safely be ignored by callers. Without #discard, calling a non-void function as a bare statement produces E5011 (“return value not used”). With #discard, callers may call the function without capturing the return value, and the compiler will not emit E5011.

#discard
do tryInsert(value i64) -> bool {
// ... returns true on success, but caller may not care
return true
}
do main() {
tryInsert(42) // OK — no E5011
mut ok bool = tryInsert(7) // also OK — capturing is still allowed
}

#discard can also be applied to struct functions:

const List struct {
items [i64]
#discard
do push(self List, value i64) -> i64 {
return len(self.items) + 1
}
}
do main() {
}

Rules:

  • #discard can only be applied to function declarations. Applying it to structs, enums, or variables is a parse error (E2094).
  • #discard cannot be applied to void functions — there is no return value to discard (E5042).

The #deprecated attribute marks a function, struct, or enum as deprecated. The compiler emits a W3007 warning at every reference to the marked item — every call, every struct-literal construction, every EnumName.VARIANT access, and every place its name appears as a declared type (variable, parameter, return type, or struct field). A replacement message is optional:

do new_format(s string) -> string {
return "[${s}]"
}
#deprecated("use new_format() instead")
do old_format(s string) -> string {
return new_format(s)
}
#deprecated
do untouched() {
// no message — warning falls back to a generic "is deprecated" text
}
do main() {
println(old_format("hello")) // warning: old_format is deprecated: use new_format() instead
untouched() // warning: untouched is deprecated
}

It applies the same way to struct and enum declarations, and to individual struct functions:

#deprecated("Point is old, use Point3D")
const Point struct {
x i64
y i64
}
const Container struct {
id i64
#deprecated("use current() instead")
do legacy(self Container) -> i64 {
return 1
}
do current(self Container) -> i64 {
return 2
}
}
do main() {
}

Rules:

  • #deprecated can be applied to function, struct, and enum declarations only (module-level or struct-scoped functions). Applying it elsewhere is a parse error (E2094).
  • The message argument, when present, must be a string literal: #deprecated("...").
  • A deprecated function’s own recursive calls to itself do not trigger the warning, and code inside a deprecated struct’s own struct-functions can reference that struct’s type without warning. A struct-function calling a different deprecated struct-function or referencing a different deprecated type still warns normally.
  • Deprecating a struct does not cascade to its struct-functions, and deprecating a struct-function does not affect the struct itself — the two are independent. Calling a non-deprecated struct-function on an instance of a deprecated struct does not warn.
  • #deprecated can be stacked with other attributes (including #discard) on the same declaration, in any order.
  • Like all warnings, W3007 can be suppressed with -q W3007 or -q all.

The #test attribute marks a function as a test. Test functions are run by the gray test command and are stripped entirely from gray build / gray run output — they add no code and no overhead to a normal binary.

do add(a i64, b i64) -> i64 {
return a + b
}
#test
do test_add() {
assert(add(2, 3) == 5)
assert(add(-1, 1) == 0)
}
#doc("Verifies the zero case")
#test
do test_add_zero() {
assert(add(0, 0) == 0)
}
do main() {
}

Running tests:

gray test Run every #test function in .gray files under the current directory (recursive)
gray test file.gray Run the #test functions in one file
gray test ./src Run the #test functions in the .gray files directly inside a directory
gray test ./src/... Run the #test functions in every .gray file under a directory (recursive)

A failed assert — or any runtime panic — inside a #test function is reported as a test failure; the runner records it and continues with the remaining tests rather than aborting. gray test exits non-zero if any test fails or any file fails to compile.

Rules:

  • #test can only be applied to top-level function declarations. Applying it to a struct function, enum, variable, or anything else is a parse error (E2094).
  • A #test function must take no parameters and declare no return type (E5046).
  • A #test function cannot be called or referenced from other code (E5047) — it is invoked only by the test runner. Factor shared logic into a normal helper function.
  • #test can be stacked with #doc in either order.
  • #test functions are type-checked in every build (so mistakes surface during gray build), but only compiled and executed by gray test.

Several attributes on one declaration can be grouped into a single-line container instead of stacking them:

#doc("This is a really long explanation of what this function does")
#discard
do something() -> i64 { return 1 }
do main() {}

becomes:

#[doc("This is a really long explanation of what this function does"), discard]
do something() -> i64 { return 1 }
do main() {}

Rules:

  • Entries are bare attribute names separated by commas — no per-item #. Parentheses appear only on attributes that take arguments (doc("..."), deprecated("...")).
  • A one-element list (#[test]) is legal and equivalent to the bare #test line.
  • The list must sit on a single physical line (E2092). It cannot be empty, carry a trailing comma, or contain an inner # (E2093). An unrecognized name is E2091.
  • Every entry is validated against the following declaration exactly as if it had been stacked: order is irrelevant, a repeated attribute is E2090, and a misapplied attribute produces the same error the stacked form would (e.g. #[json] on a function is E2094).
  • The container and the stacked form may be mixed on the same declaration.
  • Not supported on struct functions yet — stack the attributes there (E2094).

A function reference is a value that holds a pointer to a named function. Function references are created with () prefix syntax or ref() and must always be bound to a const:

do double(n i64) -> i64 { return n * 2 }
// ()func_name: implicit syntax (type is inferred)
// ref(func_name): explicit syntax (identical result)
// Optional explicit type annotation
do main() {
const f = ()double
const g = ref(double)
const h func(i64) -> i64 = ()double
}

mut is rejected for func reference variables; func references are compile-time aliases, not mutable state.

Call a func reference variable the same way you call a function:

do double(n i64) -> i64 { return n * 2 }
do main() {
const f = ()double
println(f(5)) // 10, call through variable
println(()double(5)) // 10, inline: create reference and call immediately
}

()f and ()f(5) are always rejected. The () prefix means “create a reference to the named function declaration”, not “dispatch through a variable f”. Since f is a variable (not a function declaration), both forms are compile errors. f(5) is the only valid dispatch syntax for a func reference variable.

When a function accepts another function as an argument, declare the parameter with a full typed func signature. This enables the compiler to check argument and return types at call sites:

// Single param
do apply(x i64, f func(i64) -> i64) -> i64 {
return f(x)
}
// Multiple params
do combine(a i64, b string, f func(i64, string) -> bool) -> bool {
return f(a, b)
}
// No params
do run(f func() -> i64) -> i64 {
return f()
}
// No return value
do each(arr [i64], f func(i64)) {
for_each v in arr { f(v) }
}
do double(n i64) -> i64 { return n * 2 }
do main() {
println(apply(5, ()double)) // 10
println(apply(5, ref(double))) // 10, ref() is equivalent
const f = ()double
println(apply(5, f)) // 10, pass a variable
}

Inside the function body, call through the parameter the same way: f(x).

Bare func is a valid type in arrays and maps. Elements are untyped function pointers; the cast is reconstructed from context at each call site:

import @arrays
do double(n i64) -> i64 { return n * 2 }
do triple(n i64) -> i64 { return n * 3 }
// Dynamic array of func refs
// Fixed-size array of func refs
// Map with func values
do main() {
mut arr [func] = {}
arrays.append(arr, ()double)
arr[0](5) // 10
const fns [func, 2] = {()double, ()triple}
fns[0](5) // 10
fns[1](5) // 15
mut m map[string:func] = {:}
m["dbl"] = ()double
m["trpl"] = ()triple
m["dbl"](5) // 10
m["trpl"](5) // 15
}

Typed func signatures as an array element type (e.g. [func(i64)->i64]) are not allowed. Use [func] or [func, N] instead.

Struct fields can hold func references. A typed func signature is required for struct field types — bare func is not allowed. This ensures compile-time argument checking:

const Wrapper struct {
f func(i64) -> i64
}
do double(n i64) -> i64 { return n * 2 }
do main() {
// Struct literal
const w = Wrapper{f: ()double}
println(w.f(5)) // 10
// Pointer instance (new())
mut w2 = new(Wrapper)
w2.f = ()double
println(w2.f(5)) // 10
}

Func references compare by pointer equality. Two references to the same function are equal; references to different functions are not:

do double(n i64) -> i64 { return n * 2 }
do triple(n i64) -> i64 { return n * 3 }
do main() {
const f = ()double
const g = ()double
const h = ()triple
if f == g { println("same") } // same, both point to double
if f != h { println("different") } // different
}

Func references can only point to functions declared with do. Built-in functions (println, panic, copy, etc.) and stdlib functions (arrays.append, strings.contains, etc.) cannot be turned into func references; wrap them in your own do function to pass them as a callback.

Operation Result
mut f = ()double ❌ must use const
println(f) ❌ func refs are not printable
copy(f) ❌ func refs cannot be copied
do get_fn() -> func(i64) -> i64 ❌ a function cannot declare a func return type; the result is unusable
const f = get_fn() ❌ cannot assign func-type return value; use ()func_name
get_fn()(5) ❌ cannot call a function’s return value directly
[func(i64)->i64] ❌ typed func signature as array type; use [func] or [func, N]
()println / ref(println) ❌ builtin and stdlib functions cannot be referenced
()f (f is a variable) ❌ () only works with named function declarations, not variables
()f(5) (f is a variable) ❌ same restriction; f(5) is the only valid call syntax

Rules:

  • No anonymous functions or lambdas; every reference points to a named function declaration
  • const only — func references cannot be declared mut
  • The typed signature (e.g. func(i64) -> i64) must match exactly; param types and return type must all agree
  • Each parameter in a func signature is listed as its own type: func(i64, string) -> bool. Grouped-type shorthand is not supported inside func signatures
  • Default parameter values inside func signatures are not supported
  • References work with top-level and struct-namespaced functions

Functions can be declared inside struct blocks as namespaced free functions:

import @math
const Point struct {
x i64
y i64
do create(x i64, y i64) -> Point {
return Point{x: x, y: y}
}
do distance(a Point, b Point) -> f64 {
return math.sqrt(math.pow(cast(a.x - b.x, f64), 2) + math.pow(cast(a.y - b.y, f64), 2))
}
private do validate(p Point) -> bool {
return p.x >= 0 && p.y >= 0
}
}
do main() {
// Called as Type.func()
mut p1 = Point.create(3, 4)
mut p2 = Point.create(0, 0)
mut d = Point.distance(p1, p2)
println(d)
}

Rules:

  • No implicit self or this — every parameter is explicit
  • private restricts access to other functions in the same struct
  • Called as StructName.func_name(args...)
  • Cross-module: module.StructName.func_name(args...)
  • Module-qualified types can be used in variable declarations, parameters, and return types: mut p module.Point

Inside a struct function body, a sibling function in the same struct can be called by its bare name, without the type prefix. private siblings are reachable this way too, since the caller is inside the struct:

const Calculator struct {
value i64
private do internal_add(a i64, b i64) -> i64 {
return a + b
}
do add(a i64, b i64) -> i64 {
return internal_add(a, b) // bare sibling call
}
}
do main() {
}

A bare call inside a struct function body resolves in this order:

  1. A top-level function of that name, if one exists.
  2. Otherwise, the enclosing struct’s namespace.
  3. Otherwise, E4002: undefined function.

A struct function may not share a name with a top-level function — that is a compile-time error (E4022), because the bare name would silently resolve to the top-level function and leave the struct’s own function reachable only as StructName.func_name(...). With that rejected, the order above is never ambiguous in a program that compiles.

When a struct function takes the struct (or a pointer to it) as its first parameter, callers can use the instance form instance.func(...) instead of writing the type name. The compiler rewrites the call as Type.func(instance, ...):

const Vec struct {
x i64
y i64
do len_sq(v Vec) -> i64 {
return v.x * v.x + v.y * v.y
}
do bump(&v Vec) {
v.x = v.x + 1
v.y = v.y + 1
}
}
do main() {
mut a Vec = Vec{x: 3, y: 4}
println(a.len_sq()) // sugar for Vec.len_sq(a)
println(Vec.len_sq(a)) // still valid
a.bump() // sugar for Vec.bump(a); '&v' makes it a mutable alias
println(a)
}

Both do f(v Vec) and do f(&v Vec) (mutable receiver) and do f(v ^Vec) (pointer receiver) participate in instance dispatch. The mutable-receiver form (&v) takes the instance by reference and may modify the caller’s variable.

Factory-style functions whose first parameter isn’t the struct (e.g. do make(x i64) -> Vec) keep requiring the type-namespaced form (Vec.make(...)); there is no instance to bind.

Chained struct function calls (a.f().g()) are not supported. Assign each intermediate result to a variable.

All functions in Grayscale are declared at the top level or inside struct blocks. Nested function declarations inside other functions are not permitted. Anonymous functions (lambdas/closures) are not supported.

Storing a reference to an existing function in a local variable is not the same as declaring a function and is perfectly valid:

do double(n i64) -> i64 { return n * 2 }
do main() {
const f func(i64) -> i64 = ()double // valid: f is a variable, not a function declaration
println(f(5)) // 10
}

The ? type is a wildcard placeholder that enables generic-style functions. When used in a function’s parameter types, ? is bound to the concrete type of the argument at each call site. The return type can also use ? to propagate the bound type.

do identity(x ?) -> ? {
return x
}
do main() {
mut a = identity(42) // ? binds to i64, returns i64
mut b = identity("hello") // ? binds to string, returns string
}

All ? placeholders in a function signature bind to the same concrete type:

do pick_first(a ?, b ?) -> ? {
return a
}
do main() {
println(pick_first(1, 2)) // OK, both args are i64, ? binds to i64
// pick_first(1, "hello") // Error: conflicting bindings for ?
}

Wildcard types also work with composite types in parameters and returns:

do first(arr [?]) -> ? {
return arr[0]
}
do main() {
mut x = first({1, 2, 3}) // ? binds to i64
mut y = first({"a", "b"}) // ? binds to string
}

? is only valid in function parameter types and return types. It is rejected everywhere else:

Usage Result
Function parameter type Allowed
Function return type Allowed (must have at least one ? parameter)
Variable declaration (mut x ?) Rejected
Struct field type Rejected
Array type in variable ([?]) Rejected
Map type in variable (map[string:?]) Rejected
new(?) Rejected
Named return type (-> (name ?)) Rejected; use unnamed -> (?) instead
  • The concrete type is inferred from the first argument that corresponds to a ? parameter
  • All subsequent ? parameters and the return type must be consistent with that binding
  • If the return type uses ?, at least one parameter must also use ? to provide the binding

The <?> annotation allows a function parameter to accept a type name rather than a value. This enables reusable constructors and type-aware utility functions.

const Point struct {
x i64
y i64
}
do make(T <?>) -> ^? {
return new(T)
}
do main() {
mut p = make(Point) // allocates a new Point, returns ^Point
}

The type parameter T is resolved at each call site using the same monomorphization pipeline as value wildcards (?). The compiler generates a specialized function for each concrete type used.

Where T can be used inside the function body

Section titled “Where T can be used inside the function body”

A type parameter name is valid in these positions:

Usage Example Result
new(T) new(T) Heap-allocates an instance of T
Struct literal T{x: 1, y: 2} Constructs a stack instance of T. Using this form constrains the function to struct arguments; a non-struct argument is rejected with E3127 at the literal
size_of(T) size_of(T) Returns the size of T in bytes

The return type uses ? the same way as value wildcards. -> ^? resolves to a pointer to the type argument, -> ? resolves to the type argument itself:

const Point struct {
x i64
y i64
}
do make(T <?>) -> ^? {
return new(T)
}
do make_stack(T <?>) -> ? {
return T{}
}
do main() {
mut p = make(Point) // -> ^Point
mut s = make_stack(Point) // -> Point
println(p)
println(s)
}

No mixing type and value parameters (E2087):

Type parameters and value parameters cannot appear in the same function signature:

do good(T <?>) -> ^? {
return new(T)
}
// do bad(T <?>, x i64) -> ^? { // Error E2087
// return new(T)
// }
do main() {}

Any type name, but it must name a type (E4016, E3128):

Structs, enums, primitives, and aliases of any of them may all be passed as type arguments. A name that names no type, and anything that is not a type name at all, are rejected:

const Point struct {
x i64
y i64
}
const Color enum {
RED
GREEN
BLUE
}
do make(T <?>) -> ^? {
return new(T)
}
do main() {
mut p = make(Point) // OK — struct
mut c = make(Color) // OK — enum
mut x = make(i64) // OK — primitive
// mut y = make(1 + 2) // Error E3128 — not a type name
// mut z = make(Nonexisto) // Error E4016 — names no type
}

A T{...} body constrains the function to structs (E3127):

A struct literal written against the type parameter is meaningless for a non-struct, so the function accepts only struct arguments. The error is reported at the literal, and E3058 names the call site that bound it:

const Point struct {
x i64
y i64
}
do make_stack(T <?>) -> ? {
return T{} // Error E3127 when T is bound to a non-struct
}
do main() {
mut s = make_stack(Point) // OK
// mut n = make_stack(i64) // Error E3127 — T is used as a struct literal
println(s)
}

Generic functions work through a module prefix, and the qualified spelling behaves exactly like the bare one:

// import "./utils.gray"
// const Point struct {
// x i64
// y i64
// }
// do main() {
// mut p = utils.make(Point) // same as 'using utils' + make(Point)
// }
do main() {}

The type argument may be written bare or module-qualified. A module-qualified name (utils.make(types.Point)) parses as a member expression, but as long as it names a real type it is accepted exactly as the bare spelling is. A qualified name that resolves to no type is still rejected with E3128.

Returning the type argument requires a wildcard return type (E3139):

A concrete return type is a promise that has to hold for every caller. Returning the type argument breaks it for all but the caller that happens to pass a matching type, so the declaration is rejected on its own — no call site required:

const Foo struct {
n i64
}
do new_T(t <?>) -> ^? {
return new(t)
}
// do new_T(t <?>) -> Foo {
// return new(t)^ // Error E3139 — returns whatever the caller passed
// }
do main() {
mut f = new_T(Foo)
println(f)
}

Write the return type as ? or ^? instead. The same applies to returning a wildcard-typed parameter (do id(v ?) -> Foo { return v }).

A concrete return type stays legal whenever the body returns a value of that type:

const Foo struct {
n i64
}
do new_foo(t <?>) -> Foo { return new(Foo)^ } // OK — returns an actual Foo
do size_T(t <?>) -> i64 { return size_of(t) } // OK — size_of is always i64
do main() {
println(new_foo(Foo))
println(size_T(Foo))
}