Zax compile-time code
| Field | Value |
|---|---|
| Status | Current conceptual design |
| Audience | Human developers who want the compiler to compute values, choose code, or check code while compiling |
| Applies To | Compile-time execution of ordinary Zax code, compile-time values, execution policies, environments during compilation, values crossing into the program, compile-time selection, well-formedness checks, and compilation information; not a formal grammar, a compiler architecture, or a sandbox specification |
| Implementation State | Not established by this repository |
| Owns | The two-program mental model; compile, compile{...}, and runtime; when compile points run; what counts as a compile-time value; [<execute=...>] policies, including function values and replacement compatibility; which environment’s definitions apply during compilation; conversion of values crossing into the program; compile-time state, ordering, and the compile-time ___; if compile and switch compile; wellformed{...}; the Compilation namespace |
| Does Not Own | Directive syntax and attachment (compiler directives); outward declarations (declarations and bindings); environment-relative scalar names (integers, endianness); literal declarations (literal source and operators); hook points (partials); the runtime execution context (execution context); generics |
| Source / Provenance | Legacy compiler-directive, meta-function, and compile-time execution input, reconciled with current literal, scalar, environment, partial, module, and invocation design |
Work the compiler can do for you
Some values are known before the program ever runs. Zax lets you compute them with ordinary Zax code while compiling:
cubed final : (result : Integer)(value : Integer) = {
return value * value * value
}
myTableSize := compile cubed(4)
// The compiler runs cubed(4) while compiling.
// The program contains the constant 64; no call to cubed happens at runtime.
compile asks for the expression after it to be evaluated during compilation.
The function is ordinary Zax. There is no separate macro language and no
textual preprocessing: the same cubed can also be called at runtime.
The rest of this page builds on four ideas:
- Compile-time code is ordinary code. Anything you can write, the compiler can run, as long as it does not need values that exist only at runtime.
- Your source is built twice. One build is the program you ship. The other runs on the build machine while compiling.
- Only plain values cross into the program. Compile-time work affects the shipped program through the constants it produces.
- Timing never changes meaning by accident. Code moves to compile time only when you or the called function ask for it, or when nothing could observe the difference.
Two programs from one source
When Zax compiles your source, it effectively builds two programs from it:
- the target program, the program you ship and run; and
- the host world, the same code compiled for the build machine, which runs pieces of itself while the compiler works.
The two are separate. Each has its own copy of every global variable, its own
___ execution context, and its own memory. Code that
runs at compile time runs in the host world, so its effects happen on the build
machine, during the build:
myBuildCount : Integer = 0
compile{
++myBuildCount // the host world's copy becomes 1
writeBuildLog("compiling renderer") // written on the build machine, during the build
}
main final : ()() = {
print(myBuildCount) // prints 0: the shipped program has its own copy
}
Effects on the build machine are legitimate. Compile-time code may read files, build lookup tables, or write logs. They are simply a different program’s effects from the ones the shipped program performs when it runs.
Asking for compile time
compile on an expression
compile covers the rest of the complete expression it starts. It stops at an
enclosing closing parenthesis, a comma, the end of the statement, or, in
if compile and switch compile, the opening brace of the body:
myLimit := compile cubed(4) + 10 // cubed(4) + 10 is computed while compiling
log(compile cubed(3), userCount) // stops at the comma: only cubed(3)
myTotal := (compile cubed(4)) + offset // parentheses narrow it to cubed(4)
Because compile reaches as far as it can, a runtime value caught inside it is
an error rather than a silent split between compile time and runtime:
userValue := readNumber()
myScaled := compile cubed(4) + userValue // error: userValue exists only at runtime
The result keeps the type the expression has. compile changes when the work
happens, not what it produces. See
integer literals
for how concrete results keep their types.
compile{...} for statements
To run statements at compile time, use the block form. Its brace is attached:
compile{ with no space.
compile{
loadPalette("palette.png") // fills a host-world global
}
myBackground := compile paletteEntry(0) // a later compile expression reads that global
compile{...} produces no value. From the target program’s point of view,
nothing happens where it is written. Its braces form an ordinary block, so
declarations inside it are local to it. Its work reaches the program only
through later compile expressions that read the host world’s globals.
Nesting
A compile inside an expression that is already compile time changes nothing.
It is legal; a lint may point out that it is redundant:
myResult := compile cubed(2) * compile cubed(3) // the inner compile is redundant
The opposite direction is not possible: compile-time code cannot ask for runtime values, as described next.
When compile points run
A compile expression or block is a compile point. Each one runs when the
compiler compiles the place where it is written:
- Once, not per execution. A compile point inside a loop runs once, while that loop is compiled.
- Whether or not runtime reaches it. A compile point in a function that nothing calls still runs, and if it fails, the build fails. Mistakes in unused code stay visible.
- Never by running the program. The host world does not start
mainand collect whatever compile points it happens to reach.
retry final : ()() = {
attempt := 0
while attempt < 3 {
myDelay := compile cubed(5) // runs once, during compilation, not three times
sleepFor(myDelay)
++attempt
}
}
Names inside a compile point refer to the host world. A global named there is the host world’s copy. A local runtime variable cannot be used at all, because its value does not exist yet:
handleRequest final : ()() = {
request := readRequest()
mySize := compile measure(request) // error: request is a runtime value
}
There are two exceptions to “every compile point runs”: code that
compile-time selection discards, and code
inside a wellformed{...}
check, which is checked but never run.
Compile-time values
Several features need to know whether a value is available at compile time. The rule is the same for all of them. A compile-time value is:
- a literal;
- an expression built only from compile-time values;
- the result of a compile point;
- the result of calling a function whose
execution policy is
compile, orfoldingwith every argument a compile-time value; - a
Compilationquery,size of, or another compile-time query; or - a
finalbinding, local or global, initialized from a compile-time value.
Inside compile-time code, every global is the host world’s copy, so reading any global there gives a compile-time value.
main final : ()() = {
myColumns final := 16 // a compile-time value: final, with a constant initializer
myRows := 16 // a runtime variable: it may be reassigned later
if compile myColumns > 8 { // fine
useWideLayout()
}
if compile myRows > 8 { // error: myRows is a varying runtime variable
useWideLayout()
}
}
A varying local is a runtime variable even when its initializer is a constant, because later code may assign to it.
The is constant query asks this question directly: myColumns is constant is
true here, and myRows is constant is false. It answers by exactly these
rules, never by whether the optimizer happened to pre-compute something.
Letting a function decide: execution policies
A caller can always write compile. A function’s author can also declare when
the function runs, with an execution policy written after the declaration’s
name-side words, such as final:
hashName final [<execute=folding>] : (result : U64)(name : String readonly &) = {
}
buildSineTable final [<execute=compile>] : (result : Integer[])(size : Integer) = {
}
readPort final [<execute=runtime>] : (result : Byte)(port : U16) = {
}
There are four policies:
| Policy | When the function runs | Is the result a compile-time value? |
|---|---|---|
auto (the default) |
At runtime, unless a caller writes compile. The compiler may still pre-compute work whose difference nothing could observe. |
No |
runtime |
Never at compile time. | No |
folding |
At compile time whenever every argument is a compile-time value, otherwise at runtime. | Yes, when it runs at compile time |
compile |
Always at compile time. It has no runtime form at all. | Yes |
main final : ()() = {
myKey := hashName("player") // folding, constant argument: computed while compiling
userKey := hashName(readLine()) // folding, runtime argument: computed at runtime
mySine := buildSineTable(256) // compile: always while compiling
myTotal := sumTo(1000) // auto: an ordinary runtime call
myPort := compile readPort(1) // error: readPort is runtime-only
}
auto stays invisible
An observable effect is anything visible outside the computation itself,
such as output, file or device access, the clock, or state that outlives the
call. Under auto, the compiler may pre-compute work that has no observable
effect, as an optimization. Because that choice is invisible:
- an
autoresult is never a compile-time value, sois constant, compile-time selection, and other compile-time rules never depend on optimizer choices; and - a failure in pre-computed
autowork is still a runtime failure. The compiler may warn, but it does not reject the program:
myRatio := divide(10, 0) // auto: still a runtime panic, even if pre-computed
Under folding and compile, a panic during compile-time execution is a
compiler diagnostic instead, naming the operation and where it happened.
Why folding is not the default
A function with no parameters always has “every argument constant”. If
folding were the default, main(), now(), and readConfig() would all run
while compiling and bake build-machine results into the program. folding is
a promise a function’s author makes deliberately, typically for pure
computations like hashing.
Writes under folding, and the runtime keyword
A folding function may update global state, such as a cache, a lock, or a
counter. Which world’s copy it updates follows from where the call runs:
myCallCount : Integer = 0
record final [<execute=folding>] : ()(value : Integer) = {
++myCallCount
}
record(3) // folds: updates the host world's myCallCount
record(userValue) // runs at runtime: updates the program's myCallCount
This is predictable, but it acts at a distance:
myLimit final := 64
record(myLimit) // folds, because myLimit is a compile-time value
If someone later makes myLimit configurable at runtime, that call’s write
silently moves to the running program. A lint may flag folding functions that
write globals.
When a call must run in the target program, write runtime. Like compile,
it covers the rest of the expression:
runtime record(3) // runs in the program, even with a constant argument
myTotal := runtime hashName("a") + hashName("b") // both hashes run at runtime
compile may appear inside runtime to pre-compute one piece. runtime may
not appear inside compile-time code, and it cannot be applied to a compile
function, which has no runtime form:
runtime log(compile cubed(3)) // fine: only cubed(3) is pre-computed
myValue := compile check(runtime readLine()) // error: compile-time code cannot reach runtime
runtime buildSineTable(256) // error: buildSineTable exists only at compile time
runtime is a keyword only where an expression follows it, so a variable
named runtime remains possible.
Call chains are checked
A compile-time request covers everything the call reaches. If anything along
the way is runtime-only, the compiler reports the whole chain:
error: cannot run at compile time
makeTable() calls checksum() calls readPort(), which is runtime-only
Function values and replaceable implementations
A function’s policy is part of its visible prototype. That matters when functions are stored and called through values:
- A function value known at compile time, such as a named
finalfunction, keeps its policy when called through the value. - A call through a runtime slot is a runtime call.
- A
compilefunction has no runtime form, so it has no runtime function value. Taking one is legal only inside compile-time code. - A lambda uses the same policy slot and defaults to
auto. A lambda created at compile time can cross into the program only if it captures nothing.
A replaceable function slot (a non-final function declaration) has a policy
too, written after its name. Anything installed in it must be able to run
everywhere the slot can be called from:
| Slot policy | Installable functions |
|---|---|
auto or runtime |
auto, runtime, folding |
folding |
folding |
compile |
compile, folding, auto |
readSensorScaled final [<execute=runtime>] : (result : Integer)(value : Integer) = {
return readSensor() * value
}
myHandler : (result : Integer)(value : Integer) = cubed // an auto slot
myHandler = readSensorScaled // fine: runtime-only works in an auto slot
myBuilder [<execute=compile>] : (result : Integer)(value : Integer) = cubed
myBuilder = readSensorScaled // error: cannot run at compile time
A call through a compile slot always runs at compile time and gives a
compile-time value, whichever function is installed.
Complete function-value and lambda behavior is defined by function invocation and lambdas and callable composition.
Which definitions apply while compiling
Some scalar types have environment-relative definitions. Integer, for
example, may be 64 bits on the build machine and 32 bits on the target, and
byte order may differ too. The active environment is the one whose
definitions apply to the code being run:
- In the target program, the active environment is the target.
- In compile-time code, the active environment is the compiler host, the machine running the compiler.
So compile-time code uses the build machine’s definitions unless it asks for the target’s. Ask by naming the environment explicitly:
myHostWidth := compile size of Integer // the build machine's Integer
myTargetWidth := compile size of Target.Integer // the target's Integer
myHostWord := compile size of CompilerHost.Integer // the build machine's, explicitly
Code that computes facts about the target, such as table layouts, should name the target’s types. The environment-relative routes are defined by integers and endianness.
Compile-time code may use the build machine’s hardware, but hardware must never change a result. Running an operation in hardware or in software must give the same value.
Values crossing into the program
A compile-time result crosses into the target program as a constant. Values cross by copy: an array or string crosses as its contents, which the program holds as constant data. Pointers, references, and lambdas that capture state never cross, because they would point into the build machine’s memory.
A crossing value keeps its type identity. If that type’s definition differs between the two environments, the value is converted to the target’s definition, without silently losing data:
myCount : Integer = compile countEntries()
// Integer means the same type in both worlds, but the build machine's Integer
// may be wider. The value is checked against the target's Integer range.
| Value | What happens when it crosses |
|---|---|
| Integers | Checked against the target definition’s range. A value out of range is a compiler error. |
| Reals out of range | A value beyond the target format’s finite range is a compiler error. |
| Reals in range | Rounded correctly to the nearest value in the target format. |
| Special real values | NaN payloads, signalling NaN, and negative zero follow the target format’s rules. A payload the target cannot represent is a compiler error. |
| Byte order | Converted. It changes representation, not value. |
| Composite values | Converted member by member. |
| Values whose type’s target layout is still pending | Cannot cross until that layout is final. See compile-time state. |
Rounding a real is not a warning by default, because nearly every narrowing of a
real rounds. It does have a consequence worth knowing. A value computed at
compile time with the build machine’s wider Float, then rounded once, can
differ in its last bits from the same computation done at runtime in the
target’s Float:
myRadius final := 2.5
myArea : Float = compile computeArea(myRadius) // build machine's Float, then rounded
myOtherArea : Float = computeArea(myRadius) // target Float throughout; may differ slightly
More generally, a compile-time result can depend on the build machine whenever
the code uses an environment-relative type whose definition differs between
the two environments. A smaller build machine may overflow where the target
would not; that overflow is a compile-time panic and becomes a compiler
diagnostic, never a wrong value. To make results independent of the build
machine, compute with exact types such as U32, or with explicit target types
such as Target.Integer, whose definitions are the same in both worlds:
countEntries final : (result : Target.Integer)() = {
total : Target.Integer = 0 // the target's Integer, even while compiling
// ...
return total
}
myCount := compile countEntries() // identical on every build machine
The host world’s state
One host world per module instance
Each module instance has its own host world. Its globals persist from one compile-time evaluation to the next, in source order. A compile-time call into an imported module uses that module instance’s host world.
A global is initialized in the host world the first time compile-time code reaches it. Its own initializer runs first, and may reach other globals in turn. A cycle is a diagnostic:
myTableSize : Integer = 256
myTable : Integer[] = buildSineTable(myTableSize)
myChecksum := compile sum(myTable)
// The host world initializes myTableSize, then myTable, then runs sum.
The target program initializes its globals in source order, so the two worlds may initialize in different orders. That only matters when initializers depend on each other’s effects.
Compile-time writes and order
Any compile-time code may write host-world globals, including literal bodies
and folding calls. Because evaluations happen in source order, the results are
defined:
mySeed : U64 = 0x9E3779B9
MyRandom :: type {
operator literal 'rand' final : (result : U64)(
payload : String immutable readonly final &
) unbound = {
mySeed = mix(mySeed, payload) // a host-world write from a literal body
return mySeed
}
}
first := MyRandom.rand'x'
second := MyRandom.rand'x' // differs from first; swapping the two lines swaps the values
Order has practical costs:
- changing an early evaluation that touches shared state forces every later one that depends on that state to be recomputed;
- moving code between files of a module can reorder those evaluations; and
- a compiler may evaluate in parallel, but must evaluate in order wherever evaluations share host-world state.
The compile-time ___
The host world has its own execution context. Modules
can add members to ExecutionContext through partials, and the target’s
context shape is fixed only when the whole program is assembled. Compile-time
code cannot wait for that, so the host world’s context is fixed at each compile
point instead. It contains:
- the language’s core members;
- partials from completed imports; and
- this module’s partials completed before the compile point, in source order.
Between compile points the shape only grows. Existing members keep their values, and new members start at their declared defaults:
compile{
warmCaches() // the host ___ has only the core members here
}
MyTracking :: partial ExecutionContext {
requestId : U64 = 0
}
compile{
Module.MyTracking :: expose partial
++___.requestId // the shape now includes requestId, which started at 0
}
A compile point can use a partial’s members only after granting it, as for any partial. Granting a partial whose declaration comes after the compile point is an error at that point.
Reflection describes the same ExecutionContext identity in both worlds. In
compile-time code, plain reflection describes the host world’s current shape.
A question about the target’s final shape waits until that shape is fixed, or
is rejected before then. The same applies to size of on any type whose target
layout is still pending, as described by
partials.
Choosing code at compile time
if compile and switch compile
Writing compile on a condition makes the if a compile-time selection. Only
the selected branch becomes part of the program:
if compile size of Target.Integer > 4 {
useWideTables()
} else {
useNarrowTables()
}
The same works for switch:
myRenderer final := 2
switch compile myRenderer {
case 1
useSoftwareRenderer()
case 2
useHardwareRenderer()
}
The condition or switch value must be a compile-time value, and case tests are
compile-time values too. If the selected body enters another case’s body with
goto, that body is kept as well.
An ordinary if whose condition happens to be constant is not a compile-time
selection. Both of its branches must still be valid code.
Discarded branches
An unselected branch is discarded. It must contain correctly formed tokens with balanced delimiters, but nothing in it is parsed further, resolved, or layout-checked. It may therefore name things that do not exist for this target:
if compile Compilation.target.hasFeature("avx512") {
useWideRegisters(zmm0) // never resolved for targets without the feature
} else {
useScalarPath()
}
Two consequences follow:
- a mistake in a rarely selected branch is found only when some target selects it, which an optional lint can help with; and
- formatters treat a discarded branch as opaque text.
Selecting declarations
if compile may also appear where an ordinary if cannot, such as in a type
body or namespace body. Its branches still form scopes. To make a declaration
visible after the construct, mark it
outward:
if compile size of Target.Integer > 4 {
MyIndex outward :: alias type U64
} else {
MyIndex outward :: alias type U32
}
myPosition : MyIndex // valid whichever branch was selected
Code that uses a declaration found in only one branch must itself be selected,
or go through a name every branch provides, like MyIndex above.
Checking whether code is well formed: wellformed{...}
Sometimes the question is not “what is this value?” but “would this code
compile here?”. wellformed{...} answers with a compile-time Boolean:
myValue := readSensor()
if wellformed{ ++myValue; myValue *= 3 } {
scaleUp(myValue) // the payload was checked, never run: myValue is unchanged
}
The payload is checked as if it were the body of a function written at that point, able to see the surrounding names. It is never executed. Names it uses have types but no values, and its declarations never escape.
A wellformed answer is a compile-time value, so it can drive
compile-time selection. A typical use is a
fallback when an optional facility is absent:
if compile !wellformed{ Module.UseComplexMath } {
UseComplexMath outward :: import Module.DefaultComplexMath
}
wellformed is a keyword only with its attached brace, wellformed{, so a
variable named wellformed remains possible.
How deep the check goes
The payload is checked completely against everything it can see: names, operator and overload selection, conversions, qualifiers, and the prototypes of the functions it calls. Code that already exists is trusted to be checked on its own:
increment final : ()(value : MyCounter writable &) = {
value.cuont += 1 // a typo: reported as an error in increment itself
}
ok := wellformed{ increment(myCounter) } // true: the call matches the prototype
Code the payload itself creates is checked in full. That includes a generic instantiation the payload causes; the details belong to future generic design.
Warnings and intent
A payload may compile but produce a language-defined warning, such as a
deprecation. wellformed does not guess whether that counts. If a warning
occurs and nothing says how to treat it, the wellformed itself is an error.
Options in angle brackets decide, one category at a time:
if wellformed{ fastPath(myData) } { ... } // error if fastPath is deprecated
if wellformed<allow-deprecated>{ fastPath(myData) } { ... } // deprecation counts as well formed
if wellformed<reject-deprecated>{ fastPath(myData) } { ... }// deprecation counts as failure
Without this rule, a library that merely deprecates fastPath would silently
switch every such caller onto its fallback branch. Lints never affect
wellformed; only warnings the language defines do. Deprecation is the only
such category today.
An intent error is an error, so a payload needing an acknowledgement must contain it:
ok := wellformed{ first <+> second } // false if this join needs acknowledgement
ok := wellformed{ intent<same-prefix-literal-join>{ first <+> second } } // checks the acknowledged form
Seeing why a check failed
A false hides its reason. explain keeps the result but reports the
payload’s diagnostics as compiler notes:
if wellformed<explain>{ fastPath(myData) } {
// note: fastPath expects MyBuffer readonly &, found MyBuffer
fastPath(myData)
}
Options combine: wellformed<reject-deprecated, explain>{...}. A compiler
option may also explain every wellformed failure without source edits.
Checking has no effects
Checking a payload changes nothing: no host-world state, no declarations, and no later results. A compile point inside a payload is checked but not run.
To require that some code compiles, assert it at compile time. A failed compile-time assertion is a compiler diagnostic:
compile{
assert(wellformed{ ++mySample })
}
Compilation information
The language-provided Compilation namespace answers questions about the build.
Like Integer and Scalars, it is protected: no declaration can shadow or
replace it. Every answer is a compile-time value, so no compile keyword is
needed:
myVersion := Compilation.compiler.version.major
mySse := Compilation.target.hasFeature("sse")
myProfile := Compilation.target.setting("PROFILE") // build configuration for the target
myHere := Compilation.here // this source location
| Member | Type | Meaning |
|---|---|---|
here |
SourceLocation |
Where the query is written |
caller |
SourceLocation |
The call site, in a default argument only |
compiler |
CompilerInfo |
The compiler’s name, vendor, and version |
target |
Environment |
Facts about the target environment |
compilerHost |
Environment |
Facts about the build machine |
build |
BuildInfo |
Facts about this build |
module |
ModuleInfo |
Facts about the current module instance |
file |
FileInfo |
Facts about the current source file |
| Type | Members |
|---|---|
SourceLocation |
file, line, column, function, module |
Version |
major, minor, patch, preRelease, build, text |
Environment |
name, architecture, os, endianness, hasFeature(name), setting(name) |
BuildInfo |
configuration, time (unix, nt, text), variable(name) |
ModuleInfo |
name, location, path, importVersion, hash, git (tag, branch, commit) |
FileInfo |
path (relative to the module root), name, hash, generator, modified |
setting(name) reads a value from the build configuration. variable(name)
reads an operating-system environment variable on the build machine. Some
members may be absent, such as setting, variable, importVersion, git,
and generator; their results are optional values.
The target machine’s own environment variables are not here. They exist only when the program runs, so they come from an ordinary runtime library call.
Some members make a build depend on when or where it runs: build.time,
build.variable, module.path, module.git, and file.modified. Build
configuration can pin their values when builds must be reproducible.
The caller’s location
A logging or tracing helper usually wants the location of its caller, not its
own. Compilation.caller provides it, and is valid only as a default argument,
where it reports the call site:
trace final : ()(where : SourceLocation = Compilation.caller) = {
print(where.file.path, where.line)
}
trace() // reports this line
Everywhere else, use Compilation.here.
Costs
Programmers must be able to discover:
- compile-time work performed by compile points,
foldingcalls,compilefunctions, and literal bodies, which can make builds slow; - compile-time memory and host-world state;
- constants and tables added to the program by compile-time results;
autopre-computation, which may grow or shrink the program but never changes its meaning; and- evaluations that must run in order because they share host-world state.
The language sets no time limit on compile-time work. A compiler stays silent while evaluation is quick, reports progress naming the function once a compile-time evaluation runs long, and leaves the decision to stop to the build. A stopped build shows the compile-time call stack.
Diagnostics
Diagnostics should distinguish:
- a runtime value used in compile-time code, naming the value;
- a compile-time request that reaches a
runtime-only function, naming the call chain; runtimeinside compile-time code, or applied to acompilefunction;- a compile-time panic, with the operation, location, operands, and compile-time call path;
- a crossing value out of the target’s range, or not representable in its format;
- a pending target layout needed by a crossing value or query;
- a global initialization cycle in the host world;
- a partial granted at a compile point before its declaration;
- an
if compileorswitch compilewhose condition is not a compile-time value; - a
wellformedwarning left undecided; and Compilation.calleroutside a default argument.
A redundant nested compile is a lint, not an error.
Source stability
The following changes are source- or behavior-visible:
- changing a function’s execution policy changes where its calls run and whether their results are compile-time values;
- making a value runtime-configurable can move
foldingcalls, and their writes, from compile time to runtime; - changing the order of compile-time evaluations that share host-world state changes their results;
- adding or removing a declaration can change a
wellformedanswer and the branch it selects; - deprecating a declaration makes undecided
wellformedchecks that use it into errors; and - changing a type from environment-relative to exact, or the reverse, changes whether compile-time results depend on the build machine.
Boundaries and maturity
This document is current conceptual design, not a formal grammar, a sandbox specification, or a compiler architecture.
Future work owns:
- source generation at compile time;
- compile-time parameters, meaning arguments whose values a function body uses while compiling, together with the rest of generics;
- a
purepromise and any broader effect tracking; - the complete reflection surface;
- the exact spelling for asking whether a hook point has fulfillments;
- complete member types and spellings in
Compilation; and - exact diagnostic identifiers and presentation.
For Zax’s accepted foundational direction, see the language vision.