@zio.dev/zio-blocks 0.0.1 → 0.0.21

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
@@ -2,10 +2,12 @@
2
2
 
3
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.
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.
6
+
5
7
  If you've used `try/finally`, `Using`, or ZIO `Scope`, this library lives in the same problem space, but it focuses on:
6
8
 
7
9
  - **Compile-time prevention of scope leaks**
8
- - **Zero runtime overhead for the scoped tag** (`A @@ S` is represented as `A`)
10
+ - **Unified scoped type** (`A @@ S` is a type alias for `Scoped[A, S]`)
9
11
  - **Simple, synchronous lifecycle management** (finalizers run LIFO on scope close)
10
12
 
11
13
  ---
@@ -17,10 +19,10 @@ If you've used `try/finally`, `Using`, or ZIO `Scope`, this library lives in the
17
19
  - [1) `Scope[ParentTag, Tag]`](#1-scopeparenttag-tag)
18
20
  - [2) Scoped values: `A @@ S`](#2-scoped-values-a--s)
19
21
  - [3) `Resource[A]`: acquisition + finalization](#3-resourcea-acquisition--finalization)
20
- - [4) `Scoped[Tag, A]`: deferred computations](#4-scopedtag-a-deferred-computations)
21
- - [5) `ScopeEscape` and `Unscoped`: what may escape](#5-scopeescape-and-unscoped-what-may-escape)
22
- - [6) `Wire[-In, +Out]`: dependency recipes](#6-wire-in-out-dependency-recipes)
23
- - [7) `Wireable[Out]`: DI for traits/abstract classes](#7-wireableout-di-for-traitsabstract-classes)
22
+ - [4) `A @@ S`: unified scoped type](#4-a--s-unified-scoped-type)
23
+ - [5) `Unscoped`: marking pure data types](#5-unscoped-marking-pure-data-types)
24
+ - [6) `ScopeLift[A, S]`: what may return from child scopes](#6-scopelifta-s-what-may-return-from-child-scopes)
25
+ - [7) `Wire[-In, +Out]`: dependency recipes](#7-wire-in-out-dependency-recipes)
24
26
  - [Safety model (why leaking is prevented)](#safety-model-why-leaking-is-prevented)
25
27
  - [Usage examples](#usage-examples)
26
28
  - [Allocating and using a resource](#allocating-and-using-a-resource)
@@ -28,9 +30,10 @@ If you've used `try/finally`, `Using`, or ZIO `Scope`, this library lives in the
28
30
  - [Building a `Scoped` program (map/flatMap)](#building-a-scoped-program-mapflatmap)
29
31
  - [Registering cleanup manually with `defer`](#registering-cleanup-manually-with-defer)
30
32
  - [Dependency injection with `Wire` + `Context`](#dependency-injection-with-wire--context)
31
- - [Supplying dependencies with `Resource.from[T](wire1, wire2, ...)`](#supplying-dependencies-with-resourcefromtwire1-wire2-)
32
- - [DI for traits via `Wireable`](#di-for-traits-via-wireable)
33
+ - [Dependency injection with `Resource.from[T](wires*)`](#dependency-injection-with-resourcefromtwires)
34
+ - [Injecting traits via subtype wires](#injecting-traits-via-subtype-wires)
33
35
  - [Interop escape hatch: `leak`](#interop-escape-hatch-leak)
36
+ - [Common compile errors](#common-compile-errors)
34
37
  - [API reference (selected)](#api-reference-selected)
35
38
 
36
39
  ---
@@ -49,10 +52,12 @@ Scope.global.scoped { scope =>
49
52
  val db: Database @@ scope.Tag =
50
53
  scope.allocate(Resource(new Database))
51
54
 
52
- val result: String =
53
- scope.$(db)(_.query("SELECT 1"))
54
-
55
- println(result)
55
+ // $ executes immediately and returns String @@ scope.Tag
56
+ // Use the function to work with the value
57
+ (scope $ db) { database =>
58
+ val result = database.query("SELECT 1")
59
+ println(result)
60
+ }
56
61
  }
57
62
  ```
58
63
 
@@ -60,7 +65,8 @@ Key things to notice:
60
65
 
61
66
  - `scope.allocate(...)` returns a **scoped** value: `Database @@ scope.Tag`
62
67
  - You **cannot** call `db.query(...)` directly (methods are intentionally hidden)
63
- - You must use `scope.$(db)(...)` or build a `Scoped` computation
68
+ - You must use `(scope $ db) { ... }` to access the value - the function executes immediately
69
+ - `$` and `execute` always return scoped values (`B @@ scope.Tag`), never raw values
64
70
  - When the `scoped { ... }` block exits, finalizers run **LIFO** and errors are handled safely
65
71
 
66
72
  ---
@@ -102,24 +108,33 @@ object Scope {
102
108
 
103
109
  - The global scope is intended to live for the lifetime of the process.
104
110
  - Its finalizers run on JVM shutdown.
105
- - Values allocated in `Scope.global` typically **escape** as raw values via `ScopeEscape` (see below).
111
+ - Values allocated in `Scope.global` can escape as raw values via `ScopeLift.globalScope` (see below).
106
112
 
107
113
  ---
108
114
 
109
115
  ### 2) Scoped values: `A @@ S`
110
116
 
111
- `A @@ S` means: "a value of type `A` that is locked to scope tag `S`".
117
+ `A @@ S` is a type alias for `Scoped[A, S]` — a handle to a value of type `A` that is locked to scope tag `S`.
112
118
 
113
- - **Runtime representation:** just `A` (no wrapper allocation)
114
- - **Key effect:** methods on `A` are hidden, so you can't call `a.method` without proving scope access
119
+ - **Runtime representation:** a boxed thunk (lightweight wrapper)
120
+ - **Key effect:** methods on `A` are hidden; you can't call `a.method` directly
121
+ - **Acquisition timing:** `scope.allocate(resource)` acquires the resource **immediately** (eagerly) and returns a scoped handle for accessing the already-acquired value. The thunk defers *access*, not *acquisition*.
115
122
  - **Access paths:**
116
- - `scope.$(a)(f)` to use a scoped value immediately
117
- - `a.map / a.flatMap` to build a `Scoped` computation, then run it via `scope(scoped)`
123
+ - `(scope $ a)(f)` to execute and apply a function immediately
124
+ - `a.map / a.flatMap` to build composite scoped computations
125
+ - `scope.execute(scoped)` to run a composed computation
118
126
 
119
- #### Scala 3 vs Scala 2 note
127
+ #### Scala 2 note
120
128
 
121
- - In **Scala 3**, `@@` is implemented as an `opaque` type.
122
- - In **Scala 2**, the library emulates the same "opaque-like" behavior using the *module pattern* (still zero-overhead at runtime).
129
+ In Scala 2, explicit type annotations are required when assigning scoped values to avoid existential type inference issues:
130
+
131
+ ```scala
132
+ // Scala 2 requires explicit type annotation
133
+ val db: Database @@ scope.Tag = scope.allocate(Resource[Database])
134
+
135
+ // Scala 3 can infer the type
136
+ val db = scope.allocate(Resource[Database])
137
+ ```
123
138
 
124
139
  ---
125
140
 
@@ -139,12 +154,12 @@ Common constructors:
139
154
  - Explicit lifecycle.
140
155
  - `Resource.fromAutoCloseable(thunk)`
141
156
  - A type-safe helper for `AutoCloseable`.
142
- - `Resource.from[T]` (macro)
143
- - Derives a resource from `T`'s constructor.
144
- - If `T` is `AutoCloseable`, registers `close()` automatically.
145
- - If `T` has dependencies, either:
146
- - use `Wire` + `toResource(deps)`, or
147
- - use `Resource.from[T](wire1, wire2, ...)` (see below).
157
+ - `Resource.from[T](wires*)` (macro)
158
+ - The primary entry point for dependency injection.
159
+ - Resolves `T` and all its dependencies into a single `Resource[T]`.
160
+ - Auto-creates missing wires using `Wire.shared` for concrete classes.
161
+ - Requires explicit wires for: primitives, functions, collections, and abstract types.
162
+ - If `T` or any dependency is `AutoCloseable`, registers `close()` automatically.
148
163
 
149
164
  #### Resource "sharing" vs "uniqueness"
150
165
 
@@ -162,93 +177,172 @@ Common constructors:
162
177
 
163
178
  ---
164
179
 
165
- ### 4) `Scoped[Tag, A]`: deferred computations
180
+ ### 4) `A @@ S`: unified scoped type
166
181
 
167
- `Scoped[-Tag, +A]` represents a computation that produces `A`, but can only be executed by a scope whose tag is compatible with `Tag`.
182
+ `A @@ S` (type alias for `Scoped[A, S]`) is the core type representing a deferred computation that produces `A` and requires scope tag `S` to execute.
168
183
 
169
184
  Execution happens via:
170
185
 
171
186
  ```scala
172
- scope(scopedComputation)
187
+ scope.execute(scopedComputation)
173
188
  ```
174
189
 
175
190
  How to build them:
176
191
 
177
- - From scoped values:
178
- - `val s: Scoped[S, B] = (a: A @@ S).map(f)`
179
- - `flatMap` composes scoped values while tracking combined requirements
180
- - Or directly:
181
- - `Scoped.create(() => ...)` (advanced/internal style)
192
+ - From `scope.allocate`:
193
+ - `scope.allocate(resource)` returns `A @@ scope.Tag`
194
+ - Using combinators:
195
+ - `(a: A @@ S).map(f: A => B)` returns `B @@ S`
196
+ - `(a: A @@ S).flatMap(f: A => B @@ T)` returns `B @@ (S & T)` (Scala 3) / `B @@ (S with T)` (Scala 2)
197
+ - Use for-comprehensions to chain scoped computations
198
+ - From ordinary values:
199
+ - `Scoped(value)` lifts a value into an `A @@ Any` (which can be used anywhere due to contravariance)
200
+
201
+ **Contravariance:** `A @@ S` is contravariant in `S`. This means `A @@ ParentTag` is a subtype of `A @@ ChildTag` when `ChildTag <: ParentTag`. Child scopes can execute parent-scoped computations automatically.
202
+
203
+ **For-comprehension example:**
204
+
205
+ ```scala
206
+ Scope.global.scoped { scope =>
207
+ val program: Result @@ scope.Tag = for {
208
+ pool <- scope.allocate(Resource[Pool])
209
+ conn <- scope.allocate(Resource(pool.lease()))
210
+ data <- conn.map(_.query("SELECT *"))
211
+ } yield process(data)
212
+
213
+ scope.execute(program)
214
+ }
215
+ ```
216
+
217
+ ---
218
+
219
+ ### 5) `Unscoped`: marking pure data types
220
+
221
+ The `Unscoped[A]` typeclass marks types as pure data that don't hold resources. This is used by `ScopeLift` to determine what can escape from child scopes.
222
+
223
+ **Built-in Unscoped types:**
224
+ - Primitives: `Int`, `Long`, `Boolean`, `Double`, etc.
225
+ - `String`, `Unit`, `Nothing`
226
+ - Collections of Unscoped types
227
+
228
+ **Custom Unscoped types:**
229
+ ```scala
230
+ case class Config(debug: Boolean)
231
+ object Config {
232
+ given Unscoped[Config] = Unscoped.derived
233
+ }
234
+ ```
182
235
 
183
- `Scoped` is contravariant in `Tag`, which is what allows a **child** scope (more specific tag) to run computations that only require a **parent** tag.
236
+ **Important:** `$` and `execute` always return `B @@ scope.Tag`. They never escape values directly. Escape only happens at the `.scoped` boundary via `ScopeLift`.
184
237
 
185
238
  ---
186
239
 
187
- ### 5) `ScopeEscape` and `Unscoped`: what may escape
240
+ ### 6) `ScopeLift[A, S]`: what may return from child scopes
241
+
242
+ When you call `scope.scoped { child => ... }`, the return type `A` must have a `ScopeLift[A, S]` instance where `S` is the parent scope's tag. This typeclass both **gates** what can exit child scopes and **transforms** the return type.
243
+
244
+ **ScopeLift instances and their behavior:**
245
+
246
+ | Instance | Matches | Output Type |
247
+ |----------|---------|-------------|
248
+ | `globalScope[A]` | Any `A` when `S = GlobalTag` | `A` (unchanged) |
249
+ | `nothing[S]` | `Nothing` | `Nothing` |
250
+ | `unscoped[A, S]` | `A` with `Unscoped[A]` | `A` (raw value) |
251
+ | `scoped[B, T, S]` | `B @@ T` when `S <:< T` | `B @@ T` (unchanged) |
188
252
 
189
- Whenever you access a scoped value via:
253
+ **Allowed return types:**
190
254
 
191
- - `scope.$(value)(f)`, or
192
- - `scope(scopedComputation)`,
255
+ - **`Unscoped` types**: Pure data lifts to raw `A`
256
+ - **Parent-scoped values**: `B @@ T` where `S <:< T` lifts as-is
257
+ - **`Nothing`**: For blocks that throw
258
+ - **Anything from global scope**: Global scope never closes
193
259
 
194
- …the return type is controlled by `ScopeEscape[A, S]`, which decides whether a result:
260
+ **Rejected return types (no ScopeLift instance):**
195
261
 
196
- - escapes as raw `A`, or
197
- - remains tracked as `A @@ S`.
262
+ - **Closures**: `() => A` could capture the child scope
263
+ - **Child-scoped values**: `B @@ child.Tag` would be use-after-close
264
+ - **The scope itself**: Would allow operations after close
198
265
 
199
- Rule of thumb:
266
+ ```scala
267
+ Scope.global.scoped { parent =>
268
+ // ✅ OK: String is Unscoped - lifts to raw String
269
+ val result: String = parent.scoped { child =>
270
+ "hello" // Return raw Unscoped value
271
+ }
272
+
273
+ // ✅ OK: parent-tagged value outlives child - lifts as-is
274
+ val parentDb: Database @@ parent.Tag = parent.allocate(Resource[Database])
275
+ val same: Database @@ parent.Tag = parent.scoped { _ =>
276
+ parentDb
277
+ }
200
278
 
201
- - Pure data (e.g. `Int`, `String`, small case classes you mark `Unscoped`) should escape as raw values.
202
- - Resource-like values should remain scoped unless you explicitly `leak`.
279
+ // COMPILE ERROR: closure has no ScopeLift instance
280
+ // val leak = parent.scoped { child =>
281
+ // val db = child.allocate(Resource[Database])
282
+ // () => println("captured!") // Function types rejected
283
+ // }
284
+
285
+ // ❌ COMPILE ERROR: child-scoped value has no ScopeLift instance
286
+ // val escaped = parent.scoped { child =>
287
+ // child.allocate(Resource[Database]) // Database @@ child.Tag can't escape
288
+ // }
289
+ }
290
+ ```
203
291
 
204
292
  ---
205
293
 
206
- ### 6) `Wire[-In, +Out]`: dependency recipes
294
+ ### 7) `Wire[-In, +Out]`: dependency recipes
207
295
 
208
- `Wire` is a recipe for constructing services, commonly used for dependency injection.
296
+ `Wire` is a recipe for constructing services. It describes **how** to build a service given its dependencies, but does not resolve those dependencies itself.
209
297
 
210
298
  - `In` is the required dependencies (provided as a `Context[In]`)
211
299
  - `Out` is the produced service
212
300
 
213
301
  There are two wire flavors:
214
302
 
215
- - `Wire.Shared`: a shared recipe
216
- - `Wire.Unique`: a unique recipe
303
+ - `Wire.Shared`: produces a shared (memoized) instance
304
+ - `Wire.Unique`: produces a fresh instance each time
217
305
 
218
- **Important clarification:** `Wire` itself is just a recipe. The actual memoization/sharing behavior happens when you convert the wire into a `Resource`:
306
+ **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.
219
307
 
220
- ```scala
221
- val r: Resource[Out] = wire.toResource(deps)
222
- val out: Out @@ scope.Tag = scope.allocate(r)
223
- ```
308
+ #### Creating wires
224
309
 
225
- - `Wire.Shared#toResource` produces a `Resource.Shared`, which is where the reference-counted sharing is implemented.
226
- - `Wire.Unique#toResource` produces a `Resource.Unique`.
310
+ There are exactly **3 macro entry points**:
227
311
 
228
- Macros available at package level:
312
+ | Macro | Purpose |
313
+ |-------|---------|
314
+ | `Wire.shared[T]` | Create a shared wire from `T`'s constructor |
315
+ | `Wire.unique[T]` | Create a unique wire from `T`'s constructor |
316
+ | `Resource.from[T](wires*)` | Wire up `T` and all dependencies into a `Resource` |
229
317
 
230
- - `shared[T]`: derive a shared wire from `T`'s constructor (or from a `Wireable[T]` if present)
231
- - `unique[T]`: derive a unique wire
318
+ For wrapping pre-existing values:
232
319
 
233
- ---
320
+ - `Wire(value)` — wraps a value; if `AutoCloseable`, registers `close()` automatically
321
+
322
+ #### How `Resource.from[T](wires*)` works
234
323
 
235
- ### 7) `Wireable[Out]`: DI for traits/abstract classes
324
+ 1. **Collect wires**: Uses explicit wires when provided, otherwise auto-creates with `Wire.shared`
325
+ 2. **Validate**: Checks for cycles, unmakeable types, duplicate providers
326
+ 3. **Topological sort**: Orders dependencies so leaves are allocated first
327
+ 4. **Generate composition**: Produces a `Resource[T]` via flatMap chains
236
328
 
237
- `shared[T]` / `unique[T]` can derive wires from **concrete classes** with constructors. But traits and abstract classes are not instantiable, so you need a way to tell the macros "when someone asks for `T`, build it like *this*".
329
+ Key insight: **Compose Resources, don't accumulate values.** Each wire becomes a `Resource`, and they are composed via `flatMap`. This correctly preserves:
238
330
 
239
- That's what `Wireable[T]` is: a typeclass that supplies a `Wire` for a service.
331
+ - **Sharing**: Same `Resource.Shared` instance same value (even in diamond patterns)
332
+ - **Uniqueness**: `Resource.Unique` → fresh value per injection site
240
333
 
241
- Typical use:
334
+ #### Subtype resolution
242
335
 
243
- - Define a `Wireable[MyTrait]` in `MyTrait`'s companion object.
244
- - `shared[MyTrait]` or `unique[MyTrait]` will pick it up automatically.
336
+ 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.
245
337
 
246
- This is especially useful when you want to inject an interface but construct a concrete implementation (and still register finalizers correctly).
338
+ If the same concrete wire satisfies multiple types (e.g., `Service` and `LiveService`), only **one instance** is created and reused for both.
247
339
 
248
340
  ---
249
341
 
250
342
  ## Safety model (why leaking is prevented)
251
343
 
344
+ **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.
345
+
252
346
  The library prevents scope leaks via two reinforcing mechanisms:
253
347
 
254
348
  ### A) Existential child tags (fresh, unnameable types)
@@ -268,9 +362,11 @@ The child scope has an existential tag (fresh per invocation). You can allocate
268
362
  Compile-time safety is verified in tests, e.g.:
269
363
  `ScopeCompileTimeSafetyScala3Spec`.
270
364
 
271
- ### B) Tag invariance + "opaque-like" `@@` blocks subtyping escape
365
+ ### B) Contravariance prevents child-to-parent widening
272
366
 
273
- Even if you try to "widen" a child-tagged value to a parent-tagged value, invariance and hidden members prevent it from typechecking. The only sanctioned access route is through `scope.$` / `scope.apply`, which require tag evidence.
367
+ `A @@ S` is contravariant in `S`. This means `Db @@ child.Tag` is **not** a subtype of `Db @@ parent.Tag` the subtyping goes the *other* direction. A child scope can use parent-tagged values (because `child.Tag <: parent.Tag` makes `A @@ parent.Tag <: A @@ child.Tag`), but you cannot widen a child-tagged value to a parent tag.
368
+
369
+ Additionally, the thunk-based representation hides `A`'s methods — you can't call `db.query(...)` directly on a `Database @@ scope.Tag`. The only sanctioned access routes are `scope.$` and `scope.execute`, which require a scope with a compatible tag.
274
370
 
275
371
  ---
276
372
 
@@ -289,10 +385,11 @@ final class FileHandle(path: String) extends AutoCloseable {
289
385
  Scope.global.scoped { scope =>
290
386
  val h = scope.allocate(Resource(new FileHandle("data.txt")))
291
387
 
292
- val contents: String =
293
- scope.$(h)(_.readAll())
294
-
295
- println(contents)
388
+ // $ executes immediately - work with the value inside the function
389
+ (scope $ h) { handle =>
390
+ val contents = handle.readAll()
391
+ println(contents)
392
+ }
296
393
  }
297
394
  ```
298
395
 
@@ -308,23 +405,28 @@ Scope.global.scoped { parent =>
308
405
 
309
406
  parent.scoped { child =>
310
407
  // child can use parent-scoped values:
311
- val ok: String = child.$(parentDb)(_.query("SELECT 1"))
312
- println(ok)
408
+ child.$(parentDb) { db =>
409
+ println(db.query("SELECT 1"))
410
+ }
313
411
 
314
412
  val childDb = child.allocate(Resource(new Database))
315
413
 
316
414
  // You can use childDb *inside* the child:
317
- val ok2: String = child.$(childDb)(_.query("SELECT 2"))
318
- println(ok2)
319
-
320
- // But you cannot return childDb to the parent in a usable way:
321
- // childDb : Database @@ child.Tag
322
- // parent cannot prove parent.Tag <:< child.Tag
415
+ child.$(childDb) { db =>
416
+ println(db.query("SELECT 2"))
417
+ }
418
+
419
+ // Return an Unscoped value - ScopeLift extracts it
420
+ "done"
421
+
422
+ // But you cannot return childDb to the parent:
423
+ // childDb : Database @@ child.Tag has no ScopeLift instance
323
424
  }
324
425
 
325
426
  // parentDb is still usable here:
326
- val stillOk = parent.$(parentDb)(_.query("SELECT 3"))
327
- println(stillOk)
427
+ parent.$(parentDb) { db =>
428
+ println(db.query("SELECT 3"))
429
+ }
328
430
  }
329
431
  ```
330
432
 
@@ -332,21 +434,54 @@ Scope.global.scoped { parent =>
332
434
 
333
435
  ### Building a `Scoped` program (map/flatMap)
334
436
 
437
+ You can use for-comprehensions to compose scoped computations. Each `<-` uses `flatMap`, accumulating requirements:
438
+
335
439
  ```scala
336
440
  import zio.blocks.scope._
337
441
 
338
442
  Scope.global.scoped { scope =>
339
- val db = scope.allocate(Resource(new Database))
443
+ val db: Database @@ scope.Tag = scope.allocate(Resource(new Database))
444
+
445
+ // Build a scoped computation with map
446
+ val program: String @@ scope.Tag = db.map { d =>
447
+ d.query("SELECT 1")
448
+ }
449
+
450
+ // execute runs the computation immediately, returns String @@ scope.Tag
451
+ // Use side effects inside the computation to observe results
452
+ scope.execute(program)
453
+ }
454
+ ```
455
+
456
+ This pattern shines when chaining resource acquisition:
457
+
458
+ ```scala
459
+ import zio.blocks.scope._
460
+
461
+ // A pool that leases connections
462
+ class Pool(implicit finalizer: Finalizer) extends AutoCloseable {
463
+ def lease: Resource[Connection] = Resource(new Connection)
464
+ def close(): Unit = println("pool closed")
465
+ }
466
+
467
+ class Connection extends AutoCloseable {
468
+ def query(sql: String): String = s"result: $sql"
469
+ def close(): Unit = println("connection closed")
470
+ }
471
+
472
+ Scope.global.scoped { scope =>
473
+ val pool: Pool @@ scope.Tag = scope.allocate(Resource.from[Pool])
340
474
 
341
- val program: Scoped[scope.Tag, String] =
475
+ val program: String @@ scope.Tag =
342
476
  for {
343
- a <- db.map(_.query("SELECT 1"))
344
- b <- db.map(_.query("SELECT 2"))
345
- } yield s"$a | $b"
477
+ p <- pool // extract Pool from scoped
478
+ connection <- scope.allocate(p.lease) // allocate returns Connection @@ Tag
479
+ } yield connection.query("SELECT 1")
346
480
 
347
- val result: String = scope(program)
348
- println(result)
481
+ // execute runs the computation - connection is used inside the yield
482
+ scope.execute(program)
349
483
  }
484
+ // Output: connection closed, then pool closed (LIFO)
350
485
  ```
351
486
 
352
487
  ---
@@ -375,6 +510,7 @@ import zio.blocks.scope._
375
510
 
376
511
  Scope.global.scoped { scope =>
377
512
  given Finalizer = scope
513
+
378
514
  defer { println("cleanup") }
379
515
  }
380
516
  ```
@@ -390,7 +526,7 @@ import zio.blocks.scope._
390
526
 
391
527
  class ConnectionPool(config: Config)(implicit finalizer: Finalizer) {
392
528
  private val pool = createPool(config)
393
- finalizer.defer { pool.shutdown() }
529
+ defer { pool.shutdown() }
394
530
 
395
531
  def getConnection(): Connection = pool.acquire()
396
532
  }
@@ -413,13 +549,15 @@ Why `Finalizer` instead of `Scope`?
413
549
 
414
550
  ### Dependency injection with `Wire` + `Context`
415
551
 
552
+ For manual wiring (when you already have dependencies assembled), use `wire.toResource(ctx)`:
553
+
416
554
  ```scala
417
555
  import zio.blocks.scope._
418
556
  import zio.blocks.context.Context
419
557
 
420
558
  final case class Config(debug: Boolean)
421
559
 
422
- val w: Wire.Shared[Boolean, Config] = shared[Config]
560
+ val w: Wire.Shared[Boolean, Config] = Wire.shared[Config]
423
561
  val deps: Context[Boolean] = Context[Boolean](true)
424
562
 
425
563
  Scope.global.scoped { scope =>
@@ -427,7 +565,7 @@ Scope.global.scoped { scope =>
427
565
  scope.allocate(w.toResource(deps))
428
566
 
429
567
  val debug: Boolean =
430
- scope.$(cfg)(_.debug) // Boolean typically escapes
568
+ (scope $ cfg)(_.debug) // Boolean typically escapes
431
569
 
432
570
  println(debug)
433
571
  }
@@ -438,95 +576,105 @@ Sharing vs uniqueness at the wire level:
438
576
  ```scala
439
577
  import zio.blocks.scope._
440
578
 
441
- val ws = shared[Config] // shared recipe; sharing happens via Resource.Shared when allocated
442
- val wu = unique[Config] // unique recipe; each allocation is fresh
579
+ val ws = Wire.shared[Config] // shared recipe; sharing happens via Resource.Shared when allocated
580
+ val wu = Wire.unique[Config] // unique recipe; each allocation is fresh
443
581
  ```
444
582
 
445
583
  ---
446
584
 
447
- ### Supplying dependencies with `Resource.from[T](wire1, wire2, ...)`
585
+ ### Dependency injection with `Resource.from[T](wires*)`
448
586
 
449
- `Resource.from[T]` can also be used as a "standalone mini graph" by providing wires for constructor dependencies.
587
+ `Resource.from[T](wires*)` is the **primary entry point** for dependency injection. It resolves `T` and all its dependencies into a single `Resource[T]`.
450
588
 
451
589
  ```scala
452
590
  import zio.blocks.scope._
453
- import zio.blocks.context.Context
454
591
 
455
592
  final case class Config(url: String)
456
593
 
457
- trait Logger {
458
- def info(msg: String): Unit
594
+ final class Logger {
595
+ def info(msg: String): Unit = println(msg)
459
596
  }
460
597
 
461
- final class ConsoleLogger extends Logger {
462
- def info(msg: String): Unit = println(msg)
598
+ final class Database(cfg: Config) extends AutoCloseable {
599
+ def query(sql: String): String = s"[${cfg.url}] $sql"
600
+ def close(): Unit = println("database closed")
463
601
  }
464
602
 
465
- final class Service(cfg: Config, logger: Logger) extends AutoCloseable {
466
- def run(): Unit = logger.info(s"running with ${cfg.url}")
603
+ final class Service(db: Database, logger: Logger) extends AutoCloseable {
604
+ def run(): Unit = logger.info(s"running with ${db.query("SELECT 1")}")
467
605
  def close(): Unit = println("service closed")
468
606
  }
469
607
 
470
- // Provide wires for *all* dependencies of Service:
608
+ // Only provide leaf values (primitives, configs) - the rest is auto-wired:
471
609
  val serviceResource: Resource[Service] =
472
610
  Resource.from[Service](
473
- Wire(Config("jdbc:postgresql://localhost/db")),
474
- Wire(new ConsoleLogger: Logger)
611
+ Wire(Config("jdbc:postgresql://localhost/db"))
475
612
  )
476
613
 
477
614
  Scope.global.scoped { scope =>
478
615
  val svc = scope.allocate(serviceResource)
479
- scope.$(svc)(_.run())
616
+ (scope $ svc)(_.run())
480
617
  }
618
+ // Output: running with [jdbc:postgresql://localhost/db] SELECT 1
619
+ // Then: service closed, database closed (LIFO order)
481
620
  ```
482
621
 
483
- Notes:
484
- - All dependencies of `T` must be covered by the provided wires, otherwise you get a compile-time error.
485
- - If `T` is `AutoCloseable`, `close()` is registered automatically.
622
+ **What you must provide:**
623
+ - Leaf values: primitives, configs, pre-existing instances via `Wire(value)`
624
+ - Abstract types: traits/abstract classes via `Wire.shared[ConcreteImpl]`
625
+ - Overrides: when you want `unique` instead of the default `shared`
626
+
627
+ **What is auto-created:**
628
+ - Concrete classes with accessible primary constructors (default: `Wire.shared`)
486
629
 
487
630
  ---
488
631
 
489
- ### DI for traits via `Wireable`
632
+ ### Injecting traits via subtype wires
490
633
 
491
- When you want to inject a trait (or abstract class), define a `Wireable` in the companion so `shared[T]` / `unique[T]` can resolve it.
634
+ When a dependency is a trait or abstract class, provide a wire for a concrete implementation:
492
635
 
493
636
  ```scala
494
637
  import zio.blocks.scope._
495
- import zio.blocks.context.Context
496
638
 
497
- trait DatabaseApi {
498
- def query(sql: String): String
639
+ trait Logger {
640
+ def info(msg: String): Unit
499
641
  }
500
642
 
501
- final class LiveDatabaseApi(cfg: Config) extends DatabaseApi with AutoCloseable {
502
- def query(sql: String): String = s"[${cfg.url}] $sql"
503
- def close(): Unit = println("LiveDatabaseApi closed")
643
+ final class ConsoleLogger extends Logger {
644
+ def info(msg: String): Unit = println(msg)
504
645
  }
505
646
 
506
- object DatabaseApi {
507
- // Tell Scope how to build the trait by wiring a concrete implementation.
508
- given Wireable.Typed[Config, DatabaseApi] =
509
- Wireable.fromWire(shared[LiveDatabaseApi].shared.asInstanceOf[Wire[Config, DatabaseApi]])
647
+ final class App(logger: Logger) {
648
+ def run(): Unit = logger.info("Hello!")
510
649
  }
511
650
 
512
- final case class Config(url: String)
651
+ // Wire.shared[ConsoleLogger] satisfies the Logger dependency via subtyping:
652
+ val appResource: Resource[App] =
653
+ Resource.from[App](
654
+ Wire.shared[ConsoleLogger]
655
+ )
513
656
 
514
657
  Scope.global.scoped { scope =>
515
- val deps = Context(Config("jdbc:postgresql://localhost/db"))
516
-
517
- val db: DatabaseApi @@ scope.Tag =
518
- scope.allocate(shared[DatabaseApi].toResource(deps))
519
-
520
- val out: String =
521
- scope.$(db)(_.query("SELECT 1"))
522
-
523
- println(out)
658
+ val app = scope.allocate(appResource)
659
+ (scope $ app)(_.run())
524
660
  }
525
661
  ```
526
662
 
527
- Practical guidance:
528
- - Prefer `Wireable.fromWire(...)` when you already have a `Wire` you trust.
529
- - Put `given Wireable[...]` / `implicit val wireable: Wireable[...]` in the companion of the trait being injected.
663
+ **Single instance for diamond patterns:**
664
+
665
+ ```scala
666
+ trait Service
667
+ class LiveService extends Service
668
+ class NeedsService(s: Service)
669
+ class NeedsLive(l: LiveService)
670
+ class App(a: NeedsService, b: NeedsLive)
671
+
672
+ // One LiveService instance satisfies both Service and LiveService dependencies:
673
+ val appResource = Resource.from[App](
674
+ Wire.shared[LiveService]
675
+ )
676
+ // count of LiveService instantiations: 1
677
+ ```
530
678
 
531
679
  ---
532
680
 
@@ -549,6 +697,168 @@ Scope.global.scoped { scope =>
549
697
 
550
698
  ---
551
699
 
700
+ ## Common compile errors
701
+
702
+ The scope macros produce beautiful, actionable compile-time error messages with ASCII diagrams and helpful hints:
703
+
704
+ ### Not a class (Wire.shared/unique on trait or abstract class)
705
+
706
+ ```
707
+ ── Scope Error ──────────────────────────────────────────────────────────────
708
+
709
+ Cannot derive Wire for MyTrait: not a class.
710
+
711
+ Hint: Use Wire.Shared / Wire.Unique directly.
712
+
713
+ ────────────────────────────────────────────────────────────────────────────
714
+ ```
715
+
716
+ ### Unmakeable type (primitives, functions, collections)
717
+
718
+ ```
719
+ ── Scope Error ──────────────────────────────────────────────────────────────
720
+
721
+ Cannot auto-create String
722
+
723
+ This type (primitive, collection, or function) cannot be auto-created.
724
+
725
+ Required by:
726
+ └── Config
727
+ └── App
728
+
729
+ Fix: Provide Wire(value) with the desired value:
730
+
731
+ Resource.from[...](
732
+ Wire(...), // provide a value for String
733
+ ...
734
+ )
735
+
736
+ ────────────────────────────────────────────────────────────────────────────
737
+ ```
738
+
739
+ ### Abstract type (trait or abstract class)
740
+
741
+ ```
742
+ ── Scope Error ──────────────────────────────────────────────────────────────
743
+
744
+ Cannot auto-create Logger
745
+
746
+ This type is abstract (trait or abstract class).
747
+
748
+ Required by:
749
+ └── App
750
+
751
+ Fix: Provide a wire for a concrete implementation:
752
+
753
+ Resource.from[...](
754
+ Wire.shared[ConcreteImpl], // provides Logger
755
+ ...
756
+ )
757
+
758
+ ────────────────────────────────────────────────────────────────────────────
759
+ ```
760
+
761
+ ### Duplicate providers (ambiguous wires)
762
+
763
+ ```
764
+ ── Scope Error ──────────────────────────────────────────────────────────────
765
+
766
+ Multiple providers for Service
767
+
768
+ Conflicting wires:
769
+ 1. LiveService
770
+ 2. TestService
771
+
772
+ Hint: Remove duplicate wires or use distinct wrapper types.
773
+
774
+ ────────────────────────────────────────────────────────────────────────────
775
+ ```
776
+
777
+ ### Dependency cycle
778
+
779
+ ```
780
+ ── Scope Error ──────────────────────────────────────────────────────────────
781
+
782
+ Dependency cycle detected
783
+
784
+ Cycle:
785
+ ┌───────────┐
786
+ │ ▼
787
+ A ──► B ──► C
788
+ ▲ │
789
+ └───────────┘
790
+
791
+ Break the cycle by:
792
+ • Introducing an interface/trait
793
+ • Using lazy initialization
794
+ • Restructuring dependencies
795
+
796
+ ────────────────────────────────────────────────────────────────────────────
797
+ ```
798
+
799
+ ### Subtype conflict (related dependency types)
800
+
801
+ ```
802
+ ── Scope Error ──────────────────────────────────────────────────────────────
803
+
804
+ Dependency type conflict in MyService
805
+
806
+ FileInputStream is a subtype of InputStream.
807
+
808
+ When both types are dependencies, Context cannot reliably distinguish
809
+ them. The more specific type may be retrieved when the more general
810
+ type is requested.
811
+
812
+ To fix this, wrap one or both types in a distinct wrapper:
813
+
814
+ case class WrappedInputStream(value: InputStream)
815
+ or
816
+ opaque type WrappedInputStream = InputStream
817
+
818
+ ────────────────────────────────────────────────────────────────────────────
819
+ ```
820
+
821
+ ### Duplicate parameter types in constructor
822
+
823
+ ```
824
+ ── Scope Error ──────────────────────────────────────────────────────────────
825
+
826
+ Constructor of App has multiple parameters of type String
827
+
828
+ Context is type-indexed and cannot supply distinct values for the same type.
829
+
830
+ Fix: Wrap one parameter in an opaque type to distinguish them:
831
+
832
+ opaque type FirstString = String
833
+ or
834
+ case class FirstString(value: String)
835
+
836
+ ────────────────────────────────────────────────────────────────────────────
837
+ ```
838
+
839
+ ### Leak warning
840
+
841
+ When using `leak(value)` to escape the scoped type system:
842
+
843
+ ```
844
+ ── Scope Warning ────────────────────────────────────────────────────────────
845
+
846
+ leak(db)
847
+ ^
848
+ |
849
+
850
+ Warning: db is being leaked from scope MyScope.
851
+ This may result in undefined behavior.
852
+
853
+ Hint:
854
+ If you know this data type is not resourceful, then add an Unscoped
855
+ instance for it so ScopeLift can lift it automatically.
856
+
857
+ ────────────────────────────────────────────────────────────────────────────
858
+ ```
859
+
860
+ ---
861
+
552
862
  ## API reference (selected)
553
863
 
554
864
  ### `Scope`
@@ -562,18 +872,16 @@ final class Scope[ParentTag, Tag0 <: ParentTag] {
562
872
  def allocate[A](resource: Resource[A]): A @@ Tag
563
873
  def defer(f: => Unit): Unit
564
874
 
565
- def $[A, B, S](scoped: A @@ S)(f: A => B)(
566
- using ev: this.Tag <:< S,
567
- escape: ScopeEscape[B, S]
568
- ): escape.Out
875
+ // Apply function to scoped value - executes immediately, always returns scoped
876
+ def $[A, B](scoped: A @@ this.Tag)(f: A => B): B @@ Tag
569
877
 
570
- def apply[A, S](scoped: Scoped[S, A])(
571
- using ev: this.Tag <:< S,
572
- escape: ScopeEscape[A, S]
573
- ): escape.Out
878
+ // Execute scoped computation - runs immediately, always returns scoped
879
+ def execute[A](scoped: A @@ this.Tag): A @@ Tag
574
880
 
575
- // Creates a child scope with an existential tag (fresh per call)
576
- def scoped[A](f: Scope[this.Tag, ? <: this.Tag] => A): A
881
+ // Creates a child scope - ScopeLift controls what may exit
882
+ def scoped[A](f: Scope[this.Tag, ? <: this.Tag] => A)(
883
+ using lift: ScopeLift[A, this.Tag]
884
+ ): lift.Out
577
885
  }
578
886
  ```
579
887
 
@@ -587,20 +895,16 @@ object Resource {
587
895
  def acquireRelease[A](acquire: => A)(release: A => Unit): Resource[A]
588
896
  def fromAutoCloseable[A <: AutoCloseable](thunk: => A): Resource[A]
589
897
 
590
- // Macro-derived constructors:
591
- def from[T]: Resource[T]
898
+ // Macro - DI entry point (can also be called with no args for zero-dep classes):
592
899
  def from[T](wires: Wire[?, ?]*): Resource[T]
593
900
 
594
- // Internal / produced by wires:
595
- def shared[A](f: Finalizer => A): Resource.Shared[A]
596
- def unique[A](f: Finalizer => A): Resource.Unique[A]
597
-
598
- final class Shared[+A] extends Resource[A]
599
- final class Unique[+A] extends Resource[A]
901
+ // Internal (used by generated code):
902
+ def shared[A](f: Finalizer => A): Resource[A]
903
+ def unique[A](f: Finalizer => A): Resource[A]
600
904
  }
601
905
  ```
602
906
 
603
- ### `Wire` and `Wireable`
907
+ ### `Wire`
604
908
 
605
909
  ```scala
606
910
  sealed trait Wire[-In, +Out] {
@@ -610,9 +914,16 @@ sealed trait Wire[-In, +Out] {
610
914
  def toResource(deps: zio.blocks.context.Context[In]): Resource[Out]
611
915
  }
612
916
 
613
- trait Wireable[+Out] {
614
- type In
615
- def wire: Wire[In, Out]
917
+ object Wire {
918
+ // Macro entry points:
919
+ def shared[T]: Wire[?, T] // derive from T's constructor
920
+ def unique[T]: Wire[?, T] // derive from T's constructor
921
+
922
+ // Wrap pre-existing value (auto-finalizes if AutoCloseable):
923
+ def apply[T](value: T): Wire.Shared[Any, T]
924
+
925
+ final class Shared[-In, +Out] extends Wire[In, Out]
926
+ final class Unique[-In, +Out] extends Wire[In, Out]
616
927
  }
617
928
  ```
618
929
 
@@ -621,7 +932,16 @@ trait Wireable[+Out] {
621
932
  ## Mental model recap
622
933
 
623
934
  - Use `Scope.global.scoped { scope => ... }` to create a safe region.
624
- - Allocate managed things with `scope.allocate(Resource(...))` (or `Resource.from[...]`).
625
- - Use scoped values only via `scope.$(value)(...)` or via `Scoped` computations executed by `scope(scoped)`.
935
+ - For simple resources: `scope.allocate(Resource(value))` or `scope.allocate(Resource.acquireRelease(...)(...))`
936
+ - For dependency injection: `scope.allocate(Resource.from[App](Wire(config), ...))` auto-wires concrete classes, you provide leaves and overrides.
937
+ - Use scoped values via `(scope $ value) { v => ... }` — the function executes immediately.
938
+ - `$` and `execute` always return scoped values (`B @@ scope.Tag`), never raw values.
939
+ - Escape only happens at `.scoped` boundaries via `ScopeLift`.
626
940
  - Nest with `scope.scoped { child => ... }` to create a tighter lifetime boundary.
941
+ - Return `Unscoped` types from child scopes to extract raw values.
627
942
  - If it doesn't typecheck, it would have been unsafe at runtime.
943
+
944
+ **The 3 macro entry points:**
945
+ - `Wire.shared[T]` — shared wire from constructor
946
+ - `Wire.unique[T]` — unique wire from constructor
947
+ - `Resource.from[T](wires*)` — wire up T and all dependencies