7.5.18. Macro Tutorial 18: with_ — locked binding of container slots
daslib/with_boost adds a with_ call-macro that solves a recurring
ergonomics problem: rebinding a struct field across an array or table
element. The naive form is rejected by daslang’s typer:
var arr = [A(f1 = 1, f2 = 2)]
var a : A& = arr[0] // error[31300]: local reference to non-local expression is unsafe
a.f1 = 99
Between binding a and writing through it, code could push/resize/erase
arr, leaving a dangling. with_ solves this by:
Binding the element inside a block, named
_by default;Wrapping the block in an automatic lock on the container, so push/erase/resize/clear inside the body panic at runtime instead of silently corrupting memory.
The single-arg form is a 1:1 replacement for the rejected pattern above:
require daslib/with_boost
var arr = [A(f1 = 1, f2 = 2)]
with_(arr[0]) {
_.f1 = 99 // mutation persists in arr[0]
}
7.5.18.1. Section 1 — The single-arg form
Default-name _ binding works for both struct-element and
workhorse-element arrays (workhorse coverage in Section 3):
var arr = [A(f1 = 1, f2 = 2), A(f1 = 3, f2 = 4)]
with_(arr[0]) {
_.f1 = 99
_.f2 = 100
}
// arr[0] is now A(f1 = 99, f2 = 100)
Named binding via $(name) is identical in effect — the macro strips
constness so mutations always persist:
with_(arr[1]) $(elem) {
elem.f1 = 555
}
7.5.18.2. Section 2 — Multi-arg positional form
Passing multiple containers locks each independently. The block params
are positional (no =-named args; the macro reads them in order):
var dst = [A(f1 = 0, f2 = 0)]
var src = [A(f1 = 10, f2 = 20)]
with_(dst[0], src[0]) $(d, s) {
d.f1 = s.f1 + 1
d.f2 = s.f2 + 2
}
Any arity works — the macro emits the full lock / invoke / unlock sequence inline, with one lock per container, so a call like with_(a[0], b[1], c[2], d[3], e[4]) $(va, vb, vc, vd, ve) { ... } scales naturally. Mix arrays and tables freely, subject to the single-table-arg rule (next section).
7.5.18.3. Section 3 — Workhorse element types (int, float, …)
The block-arg is bound by reference, so workhorse-element containers
work the same as struct-element ones — mutation through _ = X (or
the named x = X) propagates back to the underlying slot:
var ints = [1, 2, 3]
with_(ints[1]) {
_ = 222
}
// ints == [1, 222, 3]
The macro emits each block parameter pinned to the container’s element
type with the ref flag set, so daslang resolves the binding as int&
(or whichever workhorse type the element happens to be). No special-case
in the macro for struct vs workhorse — the same pinning path covers both.
7.5.18.4. Section 4 — Tables
Tables work the same way; tab[key] upserts (creates a default entry
if the key is missing). Only one table-keyed arg per call — a 2nd
upsert into the SAME table during the body would rehash and invalidate
the first pinned entry. The macro can’t prove two table-keyed args refer
to distinct tables, so the rule is conservative: even
with_(tab1[k1], tab2[k2]) (distinct tables) is refused:
var tab : table<string; A>
tab |> insert("k", A(f1 = 11, f2 = 22))
with_(tab["k"]) $(v) {
v.f1 = 777
}
7.5.18.5. Section 5 — Lock is real
Mutation of the container inside the body panics at runtime — exactly the failure mode the typer was trying to prevent at compile time:
var arr = [A(f1 = 1, f2 = 2)]
with_(arr[0]) $(a) {
arr |> push(A(f1 = 1000, f2 = 2000)) // panics — array is locked
}
daslang panic is fatal (not a C++/JS-style exception) — the program
prints the diagnostic and exits. try/recover exists to capture the
message before exit for nicer logging, NOT to recover-and-continue.
7.5.18.6. Section 6 — Refused container shapes
with_ is intentionally narrow:
Non-``ExprAt`` containers (plain locals, struct fields on locals, function-call results, array literals) are refused. The macro needs to ref-bind the container to a local, and only ExprVar-rooted lvalue chains (variables,
obj.field,arr[i]) have stable addressable storage outside the expression. Use built-inwithfor locals; for literal-or-call containers, hoist to avarfirst.More than one table-keyed arg is refused per the rehash hazard noted above.
Bodies that ``return`` a value are refused at typecheck time — the synthesized invoke target declares a
: voidblock return.with_is for in-place mutation; compute values via a local:var v : T; with_(arr[0]) { v = _.f }.
All refusals fire at macro-expansion time with the macro-error code
50503 and a message describing the failing arg.
7.5.18.7. Running the tutorial
daslang.exe tutorials/macros/18_with_boost.das
Expected output:
section 2: arr[0] = 99, 100
section 3: arr[1].f1 = 555
section 4: dst[0] = 11, 22
section 5: ints = [ 1, 222, 3]
section 6: tab[k].f1 = 777
section 7: see comment for the lock-panic shape
See also
Full source:
18_with_boost.das
Previous tutorial: Macro Tutorial 17: Quasi-quotation Reference
Standard library: daslib/with_boost.das
Language reference: Macros — full macro system documentation