Declarations
Zirric programs are built from declarations. Every named entity — values, functions, types, modules — is introduced by a declaration. This page covers how each declaration form works, its scoping rules, and how attributes and type hints attach to declarations.
For the grammar of each form, see Syntax § Declarations.
const
const introduces an immutable binding. Once assigned, it cannot be reassigned. The value itself may be mutable (e.g. an array), but the binding is fixed.
const answer = 42
const name = "Zirric"
Scoping. const may appear at top level (module scope) or inside a block (lexical scope). Top-level constants are visible to the entire module and to importers. Local constants are visible from the point of declaration to the end of the enclosing block.
Closure capture. When a closure captures a const, it captures the value directly. The captured value is immutable and does not require an indirection cell.
| Context | Valid | Visibility |
|---|---|---|
| Top-level | Yes | Module |
| Local | Yes | Lexical |
| Expression | No | — |
var
var introduces a mutable binding that can be reassigned with =.
var count = 0
count = count + 1
Scoping. Same rules as const: top-level or local. The _ = expr form discards the result, which is useful for calling functions for their side effects.
Closure capture. When a closure captures a var, the compiler wraps it in an upvalue cell. Both the enclosing scope and the closure share a reference to the same cell, so mutations from either side are visible to the other. See Expressions § Closures for details.
| Context | Valid | Visibility |
|---|---|---|
| Top-level | Yes | Module |
| Local | Yes | Lexical |
| Expression | No | — |
fn
fn declares a named function. The body is a block of statements. The last expression in the block is the implicit return value if no explicit return is used.
fn greet(name) {
"Hello, " + name
}
Parameters are positional. Each parameter may carry a type hint. A return type hint may follow the parameter list.
fn add(a: Int, b: Int) -> Int {
a + b
}
Scoping. Functions can be declared at top level or inside a block. Local functions capture variables from enclosing scopes (see Expressions § Closures).
Functions are first-class values. A declared function can be passed as an argument, stored in a variable, or returned from another function.
| Context | Valid | Visibility |
|---|---|---|
| Top-level | Yes | Module |
| Local | Yes | Lexical |
| Expression | Closure only | — |
data
data declares a record-like type with named fields. Instances are created by calling the type name as a constructor function.
data Person {
name
age
}
const alice = Person("Alice", 30)
A data declaration without a body declares a zero-field type:
data None
Fields. Fields are positional — the constructor expects arguments in declaration order. Each field may have type hints and attributes. Function-style fields declare a parameter list:
data Greetable {
greeting(ofValue)
}
Field access. Fields are accessed with .:
alice.name // "Alice"
alice.age // 30
Scoping. data can only appear at the top level or nested inside a union. It cannot be declared inside a function body.
| Context | Valid | Visibility |
|---|---|---|
| Top-level | Yes | Module |
In union |
Yes | Module |
| Local | No | — |
union
union declares a nominal tagged union — a type whose values may be any of a fixed set of member types. Members can be existing types referenced by name, or inline data declarations.
union Option {
data Some { value }
data None
}
union StringOption {
String
None
}
Nested declarations. data types declared inside a union body are hoisted to module scope. They are accessible both as standalone types and as members of the union.
Membership. A value belongs to a union if its concrete type is one of the declared members. This is checked at runtime. See Type System § Union Types for membership semantics and discrimination.
No attributes on members. Union member references (like String or None above) cannot carry attributes. Attributes go on the data declarations themselves.
| Context | Valid | Visibility |
|---|---|---|
| Top-level | Yes | Module |
| Local | No | — |
attr
attr declares an attribute type. Attributes are metadata schemas that can be attached to declarations and fields. They follow the same field syntax as data.
attr Deprecated {
reason: String
}
attr Doc {
description: String
}
An attr without a body declares a zero-field attribute type:
attr Marker
Scoping. Attribute declarations can only appear at the top level.
Attr vs. data. Both attr and data declare types with named fields, but they differ in construction: attribute instances can only be created through @Attr(args) application syntax, never by calling them as functions at runtime. This distinction is enforced by the compiler. See Type System § Attribute Types for their role in the type system.
| Context | Valid | Visibility |
|---|---|---|
| Top-level | Yes | Module |
| Local | No | — |
Attributes on Declarations
Attributes attach metadata to declarations and fields using the @Name(args) syntax. Parentheses are always required, even for zero-field attributes.
@Deprecated("use newGreet instead")
fn oldGreet(name) {
"Hi, " + name
}
Where attributes can appear
Attributes can precede:
- Top-level declarations:
fn,data,union,attr,extern type,extern fn,extern const,const,var - Fields of
data,attr, orextern type - Nested
datainside aunion
Attributes cannot appear on:
- Union member references (e.g.
Stringin aunionbody) - Local declarations inside function bodies
- Expressions
Validation
Only declared attr types can be applied as attributes. Applying a non-attribute type — such as @String() or @Int() — is a compile error. Type information on fields and parameters uses type hints instead.
Multiple attributes
Multiple attributes may be stacked on a single declaration or field:
@Doc("The user's full name")
@Deprecated("use displayName")
data LegacyUser {
@Doc("First and last name")
name: String
}
Qualified attributes
Attributes from other modules are accessed through their qualified name:
import json
@json.HasKey("user_name")
data User { name: String }
extern Declarations
extern declarations introduce entities that are provided by the runtime or compiler extensions, not defined in Zirric source.
extern type
Declares an opaque type whose implementation is in the runtime. Fields may be declared for access but the type cannot be constructed from Zirric code.
extern type String {
length: Int
}
extern fn
Declares a function provided by the runtime.
extern fn print(msg)
extern const
Declares a constant provided by the runtime.
extern const void: Void
All extern declarations can only appear at top level.
| Context | Valid | Visibility |
|---|---|---|
| Top-level | Yes | Module |
| Local | No | — |
mod
mod declares a reference to the current module as a value. It must be the first declaration in the file (after any shebang line). While not required, it is conventional to declare it for readability.
mod http
The declared name becomes an identifier bound to the module object. This is useful for qualified attribute references when the module defines attributes.
| Context | Valid | Visibility |
|---|---|---|
| First in file | Yes | File |
| Other positions | No | — |
import
import makes another module’s declarations available in the current file.
import prelude
import code.knabel.dev.zirric_lang.zirric.future.tasks { Call }
import xprelude = code.knabel.dev.zirric_lang.zirric.future.prelude
Module path. The import path is a dot-separated sequence of identifiers. Standard library modules like prelude can use a short form that resolves to the full path.
Alias. The last segment of the path is the default alias. An explicit alias can be provided with alias = path.
Member imports. Specific members can be imported directly into scope with { Name, OtherName }. Without member imports, declarations are accessed through the module alias (e.g. tasks.Call).
Prelude. The prelude module is automatically available — its members can be used without an explicit import.
Visibility. Declarations whose names start with _ are considered private and are not exported from a module.
| Context | Valid | Visibility |
|---|---|---|
| Top-level | Yes | File |
| Local | No | — |
Parameters and Type Hints
Parameters appear in fn, closures, extern fn, and function-style data fields. Each parameter is an identifier optionally followed by a type hint.
fn greet(name: String) -> String {
"Hello, " + name
}
Type hint positions
Type hints can appear in several positions on declarations:
| Position | Syntax | Example |
|---|---|---|
| Parameter | name: Type |
fn f(x: Int) { ... } |
| Field | name: Type |
data P { name: String } |
| Return type | -> Type |
fn f() -> Bool { ... } |
| Variable / constant | name: Type = … |
const x: Int = 42 |
| Extern const | name: Type |
extern const void: Void |
What type hints express
Type hints are documentation and tooling aids. They communicate the intended type to readers, editors, and the language server. The compiler records them for runtime is checks and switch matching, but Zirric does not perform static type checking — values may still flow dynamically.
For the full type hint grammar and composite forms ([T], [K: V], fn(P) -> R, @Attr), see Syntax § Type Hints and Type System § Type Hints.