Language design · July 29, 2026

50% Declarative, 50% Imperative

Two modes of execution. Same baseline.

Describe the world

Declarative code is wonderful when the shape is the point. You say what should exist and leave the machinery of making it exist to the language. Imperative code is wonderful when the path matters: inspect something, branch on it, transform it, reject it, or make a slightly strange decision.

Range wants both, with a visible border between them.

Core/Macro/Project.range
macro project(): Construct {
    #environment {
        construct ProjectDefaults {
            #environment.system.defaults.map { default in
                let #default.identifier: #default.value
            }
        }
    }
}

The declarative half

Inside #environment, the macro describes a piece of the program’s #environment. It says that a ProjectDefaults construct exists and shows the members that belong inside it. There is no builder to push into, no syntax tree to assemble, and no sequence of mutation calls pretending to be a language.

The block reads like Range because it is Range. It is the desired program shape, written directly.

The imperative half

The contents do not have to be static. A macro can query the system, iterate over its defaults, and project each result into the declaration. The map is compile-time execution: ordinary control and data flow used to produce declarative code.

The macro’s target is also its access type. A macro targeting a Construct receives the typed declaration and members of that construct; a macro targeting another kind of syntax receives the surface appropriate to that code. The type says what written code may be queried.

  • Query the code exposed by the macro’s access type.
  • Filter, map, branch, and validate with ordinary Range.
  • Emit declarations in the same syntax programmers use everywhere else.
Complete Equatable synthesis
macro equatable(): Construct {
    let properties: [@property](
        #environment.target.Declaration.members.filter(all: @property)
    )
    let values: [@stored](
        properties.filter(all: @stored)
    )

    #environment {
        extension #environment.target.Declaration.identifier {
            function equals(_ other: Self): Bool {
                #values.map { property in
                    if self.#property.identifier != other.#property.identifier {
                        return false
                    }
                }
                return true
            }
        }
    }
}

The macro performs the complete synthesis. Its broad @property query observes stored, derived, and binding nodes, so changes to that property graph invalidate and re-identify the generated implementation. Runtime equality then narrows to @stored values, avoiding redundant derived computation or externally backed bindings. It emits an equals function and generates one short-circuiting comparison per stored value. An empty construct naturally reaches return true.

Case iterable

An Enum access type exposes its cases and each case’s associated values. A modern @caseIterable macro can validate that every case is payload-free, then project those written cases into one generated collection.

A modern CaseIterable derivation
macro caseIterable(): Enum {
    let cases: [Enum.Case](
        #environment.target.Declaration.cases
    )
    let payloadCases: [Enum.Case](
        cases.filter { item in
            return item.associatedValues.count != 0
        }
    )

    if payloadCases.count != 0 {
        #environment.error(
            "@caseIterable requires cases without associated values"
        )
    }

    #environment {
        extension #environment.target.Declaration.identifier {
            function allCases(): [Self] {
                return [
                    #cases.map { item in
                        .#item.identifier,
                    }
                ]
            }
        }
    }
}

This is also a forward-looking derivation example. The important part is that the macro receives typed enum syntax, not an unrestricted view of the whole program.

Outside the boundary, decide what should happen. Inside it, describe what should exist.

The useful middle

“Declarative or imperative” is a false choice. A fully declarative system becomes awkward the moment its vocabulary runs out. A fully imperative metaprogramming system makes simple shapes noisy and hides intent behind construction APIs.

The interesting place is fifty-fifty: declarations for the stable shape, a real programming language for the funky parts, and one explicit boundary where values become code. That is the direction of Range’s project macro. The compiler is being brought up to meet the spelling.