@zio.dev/zio-blocks 0.0.21 → 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,396 +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
- - **Unified scoped type** (`A @@ S` is a type alias for `Scoped[A, S]`)
11
- - **Simple, synchronous lifecycle management** (finalizers run LIFO on scope close)
12
+ ## Why Scope?
12
13
 
13
- ---
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:
14
22
 
15
- ## Table of contents
16
-
17
- - [Quick start](#quick-start)
18
- - [Core concepts](#core-concepts)
19
- - [1) `Scope[ParentTag, Tag]`](#1-scopeparenttag-tag)
20
- - [2) Scoped values: `A @@ S`](#2-scoped-values-a--s)
21
- - [3) `Resource[A]`: acquisition + finalization](#3-resourcea-acquisition--finalization)
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)
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
- - [Building a `Scoped` program (map/flatMap)](#building-a-scoped-program-mapflatmap)
31
- - [Registering cleanup manually with `defer`](#registering-cleanup-manually-with-defer)
32
- - [Dependency injection with `Wire` + `Context`](#dependency-injection-with-wire--context)
33
- - [Dependency injection with `Resource.from[T](wires*)`](#dependency-injection-with-resourcefromtwires)
34
- - [Injecting traits via subtype wires](#injecting-traits-via-subtype-wires)
35
- - [Interop escape hatch: `leak`](#interop-escape-hatch-leak)
36
- - [Common compile errors](#common-compile-errors)
37
- - [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**.
38
33
 
39
34
  ---
40
35
 
41
- ## Quick start
36
+ ## Quick start (Scala 3)
42
37
 
43
38
  ```scala
44
- import zio.blocks.scope._
39
+ import zio.blocks.scope.*
45
40
 
46
- final class Database extends AutoCloseable {
41
+ final class Database extends AutoCloseable:
47
42
  def query(sql: String): String = s"result: $sql"
48
43
  def close(): Unit = println("db closed")
49
- }
50
44
 
51
- Scope.global.scoped { scope =>
52
- val db: Database @@ scope.Tag =
53
- scope.allocate(Resource(new Database))
45
+ @main def quickStart(): Unit =
46
+ val out: String =
47
+ Scope.global.scoped { scope =>
48
+ import scope.*
54
49
 
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
- }
61
- }
62
- ```
50
+ val db: $[Database] =
51
+ Resource.fromAutoCloseable(new Database).allocate
63
52
 
64
- Key things to notice:
53
+ // Safe access: the lambda parameter can only be used as a receiver
54
+ $(db)(_.query("SELECT 1"))
55
+ }
65
56
 
66
- - `scope.allocate(...)` returns a **scoped** value: `Database @@ scope.Tag`
67
- - You **cannot** call `db.query(...)` directly (methods are intentionally hidden)
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
70
- - When the `scoped { ... }` block exits, finalizers run **LIFO** and errors are handled safely
57
+ println(out)
58
+ ```
71
59
 
72
- ---
60
+ Key points:
73
61
 
74
- ## Core concepts
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.
75
67
 
76
- ### 1) `Scope[ParentTag, Tag]`
68
+ ---
77
69
 
78
- A `Scope` manages finalizers and ties values to a *type-level identity* called a **Tag**.
70
+ ## Core mental model
79
71
 
80
- - `Scope[ParentTag, Tag]` has **two** type parameters:
81
- - `ParentTag`: the parent scope's tag (capability boundary)
82
- - `Tag <: ParentTag`: this scope's unique identity (used to tag values)
72
+ ### 1) `Scope`: finalizers + type identity
83
73
 
84
- Every `Scope` also exposes a *path-dependent* member type:
74
+ `Scope` is a finalizer registry plus a unique type identity:
85
75
 
86
- ```scala
87
- type Tag = Tag0
88
- ```
76
+ - `type $[+A]` — a scope-tagged, path-dependent type (erases to `A` at runtime)
77
+ - `type Parent <: Scope` / `val parent: Parent` — the scope hierarchy
89
78
 
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
- val x: Something @@ scope.Tag = ???
83
+ import scope.*
84
+ val x: $[Int] = 1 // ok (in global, $[A] = A)
95
85
  }
96
86
  ```
97
87
 
98
88
  #### Global scope
99
89
 
100
- `Scope.global` is the root of the tag hierarchy:
90
+ `Scope.global` is the root:
91
+
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
95
+
96
+ ---
97
+
98
+ ### 2) Scoped values: `scope.$[A]` / `$[A]`
99
+
100
+ A value of type `scope.$[A]` means:
101
+
102
+ > "This is an `A`, but it is only valid while `scope` is alive."
103
+
104
+ Properties:
105
+
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
109
+
110
+ #### Access operator: `(scope $ value)(f)`
111
+
112
+ The intended way to use a scoped value is:
101
113
 
102
114
  ```scala
103
- object Scope {
104
- type GlobalTag
105
- lazy val global: Scope[GlobalTag, GlobalTag]
106
- }
115
+ (scope $ scopedValue)(a => a.method(...))
107
116
  ```
108
117
 
109
- - The global scope is intended to live for the lifetime of the process.
110
- - Its finalizers run on JVM shutdown.
111
- - Values allocated in `Scope.global` can escape as raw values via `ScopeLift.globalScope` (see below).
118
+ This is enforced by a macro that checks the lambda uses its parameter only in **receiver position**.
112
119
 
113
- ---
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
+ ```
114
128
 
115
- ### 2) Scoped values: `A @@ S`
129
+ Rejected at compile time:
116
130
 
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`.
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
136
+ ```
118
137
 
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*.
122
- - **Access paths:**
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
138
+ ##### "Auto-unwrap" rule (`Unscoped`)
126
139
 
127
- #### Scala 2 note
140
+ `$` *auto-unwraps* when the result type is known to be safe data:
128
141
 
129
- In Scala 2, explicit type annotations are required when assigning scoped values to avoid existential type inference issues:
142
+ - if `B: Unscoped` → `(scope $ sa)(f)` returns **`B`**
143
+ - otherwise → it returns **`scope.$[B]`**
130
144
 
131
145
  ```scala
132
- // Scala 2 requires explicit type annotation
133
- val db: Database @@ scope.Tag = scope.allocate(Resource[Database])
146
+ Scope.global.scoped { scope =>
147
+ import scope.*
148
+
149
+ val db: $[Database] = Resource.from[Database].allocate
134
150
 
135
- // Scala 3 can infer the type
136
- val db = scope.allocate(Resource[Database])
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
+ }
137
154
  ```
138
155
 
139
156
  ---
140
157
 
141
158
  ### 3) `Resource[A]`: acquisition + finalization
142
159
 
143
- `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).
144
161
 
145
- ```scala
146
- scope.allocate(resource)
147
- ```
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).
148
177
 
149
- Common constructors:
178
+ #### Composition
150
179
 
151
- - `Resource(a)`
152
- - Wraps a by-name value; if it's `AutoCloseable`, `close()` is registered automatically.
153
- - `Resource.acquireRelease(acquire)(release)`
154
- - Explicit lifecycle.
155
- - `Resource.fromAutoCloseable(thunk)`
156
- - A type-safe helper for `AutoCloseable`.
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.
180
+ `Resource` composes with:
163
181
 
164
- #### Resource "sharing" vs "uniqueness"
182
+ - `map`
183
+ - `flatMap`
184
+ - `zip`
165
185
 
166
- `Resource` has two important internal flavors:
186
+ Finalizers remain tied to the allocation scope; in composed resources, finalizers still run LIFO.
167
187
 
168
- - `Resource.Unique[A]`
169
- - Produces a **fresh** instance every time you allocate it (typical for `Resource(...)`, `acquireRelease`, etc.).
170
- - `Resource.Shared[A]`
171
- - Produces a **shared** instance per `Resource.Shared` value, with **reference counting**:
172
- - the first allocation initializes the value and collects finalizers
173
- - each allocating scope registers a decrement finalizer
174
- - when the reference count reaches zero, the collected finalizers run
188
+ #### Sharing vs uniqueness (important)
175
189
 
176
- **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.
177
200
 
178
201
  ---
179
202
 
180
- ### 4) `A @@ S`: unified scoped type
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).
181
212
 
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.
213
+ #### Deriving / defining your own instances
183
214
 
184
- Execution happens via:
215
+ Scala 3 (derivation via `Unscoped.derived`):
185
216
 
186
217
  ```scala
187
- scope.execute(scopedComputation)
218
+ import zio.blocks.scope.*
219
+
220
+ final case class Config(debug: Boolean)
221
+ object Config:
222
+ given Unscoped[Config] = Unscoped.derived
188
223
  ```
189
224
 
190
- How to build them:
225
+ Scala 2.13:
191
226
 
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)
227
+ ```scala
228
+ import zio.blocks.scope.*
200
229
 
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.
230
+ final case class Config(debug: Boolean)
231
+ object Config {
232
+ implicit val unscopedConfig: Unscoped[Config] = Unscoped.derived[Config]
233
+ }
234
+ ```
202
235
 
203
- **For-comprehension example:**
236
+ #### Scope boundary example
204
237
 
205
238
  ```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)
239
+ import zio.blocks.scope.*
240
+
241
+ Scope.global.scoped { parent =>
242
+ import parent.*
243
+
244
+ val ok: String =
245
+ parent.scoped { child =>
246
+ "hello" // String is Unscoped
247
+ }
212
248
 
213
- scope.execute(program)
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
214
257
  }
215
258
  ```
216
259
 
217
260
  ---
218
261
 
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.
262
+ ### 5) `lower`: using a parent-scoped value in a child scope
222
263
 
223
- **Built-in Unscoped types:**
224
- - Primitives: `Int`, `Long`, `Boolean`, `Double`, etc.
225
- - `String`, `Unit`, `Nothing`
226
- - Collections of Unscoped types
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:
227
265
 
228
- **Custom Unscoped types:**
229
266
  ```scala
230
- case class Config(debug: Boolean)
231
- object Config {
232
- given Unscoped[Config] = Unscoped.derived
267
+ import zio.blocks.scope.*
268
+
269
+ Scope.global.scoped { outer =>
270
+ import outer.*
271
+
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
+ }
233
279
  }
234
280
  ```
235
281
 
236
- **Important:** `$` and `execute` always return `B @@ scope.Tag`. They never escape values directly. Escape only happens at the `.scoped` boundary via `ScopeLift`.
282
+ This is safe because **parents always outlive children** (child finalizers run before the parent closes).
237
283
 
238
284
  ---
239
285
 
240
- ### 6) `ScopeLift[A, S]`: what may return from child scopes
286
+ ### 6) `defer`: manual finalizers (+ cancellation)
241
287
 
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.
288
+ Use `defer` to register cleanup. It returns a `DeferHandle` you can cancel.
243
289
 
244
- **ScopeLift instances and their behavior:**
290
+ ```scala
291
+ import zio.blocks.scope.*
245
292
 
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) |
293
+ Scope.global.scoped { scope =>
294
+ import scope.*
252
295
 
253
- **Allowed return types:**
296
+ val in = new java.io.ByteArrayInputStream(Array[Byte](1, 2, 3))
254
297
 
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
298
+ val h: DeferHandle =
299
+ defer(in.close())
259
300
 
260
- **Rejected return types (no ScopeLift instance):**
301
+ val first = in.read()
302
+ println(first)
261
303
 
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
304
+ // If you already cleaned up manually:
305
+ // h.cancel() // thread-safe, idempotent
306
+ }
307
+ ```
265
308
 
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
- }
309
+ There is also a **package-level** helper that only requires a `Finalizer`:
272
310
 
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
- }
311
+ ```scala
312
+ import zio.blocks.scope.*
278
313
 
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
- // }
314
+ Scope.global.scoped { scope =>
315
+ import scope.*
316
+ given Finalizer = scope
284
317
 
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
- // }
318
+ defer(println("cleanup")) // uses the package-level helper
289
319
  }
290
320
  ```
291
321
 
292
322
  ---
293
323
 
294
- ### 7) `Wire[-In, +Out]`: dependency recipes
324
+ ### 7) `open()`: non-lexical, explicitly-managed child scopes
325
+
326
+ `scoped` ties lifetime to a block. `open()` creates a child scope you close explicitly.
327
+
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
295
331
 
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.
332
+ From `Scope.global` the returned type is `Scope.OpenScope` directly (because global `$[A] = A`):
297
333
 
298
- - `In` is the required dependencies (provided as a `Context[In]`)
299
- - `Out` is the produced service
334
+ ```scala
335
+ import zio.blocks.scope.*
336
+
337
+ val os: Scope.OpenScope = Scope.global.open()
338
+
339
+ val db = os.scope.allocate(Resource.fromAutoCloseable(new Database))
300
340
 
301
- There are two wire flavors:
341
+ // ... use db ...
342
+
343
+ os.close().orThrow()
344
+ ```
302
345
 
303
- - `Wire.Shared`: produces a shared (memoized) instance
304
- - `Wire.Unique`: produces a fresh instance each time
346
+ Inside a child scope, `open()` returns `$[Scope.OpenScope]`. Prefer using it safely via `$`:
305
347
 
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.
348
+ ```scala
349
+ import zio.blocks.scope.*
307
350
 
308
- #### Creating wires
351
+ Scope.global.scoped { parent =>
352
+ import parent.*
309
353
 
310
- There are exactly **3 macro entry points**:
354
+ val os: $[Scope.OpenScope] = open()
311
355
 
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` |
356
+ $(os) { h =>
357
+ val child = h.scope
358
+ val db = child.allocate(Resource.fromAutoCloseable(new Database))
317
359
 
318
- For wrapping pre-existing values:
360
+ // ...
361
+ h.close().orThrow()
362
+ }
363
+ }
364
+ ```
319
365
 
320
- - `Wire(value)` — wraps a value; if `AutoCloseable`, registers `close()` automatically
366
+ ---
321
367
 
322
- #### How `Resource.from[T](wires*)` works
368
+ ### 8) Escape hatch: `leak`
323
369
 
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
370
+ Sometimes you must hand a raw value to code that cannot work with `$[A]`. Use `leak`:
328
371
 
329
- Key insight: **Compose Resources, don't accumulate values.** Each wire becomes a `Resource`, and they are composed via `flatMap`. This correctly preserves:
372
+ ```scala
373
+ import zio.blocks.scope.*
330
374
 
331
- - **Sharing**: Same `Resource.Shared` instance → same value (even in diamond patterns)
332
- - **Uniqueness**: `Resource.Unique` → fresh value per injection site
375
+ Scope.global.scoped { scope =>
376
+ import scope.*
333
377
 
334
- #### Subtype resolution
378
+ val db: $[Database] = Resource.fromAutoCloseable(new Database).allocate
335
379
 
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.
380
+ val raw: Database = leak(db) // emits a compiler warning
381
+ // thirdParty(raw)
382
+ }
383
+ ```
337
384
 
338
- If the same concrete wire satisfies multiple types (e.g., `Service` and `LiveService`), only **one instance** is created and reused for both.
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.
339
386
 
340
387
  ---
341
388
 
342
389
  ## Safety model (why leaking is prevented)
343
390
 
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.
391
+ Scope's safety comes from *three reinforcing layers*.
392
+
393
+ ### 1) Type barrier: scope-specific `$[A]`
394
+
395
+ Every scope has a distinct `$[A]` type. You cannot accidentally use values across scopes without an explicit conversion (`lower` for parent → child).
396
+
397
+ ### 2) Controlled access: `$` macro restricts lambda usage
345
398
 
346
- The library prevents scope leaks via two reinforcing mechanisms:
399
+ The `$` operator only allows using the unwrapped value as a **method/field receiver**. This prevents:
347
400
 
348
- ### A) Existential child tags (fresh, unnameable types)
401
+ - returning the resource
402
+ - storing it in a local val/var
403
+ - passing it as an argument
404
+ - capturing it in a closure
349
405
 
350
- Child scopes are created with:
406
+ Also note: `$` requires a **lambda literal**. Method references / variables are rejected:
351
407
 
352
408
  ```scala
353
- Scope.global.scoped { scope =>
354
- scope.scoped { child =>
355
- // allocate in child
356
- }
357
- }
409
+ // does not compile:
410
+ val f: Database => String = _.query("x")
411
+ (scope $ db)(f) // "$ requires a lambda literal ..."
358
412
  ```
359
413
 
360
- The child scope has an existential tag (fresh 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 tag.
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`).
361
419
 
