@zio.dev/zio-blocks 0.0.22 → 0.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/scope.md CHANGED
@@ -1,386 +1,466 @@
1
- # ZIO Blocks — Scope (compile-time safe resource management)
1
+ # ZIO Blocks — Scope (`zio.blocks.scope`)
2
2
 
3
- `zio.blocks.scope` provides **compile-time verified resource safety** for synchronous code by tagging values with an unnameable, type-level **scope identity**. Values allocated in a scope can only be used when you hold a compatible `Scope`, and values allocated in a *child* scope cannot be returned to the parent in a usable form.
3
+ `zio.blocks.scope` is a **compile-time safe, zero-cost** resource management library for **Scala 3** (and Scala 2.13). It prevents a large class of lifetime bugs by tagging allocated values with an *unnameable*, scope-specific type and restricting how those values may be used.
4
4
 
5
- **Structured scopes.** Scopes follow the structured-concurrency philosophy: child scopes are nested within parent scopes, resources are tied to the lifetime of the scope that allocated them, and cleanup happens deterministically when the scope exits (finalizers run LIFO). This "nesting = lifetime" structure provides clear ownership boundaries in addition to compile-time leak prevention.
5
+ At runtime the model stays simple:
6
6
 
7
- If you've used `try/finally`, `Using`, or ZIO `Scope`, this library lives in the same problem space, but it focuses on:
7
+ - **Allocate eagerly** (no lazy thunks)
8
+ - **Register finalizers**
9
+ - **Run finalizers deterministically** when a scope closes (**LIFO** order)
10
+ - Collect finalizer failures into a `Finalization` (and throw/suppress appropriately)
8
11
 
9
- - **Compile-time prevention of scope leaks**
10
- - **Zero-cost opaque type** (`$[A]` is the scoped type, equal to `A` at runtime)
11
- - **Simple, synchronous lifecycle management** (finalizers run LIFO on scope close)
12
- - **Eager evaluation** (all operations execute immediately, no deferred thunks)
12
+ ## Why Scope?
13
13
 
14
- ---
14
+ Most resource bugs in Scala are "escape" bugs:
15
+
16
+ - storing a connection/stream in a field and using it after it was closed
17
+ - capturing a resource in a closure that outlives a scope
18
+ - passing a resource to code that might retain it
19
+ - mixing values from different lifetimes ("which scope owns this?")
20
+
21
+ Scope addresses these with a *tight* design:
15
22
 
