Zax using
| Field | Value |
|---|---|
| Status | Current conceptual design |
| Audience | Human developers acquiring, coordinating, shutting down, and releasing scoped resources |
| Applies To | Programmer-facing using resource entries, lifetime extension, structural disposal, exit behavior, ordering, costs, and diagnostics; not a formal grammar or specification |
| Implementation State | Not established by this repository |
| Owns | The complete using operation; resource-entry enrollment; structural recognition of the ordinary dispose phrase; disposal compatibility; disposal and destruction order; using-specific flow eligibility and bypass; duplicate-enrollment intent; scoped-resource costs, diagnostics, formatting, and source stability |
| Does Not Own | General result mapping (function invocation); cohesive exceptional result flow; declaration behavior (declarations and bindings); ordinary reference semantics (lifetimes and references); optional presence and boxed lifetime (optional values); operator-phrase declaration and selection (operator phrases); qualification meaning (qualifiers); terminal destruction (construction and destruction); general label and transfer lookup (core flow control); general intent acknowledgement (intent acknowledgements); generic cleanup hooks; async cancellation; or compiler lowering |
| Source / Provenance | Legacy flow-control resource-lifetime intent, reconciled with current flow, invocation, lifetime, operator, and destruction design |
Mental model
using keeps selected resources available for one body, gives each compatible
resource an orderly end-of-use operation while every enrolled resource remains
alive, and only then destroys the values it owns:
using resources: (
connection := connect(),
grant := connection.acquireGrant(),
auditContract(),
sharedService
) {
performWork(connection, grant, sharedService)
}
The resource entries evaluate once in source order. When the body exits:
- body-local values are destroyed;
- compatible
disposeoperations run in reverse resource-entry order; - every disposal call finishes; and
- values owned by
usingare destroyed in reverse caller-visible entry order.
connection and grant are named values owned by this using.
auditContract() may contribute an unnamed value whose lifetime is extended
through the body. sharedService is an existing place borrowed by reference;
it may be disposed but is not destroyed by using.
For independently evaluated entries, visible entry order normally matches
construction order. Result mapping can expose producer results in another order.
The selected visible prototype controls pre-body result construction, and the
implementation controls explicit body and return construction. using does not
reorder either; it disposes and destroys its destinations according to the order
visible in the caller.
Disposal and destruction are different. dispose resource is an ordinary
operation on a still-live instance. Destruction is the terminal end of that
instance. This separation supports orderly shutdown, coordination among several
live resources, and breaking circular strong relationships before
reference-counted destruction can begin.
The word resource is descriptive. Zax does not introduce a resource base type,
marker interface, or separate lifetime category. Any enrolled receiver with a
compatible dispose phrase participates; an enrolled receiver without one is
kept alive and later destroyed according to its ordinary ownership.
Source shape
The complete conceptual form is:
using [label:] (resource-entry-list) body
Parentheses are required even for one entry:
using (resource := acquireResource()) {
use(resource)
}
The parentheses delimit a mapping-capable resource list, like a call’s argument list. They are not grouping parentheses around one expression. This distinction matters for multiple results:
using (acquirePair()) {
// Every result is enrolled.
}
using ((acquirePair())) {
// Valid only if acquirePair() forms one expression value.
}
An empty list is invalid because it performs no resource operation:
using () { // error: use an ordinary block or explicit scope
}
The body follows the ordinary effective-body and brace rules.
No initializer or post sections
using is not an initializer/post flow-header schema and accepts no ;;
section:
using (prepare() ;; resource) {
} // error: using has no separate initializer section
using (resource) ;; recordCompletion(resource) {
} // error: using has no post section
The resource list already performs ordered acquisition and initialization.
Zero-result setup work belongs before using. A value needed by later entries
can be declared as an enrolled entry.
Normal-completion work before disposal belongs at the end of the body. An inner block can end other body-local lifetimes first:
using (resource) {
{
temporary := prepare(resource)
use(temporary)
} // destroy temporary
recordCompletion(resource)
} // dispose resource, then destroy it if using owns it
Work that belongs to disposal itself belongs in dispose or in a wrapper that
provides the required operation.
Resource entries
Each resource entry is evaluated once in visible source order.
An entry becomes enrolled only after its producer selects success and completes the mapped resource destination:
using (
connection := connect(),
grant := acquireGrant(connection) catch failure {
return
}
) {
use(connection, grant)
}
If grant acquisition selects failure, no grant instance becomes enrolled,
disposed, or destroyed. The earlier connection entry is already enrolled, so
the handler’s outward transfer performs ordinary reverse disposal and
destruction for connection before continuing.
A catch handler cannot fall through into a body that requires an unconstructed
resource. Forwarding except has the same partial-header cleanup consequence.
Outcome handling and forwarding are defined by
Zax exceptional result flow.
Named owned values
A declaration establishes a named value available to later entries and the body:
using (
connection := connect(),
grant := connection.acquireGrant()
) {
use(connection, grant)
}
These values are owned by the using header and destroyed after the complete
disposal phase.
Unnamed value results
A by-value expression result becomes an unnamed owned entry:
using (
acquireContract(),
resource := acquireResource()
) {
use(resource)
}
The contract value remains alive through the body and disposal phase even though the body has no name for it. It is then destroyed with the other owned entries.
A zero-result operation cannot form a resource entry.
Existing places and reference results
An existing place is borrowed by reference:
resource := acquireResource()
using (resource) {
use(resource)
}
// using does not destroy resource
The borrowed reference remains bound to the same place. If the place is varying and receives a completely established successor, disposal observes that successor:
resource varying : Resource = acquireFirst()
using (resource) {
resource = acquireSecond()
}
// Disposal observes the acquireSecond() successor.
A reference-valued expression likewise enrolls its borrowed target. using
keeps the returned reference value available for disposal but does not acquire
ownership of the target:
using (findSharedResource()) {
}
Ordinary origin and lifetime proof must establish that every borrowed place still exists and contains a complete resident instance when disposal occurs. Borrowing does not extend the target’s owning life path.
Explicit qualification and transfer syntax keeps its ordinary meaning. A stance
such as move offers a transfer posture to the applicable consumer; using
does not reinterpret the spelling as proof that a transfer or new ownership
occurred.
Optional values
An optional entry participates only when it contains a value:
using (resource := tryObtainResource()) {
if ?resource
use(resource.)
}
At disposal, an absent resource causes no call. When it is present, using
selects the equivalent of dispose resource. on the immediate boxed value.
Compatibility is checked for that possible present path even when a particular
runtime value is absent.
The optional wrapper remains the owned or borrowed entry. It undergoes ordinary
destruction after the disposal phase. using crosses exactly one optional layer:
an immediate optional payload can participate, but nested optional depth is not
flattened recursively. Complete presence, postfix access, and nested lifetime
behavior are defined by
Zax optional values.
Multiple results and names
A bare producer with several results contributes every result in declared result order:
using (
acquirePair(),
externalResource
) {
}
If acquirePair() produces two results, the flattened enrollment order is its
first result, its second result, then externalResource.
A structured pair is one result and therefore one enrolled receiver. using
does not descend into its members automatically.
Capture by source-result label
Named capture selects producer results by their source-facing labels:
using (
resource:,
grant: = acquirePair()
) {
use(resource, grant)
}
This introduces same-named inferred bindings. Labels may select results in another order, but a label must exist and each result may be selected at most once.
A source/destination pair gives a result another body-visible name:
using (
resource: myResource:,
grant: myGrant: = acquirePair()
) {
use(myResource, myGrant)
}
resource: and grant: select the producer results. myResource: and
myGrant: introduce the caller’s destinations.
A selected result may initialize a destination with an explicit type:
using (
resource:,
grant: disposalGrant : MyCarryingGrant = acquirePair()
) {
use(resource, disposalGrant)
}
The selected grant result initializes disposalGrant. The destination’s type,
ownership, and operations determine what using later disposes and destroys.
General source-first routing and typed-destination behavior are defined by
Zax function invocation.
using does not introduce several new names through positional typed
declarations:
using (
myResource : Resource,
myGrant : Grant = acquirePair()
) {
} // error: named using results require source-result labels
This restriction prevents result reordering or API evolution from silently changing which resource a body name denotes.
Anonymous enrollment and discard
A bare destination : enrolls a source result without introducing a
body-visible name:
using (resource: : = acquire()) {
work()
}
// Dispose the anonymous resource, then destroy it.
The anonymous entry otherwise behaves like a named owned entry. In particular,
a compatible structural dispose participates.
Caller-side # also introduces no body-visible name, but explicitly suppresses
disposal for that result:
using (resource: # = acquire()) {
work()
}
// Skip disposal, then destroy the anonymous resource.
Both forms construct at the same entry position, establish the same anonymous
owned lifetime, permit the same source/destination elision, and destroy at the
same caller-visible position. Their meaningful using difference is whether
structural disposal participates.
A typed anonymous destination remains distinct:
using (resource: : MyResource = acquire()) {
work()
}
The source result initializes an anonymous MyResource. That type determines
construction, transfer, structural disposal, and destruction.
Bare positional # likewise introduces no body-visible name and suppresses
disposal for the corresponding result:
using (
resource:,
# = acquirePair()
) {
use(resource)
}
// Skip disposal of the second result.
// Destroy the second result, then resource.
The result is not destroyed immediately. It remains an anonymous
using-owned value through the body and complete disposal phase, then takes its
ordinary position in reverse caller-visible entry destruction.
# means that the caller does not need access to the value and that using
does not dispose it. It does not mean that the value’s lifetime is unimportant.
Structural disposal
A type opts into automatic disposal by declaring the exact ordinary pre-unary
phrase dispose:
Resource :: type {
operator pre unary 'dispose' final : ()() writable = {
// Shut down or complete `_` while the instance remains alive.
}
}
dispose is receiver-owned and non-protected. using does not impose
writable, readonly, or another special receiver qualification. Normal
selection may therefore choose a readonly operation:
ResourceView :: type {
operator pre unary 'dispose' final : (
diagnostic # : String
)() readonly = {
// No mutation occurs through `_`.
}
}
The operation is compatible when normal operator selection can perform the equivalent of:
dispose receiver
for the enrolled receiver’s effective type, qualifications, and transfer state, without leaving a result that the caller must acknowledge. The language selects that operation directly; it does not construct and parse hidden source.
Zero results is the common form. An operation may produce one or more results
only when every result permits omission with #. Omitted results are still
constructed and destroyed under ordinary invocation rules, so their work and
cost do not disappear.
Absence and incompatibility
When normal discovery finds no applicable exact pre-unary dispose phrase, the
entry has no disposal call. Its lifetime extension and eventual owned
destruction still apply.
When dispose is present but cannot satisfy the implicit bare-call contract,
the entry is a non-acknowledgeable intent error. Causes include:
- a required result;
- receiver qualification or transfer mismatch;
- ambiguous selection;
- a uniquely selected but unavailable operation; or
- another ordinary invocation failure.
Resource :: type {
operator pre unary 'dispose' final : (
success : Boolean
)() writable = {
}
}
using (resource) {
use(resource)
}
// error: dispose resource produces required result success
An intent<...>{...} enclosure cannot make the implicit call valid or discard a
required result. The programmer must avoid using for that value, invoke and
handle the operation explicitly, or use a wrapper that provides a compatible
dispose.
using never searches for close, shutdown, destructor bodies, or similarly
named functions. Its one explicit wrapper integration is the immediate payload
of an optional entry described above.
Ordering
The complete normal ordering is:
- evaluate resource entries once in source order;
- execute the body once;
- destroy body-local values in reverse construction order;
- invoke compatible
disposeoperations in reverse flattened entry order; - complete every disposal call; and
- destroy owned entries in reverse caller-visible entry order.
Steps 4 and 6 walk the same caller-visible entry sequence in reverse. They can
act on different subsets: a borrowed entry can be disposed but is not destroyed
by using, while an absent optional entry or # entry is not disposed but its
owned storage is still destroyed. Entries that participate in both phases keep
the same relative order.
using (
connection := connect(),
grant := connection.acquireGrant()
) {
request := prepareRequest(connection)
send(request, grant)
}
The observable end order is:
destroy body-local request
dispose using entry grant
dispose using entry connection
destroy using-owned grant
destroy using-owned connection
request is a body-local value, not an enrolled resource. Destroying body
locals first preserves ordinary nested-scope behavior and lets their destructors
use entries that are still live and not yet disposed. Both enrolled dispose
calls then finish before either owned enrolled entry is destroyed.
No enrolled receiver is destroyed between disposal calls. Every receiver
therefore remains alive throughout the complete disposal phase, including
borrowed receivers that are not eventually destroyed by using.
Results omitted from one dispose call complete and are destroyed at their
ordinary call boundary before the next disposal call. The no-destruction
guarantee covers enrolled receivers, not incidental temporaries produced by
disposal.
If one list expression produces several results, those results enter the flattened order in declared result order. Explicit result-label routing instead uses its visible mapped order. Reverse disposal applies to that flattened order.
Result construction and caller destruction are separate. Reordering selected
results does not reorder prototype-defined pre-body construction or the
implementation’s body and return construction, but it does change the order of
newly introduced using destinations:
using (
resultA: firstA:,
resultB: secondB: = makeResults()
) {
use(firstA, secondB)
}
// Dispose and destroy secondB before firstA.
using (
resultB: firstB:,
resultA: secondA: = makeResults()
) {
use(firstB, secondA)
}
// Prototype- and implementation-defined construction is unchanged.
// Dispose and destroy secondA before firstB.
An existing destination keeps the lifetime established by its existing owner. Result elision may instead unify a producer result slot with a new destination. The unified value has the destination’s lifetime and destruction position and no separate source-slot destruction, so elision can change overall destruction order. It does not reorder the producer’s explicitly sequenced result construction. Complete source-slot, destination, and elision behavior is defined by Zax function invocation.
Labels and exits
using may have an ordinary flow label before its mandatory resource-list
parentheses:
using cleanup: (resource) {
use(resource)
}
It executes its body once. It is not a loop, has no post operation, and cannot
be targeted by next, continue, or goto.
Normal completion and every ordinary transfer crossing the using boundary run
the disposal phase before continuing:
using cleanup: (resource) {
if shouldReturn()
return // dispose resource, destroy owned entries, then return
if shouldContinue()
continue outer: // dispose before reaching outer:
}
The only ordinary-flow disposal bypass is an explicit break naming that exact
using:
using cleanup: (resource) {
if transferResponsibility(resource)
break cleanup: // skip disposal, then destroy using-owned entries
}
The explicit target makes the changed resource responsibility visible. Destruction is not bypassed.
Bare break is a barrier
A bare break never targets a using. When using is the nearest break
boundary, the bare transfer is a non-acknowledgeable intent error rather than a
request to search outward:
while outer: keepWorking() {
using cleanup: (resource) {
if done()
break // error: name cleanup: to bypass disposal,
// or name outer: to perform disposal before leaving
}
}
An unlabeled using therefore offers no disposal-bypassing transfer. A normal
inner target remains available:
using cleanup: (resource) {
while pending() {
break // exits the inner while; cleanup: remains active
}
}
Nested using statements apply the target and crossing rules independently:
using outer: (first) {
using inner: (second) {
break outer:
// dispose second because inner: is crossed
// bypass dispose first because outer: is the explicit target
}
}
Repeated entries and aliases
using does not merge entries. Two entries mean two potential disposal calls
even when their source is repeated or their borrowed places are known to be the
same.
When required origin analysis establishes that a later entry enrolls a place
already enrolled by an earlier entry, the later entry requires
duplicate-resource-enrollment:
using (
resource,
intent<duplicate-resource-enrollment>{ resource }
) {
}
When a later entry repeats the same resource-producing expression but separate
evaluations may return distinct resources, it requires
repeated-resource-expression:
using (
grantMeAResource(),
intent<repeated-resource-expression>{ grantMeAResource() }
) {
}
An individual acknowledgement encloses the later complete resource entry or
producer group that creates the repetition. A known duplicate place uses
duplicate-resource-enrollment when both categories could describe the source.
Acknowledgement preserves every evaluation, enrollment, disposal call, and destruction consequence. It does not assert that disposal is idempotent or make an invalid alias relationship safe. Distinct source that cannot be established as the same place remains ordinary programmer responsibility; diagnostics do not depend on optional optimizer inference.
One surrounding acknowledgement may instead cover a complete region containing several occurrences of the same category:
intent<repeated-resource-expression>{
using (
grantMeAResource(),
grantMeAResource(),
grantMeAResource()
) {
}
}
Individual acknowledgement remains valid. The enclosing form must contain at least one matching occurrence; it does not suppress another category or a non-acknowledgeable error.
Acquisition, panic, and completion
using adds no general success test. A result object or status value remains an
ordinary enrolled value, and the body handles its operations. An optional has
the one-layer conditional disposal behavior described earlier:
using (attempt := tryAcquire()) {
if ?attempt
use(attempt.)
}
A producer selecting success completes its entire ordinary result shape before
those results are enrolled. A selected exceptional outcome enrolls no success
result from that producer. Earlier entries remain enrolled and receive ordinary
cleanup when the handler or forwarding transfer exits the partially established
using.
An unresolved panic is fatal rather than an ordinary unwinding path. using
does not promise that remaining disposal calls or destruction complete after a
body operation, acquisition, or disposal hook enters an unresolved panic.
Disposal is synchronous. Suspension, cancellation, executor movement, and async disposal remain future async design.
Costs
using makes the possibility of structural disposal visible, but the exact cost
depends on its entries:
- every resource-producing expression evaluates once;
- by-value results pay their selected construction, transfer, and destruction costs;
- borrowed entries add no ownership but require lifetime and qualification proof;
- every selected
disposeoperation pays its ordinary call and body costs; - omission-permitting disposal results are still constructed and destroyed;
- disposal calls run sequentially in reverse entry order;
- no
disposecall occurs for a receiver with no applicable exact phrase; - an absent optional causes no disposal call for its boxed value;
- body-local destruction retains ordinary reverse-construction cost;
- owned-entry destruction retains reverse caller-visible entry order, including any destruction-order change caused by result routing or elision; and
- no allocation, dynamic dispatch, control block, or runtime protocol test is implied unless a selected entry or operation independently requires it.
Diagnostics
Diagnostics should distinguish:
- an empty resource list;
- a zero-result operation used as a resource entry;
- unknown, duplicate, or already-consumed source-result labels;
- positional introduction of several named results;
- an invalid or incompatible typed result destination;
- an expression grouped into one-value mode when it has several mandatory results;
- an exceptional acquisition outcome that is neither caught nor forwarded;
- a catch handler that falls through while its resource destination remains unconstructed;
- an optional whose possible immediate payload exposes an incompatible
dispose; - a borrowed place that cannot remain valid through disposal;
- no resident instance available to the implicit disposal call;
- an applicable
disposewith required results; - receiver qualification or transfer mismatch during disposal;
- ambiguous or unavailable
dispose; - recognizable repeated expression or duplicate-place enrollment lacking its acknowledgement;
- an acknowledgement that names the wrong repetition category or encloses the wrong source unit;
- a bare
breakstopped by ausingboundary; - an explicit transfer targeting
usingwith an ineligible keyword; - an attempt to use
;;as ausinginitializer or post separator; and - incomplete result, binding, or lifetime state on an exiting path.
Incompatible dispose and bare-break barrier errors are
non-acknowledgeable. Repeated resource expressions and duplicate resource
enrollment have the exact acknowledgement forms defined above. Exact diagnostic
identifiers and presentation remain future diagnostics work.
Formatting
Canonical formatting:
- always preserves the mandatory resource-list parentheses;
- keeps an optional flow label between
usingand(; - uses ordinary comma spacing and continuation;
- preserves producer groups, result-label adjacency, source/destination label
pairs, anonymous
:,#, grouping parentheses, qualifications, and transfer stances; - keeps the body-opening
{on the final physical header line; - aligns the closing
}withusing; and - never inserts a
;;section.
using resources: (
resource:,
grant: = acquirePair(),
sharedService
) {
use(resource, grant, sharedService)
}
A formatter must not add grouping parentheses around a bare producer because that could collapse a result sequence into one expression context.
Source stability
The following changes are source- or behavior-visible:
- adding, removing, relabeling, or reordering producer results changes resource mapping;
- reordering source/destination entries changes the caller-visible disposal and owned-destination destruction order without changing prototype- or implementation-defined construction;
- changing a result between value and reference changes ownership and destruction;
- adding or changing an acquisition’s exceptional outcomes changes which resource-list paths complete and which earlier entries require exit cleanup;
- changing optional depth changes which payload, if any, participates;
- changing anonymous
:enrollment to#suppresses disposal without changing the entry lifetime or destruction position; - adding an applicable
disposemakes an existingusingentry perform disposal; - changing
disposeoverloads, qualifications, transfer requirements, availability, or result acknowledgement may change selection or make source invalid; - changing entry order changes disposal and destruction order;
- adding a recognizable repeated entry requires explicit acknowledgement;
- renaming a
usinglabel breaks its explicit bypass; and - changing a transfer target may change whether disposal runs.
An unrelated import cannot add an ordinary phrase to a receiver type it does not
own. Adding dispose to the receiver type itself remains an API compatibility
event.
Boundaries and maturity
This document describes current conceptual design, not formal grammar, compiler lowering, a compatibility contract, or an implementation guarantee.
It establishes synchronous scoped resource use. It does not establish:
- generalized
defer,finally, scope guards, or cleanup callbacks; - exception-like recovery or panic unwinding;
- async disposal, cancellation, or cross-thread teardown;
- a generic resource type, interface, concept, or constraint;
- automatic member exposure or lexical capture;
- compiler data structures or lowering; or
- exact diagnostic identifiers.
Future work may add those facilities only without silently changing the resource-entry, disposal, exit, and destruction behavior defined here.