362
- Compile-time safety is verified in tests, e.g.:
363
- `ScopeCompileTimeSafetyScala3Spec`.
420
+ ### Closed-scope defense (runtime)
364
421
 
365
- ### B) Contravariance prevents child-to-parent widening
422
+ If a scope escapes and is used after closing, operations become **no-ops returning default values**:
366
423
 
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.
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
368
428
 
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.
429
+ This prevents post-close interaction with released resources, but can produce surprising default values if scopes are misused across threads.
430
+
431
+ ### Thread ownership rule (JVM)
432
+
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.)
370
438
 
371
439
  ---
372
440
 
373
- ## Usage examples
441
+ ## Usage examples (patterns)
374
442
 
375
443
  ### Allocating and using a resource
376
444
 
377
445
  ```scala
378
- import zio.blocks.scope._
446
+ import zio.blocks.scope.*
379
447
 
380
- final class FileHandle(path: String) extends AutoCloseable {
448
+ final class FileHandle(path: String) extends AutoCloseable:
381
449
  def readAll(): String = s"contents of $path"
382
450
  def close(): Unit = println(s"closed $path")
383
- }
384
451
 
385
- Scope.global.scoped { scope =>
386
- val h = scope.allocate(Resource(new FileHandle("data.txt")))
452
+ @main def fileExample(): Unit =
453
+ Scope.global.scoped { scope =>
454
+ import scope.*
455
+
456
+ val h: $[FileHandle] =
457
+ Resource(new FileHandle("data.txt")).allocate
458
+
459
+ val contents: String =
460
+ $(h)(_.readAll())
387
461
 
388
- // $ executes immediately - work with the value inside the function
389
- (scope $ h) { handle =>
390
- val contents = handle.readAll()
391
462
  println(contents)
392
463
  }
393
- }
394
464
  ```