16
- ## Table of contents
17
-
18
- - [Quick start](#quick-start)
19
- - [Core concepts](#core-concepts)
20
- - [1) `Scope`](#1-scope)
21
- - [2) Scoped values: `$[+A]`](#2-scoped-values-a)
22
- - [3) `Resource[A]`: acquisition + finalization](#3-resourcea-acquisition--finalization)
23
- - [4) `Unscoped`: marking pure data types](#4-unscoped-marking-pure-data-types)
24
- - [5) `lower`: accessing parent-scoped values](#5-lower-accessing-parent-scoped-values)
25
- - [6) `Wire[-In, +Out]`: dependency recipes](#6-wire-in-out-dependency-recipes)
26
- - [Safety model (why leaking is prevented)](#safety-model-why-leaking-is-prevented)
27
- - [Usage examples](#usage-examples)
28
- - [Allocating and using a resource](#allocating-and-using-a-resource)
29
- - [Nested scopes (child can use parent, not vice versa)](#nested-scopes-child-can-use-parent-not-vice-versa)
30
- - [Chaining resource acquisition](#chaining-resource-acquisition)
31
- - [Registering cleanup manually with `defer`](#registering-cleanup-manually-with-defer)
32
- - [Classes with `Finalizer` parameters](#classes-with-finalizer-parameters)
33
- - [Dependency injection with `Wire` + `Context`](#dependency-injection-with-wire--context)
34
- - [Dependency injection with `Resource.from[T](wires*)`](#dependency-injection-with-resourcefromtwires)
35
- - [Injecting traits via subtype wires](#injecting-traits-via-subtype-wires)
36
- - [Interop escape hatch: `leak`](#interop-escape-hatch-leak)
37
- - [Common compile errors](#common-compile-errors)
38
- - [API reference (selected)](#api-reference-selected)
23
+ | Feature | `zio.blocks.scope` |
24
+ |---|---|
25
+ | Compile-time leak prevention | ✓ (`scope.$[A]` + `$` macro + `Unscoped` boundary) |
26
+ | Runtime overhead | ~0 (scoped values erase to `A`) |
27
+ | Allocation model | Eager (allocation happens at `allocate`) |
28
+ | Finalization | Deterministic, LIFO, errors collected |
29
+ | Structured lifetime | Parent/child scopes, `lower` for explicit lifetime widening |
30
+ | Escape hatch | `leak` (warns) |
31
+
32
+ If you've used `try/finally`, `Using`, or ZIO's `Scope`, this is the same problem space—but optimized for **synchronous code** with **compile-time boundaries**.
39
33
 
40
34
  ---
41
35
 
42
- ## Quick start
36
+ ## Quick start (Scala 3)
43
37
 
44
38
  ```scala
45
- import zio.blocks.scope._
39
+ import zio.blocks.scope.*
46
40
 
47
- final class Database extends AutoCloseable {
41
+ final class Database extends AutoCloseable:
48
42
  def query(sql: String): String = s"result: $sql"
49
43
  def close(): Unit = println("db closed")
50
- }
51
44
 
52
- Scope.global.scoped { scope =>
53
- import scope._
45
+ @main def quickStart(): Unit =
46
+ val out: String =
47
+ Scope.global.scoped { scope =>
48
+ import scope.*
54
49
 
55
- val db: $[Database] = allocate(Resource(new Database))
50
+ val db: $[Database] =
51
+ Resource.fromAutoCloseable(new Database).allocate
56
52
 
57
- // scope.use applies a function to the scoped value, returning a scoped result
58
- val result: $[String] = scope.use(db)(_.query("SELECT 1"))
59
- println(result) // $[String] = String at runtime, prints directly
60
- }
53
+ // Safe access: the lambda parameter can only be used as a receiver
54
+ $(db)(_.query("SELECT 1"))
55
+ }
56
+
57
+ println(out)
61
58
  ```
62
59
 
63
- Key things to notice:
60
+ Key points:
64
61
 
65
- - `allocate(...)` returns a **scoped** value of type `$[Database]` (the path-dependent type of the enclosing scope)
66
- - `$[A] = A` at runtime — zero-cost opaque type, no boxing
67
- - All operations are **eager** — values are computed immediately, no lazy thunks
68
- - Use `scope.use(value)(f)` to work with scoped values; returns `$[B]`
69
- - When the `scoped { ... }` block exits, finalizers run **LIFO** and errors are handled safely
70
- - The `scoped` method requires `Unscoped[A]` evidence on the return type
62
+ - `allocate(...)` returns a **scoped value**: `scope.$[Database]` (or `$[Database]` after `import scope.*`).
63
+ - You **cannot** call `db.query(...)` directly on `$[Database]`.
64
+ - You use the `$` access operator: `$(db)(...)` (or `(scope $ db)(...)` without the import).
65
+ - The `scoped` block returns a plain `String` because `String: Unscoped`.
66
+ - Finalizers run when the block exits, in **LIFO** order.
71
67
 
72
68
  ---
73
69
 
74
- ## Core concepts
70
+ ## Core mental model
75
71
 
76
- ### 1) `Scope`
72
+ ### 1) `Scope`: finalizers + type identity
77
73
 
78
- `Scope` is a `sealed abstract class` with **no** type parameters. It manages finalizers and ties values to a *type-level identity* via abstract type members.
74
+ `Scope` is a finalizer registry plus a unique type identity:
79
75
 
80
- - **`type $[+A]`** — a path-dependent opaque type that tags values to this scope. Covariant in `A`. Equal to `A` at runtime (zero-cost).
81
- - **`type Parent <: Scope`** — the parent scope's type.
82
- - **`val parent: Parent`** — reference to the parent scope.
76
+ - `type $[+A]` — a scope-tagged, path-dependent type (erases to `A` at runtime)
77
+ - `type Parent <: Scope` / `val parent: Parent` — the scope hierarchy
83
78
 
84
- Each scope instance exposes its own `$[+A]`, so a parent's `$[Database]` is a different type than a child's `$[Database]`, even though both equal `Database` at runtime.
85
-
86
- ```scala
87
- type $[+A] // = A at runtime (zero-cost)
88
- ```
89
-
90
- So in code you'll typically write:
79
+ Every scope instance defines a **different** `$` type, so values from different scopes don't accidentally mix.
91
80
 
92
81
  ```scala
93
82
  Scope.global.scoped { scope =>
94
- import scope._
95
- val x: $[Something] = ??? // or scope.$[Something]
83
+ import scope.*
84
+ val x: $[Int] = 1 // ok (in global, $[A] = A)
96
85
  }
97
86
  ```
98
87
 
99
- Child scopes are represented by `Scope.Child[P <: Scope]`, a `final class` nested in the `Scope` companion object.
100
-
101
88
  #### Global scope
102
89
 
103
- `Scope.global` is the root of the scope hierarchy:
90
+ `Scope.global` is the root:
104
91
 
105
- ```scala
106
- object Scope {
107
- object global extends Scope {
108
- type $[+A] = A
109
- type Parent = global.type
110
- val parent: Parent = this
111
- }
112
- }
113
- ```
114
-
115
- - The global scope is intended to live for the lifetime of the process.
116
- - Its finalizers run on JVM shutdown.
92
+ - In the global scope: `type $[+A] = A` (identity)
93
+ - On the JVM: global finalizers run on shutdown via a shutdown hook
94
+ - On Scala.js: there is no shutdown hook, so global finalizers are **not** run automatically
117
95
 
118
96
  ---
119
97
 
120
- ### 2) Scoped values: `$[+A]`
98
+ ### 2) Scoped values: `scope.$[A]` / `$[A]`
99
+
100
+ A value of type `scope.$[A]` means:
121
101
 
122
- `$[+A]` (or `scope.$[A]` in type annotations) is a path-dependent opaque type representing a value of type `A` that is locked to a specific scope. It is covariant in `A`.
102
+ > "This is an `A`, but it is only valid while `scope` is alive."
123
103
 
124
- - **Runtime representation:** `$[A] = A` — zero-cost opaque type, no boxing or wrapping
125
- - **Key effect:** methods on `A` are hidden at the type level; you can't call `a.method` directly
126
- - **All operations are eager:** `allocate(resource)` acquires the resource **immediately** and returns a scoped value
127
- - **Access paths:**
128
- - `scope.use(a)(f)` to apply a function and get `$[B]`
104
+ Properties:
129
105
 
130
- #### `ScopedOps`: `map` and `flatMap` on `$[A]`
106
+ - **Zero-cost**: `$[A]` is just `A` at runtime (casts/identity)
107
+ - **Incompatible across scopes**: `outer.$[A]` is not `inner.$[A]`
108
+ - **Methods are hidden** at the type level; you must use `$` to access
131
109
 
132
- `Scope` provides an implicit class `ScopedOps[A]` that adds `map` and `flatMap` to `$[A]` values, enabling for-comprehension syntax:
110
+ #### Access operator: `(scope $ value)(f)`
111
+
112
+ The intended way to use a scoped value is:
133
113
 
134
114
  ```scala
135
- Scope.global.scoped { scope =>
136
- import scope._
115
+ (scope $ scopedValue)(a => a.method(...))
116
+ ```
137
117
 
138
- val x: $[Int] = $(42)
139
- val y: $[String] = x.map(_.toString)
140
- val z: $[String] = x.flatMap(v => $(s"value: $v"))
141
- }
118
+ This is enforced by a macro that checks the lambda uses its parameter only in **receiver position**.
119
+
120
+ Allowed:
121
+
122
+ ```scala
123
+ (scope $ db)(_.query("SELECT 1"))
124
+ (scope $ db)(d => d.query("a") + d.query("b"))
125
+ (scope $ db)(_.query("x").toUpperCase)
126
+ (scope $ db)(_.field) // field access is allowed
127
+ ```
128
+
129
+ Rejected at compile time:
130
+
131
+ ```scala
132
+ (scope $ db)(d => store(d)) // parameter used as an argument
133
+ (scope $ db)(d => () => d.query("x")) // captured in a nested lambda
134
+ (scope $ db)(d => d) // returning the parameter
135
+ (scope $ db)(d => { val x = d; 1 }) // binding/storing the parameter itself
142
136
  ```
143
137
 
144
- - `sa.map(f: A => B): $[B]` — applies `f` to the unwrapped value, re-wraps the result
145
- - `sa.flatMap(f: A => $[B]): $[B]` — applies `f` to the unwrapped value (where `f` returns a scoped value)
146
- - All operations are **eager** (zero-cost)
138
+ ##### "Auto-unwrap" rule (`Unscoped`)
147
139
 
148
- #### Scala 2 note
140
+ `$` *auto-unwraps* when the result type is known to be safe data:
149
141
 
150
- In Scala 2, the `scoped` method must be called with a lambda literal. Passing a variable or method reference is not supported due to macro limitations:
142
+ - if `B: Unscoped` → `(scope $ sa)(f)` returns **`B`**
143
+ - otherwise → it returns **`scope.$[B]`**
151
144
 
152
145
  ```scala
153
- // ✅ OK: lambda literal
154
- Scope.global.scoped { scope => ... }
146
+ Scope.global.scoped { scope =>
147
+ import scope.*
155
148
 
156
- // ❌ ERROR in Scala 2 (works in Scala 3):
157
- val f: Scope.Child[_] => Any = scope => ...
158
- Scope.global.scoped(f)
149
+ val db: $[Database] = Resource.from[Database].allocate
150
+
151
+ val s: String = $(db)(_.query("SELECT 1")) // String is Unscoped => unwrapped
152
+ val n: Int = $(db)(_.query("x").length) // Int is Unscoped => unwrapped
153
+ }
159
154
  ```
160
155
 
161
156
  ---
162
157
 
163
158
  ### 3) `Resource[A]`: acquisition + finalization
164
159
 
165
- `Resource[A]` describes how to **acquire** an `A` and how to **release** it when a scope closes. It is intentionally lazy: you *describe what to do*, and allocation happens only through:
160
+ A `Resource[A]` is a **lazy description** of how to acquire a value and register cleanup in a scope. Nothing happens until you call `scope.allocate(resource)` (or `.allocate` syntax).
166
161
 
167
- ```scala
168
- allocate(resource)
169
- ```
162
+ #### Constructors
163
+
164
+ From the source:
165
+
166
+ - `Resource(value: => A)`
167
+ Wraps a by-name value; if it's `AutoCloseable`, `close()` is registered automatically (runtime check).
168
+ - `Resource.fromAutoCloseable(thunk: => A <: AutoCloseable)`
169
+ Type-safe helper that registers `close()`.
170
+ - `Resource.acquireRelease(acquire: => A)(release: A => Unit)`
171
+ - `Resource.shared(f: Scope => A)`
172
+ **Memoized + reference-counted**, thread-safe.
173
+ - `Resource.unique(f: Scope => A)`
174
+ Fresh instance per allocation.
175
+ - `Resource.from[T]` and `Resource.from[T](wires*)` (macros)
176
+ Constructor-based dependency injection (covered below).
170
177
 
171
- Common constructors:
178
+ #### Composition
172
179
 
173
- - `Resource(a)`
174
- - Wraps a by-name value; if it's `AutoCloseable`, `close()` is registered automatically.
175
- - `Resource.acquireRelease(acquire)(release)`
176
- - Explicit lifecycle.
177
- - `Resource.fromAutoCloseable(thunk)`
178
- - A type-safe helper for `AutoCloseable`.
179
- - `Resource.from[T](wires*)` (macro)
180
- - The primary entry point for dependency injection.
181
- - Resolves `T` and all its dependencies into a single `Resource[T]`.
182
- - Auto-creates missing wires using `Wire.shared` for concrete classes.
183
- - Requires explicit wires for: primitives, functions, collections, and abstract types.
184
- - If `T` or any dependency is `AutoCloseable`, registers `close()` automatically.
180
+ `Resource` composes with:
185
181
 
186
- #### Resource "sharing" vs "uniqueness"
182
+ - `map`
183
+ - `flatMap`
184
+ - `zip`
187
185
 
188
- `Resource` has two important internal flavors:
186
+ Finalizers remain tied to the allocation scope; in composed resources, finalizers still run LIFO.
189
187
 
190
- - `Resource.Unique[A]`
191
- - Produces a **fresh** instance every time you allocate it (typical for `Resource(...)`, `acquireRelease`, etc.).
192
- - `Resource.Shared[A]`
193
- - Produces a **shared** instance per `Resource.Shared` value, with **reference counting**:
194
- - the first allocation initializes the value and collects finalizers
195
- - each allocating scope registers a decrement finalizer
196
- - when the reference count reaches zero, the collected finalizers run
188
+ #### Sharing vs uniqueness (important)
197
189
 
198
- **Important clarification:** sharing is **not** "memoized within a Wire graph" or "within a scope" by magic. Sharing happens **within the specific `Resource.Shared` instance** you reuse.
190
+ There are two distinct ideas:
191
+
192
+ 1. **Uniqueness**: "each allocation yields a fresh instance"
193
+ - Use `Resource.unique(...)`, or most ordinary `Resource(...)` / `acquireRelease(...)` resources.
194
+ - Each `allocate` runs the acquisition again and registers an independent finalizer.
195
+
196
+ 2. **Sharing**: "reusing the same instance across multiple allocations"
197
+ - Use `Resource.shared(...)` (or wires/resources that convert to shared).
198
+ - Sharing is tied to **reusing the same `Resource.Shared` value**, not "magic caching inside a scope".
199
+ - The first allocation initializes via an `OpenScope` parented to `Scope.global`; subsequent allocations increment a reference count. When the last referencing scope closes, the shared scope is closed.
199
200
 
200
201
  ---
201
202
 
202
- ### 4) `Unscoped`: marking pure data types
203
+ ### 4) `Unscoped[A]`: types that may escape a scope
204
+
205
+ `Unscoped[A]` is a marker typeclass for *pure data*. It's used in two places:
206
+
207
+ 1. `Scope.scoped` requires `Unscoped[A]` for the block's result type
208
+ ⇒ prevents returning resources, closures, or scoped values.
209
+ 2. `$` auto-unwraps results of type `B` when `B: Unscoped`.
210
+
211
+ Built-in instances include primitives, `String`, many collections/containers, time values, `java.util.UUID`, and `zio.blocks.chunk.Chunk` (when element types are unscoped).
203
212
 
204
- The `Unscoped[A]` typeclass marks types as pure data that don't hold resources. The `scoped` method requires `Unscoped[A]` evidence on the return type to ensure only safe values can exit a scope.
213
+ #### Deriving / defining your own instances
205
214
 
206
- **Built-in Unscoped types:**
207
- - Primitives: `Int`, `Long`, `Boolean`, `Double`, etc.
208
- - `String`, `Unit`, `Nothing`
209
- - Collections of Unscoped types
215
+ Scala 3 (derivation via `Unscoped.derived`):
210
216
 
211
- **Custom Unscoped types:**
212
217
  ```scala
213
- // Scala 3:
214
- case class Config(debug: Boolean)
215
- object Config {
218
+ import zio.blocks.scope.*
219
+
220
+ final case class Config(debug: Boolean)
221
+ object Config:
216
222
  given Unscoped[Config] = Unscoped.derived
217
- }
223
+ ```
218
224
 
219
- // Scala 2:
220
- case class Config(debug: Boolean)
225
+ Scala 2.13:
226
+
227
+ ```scala
228
+ import zio.blocks.scope.*
229
+
230
+ final case class Config(debug: Boolean)
221
231
  object Config {
222
232
  implicit val unscopedConfig: Unscoped[Config] = Unscoped.derived[Config]
223
233
  }
224
234
  ```
225
235
 
226
- **Allowed return types from `scoped`:**
236
+ #### Scope boundary example
227
237
 
228
- - **`Unscoped` types**: Pure data that can safely exit
229
- - **`Nothing`**: For blocks that throw
238
+ ```scala
239
+ import zio.blocks.scope.*
230
240
 
231
- **Rejected return types (no Unscoped instance):**
241
+ Scope.global.scoped { parent =>
242
+ import parent.*
243
+
244
+ val ok: String =
245
+ parent.scoped { child =>
246
+ "hello" // String is Unscoped
247
+ }
232
248
 
233
- - **Closures**: `() => A` could capture the child scope
234
- - **Scoped values**: `$[A]` would be use-after-close
235
- - **The scope itself**: Would allow operations after close
249
+ // Does not compile: returning a resourceful value from a scoped block
250
+ // val leaked: Database =
251
+ // parent.scoped { child =>
252
+ // import child.*
253
+ // Resource.fromAutoCloseable(new Database).allocate
254
+ // }
255
+
256
+ ok
257
+ }
258
+ ```
259
+
260
+ ---
261
+
262
+ ### 5) `lower`: using a parent-scoped value in a child scope
263
+
264
+ Because each scope has its own `$[A]` type, a child cannot directly use a parent's `$[A]`. Use `lower` to retag a parent-scoped value into the child:
236
265
 
237
266
  ```scala
238
- Scope.global.scoped { parent =>
239
- import parent._
267
+ import zio.blocks.scope.*
240
268
 
241
- // ✅ OK: String is Unscoped
242
- val result: String = scoped { child =>
243
- "hello"
244
- }
269
+ Scope.global.scoped { outer =>
270
+ import outer.*
245
271
 
246
- // ❌ COMPILE ERROR: $[Database] has no Unscoped instance
247
- // val escaped = scoped { child =>
248
- // import child._
249
- // allocate(Resource(new Database)) // $[Database] can't escape
250
- // }
272
+ val db: $[Database] = Resource.fromAutoCloseable(new Database).allocate
273
+
274
+ outer.scoped { inner =>
275
+ import inner.*
276
+ val innerDb: $[Database] = lower(db)
277
+ $(innerDb)(_.query("child"))
278
+ }
251
279
  }
252
280
  ```
253
281
 
282
+ This is safe because **parents always outlive children** (child finalizers run before the parent closes).
283
+
254
284
  ---
255
285
 
256
- ### 5) `lower`: accessing parent-scoped values
286
+ ### 6) `defer`: manual finalizers (+ cancellation)
257
287
 
258
- When working in a child scope, you may need to access values allocated in a parent scope. Use `lower(parentValue)` to "lower" a parent-scoped value into the child scope:
288
+ Use `defer` to register cleanup. It returns a `DeferHandle` you can cancel.
259
289
 
260
290
  ```scala
261
- Scope.global.scoped { parent =>
262
- import parent._
291
+ import zio.blocks.scope.*
263
292
 
264
- val parentDb: $[Database] = allocate(Resource(new Database))
293
+ Scope.global.scoped { scope =>
294
+ import scope.*
265
295
 
266
- scoped { child =>
267
- import child._
296
+ val in = new java.io.ByteArrayInputStream(Array[Byte](1, 2, 3))
268
297
 
269
- // Use lower() to access parent-scoped value in child scope
270
- val db: $[Database] = lower(parentDb)
271
- child.use(db)(_.query("SELECT 1"))
298
+ val h: DeferHandle =
299
+ defer(in.close())
272
300
 
273
- "done"
274
- }
301
+ val first = in.read()
302
+ println(first)
303
+
304
+ // If you already cleaned up manually:
305
+ // h.cancel() // thread-safe, idempotent
275
306
  }
276
307
  ```
277
308
 
278
- The `lower` operation is necessary because each scope has its own `$[A]` opaque type. A parent's `$[A]` is a different type than a child's `$[A]`, even though both equal `A` at runtime.
309
+ There is also a **package-level** helper that only requires a `Finalizer`:
310
+
311
+ ```scala
312
+ import zio.blocks.scope.*
313
+
314
+ Scope.global.scoped { scope =>
315
+ import scope.*
316
+ given Finalizer = scope
317
+
318
+ defer(println("cleanup")) // uses the package-level helper
319
+ }
320
+ ```
279
321
 
280
322
  ---
281
323
 
282
- ### 6) `Wire[-In, +Out]`: dependency recipes
324
+ ### 7) `open()`: non-lexical, explicitly-managed child scopes
283
325
 
284
- `Wire` is a recipe for constructing services. It describes **how** to build a service given its dependencies, but does not resolve those dependencies itself.
326
+ `scoped` ties lifetime to a block. `open()` creates a child scope you close explicitly.
285
327
 
286
- - `In` is the required dependencies (provided as a `Context[In]`)
287
- - `Out` is the produced service
328
+ - The child scope is **unowned** (can be used from any thread)
329
+ - Still **linked to the parent**: parent closing will also close the child
330
+ - You must call `close()` on the handle to detach + finalize now
288
331
 
289
- There are two wire flavors:
332
+ From `Scope.global` the returned type is `Scope.OpenScope` directly (because global `$[A] = A`):
290
333
 
291
- - `Wire.Shared`: produces a shared (memoized) instance
292
- - `Wire.Unique`: produces a fresh instance each time
334
+ ```scala
335
+ import zio.blocks.scope.*
293
336
 
294
- **Important clarification:** `Wire` itself is just a recipe. The sharing/uniqueness behavior is realized when the wire is used inside `Resource.from`, which composes `Resource.Shared` or `Resource.Unique` instances accordingly.
337
+ val os: Scope.OpenScope = Scope.global.open()
295
338
 
296
- #### Creating wires
339
+ val db = os.scope.allocate(Resource.fromAutoCloseable(new Database))
297
340
 
298
- There are exactly **3 macro entry points**:
341
+ // ... use db ...
299
342
 
300
- | Macro | Purpose |
301
- |-------|---------|
302
- | `Wire.shared[T]` | Create a shared wire from `T`'s constructor |
303
- | `Wire.unique[T]` | Create a unique wire from `T`'s constructor |
304
- | `Resource.from[T](wires*)` | Wire up `T` and all dependencies into a `Resource` |
343
+ os.close().orThrow()
344
+ ```
305
345
 
306
- For wrapping pre-existing values:
346
+ Inside a child scope, `open()` returns `$[Scope.OpenScope]`. Prefer using it safely via `$`:
307
347
 
308
- - `Wire(value)` — wraps a value; if `AutoCloseable`, registers `close()` automatically
348
+ ```scala
349
+ import zio.blocks.scope.*
309
350
 
310
- #### How `Resource.from[T](wires*)` works
351
+ Scope.global.scoped { parent =>
352
+ import parent.*
311
353
 
312
- 1. **Collect wires**: Uses explicit wires when provided, otherwise auto-creates with `Wire.shared`
313
- 2. **Validate**: Checks for cycles, unmakeable types, duplicate providers
314
- 3. **Topological sort**: Orders dependencies so leaves are allocated first
315
- 4. **Generate composition**: Produces a `Resource[T]` via flatMap chains
354
+ val os: $[Scope.OpenScope] = open()
316
355
 
317
- Key insight: **Compose Resources, don't accumulate values.** Each wire becomes a `Resource`, and they are composed via `flatMap`. This correctly preserves:
356
+ $(os) { h =>
357
+ val child = h.scope
358
+ val db = child.allocate(Resource.fromAutoCloseable(new Database))
318
359
 
319
- - **Sharing**: Same `Resource.Shared` instance → same value (even in diamond patterns)
320
- - **Uniqueness**: `Resource.Unique` → fresh value per injection site
360
+ // ...
361
+ h.close().orThrow()
362
+ }
363
+ }
364
+ ```
321
365
 
322
- #### Subtype resolution
366
+ ---
323
367
 
324
- When `Resource.from` needs a dependency of type `Service`, it will accept a wire whose output is a subtype (e.g., `Wire.shared[LiveService]` where `LiveService extends Service`). This enables trait injection without extra boilerplate.
368
+ ### 8) Escape hatch: `leak`
325
369
 
326
- If the same concrete wire satisfies multiple types (e.g., `Service` and `LiveService`), only **one instance** is created and reused for both.
370
+ Sometimes you must hand a raw value to code that cannot work with `$[A]`. Use `leak`:
371
+
372
+ ```scala
373
+ import zio.blocks.scope.*
374
+
375
+ Scope.global.scoped { scope =>
376
+ import scope.*
377
+
378
+ val db: $[Database] = Resource.fromAutoCloseable(new Database).allocate
379
+
380
+ val raw: Database = leak(db) // emits a compiler warning
381
+ // thirdParty(raw)
382
+ }
383
+ ```
384
+
385
+ `leak` bypasses compile-time guarantees—use only for unavoidable interop. If the type is genuinely pure data, prefer adding `Unscoped` so you don't need to leak.
327
386
 
328
387
  ---
329
388
 
330
389
  ## Safety model (why leaking is prevented)
331
390
 
332
- **Pragmatic safety.** The type-level tagging prevents *accidental* scope misuse in normal code, but it is not a security boundary. A determined developer can bypass it via `leak` (which emits a compiler warning), unsafe casts (`asInstanceOf`), or storing scoped references in mutable state (`var`). The guarantees are "good enough" to catch mistakes in regular usage, not protection against intentional circumvention.
391
+ Scope's safety comes from *three reinforcing layers*.
392
+
393
+ ### 1) Type barrier: scope-specific `$[A]`
333
394
 
334
- The library prevents scope leaks via two reinforcing mechanisms:
395
+ Every scope has a distinct `$[A]` type. You cannot accidentally use values across scopes without an explicit conversion (`lower` for parent → child).
335
396
 
336
- ### A) Existential child tags (fresh, unnameable types)
397
+ ### 2) Controlled access: `$` macro restricts lambda usage
337
398
 
338
- Child scopes are created with:
399
+ The `$` operator only allows using the unwrapped value as a **method/field receiver**. This prevents:
400
+
401
+ - returning the resource
402
+ - storing it in a local val/var
403
+ - passing it as an argument
404
+ - capturing it in a closure
405
+
406
+ Also note: `$` requires a **lambda literal**. Method references / variables are rejected:
339
407
 
340
408
  ```scala
341
- Scope.global.scoped { scope =>
342
- import scope._
343
- scoped { child =>
344
- import child._
345
- // allocate in child
346
- }
347
- }
409
+ // does not compile:
410
+ val f: Database => String = _.query("x")
411
+ (scope $ db)(f) // "$ requires a lambda literal ..."
348
412
  ```
349
413
 
350
- The child scope has a fresh, unnameable `$[A]` type (created per invocation). You can allocate in the child, but you can't return those values to the parent in a usable form because the parent cannot name (or satisfy) the child's `$[A]` type.
414
+ ### 3) Scope boundary rule: `scoped` requires `Unscoped[A]`
415
+
416
+ A `scoped { ... }` block can only return pure data (or `Nothing`). Resources and closures cannot escape.
417
+
418
+ **Pragmatic safety.** The type-level tagging prevents *accidental* scope misuse in normal code, but it is not a security boundary. A determined developer can bypass it via `leak` (which emits a compiler warning), unsafe casts (`asInstanceOf`), or storing scoped references in mutable state (`var`).
419
+
420
+ ### Closed-scope defense (runtime)
421
+
422
+ If a scope escapes and is used after closing, operations become **no-ops returning default values**:
351
423
 
352
- Compile-time safety is verified in tests, e.g.:
353
- `ScopeCompileTimeSafetyScala3Spec`.
424
+ - `$`, `allocate`, `open`, `lower` return defaults (`null`, `0`, `false`, …)
425
+ - `$` does not run the lambda when closed
426
+ - `defer` on a closed scope is ignored
427
+ - `scoped` creates a born-closed child if the parent is already closed
354
428
 
355
- ### B) Opaque types prevent escape
429
+ This prevents post-close interaction with released resources, but can produce surprising default values if scopes are misused across threads.
356
430
 
357
- Each scope defines its own `$[A]` opaque type. Even though `$[A] = A` at runtime, the compiler treats each scope's `$[A]` as distinct. A child's `$[Database]` is a different type than the parent's `$[Database]`.
431
+ ### Thread ownership rule (JVM)
358
432
 
359
- Additionally, the opaque type hides `A`'s methods at the type level — you can't call `db.query(...)` directly on a `$[Database]`. Access routes are `scope.use(value)(f)` and the `ScopedOps` methods (`map`, `flatMap`) for for-comprehensions.
433
+ - Scopes created by `scoped` are **owned** by the entering thread.
434
+ - Calling `scoped` on a scope you don't own throws `IllegalStateException`.
435
+ - `open()` creates an **unowned** child scope (`isOwner == true` from any thread).
436
+
437
+ (Scala.js uses a trivial ownership model; `isOwner` is effectively always true.)
360
438
 
361
439
  ---
362
440
 
363
- ## Usage examples
441
+ ## Usage examples (patterns)
364
442
 
365
443
  ### Allocating and using a resource
366
444
 
367
445
  ```scala
368
- import zio.blocks.scope._
446
+ import zio.blocks.scope.*
369
447
 
370
- final class FileHandle(path: String) extends AutoCloseable {
448
+ final class FileHandle(path: String) extends AutoCloseable:
371
449
  def readAll(): String = s"contents of $path"
372
450
  def close(): Unit = println(s"closed $path")
373
- }
374
451
 
375
- Scope.global.scoped { scope =>
376
- import scope._
452
+ @main def fileExample(): Unit =
453
+ Scope.global.scoped { scope =>
454
+ import scope.*
377
455
 
378
- val h: $[FileHandle] = allocate(Resource(new FileHandle("data.txt")))
456
+ val h: $[FileHandle] =
457
+ Resource(new FileHandle("data.txt")).allocate
379
458
 
380
- // scope.use applies function to scoped value, returns $[String]
381
- val contents: $[String] = scope.use(h)(_.readAll())
382
- println(contents) // $[String] = String at runtime
383
- }
459
+ val contents: String =
460
+ $(h)(_.readAll())
461
+
462
+ println(contents)
463
+ }
384
464
  ```
385
465
 
386
466
  ---
@@ -388,312 +468,385 @@ Scope.global.scoped { scope =>
388
468
  ### Nested scopes (child can use parent, not vice versa)
389
469
 
390
470
  ```scala
391
- import zio.blocks.scope._
471
+ import zio.blocks.scope.*
392
472
 
393
- Scope.global.scoped { parent =>
394
- import parent._
473
+ final class Database extends AutoCloseable:
474
+ def query(sql: String): String = s"result: $sql"
475
+ def close(): Unit = println("db closed")
395
476
 
396
- val parentDb: $[Database] = allocate(Resource(new Database))
477
+ @main def nested(): Unit =
478
+ Scope.global.scoped { parent =>
479
+ import parent.*
397
480
 
398
- scoped { child =>
399
- import child._
481
+ val parentDb: $[Database] = Resource.fromAutoCloseable(new Database).allocate
400
482
 
401
- // Use lower() to access parent-scoped values in child scope:
402
- val db: $[Database] = lower(parentDb)
403
- println(child.use(db)(_.query("SELECT 1")))
483
+ val done: String =
484
+ parent.scoped { child =>
485
+ import child.*
404
486
 
405
- val childDb: $[Database] = allocate(Resource(new Database))
487
+ val db: $[Database] = lower(parentDb)
488
+ println($(db)(_.query("SELECT 1")))
406
489
 
407
- // You can use childDb *inside* the child:
408
- println(child.use(childDb)(_.query("SELECT 2")))
490
+ val childDb: $[Database] = Resource.fromAutoCloseable(new Database).allocate
491
+ println($(childDb)(_.query("SELECT 2")))
409
492
 
410
- // But you cannot return childDb to the parent:
411
- // $[Database] has no Unscoped instance — compile error
493
+ // childDb cannot be returned to the parent (not Unscoped)
494
+ "done"
495
+ }
412
496
 
413
- // Return an Unscoped value
414
- "done"
497
+ println($(parentDb)(_.query("SELECT 3")))
498
+ done
415
499
  }
416
-
417
- // parentDb is still usable here:
418
- println(parent.use(parentDb)(_.query("SELECT 3")))
419
- }
420
500
  ```
421
501
 
502
+ Finalizers run **child first, then parent**.
503
+
422
504
  ---
423
505
 
424
- ### Chaining resource acquisition
506
+ ### Chaining resource acquisition (`$[Resource[A]]` + `.allocate`)
425
507
 
426
- Since `$[A]` supports `map` and `flatMap` via `ScopedOps`, you can chain resource acquisitions in for-comprehensions:
508
+ If a method returns `Resource[A]`, `$` returns a **scoped** `Resource[A]` (because `Resource[A]` is not `Unscoped`). Allocate it without leaking:
427
509
 
428
510
  ```scala
429
- import zio.blocks.scope._
511
+ import zio.blocks.scope.*
430
512
 
431
- class Pool extends AutoCloseable {
432
- def lease(): Connection = new Connection
513
+ final class Pool extends AutoCloseable:
514
+ def lease(): Resource[Conn] = Resource.fromAutoCloseable(new Conn)
433
515
  def close(): Unit = println("pool closed")
434
- }
435
516
 
436
- class Connection extends AutoCloseable {
517
+ final class Conn extends AutoCloseable:
437
518
  def query(sql: String): String = s"result: $sql"
438
519
  def close(): Unit = println("connection closed")
439
- }
440
520
 
441
- Scope.global.scoped { scope =>
442
- import scope._
521
+ @main def chaining(): Unit =
522
+ Scope.global.scoped { scope =>
523
+ import scope.*
443
524
 
444
- // Chain allocations in a for-comprehension:
445
- // flatMap unwraps $[Pool] to Pool, so pool.lease() returns a raw Connection
446
- val result: $[String] = for {
447
- pool <- allocate(Resource.from[Pool])
448
- conn <- allocate(Resource(pool.lease()))
449
- } yield conn.query("SELECT 1")
525
+ val pool: $[Pool] = Resource.fromAutoCloseable(new Pool).allocate
450
526
 
451
- println(result)
452
- }
453
- // Output: result: SELECT 1
454
- // Then: connection closed, pool closed (LIFO)
527
+ // $(pool)(_.lease()) : $[Resource[Conn]]
528
+ val conn: $[Conn] =
529
+ $(pool)(_.lease()).allocate
530
+
531
+ val result: String =
532
+ $(conn)(_.query("SELECT 1"))
533
+
534
+ println(result)
535
+ }
455
536
  ```
456
537
 
538
+ This `.allocate` comes from `Scope.ScopedResourceOps` (an extension on `$[Resource[A]]`).
539
+
457
540
  ---
458
541
 
459
- ### Registering cleanup manually with `defer`
542
+ ### Allocating a bare `Resource[A]` with `.allocate`
460
543
 
461
- Use `defer` when you already have a value and just need to register cleanup.
544
+ A plain `Resource[A]` also has `.allocate` as syntax sugar for `scope.allocate(resource)`:
462
545
 
463
546
  ```scala
464
- import zio.blocks.scope._
547
+ import zio.blocks.scope.*
465
548
 
466
549
  Scope.global.scoped { scope =>
467
- import scope._
550
+ import scope.*
468
551
 
469
- val handle = new java.io.ByteArrayInputStream(Array[Byte](1, 2, 3))
552
+ val db: $[Database] =
553
+ Resource.fromAutoCloseable(new Database).allocate
470
554
 
471
- defer { handle.close() }
472
-
473
- val firstByte = handle.read()
474
- println(firstByte)
555
+ $(db)(_.query("SELECT 1"))
475
556
  }
476
557
  ```
477
558
 
478
- There is also a package-level helper `defer` that only requires a `Finalizer`:
559
+ ---
560
+
561
+ ### Classes with `Finalizer` parameters (cleanup-only capability)
562
+
563
+ If a class only needs cleanup registration, accept a `Finalizer`. DI macros inject it automatically.
479
564
 
480
565
  ```scala
481
- import zio.blocks.scope._
566
+ import zio.blocks.scope.*
482
567
 
483
- Scope.global.scoped { scope =>
484
- import scope._
485
- given Finalizer = scope
568
+ final case class Config(url: String)
569
+ object Config:
570
+ given Unscoped[Config] = Unscoped.derived
486
571
 
487
- defer { println("cleanup") }
488
- }
572
+ final class ConnectionPool(config: Config)(using Finalizer):
573
+ private val pool = s"pool(${config.url})"
574
+ defer(println(s"shutdown $pool"))
575
+
576
+ val poolResource: Resource[ConnectionPool] =
577
+ Resource.from[ConnectionPool](
578
+ Wire(Config("jdbc://localhost"))
579
+ )
580
+
581
+ @main def finalizerInjection(): Unit =
582
+ Scope.global.scoped { scope =>
583
+ import scope.*
584
+ val pool: $[ConnectionPool] = poolResource.allocate
585
+ ()
586
+ }
489
587
  ```
490
588
 
589
+ When to prefer `Finalizer` over `Scope`:
590
+
591
+ - you only need `defer`
592
+ - you want to expose minimal power to the class
593
+
491
594
  ---
492
595
 
493
- ### Classes with `Finalizer` parameters
596
+ ### Classes with `Scope` parameters (scope injection)
494
597
 
495
- If your class needs to register cleanup logic, accept a `Finalizer` parameter (not `Scope`). The wire and resource macros automatically inject the `Finalizer` when constructing such classes.
598
+ If a class needs to allocate resources or create child scopes, accept a `Scope`:
496
599
 
497
600
  ```scala
498
- import zio.blocks.scope._
601
+ import zio.blocks.scope.*
499
602
 
500
- class ConnectionPool(config: Config)(implicit finalizer: Finalizer) {
501
- private val pool = createPool(config)
502
- finalizer.defer { pool.shutdown() } // or: defer { ... } with import zio.blocks.scope._
503
-
504
- def getConnection(): Connection = pool.acquire()
505
- }
603
+ final case class Config(url: String)
604
+ object Config:
605
+ given Unscoped[Config] = Unscoped.derived
506
606
 
507
- // The macro sees the implicit Finalizer and injects it automatically:
508
- val resource = Resource.from[ConnectionPool](Wire(Config("jdbc://localhost")))
607
+ final class Connection(config: Config) extends AutoCloseable:
608
+ def query(sql: String): String = s"[${config.url}] $sql"
609
+ def close(): Unit = println("connection closed")
509
610
 
510
- Scope.global.scoped { scope =>
511
- import scope._
512
- val pool = allocate(resource)
513
- // pool.shutdown() will be called when scope closes
514
- }
611
+ final class RequestHandler(config: Config)(using scope: Scope):
612
+ def handle(sql: String): String =
613
+ scope.scoped { child =>
614
+ import child.*
615
+ val conn: $[Connection] = Resource.fromAutoCloseable(new Connection(config)).allocate
616
+ $(conn)(_.query(sql))
617
+ }
618
+
619
+ val handlerResource: Resource[RequestHandler] =
620
+ Resource.from[RequestHandler](
621
+ Wire(Config("jdbc://localhost"))
622
+ )
623
+
624
+ @main def scopeInjection(): Unit =
625
+ Scope.global.scoped { scope =>
626
+ import scope.*
627
+ val handler: $[RequestHandler] = handlerResource.allocate
628
+ val out: String = $(handler)(_.handle("SELECT 1"))
629
+ println(out)
630
+ }
515
631
  ```
516
632
 
517
- Why `Finalizer` instead of `Scope`?
518
- - `Finalizer` is the minimal interface—it only has `defer`
519
- - Classes that need cleanup should not have access to `allocate` or `use`
520
- - The macros pass a `Finalizer` at runtime, so declaring `Scope` would be misleading
633
+ The `Scope`/`Finalizer` parameter can appear in any parameter list position; it's recognized specially by the derivation macros.
521
634
 
522
635
  ---
523
636
 
524
- ### Dependency injection with `Wire` + `Context`
637
+ ## Dependency injection (DI) with `Wire` + `Resource.from`
638
+
639
+ Scope includes a small constructor-based DI layer built on top of `zio.blocks.context.Context`.
525
640
 
526
- For manual wiring (when you already have dependencies assembled), use `wire.toResource(ctx)`:
641
+ ### `Wire[-In, +Out]`: a dependency recipe
642
+
643
+ A `Wire` is a recipe for constructing `Out` from a `Context[In]` (and a `Scope` for finalization):
644
+
645
+ - `Wire.Shared` → converts to `Resource.shared` (ref-counted sharing)
646
+ - `Wire.Unique` → converts to `Resource.unique` (fresh instance)
647
+
648
+ #### Manual wire + Context
527
649
 
528
650
  ```scala
529
- import zio.blocks.scope._
651
+ import zio.blocks.scope.*
530
652
  import zio.blocks.context.Context
531
653
 
532
654
  final case class Config(debug: Boolean)
655
+ object Config:
656
+ given Unscoped[Config] = Unscoped.derived
533
657
 
534
- val w: Wire.Shared[Boolean, Config] = Wire.shared[Config]
535
- val deps: Context[Boolean] = Context[Boolean](true)
658
+ val w: Wire.Shared[Boolean, Config] =
659
+ Wire.shared[Config] // Boolean => Config
536
660
 
537
- Scope.global.scoped { scope =>
538
- import scope._
661
+ val deps: Context[Boolean] =
662
+ Context(true)
539
663
 
540
- val cfg: $[Config] = allocate(w.toResource(deps))
664
+ @main def wireAndContext(): Unit =
665
+ Scope.global.scoped { scope =>
666
+ import scope.*
541
667
 
542
- val debug: $[Boolean] = scope.use(cfg)(_.debug)
668
+ val cfg: $[Config] =
669
+ allocate(w.toResource(deps))
543
670
 
544
- println(debug) // $[Boolean] = Boolean at runtime
545
- }
671
+ val debug: Boolean =
672
+ $(cfg)(_.debug)
673
+
674
+ println(debug)
675
+ }
546
676
  ```
547
677
 
548
- Sharing vs uniqueness at the wire level:
678
+ #### Sharing vs uniqueness at the wire level
549
679
 
550
680
  ```scala
551
- import zio.blocks.scope._
681
+ import zio.blocks.scope.*
552
682
 
553
- val ws = Wire.shared[Config] // shared recipe; sharing happens via Resource.Shared when allocated
554
- val wu = Wire.unique[Config] // unique recipe; each allocation is fresh
683
+ val ws = Wire.shared[Config] // shared recipe
684
+ val wu = Wire.unique[Config] // unique recipe
555
685
  ```
556
686
 
687
+ The difference is realized when converting to resources (`toResource`) and allocating.
688
+
557
689
  ---
558
690
 
559
- ### Dependency injection with `Resource.from[T](wires*)`
691
+ ### `Resource.from[T](wires*)`: derive a whole object graph
692
+
693
+ `Resource.from[T](wires*)` is the primary entry point for DI. It:
560
694
 
561
- `Resource.from[T](wires*)` is the **primary entry point** for dependency injection. It resolves `T` and all its dependencies into a single `Resource[T]`.
695
+ - uses provided wires as overrides
696
+ - auto-creates missing wires for **concrete classes** (defaulting to shared)
697
+ - rejects unmakeable/abstract types unless you provide a wire
698
+ - detects cycles, duplicate providers, and subtype conflicts
699
+ - generates a composed `Resource[T]` via `flatMap` chains (preserving sharing/uniqueness)
700
+
701
+ Example:
562
702
 
563
703
  ```scala
564
- import zio.blocks.scope._
704
+ import zio.blocks.scope.*
565
705
 
566
706
  final case class Config(url: String)
707
+ object Config:
708
+ given Unscoped[Config] = Unscoped.derived
567
709
 
568
- final class Logger {
710
+ final class Logger:
569
711
  def info(msg: String): Unit = println(msg)
570
- }
571
712
 
572
- final class Database(cfg: Config) extends AutoCloseable {
713
+ final class Database(cfg: Config) extends AutoCloseable:
573
714
  def query(sql: String): String = s"[${cfg.url}] $sql"
574
715
  def close(): Unit = println("database closed")
575
- }
576
716
 
577
- final class Service(db: Database, logger: Logger) extends AutoCloseable {
717
+ final class Service(db: Database, logger: Logger) extends AutoCloseable:
578
718
  def run(): Unit = logger.info(s"running with ${db.query("SELECT 1")}")
579
719
  def close(): Unit = println("service closed")
580
- }
581
720
 
582
- // Only provide leaf values (primitives, configs) - the rest is auto-wired:
583
721
  val serviceResource: Resource[Service] =
584
722
  Resource.from[Service](
585
- Wire(Config("jdbc:postgresql://localhost/db"))
723
+ Wire(Config("jdbc:postgresql://localhost/db")) // leaf value
586
724
  )
587
725
 
588
- Scope.global.scoped { scope =>
589
- import scope._
590
- val svc: $[Service] = allocate(serviceResource)
591
- scope.use(svc)(_.run())
592
- }
593
- // Output: running with [jdbc:postgresql://localhost/db] SELECT 1
594
- // Then: service closed, database closed (LIFO order)
726
+ @main def di(): Unit =
727
+ Scope.global.scoped { scope =>
728
+ import scope.*
729
+ val svc: $[Service] = serviceResource.allocate
730
+ $(svc)(_.run())
731
+ }
595
732
  ```
596
733
 
597
- **What you must provide:**
598
- - Leaf values: primitives, configs, pre-existing instances via `Wire(value)`
599
- - Abstract types: traits/abstract classes via `Wire.shared[ConcreteImpl]`
600
- - Overrides: when you want `unique` instead of the default `shared`
734
+ #### Injecting traits via subtype wires
601
735
 
602
- **What is auto-created:**
603
- - Concrete classes with accessible primary constructors (default: `Wire.shared`)
604
-
605
- ---
606
-
607
- ### Injecting traits via subtype wires
608
-
609
- When a dependency is a trait or abstract class, provide a wire for a concrete implementation:
736
+ When a dependency is abstract, provide a wire for a concrete implementation:
610
737
 
611
738
  ```scala
612
- import zio.blocks.scope._
739
+ import zio.blocks.scope.*
613
740
 
614
- trait Logger {
741
+ trait Logger:
615
742
  def info(msg: String): Unit
616
- }
617
743
 
618
- final class ConsoleLogger extends Logger {
744
+ final class ConsoleLogger extends Logger:
619
745
  def info(msg: String): Unit = println(msg)
620
- }
621
746
 
622
- final class App(logger: Logger) {
747
+ final class App(logger: Logger):
623
748
  def run(): Unit = logger.info("Hello!")
624
- }
625
749
 
626
- // Wire.shared[ConsoleLogger] satisfies the Logger dependency via subtyping:
627
750
  val appResource: Resource[App] =
628
751
  Resource.from[App](
629
- Wire.shared[ConsoleLogger]
752
+ Wire.shared[ConsoleLogger] // satisfies Logger via subtyping
630
753
  )
631
754
 
632
- Scope.global.scoped { scope =>
633
- import scope._
634
- val app: $[App] = allocate(appResource)
635
- scope.use(app)(_.run())
636
- }
755
+ @main def traitInjection(): Unit =
756
+ Scope.global.scoped { scope =>
757
+ import scope.*
758
+ val app: $[App] = appResource.allocate
759
+ $(app)(_.run())
760
+ }
637
761
  ```
638
762
 
639
- **Single instance for diamond patterns:**
763
+ #### Diamond patterns share a single instance (when appropriate)
640
764
 
641
765
  ```scala
766
+ import zio.blocks.scope.*
767
+
642
768
  trait Service
643
- class LiveService extends Service
644
- class NeedsService(s: Service)
645
- class NeedsLive(l: LiveService)
646
- class App(a: NeedsService, b: NeedsLive)
769
+ final class LiveService extends Service
770
+ final class NeedsService(s: Service)
771
+ final class NeedsLive(l: LiveService)
772
+ final class App(a: NeedsService, b: NeedsLive)
647
773
 
648
- // One LiveService instance satisfies both Service and LiveService dependencies:
649
- val appResource = Resource.from[App](
650
- Wire.shared[LiveService]
651
- )
652
- // count of LiveService instantiations: 1
774
+ val appResource: Resource[App] =
775
+ Resource.from[App](
776
+ Wire.shared[LiveService]
777
+ )
778
+ // LiveService instantiations: 1
653
779
  ```
654
780
 
655
781
  ---
656
782
 
657
- ### Interop escape hatch: `leak`
783
+ ## Common compile errors (and what they mean)
658
784
 
659
- Sometimes you must hand a raw value to code that cannot work with `$[A]` types.
785
+ This module produces two kinds of compile-time feedback:
660
786
 
661
- ```scala
662
- import zio.blocks.scope._
663
-
664
- Scope.global.scoped { scope =>
665
- import scope._
666
- val db: $[Database] = allocate(Resource(new Database))
787
+ - **Plain macro aborts** for unsafe `$` usage
788
+ - **ASCII-rendered** errors/warnings for DI derivation + leak warnings (via `internal.ErrorMessages`)
667
789
 
668
- val raw: Database = leak(db) // emits a compiler warning
669
- // thirdParty(raw)
670
- }
671
- ```
790
+ ### Unsafe use inside `$`
672
791
 
673
- **Warning:** leaking bypasses compile-time guarantees. The value may be used after its scope closes. Use only when unavoidable.
792
+ Typical messages include:
674
793
 
675
- ---
794
+ ```
795
+ Unsafe use of scoped value: the lambda parameter cannot be passed as an argument to a function or method.
796
+ ```
676
797
 
677
- ## Common compile errors
798
+ Other variants:
678
799
 
679
- The scope macros produce beautiful, actionable compile-time error messages with ASCII diagrams and helpful hints:
800
+ - `Unsafe use of scoped value: the lambda parameter cannot be captured in a nested lambda or closure.`
801
+ - `Unsafe use of scoped value: the lambda parameter must only be used as a method receiver ...`
802
+ - `$ requires a lambda literal: (scope $ x)(a => a.method()). Method references and variables are not supported.`
680
803
 
681
- ### Not a class (Wire.shared/unique on trait or abstract class)
804
+ ### Not a class (`Wire.shared/unique` on a trait / abstract)
682
805
 
683
806
  ```
684
- ── Scope Error ──────────────────────────────────────────────────────────────
807
+ ── Scope Error ─────────────────────────────────────────────────────────────────
685
808
 
686
809
  Cannot derive Wire for MyTrait: not a class.
687
810
 
688
811
  Hint: Use Wire.Shared / Wire.Unique directly.
689
812
 
690
- ────────────────────────────────────────────────────────────────────────────
813
+ ───────────────────────────────────────────────────────────────────────────────
814
+ ```
815
+
816
+ ### No primary constructor
817
+
818
+ ```
819
+ ── Scope Error ─────────────────────────────────────────────────────────────────
820
+
821
+ MyType has no primary constructor.
822
+
823
+ Hint: Use Wire.Shared / Wire.Unique directly
824
+ with a custom construction strategy.
825
+
826
+ ───────────────────────────────────────────────────────────────────────────────
827
+ ```
828
+
829
+ ### `Resource.from[T]` used when `T` has dependencies
830
+
831
+ ```
832
+ ── Scope Error ─────────────────────────────────────────────────────────────────
833
+
834
+ Resource.from[MyService] cannot be derived.
835
+
836
+ MyService has dependencies that must be provided:
837
+ • Config
838
+ • Logger
839
+
840
+ Hint: Use Resource.from[MyService](wire1, wire2, ...)
841
+ to provide wires for all dependencies.
842
+
843
+ ───────────────────────────────────────────────────────────────────────────────
691
844
  ```
692
845
 
693
846
  ### Unmakeable type (primitives, functions, collections)
694
847
 
695
848
  ```
696
- ── Scope Error ──────────────────────────────────────────────────────────────
849
+ ── Scope Error ─────────────────────────────────────────────────────────────────
697
850
 
698
851
  Cannot auto-create String
699
852
 
@@ -710,13 +863,13 @@ The scope macros produce beautiful, actionable compile-time error messages with
710
863
  ...
711
864
  )
712
865
 
713
- ────────────────────────────────────────────────────────────────────────────
866
+ ───────────────────────────────────────────────────────────────────────────────
714
867
  ```
715
868
 
716
- ### Abstract type (trait or abstract class)
869
+ ### Abstract type (trait / abstract class dependency)
717
870
 
718
871
  ```
719
- ── Scope Error ──────────────────────────────────────────────────────────────
872
+ ── Scope Error ─────────────────────────────────────────────────────────────────
720
873
 
721
874
  Cannot auto-create Logger
722
875
 
@@ -732,13 +885,13 @@ The scope macros produce beautiful, actionable compile-time error messages with
732
885
  ...
733
886
  )
734
887
 
735
- ────────────────────────────────────────────────────────────────────────────
888
+ ───────────────────────────────────────────────────────────────────────────────
736
889
  ```
737
890
 
738
891
  ### Duplicate providers (ambiguous wires)
739
892
 
740
893
  ```
741
- ── Scope Error ──────────────────────────────────────────────────────────────
894
+ ── Scope Error ────────────────────────────────────────────────────────────────
742
895
 
743
896
  Multiple providers for Service
744
897
 
@@ -748,13 +901,13 @@ The scope macros produce beautiful, actionable compile-time error messages with
748
901
 
749
902
  Hint: Remove duplicate wires or use distinct wrapper types.
750
903
 
751
- ────────────────────────────────────────────────────────────────────────────
904
+ ───────────────────────────────────────────────────────────────────────────────
752
905
  ```
753
906
 
754
907
  ### Dependency cycle
755
908
 
756
909
  ```
757
- ── Scope Error ──────────────────────────────────────────────────────────────
910
+ ── Scope Error ────────────────────────────────────────────────────────────────
758
911
 
759
912
  Dependency cycle detected
760
913
 
@@ -770,13 +923,13 @@ The scope macros produce beautiful, actionable compile-time error messages with
770
923
  • Using lazy initialization
771
924
  • Restructuring dependencies
772
925
 
773
- ────────────────────────────────────────────────────────────────────────────
926
+ ───────────────────────────────────────────────────────────────────────────────
774
927
  ```
775
928
 
776
929
  ### Subtype conflict (related dependency types)
777
930
 
778
931
  ```
779
- ── Scope Error ──────────────────────────────────────────────────────────────
932
+ ── Scope Error ────────────────────────────────────────────────────────────────
780
933
 
781
934
  Dependency type conflict in MyService
782
935
 
@@ -789,16 +942,16 @@ The scope macros produce beautiful, actionable compile-time error messages with
789
942
  To fix this, wrap one or both types in a distinct wrapper:
790
943
 
791
944
  case class WrappedInputStream(value: InputStream)
792
- or
945
+ or
793
946
  opaque type WrappedInputStream = InputStream
794
947
 
795
- ────────────────────────────────────────────────────────────────────────────
948
+ ───────────────────────────────────────────────────────────────────────────────
796
949
  ```
797
950
 
798
- ### Duplicate parameter types in constructor
951
+ ### Duplicate parameter types in a constructor
799
952
 
800
953
  ```
801
- ── Scope Error ──────────────────────────────────────────────────────────────
954
+ ── Scope Error ────────────────────────────────────────────────────────────────
802
955
 
803
956
  Constructor of App has multiple parameters of type String
804
957
 
@@ -807,139 +960,240 @@ The scope macros produce beautiful, actionable compile-time error messages with
807
960
  Fix: Wrap one parameter in an opaque type to distinguish them:
808
961
 
809
962
  opaque type FirstString = String
810
- or
963
+ or
811
964
  case class FirstString(value: String)
812
965
 
813
- ────────────────────────────────────────────────────────────────────────────
966
+ ───────────────────────────────────────────────────────────────────────────────
814
967
  ```
815
968
 
816
969
  ### Leak warning
817
970
 
818
- When using `leak(value)` to escape the scoped type system:
819
-
820
971
  ```
821
- ── Scope Warning ────────────────────────────────────────────────────────────
972
+ ── Scope Warning ───────────────────────────────────────────────────────────────
822
973
 
823
974
  leak(db)
824
975
  ^
825
976
  |
826
977
 
827
- Warning: db is being leaked from scope MyScope.
978
+ Warning: db is being leaked from scope zio.blocks.scope.Scope.Child[...].
828
979
  This may result in undefined behavior.
829
980
 
830
981
  Hint:
831
982
  If you know this data type is not resourceful, then add an Unscoped
832
983
  instance for it so you do not need to leak it.
833
984
 
834
- ────────────────────────────────────────────────────────────────────────────
985
+ ───────────────────────────────────────────────────────────────────────────────
835
986
  ```
836
987
 
837
988
  ---
838
989
 
839
- ## API reference (selected)
990
+ ## API reference (from source)
991
+
992
+ Examples below use Scala 3 syntax. Scala 2.13 has equivalent APIs, but macro signatures differ slightly (notably `$`'s return type encoding).
840
993
 
841
994
  ### `Scope`
842
995
 
843
- Core methods (Scala 3 `using` vs Scala 2 `implicit` differs, but the shapes are the same):
996
+ ```scala
997
+ sealed abstract class Scope extends Finalizer with ScopeVersionSpecific
998
+ ```
999
+
1000
+ Associated types and hierarchy:
1001
+
1002
+ - `type $[+A]`
1003
+ - `type Parent <: Scope`
1004
+ - `val parent: Parent`
1005
+ - `def isClosed: Boolean`
1006
+ - `def isOwner: Boolean`
1007
+
1008
+ Core operations:
844
1009
 
845
1010
  ```scala
846
- sealed abstract class Scope extends Finalizer {
847
- type $[+A] // = A at runtime (zero-cost)
848
- type Parent <: Scope
849
- val parent: Parent
1011
+ def scoped[A](f: (child: Scope.Child[this.type]) => A)(using Unscoped[A]): A
850
1012
 
851
- def allocate[A](resource: Resource[A]): $[A]
852
- def allocate[A <: AutoCloseable](value: => A): $[A]
853
- def defer(f: => Unit): Unit
1013
+ def allocate[A](resource: Resource[A]): $[A]
1014
+ def allocate[A <: AutoCloseable](value: => A): $[A]
854
1015
 
855
- // Apply function to scoped value, returns scoped result
856
- def use[A, B](scoped: $[A])(f: A => B): $[B]
1016
+ infix transparent inline def $[A, B](sa: $[A])(inline f: A => B): B | $[B]
857
1017
 
858
- // Construct a scoped value from a raw value
859
- def $[A](a: A): $[A]
1018
+ def lower[A](value: parent.$[A]): $[A]
860
1019
 
861
- // Lower parent-scoped value into this scope
862
- def lower[A](value: parent.$[A]): $[A]
1020
+ override def defer(f: => Unit): DeferHandle
863
1021
 
864
- // Escape hatch: unwrap scoped value (emits compiler warning)
865
- // Scala 3:
866
- inline def leak[A](inline sa: $[A]): A // macro — emits warning
867
- // Scala 2:
868
- def leak[A](sa: $[A]): A // macro — emits warning
1022
+ def open(): $[Scope.OpenScope]
869
1023
 
870
- // Creates a child scope - requires Unscoped evidence on return type
871
- // Scala 3:
872
- def scoped[A](f: (child: Scope.Child[self.type]) => child.$[A])(using Unscoped[A]): A
873
- // Scala 2 (macro rewrites the types; declared signature is untyped):
874
- def scoped(f: Scope.Child[self.type] => Any): Any // macro
1024
+ inline def leak[A](inline sa: $[A]): A
1025
+ ```
875
1026
 
876
- implicit class ScopedOps[A](sa: $[A]) {
877
- def map[B](f: A => B): $[B]
878
- def flatMap[B](f: A => $[B]): $[B]
879
- }
1027
+ Notes:
880
1028
 
881
- implicit def wrapUnscoped[A: Unscoped](a: A): $[A]
882
- }
1029
+ - `$` requires a **lambda literal** and enforces safe receiver-only usage.
1030
+ - `$` returns `B` if `Unscoped[B]` exists; otherwise returns `$[B]`.
1031
+ - If the scope is closed, `$` / `allocate` / `open` / `lower` return default values and perform no work.
1032
+
1033
+ Syntax enrichments available after `import scope.*` inside a scope:
1034
+
1035
+ ```scala
1036
+ implicit class ScopedResourceOps[A](sr: $[Resource[A]]):
1037
+ def allocate: $[A]
1038
+
1039
+ implicit class ResourceOps[A](r: Resource[A]):
1040
+ def allocate: $[A]
883
1041
  ```
884
1042
 
885
- ### `Resource`
1043
+ ---
1044
+
1045
+ ### `Scope.global`
886
1046
 
887
1047
  ```scala
888
- sealed trait Resource[+A]
1048
+ object Scope:
1049
+ object global extends Scope
1050
+ ```
1051
+
1052
+ Properties:
1053
+
1054
+ - `type $[+A] = A` (identity)
1055
+ - `isOwner` always returns `true`
1056
+ - JVM: finalizers run at shutdown via a shutdown hook
1057
+ - Scala.js: shutdown hook is not available
1058
+
1059
+ ---
1060
+
1061
+ ### `Scope.OpenScope`
889
1062
 
890
- object Resource {
1063
+ ```scala
1064
+ case class OpenScope(scope: Scope, close: () => Finalization)
1065
+ ```
1066
+
1067
+ - `scope`: the child scope
1068
+ - `close()`: detaches from parent, runs child finalizers (LIFO), returns `Finalization`
1069
+
1070
+ ---
1071
+
1072
+ ### `Finalizer`
1073
+
1074
+ ```scala
1075
+ trait Finalizer:
1076
+ def defer(f: => Unit): DeferHandle
1077
+ ```
1078
+
1079
+ A minimal capability interface for registering cleanup.
1080
+
1081
+ Also available as a package-level helper:
1082
+
1083
+ ```scala
1084
+ def defer(finalizer: => Unit)(using fin: Finalizer): DeferHandle
1085
+ ```
1086
+
1087
+ ---
1088
+
1089
+ ### `DeferHandle`
1090
+
1091
+ ```scala
1092
+ abstract class DeferHandle:
1093
+ def cancel(): Unit
1094
+ ```
1095
+
1096
+ - `cancel()` is thread-safe and idempotent
1097
+ - cancellation is O(1) (true removal from a concurrent map)
1098
+
1099
+ ---
1100
+
1101
+ ### `Finalization`
1102
+
1103
+ ```scala
1104
+ final class Finalization(val errors: zio.blocks.chunk.Chunk[Throwable]):
1105
+ def isEmpty: Boolean
1106
+ def nonEmpty: Boolean
1107
+ def orThrow(): Unit
1108
+ def suppress(initial: Throwable): Throwable
1109
+
1110
+ object Finalization:
1111
+ val empty: Finalization
1112
+ def apply(errors: Chunk[Throwable]): Finalization
1113
+ ```
1114
+
1115
+ ---
1116
+
1117
+ ### `Resource[+A]`
1118
+
1119
+ ```scala
1120
+ sealed trait Resource[+A]:
1121
+ def map[B](f: A => B): Resource[B]
1122
+ def flatMap[B](f: A => Resource[B]): Resource[B]
1123
+ def zip[B](that: Resource[B]): Resource[(A, B)]
1124
+ ```
1125
+
1126
+ Companion constructors:
1127
+
1128
+ ```scala
1129
+ object Resource:
891
1130
  def apply[A](value: => A): Resource[A]
892
- def acquireRelease[A](acquire: => A)(release: A => Unit): Resource[A]
893
1131
  def fromAutoCloseable[A <: AutoCloseable](thunk: => A): Resource[A]
1132
+ def acquireRelease[A](acquire: => A)(release: A => Unit): Resource[A]
1133
+ def shared[A](f: Scope => A): Resource[A]
1134
+ def unique[A](f: Scope => A): Resource[A]
894
1135
 
895
- // Macro - DI entry point:
896
- def from[T]: Resource[T] // zero-dep classes
897
- def from[T](wires: Wire[?, ?]*): Resource[T] // with dependency wires
898
-
899
- // Internal (used by generated code):
900
- def shared[A](f: Finalizer => A): Resource[A]
901
- def unique[A](f: Finalizer => A): Resource[A]
902
- }
1136
+ inline def from[T]: Resource[T]
1137
+ inline def from[T](inline wires: Wire[?, ?]*): Resource[T]
903
1138
  ```
904
1139
 
905
- ### `Wire`
1140
+ Notes:
1141
+
1142
+ - `Resource.from[T]` (no args) only works when `T` has **no non-scope dependencies** (constructor params may include `Scope`/`Finalizer`).
1143
+ - Use `Resource.from[T](wires*)` to provide/override dependencies and derive the full graph.
1144
+
1145
+ ---
1146
+
1147
+ ### `Wire[-In, +Out]`
906
1148
 
907
1149
  ```scala
908
- sealed trait Wire[-In, +Out] {
1150
+ sealed trait Wire[-In, +Out]:
909
1151
  def isShared: Boolean
1152
+ def isUnique: Boolean = !isShared
1153
+
910
1154
  def shared: Wire.Shared[In, Out]
911
1155
  def unique: Wire.Unique[In, Out]
1156
+
912
1157
  def toResource(deps: zio.blocks.context.Context[In]): Resource[Out]
913
- }
1158
+ ```
914
1159
 
915
- object Wire {
916
- // Macro entry points:
917
- def shared[T]: Wire.Shared[?, T] // derive from T's constructor
918
- def unique[T]: Wire.Unique[?, T] // derive from T's constructor
919
-
920
- // Wrap pre-existing value (auto-finalizes if AutoCloseable):
921
- def apply[T](value: T): Wire.Shared[Any, T]
922
-
923
- final case class Shared[-In, +Out] extends Wire[In, Out]
924
- final case class Unique[-In, +Out] extends Wire[In, Out]
925
- }
1160
+ Wires:
1161
+
1162
+ ```scala
1163
+ object Wire:
1164
+ final case class Shared[-In, +Out](makeFn: (Scope, Context[In]) => Out) extends Wire[In, Out]
1165
+ final case class Unique[-In, +Out](makeFn: (Scope, Context[In]) => Out) extends Wire[In, Out]
1166
+
1167
+ def apply[T](t: T): Wire.Shared[Any, T]
1168
+
1169
+ transparent inline def shared[T]: Wire.Shared[?, T]
1170
+ transparent inline def unique[T]: Wire.Unique[?, T]
1171
+ ```
1172
+
1173
+ Notes:
1174
+
1175
+ - `Wire(t)` wraps a pre-existing value; if it's `AutoCloseable`, `close()` is registered automatically when used.
1176
+
1177
+ ---
1178
+
1179
+ ### `Unscoped[A]`
1180
+
1181
+ ```scala
1182
+ trait Unscoped[A]
1183
+
1184
+ object Unscoped:
1185
+ inline given derived[A](using scala.deriving.Mirror.Of[A]): Unscoped[A]
1186
+ // plus many built-in givens (primitives, collections, time, UUID, Chunk, ...)
926
1187
  ```
927
1188
 
928
1189
  ---
929
1190
 
930
- ## Mental model recap
931
-
932
- - Use `Scope.global.scoped { scope => import scope._; ... }` to create a safe region.
933
- - For simple resources: `allocate(Resource(value))` or `allocate(Resource.acquireRelease(...)(...))`
934
- - For dependency injection: `allocate(Resource.from[App](Wire(config), ...))` — auto-wires concrete classes, you provide leaves and overrides.
935
- - Use `scope.use(value)(f)` to work with scoped values — all operations are eager.
936
- - `$[A] = A` at runtime — zero-cost opaque type.
937
- - The `scoped` method requires `Unscoped[A]` evidence on the return type.
938
- - Use `lower(parentValue)` to access parent-scoped values in child scopes.
939
- - Return `Unscoped` types from child scopes to extract raw values.
940
- - If it doesn't typecheck, it would have been unsafe at runtime.
941
-
942
- **The 3 macro entry points:**
943
- - `Wire.shared[T]` — shared wire from constructor
944
- - `Wire.unique[T]` — unique wire from constructor
945
- - `Resource.from[T](wires*)` — wire up T and all dependencies
1191
+ ## Practical guidance (summary)
1192
+
1193
+ - Allocate in a scope: `resource.allocate` (inside `Scope.global.scoped { scope => import scope.* ... }`)
1194
+ - Use scoped values only through: `(scope $ value)(...)`
1195
+ - Return only `Unscoped` data from `scoped` blocks
1196
+ - Use `lower` to use parent values inside a child
1197
+ - If `$` returns `$[Resource[A]]`, call `.allocate` on it (scoped resource chaining)
1198
+ - Use `open()` for explicitly-managed, cross-thread capable scopes
1199
+ - Use `leak` only when interop forces it; prefer `Unscoped` for pure data