5.1.37. Utility Patterns (defer + static_let)
This tutorial covers two powerful utility macros: defer for Go-style
scope-exit cleanup, and static_let for C-style persistent local variables.
require daslib/defer
require daslib/static_let
5.1.37.1. defer — scope-exit cleanup
defer moves a block of code to the finally section of the enclosing
scope. It runs when the scope exits — whether normally or via an early
return. This is Go’s defer pattern: set up a resource, then immediately
defer its cleanup, keeping the two related operations adjacent.
print("step 1\n")
defer() {
print("cleanup (deferred)\n")
}
print("step 2\n")
print("step 3\n")
// output:
// step 1
// step 2
// step 3
// cleanup (deferred)
5.1.37.1.1. LIFO ordering
When multiple defers exist in the same scope, they run in LIFO order — the last deferred block runs first, just like Go:
defer() {
print("first defer (runs last)\n")
}
defer() {
print("second defer (runs first)\n")
}
print("main body\n")
// output:
// main body
// second defer (runs first)
// first defer (runs last)
5.1.37.1.2. Early return
Deferred blocks run even when the function returns early. This makes
defer ideal for resource cleanup:
def defer_early_return(do_early : bool) {
defer() {
print("cleanup always runs\n")
}
if (do_early) {
print("returning early\n")
return
}
print("reached the end\n")
}
5.1.37.1.3. Scope attachment
defer attaches to the nearest enclosing scope — so inside an if
block, it only runs when that if block exits, not the function scope:
print("before if\n")
if (true) {
defer() {
print("deferred inside if\n")
}
print("inside if\n")
}
print("after if\n")
// output:
// before if
// inside if
// deferred inside if
// after if
5.1.37.1.4. Practical: paired acquire/release
The classic use case: acquire a resource, immediately defer its release:
acquire_resource("database")
defer() {
release_resource("database")
}
acquire_resource("file")
defer() {
release_resource("file")
}
// On scope exit: file released first (LIFO), then database
Note
defer is transformed at compile time — the deferred block ALWAYS
moves to finally, regardless of whether execution “reached” the
defer() call at runtime. This differs from Go, where defer is
runtime-conditional.
5.1.37.2. static_let — persistent locals
static_let makes local variable declarations persistent across calls.
The variable is initialized once and retains its value, just like C’s
static local variables:
def call_counter() : int {
static_let() {
var count = 0
}
count ++
return count
}
// call_counter() returns 1, 2, 3, ... on successive calls
Variables declared inside static_let are promoted to global scope
but remain accessible by name in the declaring function.
5.1.37.2.1. One-time initialization
Use static_let for expensive one-time setup — the initializer runs
once:
def get_lookup_table() : int {
static_let() {
var lookup <- [10, 20, 30, 40, 50]
}
var total = 0
for (v in lookup) {
total += v
}
return total
}
// The array is created once, reused on every call
5.1.37.2.2. Named static_let
The name argument helps disambiguate when you need multiple static_let
blocks in different functions that might otherwise collide:
def named_counter_a() : int {
static_let("counter_a") {
var n = 0
}
n ++
return n
}
5.1.37.3. Combining defer and static_let
A practical pattern: cache a result with static_let, and use defer
to log when the function exits:
def cached_computation(input : int) : int {
static_let() {
var last_input = -1
var last_result = 0
}
defer() {
print(" exiting cached_computation({input})\n")
}
if (input == last_input) {
print(" cache hit for {input}\n")
return last_result
}
last_result = input * input + input
last_input = input
print(" computed {input} -> {last_result}\n")
return last_result
}
5.1.37.4. Summary
Feature |
Description |
|---|---|
|
Moves block to scope’s |
|
Promotes local declarations to persistent globals |
|
Same, with a name prefix to avoid collisions |
|
Same as |
See also
Full source: tutorials/language/37_utility_patterns.das
Previous tutorial: Pointers
Next tutorial: Random Numbers