Resource Management
Shape provides deterministic resource cleanup through automatic scope-based drop (RAII-style) and the Drop trait. When a binding goes out of scope, its drop() method runs automatically — no explicit cleanup blocks required.
Automatic Scope-Based Drop
Section titled “Automatic Scope-Based Drop”Every let binding that implements the Drop trait is automatically dropped when its enclosing scope exits. This applies to normal block exit, early return, break, continue, and error propagation.
fn query_total(uri: string) -> number { let conn = db.connect(uri) let rows = conn.query("SELECT * FROM records WHERE date = today()") rows.map(|r| r.value).sum()}// conn.drop() called automatically when the function exitsThere is no special syntax for resource management. Regular let bindings are sufficient. The compiler tracks which bindings implement Drop and emits drop calls at every scope exit point.
Block Scoping
Section titled “Block Scoping”You can use ordinary blocks to limit a resource’s lifetime:
fn process(uri: string) { let result = { let conn = db.connect(uri) conn.query("SELECT sum(value) FROM records").first().value } // conn is dropped here, at the end of the block // result holds the query value and is still available print(f"Total volume: {result}")}Multiple Resources and Reverse Drop Order
Section titled “Multiple Resources and Reverse Drop Order”When a scope contains multiple droppable bindings, they are dropped in reverse declaration order (last declared, first dropped). This naturally matches dependency graphs where later resources depend on earlier ones:
fn pipeline() { let conn = db.connect("postgres://localhost/analytics") let tx = conn.begin_transaction() tx.execute("INSERT INTO results VALUES (42)") tx.commit()}// Drop order: tx first, then conn// Transaction is finalized before the connection is closedThe reverse-order guarantee holds across all exit paths, including early returns:
fn pipeline() { let a = acquire("first") let b = acquire("second") let c = acquire("third") if should_abort() { return // drops: c, b, a (reverse order) } run_pipeline(a, b, c)}// normal exit also drops: c, b, aDrop in Loops
Section titled “Drop in Loops”When droppable bindings are declared inside a loop body, they are dropped at the end of each iteration. break and continue both trigger cleanup:
let uris = ["postgres://a", "postgres://b", "postgres://c"]
for uri in uris { let conn = db.connect(uri) let count = conn.query("SELECT count(*) FROM records").first().value if count == 0 { continue // conn.drop() runs, loop advances } if count > 1000000 { break // conn.drop() runs, loop exits } process(conn, count) // conn.drop() runs here on normal iteration}The Drop Trait
Section titled “The Drop Trait”Drop is a builtin trait. You do not need to import it. Any type that implements Drop gets automatic cleanup when its bindings go out of scope.
Definition
Section titled “Definition”The trait has one required method:
trait Drop { drop(self)}Implementing Drop
Section titled “Implementing Drop”Implement Drop for your own types to define cleanup behavior:
type FileHandle { path: string, fd: int}
impl Drop for FileHandle { method drop() { close_fd(self.fd) print(f"Closed file: {self.path}") }}Now any FileHandle binding is cleaned up automatically:
fn read_data() -> Vec<Row> { let f = open_file("/data/records.csv") let lines = f.read_lines() lines.map(parse_row)}// f.drop() closes the file descriptor when the function returnsCustom Connection Type
Section titled “Custom Connection Type”A more complete example showing a database wrapper:
type PooledConnection { pool_id: string, conn_id: int, active: bool}
impl Drop for PooledConnection { method drop() { if self.active { return_to_pool(self.pool_id, self.conn_id) } }}
impl Display for PooledConnection { method display() { f"Connection({self.conn_id} @ pool {self.pool_id})" }}Error Handling in Drop
Section titled “Error Handling in Drop”When drop() raises an error, the error is logged but does not prevent other pending drops from executing. This ensures that one failing cleanup does not leak other resources:
fn transfer() { let a = open_resource("alpha") let b = open_resource("beta") process(a, b)}// if b.drop() errors: error is logged, execution continues// a.drop() still runs regardless of whether b.drop() succeededIf both a.drop() and b.drop() fail, both errors are logged. The original return value or error from the scope body is preserved; drop errors never replace it.
Drop and Control Flow
Section titled “Drop and Control Flow”Early Return
Section titled “Early Return”Returning from a function runs all pending drops for every scope being exited, before the function actually returns:
fn find_first_match(uri: string, pattern: string) -> string { let conn = db.connect(uri) let rows = conn.query(f"SELECT * FROM events") for row in rows { if row.name.contains(pattern) { return row.name // conn.drop() runs before return completes } } "no match"}Nested Scopes
Section titled “Nested Scopes”You can use nested blocks to control exactly when resources are dropped:
fn load_and_transform(source_uri: string, dest_uri: string) { let data = { let src = db.connect(source_uri) src.query("SELECT * FROM records") } // src is dropped here -- connection released
let dest = db.connect(dest_uri) dest.bulk_insert("records_backup", data) // dest is dropped when the function exits}Drop and Structured Concurrency
Section titled “Drop and Structured Concurrency”Automatic drop integrates with Shape’s async scope blocks. When an async scope exits, drops run before child task cancellation occurs. This guarantees that shared resources are cleaned up while tasks can still observe them.
Per-Type Async Drop
Section titled “Per-Type Async Drop”Drop opcode selection is based on the type’s Drop implementation, not the calling context. When a type’s drop() method is declared async, the compiler emits an async drop opcode (DropCallAsync). When it is sync, a sync drop opcode (DropCall) is emitted. This happens on a per-binding basis — different types in the same function can use different drop opcodes.
A type can provide both sync and async drop methods in the same impl Drop block:
type DbConnection { handle: int, uri: string }
impl Drop for DbConnection { // Sync fallback -- used when dropped in a sync function method drop() { close_sync(self.handle) }
// Async variant -- used when dropped in an async function async method drop() { await flush(self.handle) await close(self.handle) }}The compiler selects the drop variant based on the type’s available implementations and the calling context:
| Type has | Async context | Sync context |
|---|---|---|
| Sync drop only | DropCall | DropCall |
| Async drop only | DropCallAsync | compile error |
| Both sync and async | DropCallAsync (prefers async) | DropCall (sync fallback) |
| No Drop impl | DropCall (silently skips) | DropCall (silently skips) |
If a type only has an async drop() and is used inside a sync function, the compiler rejects it with a clear error. Either add a sync method drop() fallback or move the binding into an async function.
Async Drop in Async Scopes
Section titled “Async Drop in Async Scopes”When an async scope exits, async drops run before child task cancellation occurs. This guarantees that shared resources are cleaned up while tasks can still observe them.
async scope { let conn = db.connect("postgres://localhost/analytics") let stream_a = conn.subscribe("events") let stream_b = conn.subscribe("updates")
for await event in merge(stream_a, stream_b) { if event.type == "close" { break } handle(event) }}// Cleanup order:// 1. stream_b.drop(), stream_a.drop(), conn.drop() (reverse order, async drops awaited)// 2. Pending async tasks are cancelledScoped Lifetimes in Async
Section titled “Scoped Lifetimes in Async”Use blocks within an async scope to release resources before the scope finishes:
async scope { let results = { let conn = db.connect("postgres://localhost/analytics") async let a = conn.query("SELECT * FROM events") async let b = conn.query("SELECT * FROM updates") join(a, b) } // conn is dropped here, connection released // results are still available for further processing process(results)}Best Practices
Section titled “Best Practices”Keep drop() fast and infallible. The drop() method should not perform expensive computation or operations that are likely to fail. If cleanup requires fallible I/O, handle the error gracefully inside drop() rather than letting it propagate.
Use blocks to limit resource lifetimes. When a resource is only needed for part of a function, wrap it in a block so it is dropped as soon as possible:
fn process(uri: string) { // conn lives only as long as this block let data = { let conn = db.connect(uri) conn.query("SELECT * FROM dataset") } // conn is already dropped
// expensive processing happens without holding the connection transform(data)}Rely on declaration order for dependencies. Declare resources in dependency order (connection before transaction, file before cursor). Reverse drop order ensures dependents are cleaned up before their dependencies:
fn transact(uri: string) { let conn = db.connect(uri) let tx = conn.begin_transaction() let cursor = tx.cursor() cursor.execute(query) tx.commit()}// Drop order: cursor, tx, connImplement Drop for any type that holds external resources. Files, connections, locks, handles, and temporary allocations should all implement Drop so that cleanup is automatic and cannot be forgotten.
For more on trait definitions and implementations, see Traits. For async scope semantics and task cancellation, see Async and Structured Concurrency.