Functions
Function Declarations
Section titled “Function Declarations”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.
Parameters
Section titled “Parameters”Immutable Parameters
Section titled “Immutable Parameters”By default, parameters are passed by value and cannot modify the caller’s variables:
do double(x i64) -> i64 { return x * 2}
do main() {
}Mutable Parameters
Section titled “Mutable Parameters”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
mutvariables can be passed to¶meters. Passing aconstvariable is a compile-time error (E3027). ¶meters also accept struct fields (increment(point.x)), array elements (increment(arr[0])), and map values (increment(map["key"])).
Grouped Parameters
Section titled “Grouped Parameters”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() {
}Default Parameters
Section titled “Default Parameters”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=falseconnect("localhost", 3000) // port=3000, verbose=falseconnect("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)}Named Arguments
Section titled “Named Arguments”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 8080connect(verbose: true, host: "localhost") // same — order doesn't matterconnect("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 paramsmut 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}Return Types
Section titled “Return Types”Single Return Value
Section titled “Single Return Value”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)}Multiple Return Values
Section titled “Multiple Return Values”do divide(a, b i64) -> (i64, i64) { return a / b, a % b}
do main() {
mut quotient, remainder = divide(17, 5)}Error Returns
Section titled “Error Returns”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}}Named Return Values
Section titled “Named Return Values”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}))}Void Functions
Section titled “Void Functions”Functions without a return type return no value:
do print_greeting() { println("Hello!")}
do main() {
}Visibility
Section titled “Visibility”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
Section titled “Attributes”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")#jsonconst 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.
#docthen#jsonand#jsonthen#docproduce 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,
#jsonon a function produces an error;#jsoncan only be applied to struct declarations.
Available Attributes
Section titled “Available Attributes”| 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 |
#doc Attribute
Section titled “#doc Attribute”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}#json Attribute
Section titled “#json Attribute”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
#jsonconst 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
#jsonconst 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"}
#jsonconst 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
#jsonstruct’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
#jsonstruct cannot serialize under the same key (E3172). - A
#jsonstruct requiresimport @jsonin the same file; the generated serializer helpers depend on the json module (E6012). - Without
#json, the struct has no serialization machinery andjson.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#jsonstruct ([Task]) parses each element independently, so an enum field works there with no extra handling.
#discard Attribute
Section titled “#discard Attribute”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.
#discarddo 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:
#discardcan only be applied to function declarations. Applying it to structs, enums, or variables is a parse error (E2094).#discardcannot be applied to void functions — there is no return value to discard (E5042).
#deprecated Attribute
Section titled “#deprecated Attribute”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)}
#deprecateddo 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:
#deprecatedcan 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.
#deprecatedcan be stacked with other attributes (including#discard) on the same declaration, in any order.- Like all warnings,
W3007can be suppressed with-q W3007or-q all.
#test Attribute
Section titled “#test Attribute”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}
#testdo test_add() { assert(add(2, 3) == 5) assert(add(-1, 1) == 0)}
#doc("Verifies the zero case")#testdo 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 filegray test ./src Run the #test functions in the .gray files directly inside a directorygray 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:
#testcan 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
#testfunction must take no parameters and declare no return type (E5046). - A
#testfunction 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. #testcan be stacked with#docin either order.#testfunctions are type-checked in every build (so mistakes surface duringgray build), but only compiled and executed bygray test.
#[...] Attribute Lists
Section titled “#[...] Attribute Lists”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")#discarddo 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#testline. - 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).
Function References
Section titled “Function References”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 = ()doubleconst 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.
Calling Through a Reference
Section titled “Calling Through a Reference”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.
Func as a Parameter Type
Section titled “Func as a Parameter Type”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 paramdo apply(x i64, f func(i64) -> i64) -> i64 { return f(x)}
// Multiple paramsdo combine(a i64, b string, f func(i64, string) -> bool) -> bool { return f(a, b)}
// No paramsdo run(f func() -> i64) -> i64 { return f()}
// No return valuedo 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).
Func References in Composite Types
Section titled “Func References in Composite Types”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) // 10const fns [func, 2] = {()double, ()triple}fns[0](5) // 10fns[1](5) // 15mut m map[string:func] = {:}m["dbl"] = ()doublem["trpl"] = ()triplem["dbl"](5) // 10m["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.
Func Fields in Structs
Section titled “Func Fields in Structs”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}Comparing Func References
Section titled “Comparing Func References”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}Restrictions
Section titled “Restrictions”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
constonly — func references cannot be declaredmut- The typed signature (e.g.
func(i64) -> i64) must match exactly; param types and return type must all agree - Each parameter in a
funcsignature is listed as its own type:func(i64, string) -> bool. Grouped-type shorthand is not supported insidefuncsignatures - Default parameter values inside
funcsignatures are not supported - References work with top-level and struct-namespaced functions
Struct-Namespaced Functions
Section titled “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
selforthis— every parameter is explicit privaterestricts 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
Calling a Sibling Function
Section titled “Calling a Sibling Function”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:
- A top-level function of that name, if one exists.
- Otherwise, the enclosing struct’s namespace.
- 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.
Instance Dispatch
Section titled “Instance Dispatch”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.
Function Scope
Section titled “Function Scope”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}Wildcard Types (?)
Section titled “Wildcard Types (?)”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 i64mut 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 i64mut y = first({"a", "b"}) // ? binds to string}Where ? is allowed
Section titled “Where ? is allowed”? 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 |
Binding rules
Section titled “Binding rules”- 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
Type Parameters (<?>)
Section titled “Type Parameters (<?>)”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 |
Return type inference
Section titled “Return type inference”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)}Restrictions
Section titled “Restrictions”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)}Across module boundaries
Section titled “Across module boundaries”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.
More restrictions
Section titled “More restrictions”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 Foodo size_T(t <?>) -> i64 { return size_of(t) } // OK — size_of is always i64
do main() { println(new_foo(Foo)) println(size_T(Foo))}