Zax iteration
| Field | Value |
|---|---|
| Status | Current conceptual design |
| Audience | Human developers traversing arrays, enums, structured values, ranges, views, or generated sequences |
| Applies To | Programmer-facing each in and each from behavior; not a formal grammar, collection contract, or implementation specification |
| Implementation State | Not established by this repository |
| Owns | The each mental model; direct and cursor-driven traversal; header phases and entry bindings; positional and named roles; compiler-known array, enum, and structural traversal; cursor acquisition, value production, progression, erasure, direct re-entry to a live current entry, and active-each access; traversal costs, diagnostics, mutation responsibilities, and source stability |
| Does Not Own | Shared flow-transfer and unwinding rules (core flow control); general token and layout rules (source structure); collection-specific place stability (lifetimes and references); complete reflection or type-declaration traversal; generic constraint syntax; or compiler lowering |
| Source / Provenance | Legacy flow control together with current array, enum, flow, declaration, qualifier, lifetime, safety, operator, and transfer designs |
Start with one entry at a time
each runs one body for every entry supplied by a traversal source:
values : String[3]
values[0] = "bird"
values[1] = "plane"
values[2] = "superman"
each value : in values {
print(value)
}
This visits the array elements in increasing index order. The omitted binding
type gives value a reference to each element place; it does not silently copy
every string.
Zax has two traversal mechanisms:
each value : in values {
// Direct traversal recognized by the language.
}
each value : from values {
// Traversal through a first-class cursor.
}
inknows the source’s structure directly. Its source families are intrinsic arrays and slices, enum declarations, and stored instance members.fromobtains a cursor and uses its declared progression and value operations.
Arrays may support both. The similar loop spelling does not make their mechanisms or costs identical.
Header and body
The traversal clause is required. An initializer and post operation are independently optional:
each traversal-clause body
each initializer ;;
traversal-clause body
each initializer ;;
traversal-clause ;;
post body
each ;;
traversal-clause ;;
post body
The initializer, post, and body each accept one effective statement.
each count := 0;
values := loadValues() ;;
value : in values ;;
++count {
consume(value)
}
count and values are established once. The traversal binding is established
separately for each entry. Both the initializer bindings and current traversal
binding are visible to the post and body.
An initializer block retains its own nested scope:
each count := 0;
values := loadValues();
{ reset := findReset(values); applyReset(values, reset) } ;;
value : in values ;;
++count {
use(count, values, value)
// reset = replacement // error: reset belonged to the completed block
}
The body is required. As with other flow statements, it may be one simple, composed, or braced effective statement.
Progression and transfers
For one current entry:
- establish fresh entry bindings;
- execute the body;
- on fallthrough or
next, execute post while those bindings remain live; - end the entry bindings;
- perform the source’s progression transition; and
- establish bindings for the successor or complete on exhaustion.
continue skips post but still progresses. goto skips both and re-enters the
current entry body directly. break skips post and exits.
each value : in values ;; record(value) {
if shouldSkipPost(value)
continue
if shouldStop(value)
break
if bodyWorkIsDone(value)
next
}
All four transfers use the shared target, label, and unwinding rules from core flow control.
each outer: value : in values {
while needsWork(value) {
if skipOuterEntry(value)
continue outer:
}
}
goto and the current entry
Bare goto targets the nearest eligible unlabeled active each. An explicit
goto each_label: selects a labeled active each. Either form enters the body
again without running post, advancing the source, or establishing another entry:
each repeat_entry: value : in values ;; record(value) {
use(value)
if needsAnotherPass(value)
goto repeat_entry:
}
The current entry binding and cursor access must still be live. Body locals from the source entry are destroyed before the target body begins with a fresh body scope. The traversal binding remains the same current entry.
This direct entry may intentionally repeat forever. It does not request another
cursor value and does not imply progression. Shared active-body goto behavior
is defined by
core flow control.
The label attaches to the each statement as outer:. A traversal declaration
uses a spaced colon as value :; value: would instead present label intent
and is invalid as a binding.
Body-local values are destroyed before post or transfer. Entry bindings end
before progression. Header bindings and statement-owned cursor state are
destroyed when the complete each exits.
Binding entries
Positional bindings
Each source family defines an ordered role list:
each value :, memberName : in MyEnum {
print(value, memberName)
}
A declaration consumes the next role. # consumes and discards one role:
each #, memberName : in MyEnum {
print(memberName)
}
each # in MyEnum {
recordOneDeclaration()
}
Named bindings
Parentheses select a wholly named binding form:
each (name:) in MyEnum {
print(name)
}
each (
name: memberName :,
value: memberValue :
) in MyEnum {
print(memberName, memberValue)
}
Inside the parentheses:
- every entry selects a role by name;
name:selectsnameand introduces the same local spelling;name: memberName :selectsnameand introducesmemberName;- roles may appear in any order;
- an omitted role creates no binding;
- a role may be selected at most once; and
- positional declarations and
#do not mix with the named form.
A flow label remains outside the binding packet:
each outer: (name: memberName :) in MyEnum {
}
Direct in traversal
each in recognizes a closed set of language-defined source families. It
creates no first-class cursor and does not search for methods whose names happen
to resemble a traversal protocol.
Every direct source is finite. Its contract defines entry order, binding roles, value or place behavior, and exhaustion.
Intrinsic arrays and slices
A fixed array, resizable array, or slice gives the loop one element on each pass. By default the loop accesses that element directly:
each element : in values {
inspect(element)
}
The programmer may state a compatible reference explicitly:
each element : Integer readonly & in values {
inspect(element)
}
An explicit non-reference type constructs a separate value:
each element : Integer in values {
keep(element)
}
That construction follows ordinary transfer and construction rules. It may call programmer code, share storage, allocate, or be unavailable.
The source expression is evaluated once. Elements are visited in increasing index order. Each pass establishes a fresh binding; a reference is not rebound from one element place to another.
A fixed array’s element places remain stable for its life path. A resizable array or slice uses the place-stability and invalidation contract defined by Zax arrays and slices. Writable element access is available only when the source grants it.
Element mutation that preserves the current place may remain valid. Structural
mutation invalidates direct traversal rather than silently changing the
remaining entry set. Use cursor-driven each from when controlled erasure or
specialized progression is required.
Enum declarations
An enum declaration supplies:
value, name, [future reflection]
The current value and name roles visit every declaration in source order,
including duplicate-valued aliases:
Box :: enum U8 {
RedBox = 1
Redbox = 2
GreenBox = 5
Greenbox = 5
BlueBox = 6
Bluebox = 7
}
each value : in Box {
print(value as underlying value)
}
// 1, 2, 5, 5, 6, 7
Names are available without requiring general reflection:
each value :, memberName : in Box {
print(value as underlying value, memberName)
}
each (name: memberName :) in Box {
print(memberName)
}
An inferred name is a compiler-provided
String readonly immutable final &. An explicit String requests a copy:
each (name: memberName : String) in Box {
keep(memberName)
}
The future reflection role follows value and name, but no reflection
binding, type, or source is currently defined.
each provides no special case-insensitive match traversal. Use the generated
singular enum lookup for ordinary lookup, or walk declarations and apply custom
matching and deduplication when an application needs several results.
Stored instance members
Traversal over a structured value visits:
accessible stored instance member places of the source’s static type, in declaration order.
MyRecord :: type {
count : Integer
ratio : Float
name : String
}
record : MyRecord
each value : in record {
print(value)
}
The compiler checks the binding, body, and post separately using every selected member’s concrete type. Runtime body executions occur in declaration order. This is an observable typing rule, not a required code-generation strategy.
The value role is heterogeneous and does not accept a concrete type name:
each value : in record {
// Default inferred reference.
}
each value : & in record {
// Explicit inferred reference.
}
each value : copy in record {
// Owned copy with a separately inferred type for every member.
}
value : copy must be viable for every selected member. If one member cannot be
copied, the statement is invalid and the diagnostic identifies that member
specialization.
Stored callback values participate because they occupy instance places.
Function declarations, operator declarations, and type-owned once
declarations do not. Private members participate only from an access context
that may ordinarily reach them.
This form has no structural filter. Traversal of functions, operators,
generated declarations, once state, or other declaration metadata is a
separate reflection concern rather than a wider interpretation of instance
places.
Compiler-known member selection and per-member checking do not imply compile-time execution. Runtime effects in the body remain runtime effects unless an independent compile-time execution context says otherwise.
Cursor-driven from traversal
each from drives a first-class cursor. Its protocol uses five exact
receiver-owned phrases:
iterate, used asiterate <source>;value at, used asvalue at <cursor>;advance, used asadvance <cursor>;erase and advance, used aserase and advance <cursor>; anderase, used aserase <cursor>.
The first three support ordinary traversal. The erase operations are optional capabilities.
A concrete cursor needs no generic concept at a concrete each site. The
compiler checks the selected type’s exact operations directly. Future generic
constraints will express this already-defined protocol for an unknown type.
Optional exact count
An iterable source may expose an exact non-consuming count:
operator post unary 'count' final : (
result : IndexSize
)() readonly = {
}
An iterable need not provide count. When it does, the returned number must
exactly match the values produced by the next corresponding traversal, and
asking for it must not consume those values. Array construction can use that
number to reserve storage once before traversal.
If the iterable later produces a different number of values, the array
operation panics because the iterable broke its promise. A source that knows
only an estimate does not provide this exact operation. Complete array-entry
from, reservation, element-count checking, and memory-domain behavior is
explained by
Zax arrays and slices.
Acquiring the first cursor
An iterate operation returns a cursor when a first entry always exists:
operator pre unary 'iterate' final : (
first : MyCursor
)() readonly = {
// ...
}
A possibly empty source returns an optional cursor:
operator pre unary 'iterate' final : (
first : MyCursor?
)() readonly = {
// ...
}
A present cursor always designates one real current entry. Absence means that no cursor exists and the body runs zero times. Zax exposes no before-first, after-last, or erased-current cursor value.
If the from expression already supplies a viable cursor protocol, it may
initialize the statement’s cursor state directly. Otherwise each invokes
iterate source once. Acquisition never recursively searches for more
conversions.
The statement owns its current cursor state. Ordinary construction and transfer
rules decide whether a source cursor is copied, moved, or produced as a
temporary; each does not silently invent another transfer policy.
Producing the current value
The cursor supplies one current value:
operator pre unary 'value at' final : (
result : MyValue readonly &
)() readonly = {
// ...
}
The result may instead be an owned value or proxy. Producing it may be substantially more expensive than finding the next valid cursor.
An ordinary binding requests the value once before each body entry:
each value : from values {
consume(value)
}
An explicit compatible type may bind a reference or construct a value:
each value : MyValue readonly & from values {
inspect(value)
}
each value : MyValue from values {
keep(value)
}
Both discard spellings avoid requesting the value:
each # from values {
recordOnePosition()
}
each # : from values {
recordAnotherPosition()
}
# is the compact positional discard. # : is its explicit
declaration-shaped spelling. Neither creates a binding, requires value at, or
produces a value. A progression-only cursor may therefore use either form.
A named binding must retain its declaration colon:
each value : from values {
consume(value)
}
each value from values { // error: `value` is not a declaration
}
Advancing
A cursor may mutate its statement-owned storage and report whether it now designates a successor:
operator pre unary 'advance' final : (
hasNext : Boolean
)() mutable writable = {
// ...
}
Or it may return replacement cursor state:
operator pre unary 'advance' final : (
next : MyCursor?
)() readonly = {
// ...
}
An inexhaustible replacement form may return MyCursor. Replacement forms may
also use viable move or last receiver variants, but no terminal stance is
required.
The choice is operation-driven rather than inferred from whether the cursor type is generally mutable. The compiler:
- forms progression declarations viable for the current cursor qualifications and stance;
- accepts an in-place
Booleanshape or a same-cursor replacement shape, optionally wrapped once for exhaustion; - applies ordinary receiver and overload preference;
- diagnoses unresolved ambiguity; and
- does not prefer one result shape merely because cursor mutation is possible.
Before transition, current entry bindings end. A successful transition establishes fresh bindings. Failure or absence completes the statement.
Accessing the active each
The body does not receive a cursor or position binding. It uses one targetable family when ordinary value binding is insufficient:
from each [label:] value
from each [label:] underlying cursor
from each [label:] unsafe underlying cursor
from each [label:] erase
The label follows the complete from each introducer:
from each value
from each outer: value
Without a label, the family selects the lexically innermost active each, even
when that statement has a label. An explicit label selects that exact visible
each. The compiler does not skip an incapable inner target to find a capable
outer one.
Selective value production
from each value requests the same current value shape and authority available
to an ordinary binding:
each index := 0 ;;
# from expensiveSequence ;;
++index {
if index % 15 == 0 {
sampled : & = from each value
mutate(sampled)
}
}
The other positions advance without producing their expensive values.
The operation applies to each in and each from when the selected source
provides a value. Repeated requests may repeat value-production cost unless the
source contract promises caching.
Underlying cursor
from each underlying cursor applies only to each from and returns readonly
access to the actual current cursor:
cursorView := from each underlying cursor
It permits observation and readonly-compatible cursor operations. It does not expose in-place progression.
Direct access with the cursor’s existing qualifications requires narrow unsafe responsibility:
rawCursor := from each unsafe underlying cursor
Unsafe access does not manufacture mutable, writable, transfer, erase, or lifetime authority. The programmer accepts responsibility for any effect on progression, exhaustion, current-value validity, replacement results, and the statement’s later automatic transition. It cannot restore access after the current cursor life has ended.
Erasing during traversal
Erasure is available only when the target cursor supplies the exact required operation. The compiler does not synthesize removal or redirect to another active traversal.
Erase and advance
Continuing erasure may mutate cursor state:
operator pre unary 'erase and advance' final : (
hasNext : Boolean
)() mutable writable = {
// ...
}
Or it may return replacement state:
operator pre unary 'erase and advance' final : (
next : MyCursor?
)() readonly = {
// ...
}
An always-successful replacement may return MyCursor. Either form also needs
real authority to mutate the backing source. Returning a new cursor does not
create that authority.
Erase and exit
Exiting erasure uses a no-result operation:
operator pre unary 'erase' final : (
)() mutable writable = {
// ...
}
Other receiver variants may provide the same operation when they retain source
mutation authority. There is no automatic fallback to erase and advance;
finding or constructing a successor solely to discard it can have visible cost.
Common erase transfers
The common forms combine erasure with an immediate transfer:
continue with erase
next with erase
break with erase
continue outer: with erase
next outer: with erase
break outer: with erase
with erase modifies the transfer’s ordinary target. It has no independent
target and never causes target search to skip an intervening construct.
continue with eraseskips post, ends current entry access, invokeserase and advance, and enters the successor or exits.next with eraseruns post while current entry access remains valid, ends that access, invokeserase and advance, and enters the successor or exits.break with eraseskips post, ends current entry access, invokeserase, and exits without finding a successor.
Every combined goto ... with erase form is invalid. goto directly re-enters
the current body, while erasure ends the entry that body requires.
Erase, perform work, then transfer
The active-each family permits work after erasure:
each value : from values {
if shouldErase(value) {
summary := summarize(value)
hasNext := from each erase
updateStatistics(summary)
if !hasNext
beginSecondaryPhase()
continue
}
use(value)
}
hasNext is a discardable Boolean convenience result. It reports whether the
erase transition produced a successor; it does not control the loop. The
compiler separately retains the successor or absence.
After from each erase:
- the target’s current value and underlying cursor access are unavailable;
- other body locals, header bindings, and outer state remain available;
continuetargeting thateachskips post and adopts the prepared successor without another advance;nexttargeting it runs post after erase, diagnoses any post use of invalidated entry state, and then adopts the prepared successor without another advance;breaktargeting it discards the prepared successor and exits;gototargeting thateachis invalid because the current entry lifetime has ended and direct body entry cannot recreate it;- a transfer beyond it,
return, or panic exits normally; and - normal fallthrough is invalid while the erase transition remains pending.
This ordering is intentional:
next with erase
// Run post while current entry access is valid, then erase.
from each erase
next
// Erase first, then run only a post independent of old entry access.
Direct transfer beyond the erased traversal names both operations separately:
from each current_iteration: erase
goto outer:
goto outer: with erase is invalid because its one target label cannot also name
which crossed traversal owns the erase operation.
For targeted erase:
from each outer: erase
every completing path must resume or exit outer: or transfer beyond it. A
transfer targeting an inner construct does not resolve the outer pending state.
Availability
| Source form | Required cursor operation |
|---|---|
| ordinary progression | viable advance |
from each erase |
viable erase and advance |
continue with erase |
viable erase and advance |
next with erase |
viable erase and advance |
break with erase |
viable erase |
Missing capability is a compile error on the selected target. break with
erase does not fall back to erase and advance.
Cursor validity and source mutation
Active traversal creates a cursor-validity obligation, not an exclusive compiler lock over the source.
The compiler rejects established contradictions and diagnoses source operations whose contracts establish invalidation:
each value : from values {
from each erase
use(value) // error: the current value ended
}
It does not reject an arbitrary call merely because hidden code might mutate the source:
each value : from values {
opaqueCallback()
}
Cursor and collection contracts must state:
- which element mutations preserve traversal;
- which structural mutations preserve, reposition, or invalidate cursors;
- which operations invalidate yielded references;
- whether simultaneous cursors remain valid;
- whether opaque or reentrant mutation is stable, runtime-checked, or the programmer’s responsibility; and
- the cost of any runtime validation.
A cursor may use stable positions, restrict selected operations, or detect
invalidation at runtime. Zax does not impose one strategy or its cost on every
cursor. A hidden mutation that violates the cursor’s documented requirements
remains a program or implementation error rather than something each can
universally discover.
Compiler-coordinated erase is the preferred removal path, not a prohibition against all other mutation.
Costs
For a runtime source of n entries, each enters the body n times unless
control transfer exits early.
- Direct
each increates no cursor allocation. each fromdoes not imply allocation;iterateowns cursor-construction cost.- An ordinary cursor value binding invokes
value atonce per entered body. #and# :invoke it zero times.- A reference binding performs no value construction.
- A copied binding performs ordinary construction and destruction once per visited entry.
gotocan repeat the current body and its per-entry work without paying post, value production, or progression cost.- Structural
copyperforms and checks that work separately for each selected member type. - Runtime invalidation tracking is present only when a cursor or collection contract chooses it.
- Replacement progression may construct and destroy cursor values; an applicable receiver variant may reuse storage without changing the observable transition.
The compiler may unroll, table-drive, specialize, or otherwise optimize compiler-known traversal only when source evaluation, order, binding behavior, effects, and cleanup remain unchanged.
Diagnostics
Representative diagnostics distinguish:
- unsupported direct
insource; - unavailable or ambiguous
iterate; - unavailable or ambiguous
advance; - a value binding when the cursor supplies no viable
value at; - incompatible explicit reference or value binding;
- unavailable
copyfor one structural member specialization; - unavailable
eraseorerase and advance; with eraseon a transfer target other thaneach from;- any combined
goto ... with eraseform; - an incapable inner target rather than silently selecting an outer one;
- use of current value or underlying cursor after erase;
- post access to invalidated entry state after
from each erase; next; gototo aneachwhose current entry has ended or whose progression transition is pending;- fallthrough or wrong-target re-entry with an erase transition pending;
- known source mutation that invalidates active traversal;
- unsafe underlying cursor access without explicit responsibility;
- named and positional binding forms mixed in one clause; and
- a named traversal binding that omits its declaration colon.
Diagnostics name the selected each target, source family, binding role, or
missing cursor operation. They do not suggest informal method-name conventions
or a generic concept when a concrete protocol operation is absent.
Formatting
each, its optional label, and the first header section begin on one physical line unless explicit continuation moves the complete header.- A label uses attached
label:spelling; a declaration usesname :. - A named traversal binding always retains its spaced declaration colon; only
discard has the compact
#form. ;;has whitespace on both sides.- Continued header sections use the common two-space continuation level.
- Parentheses around bindings mean the wholly named form.
- A multiline body-opening
{remains on the final physical header line. -
with erasefollows the complete transfer target:continue outer: with erase -
The active-
eachtarget follows thefrom eachintroducer:from each outer: value
Source stability
The language preserves these distinctions:
inis direct compiler-known traversal;fromis cursor-driven traversal;- adding a generic constraint later does not reinterpret existing concrete cursors;
#and# :guarantee that cursor value production is not requested;- a present cursor always designates a real entry;
- transition absence is not an exposed end cursor;
- adding an erase capability makes new source valid but does not change ordinary progression;
gotoretains the current live entry and never silently progresses;- named role selection is explicit and independent of local identifier spelling;
- the future reflection role follows enum and structural
value/namepositions without making reflection current; and - postfix
_changes only keyword interpretation, not semantic identity.
Boundaries and maturity
This document defines current conceptual iteration behavior. It is not a formal grammar, ABI, conformance contract, or compiler lowering.
Still future:
- generic syntax for constraining an unknown iterable or cursor type;
- the reflection payload and type-declaration traversal;
- generic collection, proxy, range-value, and non-array view catalogs;
- generator and coroutine production;
- asynchronous, concurrent, and parallel traversal;
- compile-time execution of traversal bodies;
- formal cursor invalidation and effect contracts; and
- implementation-specific optimization and lowering.
Those future concerns must preserve the programmer-visible source, ordering, binding, progression, erasure, lifetime, cost, and diagnostic boundaries established here.