395
465
 
396
466
  ---
@@ -398,333 +468,393 @@ Scope.global.scoped { scope =>
398
468
  ### Nested scopes (child can use parent, not vice versa)
399
469
 
400
470
  ```scala
401
- import zio.blocks.scope._
471
+ import zio.blocks.scope.*
402
472
 
403
- Scope.global.scoped { parent =>
404
- val parentDb = parent.allocate(Resource(new Database))
473
+ final class Database extends AutoCloseable:
474
+ def query(sql: String): String = s"result: $sql"
475
+ def close(): Unit = println("db closed")
405
476
 
406
- parent.scoped { child =>
407
- // child can use parent-scoped values:
408
- child.$(parentDb) { db =>
409
- println(db.query("SELECT 1"))
410
- }
477
+ @main def nested(): Unit =
478
+ Scope.global.scoped { parent =>
479
+ import parent.*
411
480
 
412
- val childDb = child.allocate(Resource(new Database))
481
+ val parentDb: $[Database] = Resource.fromAutoCloseable(new Database).allocate
413
482
 
414
- // You can use childDb *inside* the child:
415
- child.$(childDb) { db =>
416
- println(db.query("SELECT 2"))
417
- }
483
+ val done: String =
484
+ parent.scoped { child =>
485
+ import child.*
418
486
 
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
424
- }
487
+ val db: $[Database] = lower(parentDb)
488
+ println($(db)(_.query("SELECT 1")))
425
489
 
426
- // parentDb is still usable here:
427
- parent.$(parentDb) { db =>
428
- println(db.query("SELECT 3"))
429
- }
430
- }
431
- ```
432
-
433
- ---
434
-
435
- ### Building a `Scoped` program (map/flatMap)
490
+ val childDb: $[Database] = Resource.fromAutoCloseable(new Database).allocate
491
+ println($(childDb)(_.query("SELECT 2")))
436
492
 
437
- You can use for-comprehensions to compose scoped computations. Each `<-` uses `flatMap`, accumulating requirements:
493
+ // childDb cannot be returned to the parent (not Unscoped)
494
+ "done"
495
+ }
438
496
 
