Skip to content

Declarations

Accepted

Accepted for V1 declaration visibility, lazy top-level declaration analysis, local declaration source semantics, declaration initializers, shallow binding mutability, and shadowing.

Statement syntax should generally follow:

designator subject operation

Where:

  • designator is the keyword that identifies the kind of statement.
  • subject is the entity the statement is about.
  • operation is the optional part that defines what is attached to or performed on the subject.

This is a guideline, not a hard grammar rule. Clarity wins when the pattern conflicts with readability or orthogonality.

Top-Level Declarations

Top-level declarations are order-independent for name resolution. The compiler first collects the file namespace's declarations, visibility markers, imports, and direct declaration-scope dependency edges before fully analyzing individual declaration bodies or initializers.

Top-level const declarations normally require an initializer. Initializer omission is valid only when finalized declaration metadata supplies an accepted value source, such as V1 @extern(.c) function-pointer constants. See C ABI Interop.

Top-level file declarations are private by default. pub exports a declaration to the file namespace's public surface:

pub const Parser = parser_impl.Parser
pub fn parse(source: []const u8) Ast!ParseError {
}

priv is accepted as an explicit private marker equivalent to omitting visibility:

priv const parser_impl = include("parser_impl.ct")
const scanner_impl = include("scanner_impl.ct")

pub and priv are mutually exclusive. Duplicate or mixed visibility markers are invalid.

pub is valid only on declarations that can contribute to a namespace public surface or public impl visibility. Local declarations inside functions or blocks cannot be pub. import declarations cannot be pub or priv because imports affect local checking scope and are never re-exported by themselves.

Top-level priv is redundant but valid. Style lints may prefer omitted private visibility or explicit priv in a given file, but the compiler does not require a consistent style.

Struct fields and methods use their own member visibility rule: they are public by default and use priv for private members. See Structs and Methods.

Lazy Top-Level Analysis

Top-level declarations are semantically analyzed on demand. A demand root is a source or toolchain requirement that starts analysis of a top-level declaration. A declaration is demanded by:

  • a reference from another analyzed declaration;
  • selection through a namespace value;
  • a public signature, public impl, public namespace re-export, or entry-point surface that needs it;
  • a test, documentation, or tool root that asks for it;
  • type annotations, contract applications, reflection queries, or operator lowering that need its value or conformance facts;
  • a forced comptime block that explicitly validates it.

Demand is granular. Name lookup, imports, and namespace selection may demand only a declaration's identity, visibility, and public signature facts. They do not force initializer evaluation or function body checking unless the selected declaration's value, implementation body, conformance facts, or public API validation requires that deeper analysis.

The compiler still performs eager checks needed to construct a coherent namespace:

  • syntax validity;
  • duplicate declaration names;
  • visibility marker validity;
  • import conflict checks;
  • direct declaration-scope module(...) and include(...) graph edges, including evaluation of the comptime constants needed to resolve their names or paths;
  • impl coherence facts that can be checked from declaration headers alone.

Lazy analysis applies to declaration bodies and initializer semantics:

  • const initializer evaluation;
  • function body checking;
  • comptime fn body execution;
  • contract default bodies;
  • type factory and contract factory bodies;
  • generic impl body checking for each demanded specialization.

An unused top-level declaration with an invalid initializer does not make a program invalid merely because it exists. The error is reported when the declaration is demanded:

const broken = Compiler.err(.{ .message = "not demanded" })

pub fn main() void {
}

This does not hide public API errors. Public signatures, public impls, and public namespace re-exports must demand the declarations needed to validate their reachable public API surface. Build modes, test roots, documentation roots, package-validation tools, and explicit validation blocks may add broader demand roots to verify additional declarations. Once demanded, diagnostics are deterministic and point at the failing declaration and the demand chain when available.

Local Declarations

Variable declarations should use var:

var index: usize = 5

Shape:

var binding_pattern (: Type)? = expression
const binding_pattern (: Type)? = expression

Every local declaration has an initializer expression. To request unspecified storage explicitly, use undefined with a concrete storage type:

var scratch: [128]u8 = undefined

Bare declarations without initializers are rejected:

var x: i32
var x
const x: i32
const x

var declares mutable local storage. const declares immutable local storage. This is shallow binding mutability: it controls reassignment of the binding, not ownership or every reachable referent.

const data: []f32 = buffer.slice()
data[0] = 1.0f32

The assignment to data[0] is governed by the slice type. The const binding prevents reassigning the local binding itself:

data = other.slice()

Binding patterns include ordinary identifiers, struct destructuring, and array destructuring:

const name = user.name
const { alloc, diag } = ctx
const { allocator = alloc, diag } = ctx
const { alloc, _ = diag } = ctx
var [left, right] = pair
const [x, _] = pair

See Destructuring for struct and array destructuring details.

Assignment

Assignment is a non-overloadable expression form:

place = expression

The right-hand side evaluates before the store. The result of the assignment expression is the assigned value observed through the destination place. If that result is consumed by value, ordinary copy and move rules apply.

var x: u32 = 0
const y = (x = 5)

_ = expression is the explicit discard expression, not ordinary assignment to a place. It evaluates the right-hand side, discards the produced value, and completes as void.

Assignment expressions do not chain implicitly in V1:

a = b = c

Use parentheses when the assignment result is intentionally used as another assignment's source:

a = (b = c)

Assignment is non-overloadable. Compound assignment forms such as +=, -=, *=, /=, and %= are deferred from V1; write the operation explicitly. Future compound assignment sugar and optional mutating override contracts are tracked in CEP-0055: Compound Assignment Operators:

x = x + 1

Assignments in conditions and assignment results bound to named locals are lintable because they are often accidental. Resource-owning assignments also participate in ownership/live-overwrite.

Shadowing

Local declarations may shadow earlier local bindings, including bindings from outer scopes and earlier bindings in the same block:

const value = parse(text)
const value = normalize(value)
var value = Buffer.from(value)

if condition {
  const value = value.slice()
  use(value)
}

Each declaration creates a new binding with a distinct semantic identity. Shadowing may change type and mutability. Sema resolves every name use to one binding identity; lowering may give shadowed bindings distinct internal names.

The declared name is introduced only after its initializer is resolved. In const value = normalize(value), the value inside normalize(value) resolves to the previously visible binding. After the initializer, later uses resolve to the new binding until another shadowing declaration or the end of scope. An outer shadowed binding becomes visible again after the inner scope exits.

Shadowing does not move, overwrite, dispose, or otherwise clean up the previous binding. move name and assignment to name always apply to the currently visible binding identity. If an earlier binding has been shadowed, it is no longer accessible by that name.

Shadowing a live resource before it is cleaned up is covered by cleanup lint candidates such as ownership/missing-cleanup. Shadowing a moved/uninitialized binding is allowed.

See Reference and Mutability for the detailed mutability model.