Zax compiler directives
| Field | Value |
|---|---|
| Status | Current conceptual design |
| Audience | Human developers reading or writing [<...>] directives, and anyone deciding whether a control belongs in a directive at all |
| Applies To | What a compiler directive is, how one is written and named, where it attaches, directive ranges, the promises a directive may carry, deprecate, source mapping, and the catalog of current directives; not a formal grammar |
| Implementation State | Not established by this repository |
| Owns | The directive mental model; directive, enclosure, and compile-time-expression families; directive grammar; official and x- names; unattached, attached, and slot attachment; directive ranges and how openings, files, inclusion, and imports bound them; hint, unchecked-promise, and semantic-control kinds; deprecate; source-mapping directives; the directive catalog |
| Does Not Own | The [< ... >] token enclosure (source structure); execution policies and compile-time code (compile-time code); individual directives’ meanings, which belong to the owners named in the catalog |
| Source / Provenance | Legacy compiler-directive input, reconciled with current source structure, Nothing policy, module, flow, safety, and compile-time design |
Telling the compiler something about your code
Some facts about code are not part of what the code computes, but the compiler still needs them: a declaration is deprecated, one branch is the common case, a function should be inlined. A compiler directive states such a fact:
[<deprecate>]
drawLineOld final : ()(from : Point, to : Point) = {
}
render final : ()() = {
if cacheHit [<likely>] {
drawFromCache()
} else {
redrawEverything()
}
}
A directive is written between [< and >]. It describes something to the
compiler: one construct, one region of declarations, or one point in the code.
It is not a value and not an expression. [<likely>] above does not compute
anything; it tells the compiler which branch to optimize for.
Some directives change what a program means, such as deprecate or the Nothing
policy. Others are only hints that a compiler may ignore. Each directive says
which it is.
Directives, enclosures, and compile-time expressions
Zax has three different source forms that can look related. Choosing between them follows what the form does:
| Form | What it does | Examples |
|---|---|---|
Directive, [<name>] |
Describes a construct, a region, or a point to the compiler | [<inline>], [<deprecate>], [<likely>] |
Source enclosure, word<category>{ ... } |
Changes how a visibly bounded piece of source is checked or authorized. It creates no scope and no value. | intent<...>{ }, unsafe<...>{ }, bare{ } |
| Keyword or compile-time expression | Computes or asks something and yields a value, or changes what a declaration means | compile, wellformed{ }, size of, outward |
So wellformed{...} is an expression, because it yields a Boolean, and
outward is a declaration keyword, because it changes where a name belongs.
Neither is a directive. Enclosure heads are single plain words; any hyphenated
name sits in the <category> slot, as in intent<same-prefix-literal-join>.
Writing a directive
A directive starts with its name, which may carry a value. Later items are named options or bare flags:
[<inline>] // name only
[<inline=always>] // name with a value
[<deprecate, context=all, error>] // name, a named option, and a flag
[<file, generator="Acme Report Generator">] // name and a named option
Every item after the first is either key=value or a flag; meaning never
depends on an item’s position in the list. Several directives may sit side by
side:
[<deprecate>][<inline>]
drawLineOld final : ()(from : Point, to : Point) = {
}
The [< and >] tokens themselves are described by
source structure.
Where a directive goes
A directive’s position decides what it applies to. There are three kinds of position:
[<never>] // unattached: alone on its line
[<deprecate>]
drawLineOld final : ()() = { // attached: the line directly below
}
if cacheHit [<likely>] { // slot: inside a construct, at a defined place
drawFromCache()
}
- Unattached. The directive is on its own line, alone or with other unattached directives, and the lines immediately before and after it hold no code. It applies to a point or begins a range.
- Attached. The directive is on the line directly above one statement or declaration, and applies to exactly that. This is the usual way to annotate a declaration: the declaration keeps its own line and indentation.
- Slot. The directive sits at a defined position inside a construct: after
an
if,else if, orelseheader for one branch, after acaseordefaulttest, or afterfinalin a function declaration.
Because a blank line separates unattached from attached, a blank line between a directive and code changes its meaning. Formatters must preserve the blank lines around directives.
Each directive accepts only certain kinds of position, listed in the catalog. Placing a directive where it is not accepted is an error, so a blank line that accidentally separates an attached-only directive from its declaration is reported rather than ignored:
[<inline>]
smallHelper final : ()() = { // error: inline is unattached here and accepts no unattached position
}
Directive ranges
deprecate, panic controls, and export can also apply to a run of
declarations. An unattached state marker sets the state from that point
onward:
| Marker | State from here on |
|---|---|
=always |
On: declarations are deprecated or exported; for panic controls, the check is enabled |
=never |
Off: declarations are not deprecated or exported; for panic controls, the check is disabled |
=inherit |
Back to the state the enclosing opening or file started with |
=default |
Back to the language’s default |
The state lasts until a later marker for the same directive changes it, or the region ends. There are no separate “end” markers: a range ends when another marker sets a different state.
MyGeometry :: type {
[<deprecate=always>]
oldArea final : ()() = {
}
oldPerimeter final : ()() = {
}
[<deprecate=never>]
area final : ()() = {
}
}
MyGeometry :: type {
[<deprecate=always>]
oldArea final : ()() = {
}
oldPerimeter final : ()() = {
}
[<deprecate=never>]
area final : ()() = {
}
}
oldArea and oldPerimeter are deprecated; area is not.
What bounds a range
A range never escapes the region it is written in:
- Openings. A namespace, type, function body, or other opening starts with the state of the code around it. A range inside it ends at its closing brace, which restores the outer state.
- Files. A range at a file’s top level ends at the end of that file. Every file starts from its module’s state, so moving or reordering files never changes what a range covers.
- Included files. A file brought in by source inclusion starts with the state in effect at the line that includes it. Placing directives before or after an inclusion controls what the included file starts with; the included file’s own ranges still end at its end.
- Imports. Directive state never crosses an
import. An imported module starts from the language’s defaults, or from a starting state its build configuration sets, and then applies its own source, never its importer’s state.
Because closing braces and file ends restore the previous state, there are no
push and pop directives.
State markers and attached forms are different
A state marker is always unattached. The attached form instead applies to one
declaration or statement and has its own spelling: [<deprecate>] or
[<export>] alone, and [<panic=yes, ...>] or [<panic=no, ...>] for one
statement’s check. Writing a state marker directly above a declaration makes it
look attached, so it is an intent error:
[<deprecate=always>]
drawLineOld final : ()() = { // intent error: a state marker placed as an attachment
}
Write the attached form, [<deprecate>], to mark one declaration, or put blank
lines around the state marker.
Only deprecate, panic controls, and export have ranges. Other directives,
such as the Nothing policy, attach
to one opening and deliberately have no “from here onward” form.
What a directive may promise
Every directive is one of three kinds:
| Kind | What it does | Examples |
|---|---|---|
| Hint | Suggests an optimization. A compiler may ignore it, and the program’s meaning never changes. | inline levels, likely, unlikely |
| Unchecked promise | States a fact the compiler may rely on. Where a check exists, it is on by default; with the check disabled, breaking the promise is undefined behavior. | always, never, void, disabling a panic category |
| Semantic control | Changes what the program means | execute, deprecate, export, the Nothing policy, layout |
An unchecked-promise directive is itself the visible marker of the promise. It
needs no separate unsafe enclosure. Whether a check exists is controlled by
its panic category, never by a debug or release mode.
Directive names
Directives defined by the language have plain names such as inline. Names
starting with x- are unofficial:
run final [<x-acme-hot-path>] : ()() = {
}
- An unknown plain name is an error, so a misspelled
[<inlien>]cannot silently do nothing. - An unknown
x-name may be ignored, optionally with a warning. - Compilers are recommended to put their own name after
x-, as inx-acme-, unless several vendors deliberately share a directive. - A vendor directive may refine meaning but must never change it. A change of meaning needs a new name, with the old one deprecated. Vendors who share directives are recommended to keep a registry among themselves. The language cannot enforce this.
Deprecating declarations
deprecate marks declarations that callers should stop using. Using a
deprecated declaration produces a deprecation warning:
[<deprecate>]
drawLineOld final : ()(from : Point, to : Point) = {
}
drawLineOld(start, finish) // warning: drawLineOld is deprecated
Attached to a type or namespace, it covers everything declared inside. As a range, it covers the declarations between its markers.
Options refine where and how the warning appears:
| Option | Meaning |
|---|---|
context=import (default) |
Warn for uses from modules that import this one |
context=all |
Warn for every use, including this module’s own |
context=local |
Warn only for uses inside this module |
error |
Make the warning an error |
min="x.y" |
Warn when an importer requests a version below x.y |
max="x.y" |
Warn when an importer requests a version above x.y |
[<deprecate, context=all, error>]
legacyChecksum final : (result : U32)(data : Byte[] readonly &) = {
}
[<deprecate, max="1.1">]
classicRenderer final : ()() = {
}
min and max compare against the version an importer requests. Import
versioning itself belongs to future build and dependency design, which may
refine these two options. context=local cannot be combined with min or
max, because those concern importers.
A standalone [<deprecate>] with nothing attached is an intent error. A
deprecation inside a wellformed{...}
check must be explicitly allowed or rejected.
Source mapping for generated code
Tools that generate Zax from other sources want diagnostics to point at the original input, not at the generated file. Source-mapping directives say where the following source came from:
[<file="inventory.csv">]
[<file, generator="Acme Inventory Generator">]
[<line=21>]
apple : Integer // reported as inventory.csv line 21
banana : Float // line 22
[<line=400, increment=0>]
donut : Float // line 400
eclair : String // still line 400
filenames the original input, andgeneratornames the tool.linesets the reported line of the first source line after it that holds code.incrementsays how far the reported line advances per physical line;0keeps reporting the same original line.[<module, location="https://example.com/inventory">]sets the reported location of a generated module.
These are unattached and positional by nature, and that is safe because they
change only what diagnostics and reflection report, never what the program
does. Reading the same information back is done with
Compilation queries, not
directives.
Directive catalog
| Directive | Kind | Positions | Defined by |
|---|---|---|---|
execute |
Semantic control | Slot after final |
Compile-time code |
inline |
Hint | Slot after final |
Function invocation |
likely, unlikely |
Hint | if/else branch slot, case/default slot |
Core flow control, switch |
always, never |
Unchecked promise | if/else branch slot, case slot; never also unattached |
Core flow control, switch |
deprecate |
Semantic control | Attached; range | This page |
export |
Semantic control | Attached; range | Namespaces and modules |
Panic controls (panic) |
Unchecked promise when disabling | Attached; range | Safety and analysis |
nothing-instance-default |
Semantic control | Attached to a module or namespace opening | Nothing instances |
align, reserve, void |
Semantic control; void is also an unchecked promise |
Attached to a member; reserve unattached in a type body |
Type definitions |
reorder |
Semantic control | Attached to a type definition | Type definitions |
file, line, module |
Source mapping | Unattached | This page |
Diagnostics
Diagnostics should distinguish:
- an unknown plain directive name;
- a directive in a position it does not accept;
- a state marker placed as an attachment, or a standalone directive that needs something to attach to;
- a state marker that sets the state already in effect, which is redundant;
- incompatible options in one directive, such as
context=localwithmin; and - a deprecated use, naming the directive that deprecated it.
Source stability
The following changes are source- or behavior-visible:
- adding or removing a blank line between a directive and code changes whether it is attached;
- moving a declaration into or out of a range changes whether it is deprecated or exported;
- moving source inclusion lines relative to directives changes what an included file starts with; and
- deprecating a declaration adds warnings for its users, and errors where
erroris given or where an undecidedwellformedcheck uses it.
Moving whole files, or reordering a module’s files, does not change what any range covers.
Boundaries and maturity
This document is current conceptual design, not a formal grammar.
Future work owns:
- exact
export, selective exposure, alias export, and re-export option sets; - panic category names and their registry;
- source generation directives;
- asynchronous-execution, ABI, and
once-value directives, together with the features they describe; - import versioning used by
deprecateminandmax; and - exact diagnostic identifiers and presentation.
For Zax’s accepted foundational direction, see the language vision.