439
- ```scala
440
- import zio.blocks.scope._
497
+ println($(parentDb)(_.query("SELECT 3")))
498
+ done
499
+ }
500
+ ```
441
501
 
442
- Scope.global.scoped { scope =>
443
- val db: Database @@ scope.Tag = scope.allocate(Resource(new Database))
502
+ Finalizers run **child first, then parent**.
444
503
 
445
- // Build a scoped computation with map
446
- val program: String @@ scope.Tag = db.map { d =>
447
- d.query("SELECT 1")
448
- }
504
+ ---
449
505
 
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
- ```
506
+ ### Chaining resource acquisition (`$[Resource[A]]` + `.allocate`)
455
507
 
456
- This pattern shines when chaining resource acquisition:
508
+ If a method returns `Resource[A]`, `$` returns a **scoped** `Resource[A]` (because `Resource[A]` is not `Unscoped`). Allocate it without leaking:
457
509
 
458
510
  ```scala
459
- import zio.blocks.scope._
511
+ import zio.blocks.scope.*
460
512
 
461
- // A pool that leases connections
462
- class Pool(implicit finalizer: Finalizer) extends AutoCloseable {
463
- def lease: Resource[Connection] = Resource(new Connection)
513
+ final class Pool extends AutoCloseable:
514
+ def lease(): Resource[Conn] = Resource.fromAutoCloseable(new Conn)
464
515
  def close(): Unit = println("pool closed")
465
- }
466
516
 
467
- class Connection extends AutoCloseable {
517
+ final class Conn extends AutoCloseable:
468
518
  def query(sql: String): String = s"result: $sql"
469
519
  def close(): Unit = println("connection closed")
470
- }
471
520
 
472
- Scope.global.scoped { scope =>
473
- val pool: Pool @@ scope.Tag = scope.allocate(Resource.from[Pool])
521
+ @main def chaining(): Unit =
522
+ Scope.global.scoped { scope =>
523
+ import scope.*
474
524
 
475
- val program: String @@ scope.Tag =
476
- for {
477
- p <- pool // extract Pool from scoped
478
- connection <- scope.allocate(p.lease) // allocate returns Connection @@ Tag
479
- } yield connection.query("SELECT 1")
525
+ val pool: $[Pool] = Resource.fromAutoCloseable(new Pool).allocate
480
526
 
481
- // execute runs the computation - connection is used inside the yield
482
- scope.execute(program)
483
- }
484
- // Output: connection closed, then 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
+ }
485
536
  ```
486
537
 
538
+ This `.allocate` comes from `Scope.ScopedResourceOps` (an extension on `$[Resource[A]]`).
539
+
487
540
  ---
488
541
 
489
- ### Registering cleanup manually with `defer`
542
+ ### Allocating a bare `Resource[A]` with `.allocate`
490
543
 
491
- Use `scope.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)`:
492
545
 
493
546
  ```scala
494
- import zio.blocks.scope._
547
+ import zio.blocks.scope.*
495
548
 
496
549
  Scope.global.scoped { scope =>
497
- val handle = new java.io.ByteArrayInputStream(Array[Byte](1, 2, 3))
550
+ import scope.*
498
551
 
499
- scope.defer { handle.close() }
552
+ val db: $[Database] =
553
+ Resource.fromAutoCloseable(new Database).allocate
500
554
 
501
- val firstByte = handle.read()
502
- println(firstByte)
555
+ $(db)(_.query("SELECT 1"))
503
556
  }
504
557
  ```
505
558
 
506
- 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.
507
564
 
508
565
  ```scala
509
- import zio.blocks.scope._
566
+ import zio.blocks.scope.*
510
567
 
511
- Scope.global.scoped { scope =>
512
- given Finalizer = scope
568
+ final case class Config(url: String)
569
+ object Config:
570
+ given Unscoped[Config] = Unscoped.derived
513
571
 
514
- defer { println("cleanup") }
515
- }
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
+ }
516
587
  ```
517
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
+
518
594
  ---
519
595
 
520
- ### Classes with `Finalizer` parameters
596
+ ### Classes with `Scope` parameters (scope injection)
521
597
 
522
- 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`:
523
599
 
524
600
  ```scala
525
- import zio.blocks.scope._
601
+ import zio.blocks.scope.*
526
602
 
527
- class ConnectionPool(config: Config)(implicit finalizer: Finalizer) {
528
- private val pool = createPool(config)
529
- defer { pool.shutdown() }
530
-
531
- def getConnection(): Connection = pool.acquire()
532
- }
603
+ final case class Config(url: String)
604
+ object Config:
605
+ given Unscoped[Config] = Unscoped.derived
533
606
 
534
- // The macro sees the implicit Finalizer and injects it automatically:
535
- 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")
536
610
 
537
- Scope.global.scoped { scope =>
538
- val pool = scope.allocate(resource)
539
- // pool.shutdown() will be called when scope closes
540
- }
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
+ }
541
631
  ```
542
632
 
543
- Why `Finalizer` instead of `Scope`?
544
- - `Finalizer` is the minimal interface—it only has `defer`
545
- - Classes that need cleanup should not have access to `allocate` or `$`
546
- - 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.
547
634
 
548
635
  ---
549
636
 
550
- ### 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`.
640
+
641
+ ### `Wire[-In, +Out]`: a dependency recipe
551
642
 
552
- For manual wiring (when you already have dependencies assembled), use `wire.toResource(ctx)`:
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
553
649
 
554
650
  ```scala
555
- import zio.blocks.scope._
651
+ import zio.blocks.scope.*
556
652
  import zio.blocks.context.Context
557
653
 
558
654
  final case class Config(debug: Boolean)
655
+ object Config:
656
+ given Unscoped[Config] = Unscoped.derived
559
657
 
560
- val w: Wire.Shared[Boolean, Config] = Wire.shared[Config]
561
- val deps: Context[Boolean] = Context[Boolean](true)
658
+ val w: Wire.Shared[Boolean, Config] =
659
+ Wire.shared[Config] // Boolean => Config
562
660
 
563
- Scope.global.scoped { scope =>
564
- val cfg: Config @@ scope.Tag =
565
- scope.allocate(w.toResource(deps))
661
+ val deps: Context[Boolean] =
662
+ Context(true)
566
663
 
567
- val debug: Boolean =
568
- (scope $ cfg)(_.debug) // Boolean typically escapes
664
+ @main def wireAndContext(): Unit =
665
+ Scope.global.scoped { scope =>
666
+ import scope.*
569
667
 
570
- println(debug)
571
- }
668
+ val cfg: $[Config] =
669
+ allocate(w.toResource(deps))
670
+
671
+ val debug: Boolean =
672
+ $(cfg)(_.debug)
673
+
674
+ println(debug)
675
+ }
572
676
  ```
573
677
 
574
- Sharing vs uniqueness at the wire level:
678
+ #### Sharing vs uniqueness at the wire level
575
679
 
576
680
  ```scala
577
- import zio.blocks.scope._
681
+ import zio.blocks.scope.*
578
682
 
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
683
+ val ws = Wire.shared[Config] // shared recipe
684
+ val wu = Wire.unique[Config] // unique recipe
581
685
  ```
582
686
 
687
+ The difference is realized when converting to resources (`toResource`) and allocating.
688
+
583
689
  ---
584
690
 
585
- ### 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:
586
694
 
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]`.
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:
588
702
 
589
703
  ```scala
590
- import zio.blocks.scope._
704
+ import zio.blocks.scope.*
591
705
 
592
706
  final case class Config(url: String)
707
+ object Config:
708
+ given Unscoped[Config] = Unscoped.derived
593
709
 
594
- final class Logger {
710
+ final class Logger:
595
711
  def info(msg: String): Unit = println(msg)
596
- }
597
712
 
598
- final class Database(cfg: Config) extends AutoCloseable {
713
+ final class Database(cfg: Config) extends AutoCloseable:
599
714
  def query(sql: String): String = s"[${cfg.url}] $sql"
600
715
  def close(): Unit = println("database closed")
601
- }
602
716
 
603
- final class Service(db: Database, logger: Logger) extends AutoCloseable {
717
+ final class Service(db: Database, logger: Logger) extends AutoCloseable:
604
718
  def run(): Unit = logger.info(s"running with ${db.query("SELECT 1")}")
605
719
  def close(): Unit = println("service closed")
606
- }
607
720
 
608
- // Only provide leaf values (primitives, configs) - the rest is auto-wired:
609
721
  val serviceResource: Resource[Service] =
610
722
  Resource.from[Service](
611
- Wire(Config("jdbc:postgresql://localhost/db"))
723
+ Wire(Config("jdbc:postgresql://localhost/db")) // leaf value
612
724
  )
613
725
 
614
- Scope.global.scoped { scope =>
615
- val svc = scope.allocate(serviceResource)
616
- (scope $ svc)(_.run())
617
- }
618
- // Output: running with [jdbc:postgresql://localhost/db] SELECT 1
619
- // 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
+ }
620
732
  ```
621
733
 
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`)
629
-
630
- ---
631
-
632
- ### Injecting traits via subtype wires
734
+ #### Injecting traits via subtype wires
633
735
 
634
- 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:
635
737
 
636
738
  ```scala
637
- import zio.blocks.scope._
739
+ import zio.blocks.scope.*
638
740
 
639
- trait Logger {
741
+ trait Logger:
640
742
  def info(msg: String): Unit
641
- }
642
743
 
643
- final class ConsoleLogger extends Logger {
744
+ final class ConsoleLogger extends Logger:
644
745
  def info(msg: String): Unit = println(msg)
645
- }
646
746
 
647
- final class App(logger: Logger) {
747
+ final class App(logger: Logger):
648
748
  def run(): Unit = logger.info("Hello!")
649
- }
650
749
 
651
- // Wire.shared[ConsoleLogger] satisfies the Logger dependency via subtyping:
652
750
  val appResource: Resource[App] =
653
751
  Resource.from[App](
654
- Wire.shared[ConsoleLogger]
752
+ Wire.shared[ConsoleLogger] // satisfies Logger via subtyping
655
753
  )
656
754
 
657
- Scope.global.scoped { scope =>
658
- val app = scope.allocate(appResource)
659
- (scope $ app)(_.run())
660
- }
755
+ @main def traitInjection(): Unit =
756
+ Scope.global.scoped { scope =>
757
+ import scope.*
758
+ val app: $[App] = appResource.allocate
759
+ $(app)(_.run())
760
+ }
661
761
  ```
662
762
 
663
- **Single instance for diamond patterns:**
763
+ #### Diamond patterns share a single instance (when appropriate)
664
764
 
665
765
  ```scala
766
+ import zio.blocks.scope.*
767
+
666
768
  trait Service
667
- class LiveService extends Service
668
- class NeedsService(s: Service)
669
- class NeedsLive(l: LiveService)
670
- 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)
671
773
 
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
774
+ val appResource: Resource[App] =
775
+ Resource.from[App](
776
+ Wire.shared[LiveService]
777
+ )
778
+ // LiveService instantiations: 1
677
779
  ```
678
780
 
679
781
  ---
680
782
 
681
- ### Interop escape hatch: `leak`
783
+ ## Common compile errors (and what they mean)
682
784
 
683
- Sometimes you must hand a raw value to code that cannot work with `@@` types.
785
+ This module produces two kinds of compile-time feedback:
684
786
 
685
- ```scala
686
- import zio.blocks.scope._
687
-
688
- Scope.global.scoped { scope =>
689
- val db = scope.allocate(Resource(new Database))
787
+ - **Plain macro aborts** for unsafe `$` usage
788
+ - **ASCII-rendered** errors/warnings for DI derivation + leak warnings (via `internal.ErrorMessages`)
690
789
 
691
- val raw: Database = leak(db) // emits a compiler warning
692
- // thirdParty(raw)
693
- }
694
- ```
790
+ ### Unsafe use inside `$`
695
791
 
696
- **Warning:** leaking bypasses compile-time guarantees. The value may be used after its scope closes. Use only when unavoidable.
792
+ Typical messages include:
697
793
 
698
- ---
794
+ ```
795
+ Unsafe use of scoped value: the lambda parameter cannot be passed as an argument to a function or method.
796
+ ```
699
797
 
700
- ## Common compile errors
798
+ Other variants:
701
799
 
702
- 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.`
703
803
 
704
- ### Not a class (Wire.shared/unique on trait or abstract class)
804
+ ### Not a class (`Wire.shared/unique` on a trait / abstract)
705
805
 
706
806
  ```
707
- ── Scope Error ──────────────────────────────────────────────────────────────
807
+ ── Scope Error ─────────────────────────────────────────────────────────────────
708
808
 
709
809
  Cannot derive Wire for MyTrait: not a class.
710
810
 
711
811
  Hint: Use Wire.Shared / Wire.Unique directly.
712
812
 
713
- ────────────────────────────────────────────────────────────────────────────
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
+ ───────────────────────────────────────────────────────────────────────────────
714
844
  ```
715
845
 
716
846
  ### Unmakeable type (primitives, functions, collections)
717
847
 
718
848
  ```
719
- ── Scope Error ──────────────────────────────────────────────────────────────
849
+ ── Scope Error ─────────────────────────────────────────────────────────────────
720
850
 
721
851
  Cannot auto-create String
722
852
 
723
853
  This type (primitive, collection, or function) cannot be auto-created.
724
854
 
725
855
  Required by:
726
- └── Config
727
- └── App
856
+ ├── Config
857
+ └── App
728
858
 
729
859
  Fix: Provide Wire(value) with the desired value:
730
860
 
@@ -733,20 +863,20 @@ The scope macros produce beautiful, actionable compile-time error messages with
733
863
  ...
734
864
  )
735
865
 
736
- ────────────────────────────────────────────────────────────────────────────
866
+ ───────────────────────────────────────────────────────────────────────────────
737
867
  ```
738
868
 
739
- ### Abstract type (trait or abstract class)
869
+ ### Abstract type (trait / abstract class dependency)
740
870
 
741
871
  ```
742
- ── Scope Error ──────────────────────────────────────────────────────────────
872
+ ── Scope Error ─────────────────────────────────────────────────────────────────
743
873
 
744
874
  Cannot auto-create Logger
745
875
 
746
876
  This type is abstract (trait or abstract class).
747
877
 
748
878
  Required by:
749
- └── App
879
+ └── App
750
880
 
751
881
  Fix: Provide a wire for a concrete implementation:
752
882
 
@@ -755,13 +885,13 @@ The scope macros produce beautiful, actionable compile-time error messages with
755
885
  ...
756
886
  )
757
887
 
758
- ────────────────────────────────────────────────────────────────────────────
888
+ ───────────────────────────────────────────────────────────────────────────────
759
889
  ```
760
890
 
761
891
  ### Duplicate providers (ambiguous wires)
762
892
 
763
893
  ```
764
- ── Scope Error ──────────────────────────────────────────────────────────────
894
+ ── Scope Error ────────────────────────────────────────────────────────────────
765
895
 
766
896
  Multiple providers for Service
767
897
 
@@ -771,13 +901,13 @@ The scope macros produce beautiful, actionable compile-time error messages with
771
901
 
772
902
  Hint: Remove duplicate wires or use distinct wrapper types.
773
903
 
774
- ────────────────────────────────────────────────────────────────────────────
904
+ ───────────────────────────────────────────────────────────────────────────────
775
905
  ```
776
906
 
777
907
  ### Dependency cycle
778
908
 
779
909
  ```
780
- ── Scope Error ──────────────────────────────────────────────────────────────
910
+ ── Scope Error ────────────────────────────────────────────────────────────────
781
911
 
782
912
  Dependency cycle detected
783
913
 
@@ -793,13 +923,13 @@ The scope macros produce beautiful, actionable compile-time error messages with
793
923
  • Using lazy initialization
794
924
  • Restructuring dependencies
795
925
 
796
- ────────────────────────────────────────────────────────────────────────────
926
+ ───────────────────────────────────────────────────────────────────────────────
797
927
  ```
798
928
 
799
929
  ### Subtype conflict (related dependency types)
800
930
 
801
931
  ```
802
- ── Scope Error ──────────────────────────────────────────────────────────────
932
+ ── Scope Error ────────────────────────────────────────────────────────────────
803
933
 
804
934
  Dependency type conflict in MyService
805
935
 
@@ -812,16 +942,16 @@ The scope macros produce beautiful, actionable compile-time error messages with
812
942
  To fix this, wrap one or both types in a distinct wrapper:
813
943
 
814
944
  case class WrappedInputStream(value: InputStream)
815
- or
945
+ or
816
946
  opaque type WrappedInputStream = InputStream
817
947
 
818
- ────────────────────────────────────────────────────────────────────────────
948
+ ───────────────────────────────────────────────────────────────────────────────
819
949
  ```
820
950
 
821
- ### Duplicate parameter types in constructor
951
+ ### Duplicate parameter types in a constructor
822
952
 
823
953
  ```
824
- ── Scope Error ──────────────────────────────────────────────────────────────
954
+ ── Scope Error ────────────────────────────────────────────────────────────────
825
955
 
826
956
  Constructor of App has multiple parameters of type String
827
957
 
@@ -830,118 +960,240 @@ The scope macros produce beautiful, actionable compile-time error messages with
830
960
  Fix: Wrap one parameter in an opaque type to distinguish them:
831
961
 
832
962
  opaque type FirstString = String
833
- or
963
+ or
834
964
  case class FirstString(value: String)
835
965
 
836
- ────────────────────────────────────────────────────────────────────────────
966
+ ───────────────────────────────────────────────────────────────────────────────
837
967
  ```
838
968
 
839
969
  ### Leak warning
840
970
 
841
- When using `leak(value)` to escape the scoped type system:
842
-
843
971
  ```
844
- ── Scope Warning ────────────────────────────────────────────────────────────
972
+ ── Scope Warning ───────────────────────────────────────────────────────────────
845
973
 
846
974
  leak(db)
847
975
  ^
848
976
  |
849
977
 
850
- Warning: db is being leaked from scope MyScope.
978
+ Warning: db is being leaked from scope zio.blocks.scope.Scope.Child[...].
851
979
  This may result in undefined behavior.
852
980
 
853
981
  Hint:
854
982
  If you know this data type is not resourceful, then add an Unscoped
855
- instance for it so ScopeLift can lift it automatically.
983
+ instance for it so you do not need to leak it.
856
984
 
857
- ────────────────────────────────────────────────────────────────────────────
985
+ ───────────────────────────────────────────────────────────────────────────────
858
986
  ```
859
987
 
860
988
  ---
861
989
 
862
- ## 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).
863
993
 
864
994
  ### `Scope`
865
995
 
866
- 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:
867
1009
 
868
1010
  ```scala
869
- final class Scope[ParentTag, Tag0 <: ParentTag] {
870
- type Tag = Tag0
1011
+ def scoped[A](f: (child: Scope.Child[this.type]) => A)(using Unscoped[A]): A
871
1012
 
872
- def allocate[A](resource: Resource[A]): A @@ Tag
873
- def defer(f: => Unit): Unit
1013
+ def allocate[A](resource: Resource[A]): $[A]
1014
+ def allocate[A <: AutoCloseable](value: => A): $[A]
874
1015
 
875
- // Apply function to scoped value - executes immediately, always returns scoped
876
- def $[A, B](scoped: A @@ this.Tag)(f: A => B): B @@ Tag
1016
+ infix transparent inline def $[A, B](sa: $[A])(inline f: A => B): B | $[B]
877
1017
 
878
- // Execute scoped computation - runs immediately, always returns scoped
879
- def execute[A](scoped: A @@ this.Tag): A @@ Tag
1018
+ def lower[A](value: parent.$[A]): $[A]
880
1019
 
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
885
- }
1020
+ override def defer(f: => Unit): DeferHandle
1021
+
1022
+ def open(): $[Scope.OpenScope]
1023
+
1024
+ inline def leak[A](inline sa: $[A]): A
886
1025
  ```
887
1026
 
888
- ### `Resource`
1027
+ Notes:
1028
+
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]
1041
+ ```
1042
+
1043
+ ---
1044
+
1045
+ ### `Scope.global`
1046
+
1047
+ ```scala
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`
1062
+
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]`
889
1118
 
890
1119
  ```scala
891
- sealed trait Resource[+A]
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
+ ```
892
1125
 
893
- object Resource {
1126
+ Companion constructors:
1127
+
1128
+ ```scala
1129
+ object Resource:
894
1130
  def apply[A](value: => A): Resource[A]
895
- def acquireRelease[A](acquire: => A)(release: A => Unit): Resource[A]
896
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]
897
1135
 
898
- // Macro - DI entry point (can also be called with no args for zero-dep classes):
899
- def from[T](wires: Wire[?, ?]*): Resource[T]
900
-
901
- // Internal (used by generated code):
902
- def shared[A](f: Finalizer => A): Resource[A]
903
- def unique[A](f: Finalizer => A): Resource[A]
904
- }
1136
+ inline def from[T]: Resource[T]
1137
+ inline def from[T](inline wires: Wire[?, ?]*): Resource[T]
905
1138
  ```
906
1139
 
907
- ### `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]`
908
1148
 
909
1149
  ```scala
910
- sealed trait Wire[-In, +Out] {
1150
+ sealed trait Wire[-In, +Out]:
911
1151
  def isShared: Boolean
1152
+ def isUnique: Boolean = !isShared
1153
+
912
1154
  def shared: Wire.Shared[In, Out]
913
1155
  def unique: Wire.Unique[In, Out]
1156
+
914
1157
  def toResource(deps: zio.blocks.context.Context[In]): Resource[Out]
915
- }
1158
+ ```
916
1159
 
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]
927
- }
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]
928
1171
  ```
929
1172
 
1173
+ Notes:
1174
+
1175
+ - `Wire(t)` wraps a pre-existing value; if it's `AutoCloseable`, `close()` is registered automatically when used.
1176
+
930
1177
  ---
931
1178
 
932
- ## Mental model recap
933
-
934
- - Use `Scope.global.scoped { scope => ... }` to create a safe region.
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`.
940
- - Nest with `scope.scoped { child => ... }` to create a tighter lifetime boundary.
941
- - Return `Unscoped` types from child scopes to extract raw values.
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
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, ...)
1187
+ ```
1188
+
1189
+ ---
1190
+
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