@shd101wyy/yo 0.1.28 → 0.1.30

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.
Files changed (183) hide show
  1. package/.github/skills/yo-async-effects/SKILL.md +15 -15
  2. package/.github/skills/yo-async-effects/async-effects-recipes.md +118 -121
  3. package/.github/skills/yo-core-patterns/core-patterns-cheatsheet.md +33 -13
  4. package/.github/skills/yo-project-workflow/workflow-cheatsheet.md +1 -1
  5. package/.github/skills/yo-syntax/SKILL.md +2 -2
  6. package/.github/skills/yo-syntax/syntax-cheatsheet.md +108 -96
  7. package/README.md +6 -3
  8. package/out/cjs/index.cjs +812 -706
  9. package/out/cjs/yo-cli.cjs +1023 -907
  10. package/out/cjs/yo-lsp.cjs +836 -730
  11. package/out/esm/index.mjs +757 -651
  12. package/out/types/src/codegen/exprs/async.d.ts +2 -0
  13. package/out/types/src/codegen/exprs/await.d.ts +1 -0
  14. package/out/types/src/codegen/exprs/closures.d.ts +4 -0
  15. package/out/types/src/codegen/functions/context.d.ts +6 -0
  16. package/out/types/src/codegen/functions/declarations.d.ts +1 -1
  17. package/out/types/src/doc/model.d.ts +0 -1
  18. package/out/types/src/env.d.ts +2 -2
  19. package/out/types/src/evaluator/builtins/pragma.d.ts +9 -0
  20. package/out/types/src/evaluator/builtins/unsafe.d.ts +8 -0
  21. package/out/types/src/evaluator/context.d.ts +3 -1
  22. package/out/types/src/evaluator/exprs/{escape.d.ts → unwind.d.ts} +1 -1
  23. package/out/types/src/evaluator/index.d.ts +1 -1
  24. package/out/types/src/evaluator/memory-safety.d.ts +14 -0
  25. package/out/types/src/evaluator/types/flowability.d.ts +6 -0
  26. package/out/types/src/evaluator/types/function.d.ts +1 -2
  27. package/out/types/src/evaluator/utils.d.ts +0 -1
  28. package/out/types/src/expr-traversal.d.ts +1 -0
  29. package/out/types/src/expr.d.ts +9 -7
  30. package/out/types/src/public-safe-report.d.ts +19 -0
  31. package/out/types/src/tests/comptime-ref-gate.test.d.ts +1 -0
  32. package/out/types/src/tests/pragma-validation.test.d.ts +1 -0
  33. package/out/types/src/tests/public-safe-report.test.d.ts +1 -0
  34. package/out/types/src/tests/type-representation-pointer.test.d.ts +1 -0
  35. package/out/types/src/tests/unsafe-gate.test.d.ts +1 -0
  36. package/out/types/src/tests/unsafe-report-classify.test.d.ts +1 -0
  37. package/out/types/src/types/creators.d.ts +4 -6
  38. package/out/types/src/types/definitions.d.ts +9 -16
  39. package/out/types/src/types/guards.d.ts +1 -2
  40. package/out/types/src/types/tags.d.ts +0 -1
  41. package/out/types/src/types/utils.d.ts +5 -0
  42. package/out/types/src/unsafe-report.d.ts +29 -0
  43. package/out/types/src/value.d.ts +1 -0
  44. package/out/types/tsconfig.tsbuildinfo +1 -1
  45. package/package.json +1 -1
  46. package/scripts/add-pragma-for-pointer-decls.ts +134 -0
  47. package/scripts/add-pragma.ts +58 -0
  48. package/scripts/migrate-amp-method-calls.ts +186 -0
  49. package/scripts/migrate-clone-calls.ts +93 -0
  50. package/scripts/migrate-get-unwrap.ts +166 -0
  51. package/scripts/migrate-index-patterns.ts +210 -0
  52. package/scripts/migrate-index-trait.ts +142 -0
  53. package/scripts/migrate-iterator.ts +150 -0
  54. package/scripts/migrate-self-ptr.ts +220 -0
  55. package/scripts/migrate-skip-pragmas.ts +109 -0
  56. package/scripts/migrate-tostring.ts +134 -0
  57. package/scripts/trim-pragma.ts +130 -0
  58. package/scripts/wrap-extern-calls.ts +161 -0
  59. package/std/alg/hash.yo +3 -2
  60. package/std/allocator.yo +6 -5
  61. package/std/async.yo +2 -2
  62. package/std/collections/array_list.yo +59 -40
  63. package/std/collections/btree_map.yo +19 -18
  64. package/std/collections/deque.yo +9 -8
  65. package/std/collections/hash_map.yo +101 -13
  66. package/std/collections/hash_set.yo +5 -4
  67. package/std/collections/linked_list.yo +39 -4
  68. package/std/collections/ordered_map.yo +3 -3
  69. package/std/collections/priority_queue.yo +14 -13
  70. package/std/crypto/md5.yo +2 -1
  71. package/std/crypto/random.yo +21 -20
  72. package/std/crypto/sha256.yo +2 -1
  73. package/std/encoding/base64.yo +18 -18
  74. package/std/encoding/hex.yo +5 -5
  75. package/std/encoding/json.yo +62 -13
  76. package/std/encoding/punycode.yo +24 -23
  77. package/std/encoding/toml.yo +4 -3
  78. package/std/encoding/utf16.yo +3 -3
  79. package/std/env.yo +43 -28
  80. package/std/error.yo +15 -3
  81. package/std/fmt/display.yo +2 -2
  82. package/std/fmt/index.yo +6 -5
  83. package/std/fmt/to_string.yo +39 -38
  84. package/std/fmt/writer.yo +9 -8
  85. package/std/fs/dir.yo +61 -66
  86. package/std/fs/file.yo +121 -126
  87. package/std/fs/metadata.yo +13 -18
  88. package/std/fs/temp.yo +35 -30
  89. package/std/fs/walker.yo +14 -19
  90. package/std/gc.yo +1 -0
  91. package/std/glob.yo +7 -7
  92. package/std/http/client.yo +33 -36
  93. package/std/http/http.yo +6 -6
  94. package/std/http/index.yo +4 -4
  95. package/std/imm/list.yo +33 -0
  96. package/std/imm/map.yo +2 -1
  97. package/std/imm/set.yo +1 -0
  98. package/std/imm/sorted_map.yo +1 -0
  99. package/std/imm/sorted_set.yo +1 -0
  100. package/std/imm/string.yo +27 -23
  101. package/std/imm/vec.yo +18 -2
  102. package/std/io/reader.yo +2 -1
  103. package/std/io/writer.yo +3 -2
  104. package/std/libc/assert.yo +1 -0
  105. package/std/libc/ctype.yo +1 -0
  106. package/std/libc/dirent.yo +1 -0
  107. package/std/libc/errno.yo +1 -0
  108. package/std/libc/fcntl.yo +1 -0
  109. package/std/libc/float.yo +1 -0
  110. package/std/libc/limits.yo +1 -0
  111. package/std/libc/math.yo +1 -0
  112. package/std/libc/signal.yo +1 -0
  113. package/std/libc/stdatomic.yo +1 -0
  114. package/std/libc/stdint.yo +1 -0
  115. package/std/libc/stdio.yo +1 -0
  116. package/std/libc/stdlib.yo +1 -0
  117. package/std/libc/string.yo +1 -0
  118. package/std/libc/sys/stat.yo +1 -0
  119. package/std/libc/time.yo +1 -0
  120. package/std/libc/unistd.yo +1 -0
  121. package/std/libc/wctype.yo +1 -0
  122. package/std/libc/windows.yo +2 -0
  123. package/std/log.yo +7 -6
  124. package/std/net/addr.yo +6 -5
  125. package/std/net/dns.yo +13 -16
  126. package/std/net/errors.yo +9 -9
  127. package/std/net/tcp.yo +71 -74
  128. package/std/net/udp.yo +40 -43
  129. package/std/os/signal.yo +5 -5
  130. package/std/path.yo +1 -0
  131. package/std/prelude.yo +377 -200
  132. package/std/process/command.yo +57 -46
  133. package/std/process/index.yo +2 -1
  134. package/std/regex/compiler.yo +10 -9
  135. package/std/regex/index.yo +41 -41
  136. package/std/regex/match.yo +2 -2
  137. package/std/regex/parser.yo +31 -31
  138. package/std/regex/vm.yo +42 -41
  139. package/std/string/string.yo +95 -40
  140. package/std/string/string_builder.yo +9 -9
  141. package/std/string/unicode.yo +50 -49
  142. package/std/sync/channel.yo +2 -1
  143. package/std/sync/cond.yo +5 -4
  144. package/std/sync/mutex.yo +4 -3
  145. package/std/sys/advise.yo +1 -0
  146. package/std/sys/bufio/buf_reader.yo +27 -26
  147. package/std/sys/bufio/buf_writer.yo +22 -21
  148. package/std/sys/clock.yo +1 -0
  149. package/std/sys/copy.yo +1 -0
  150. package/std/sys/dir.yo +10 -9
  151. package/std/sys/dns.yo +6 -5
  152. package/std/sys/errors.yo +12 -12
  153. package/std/sys/events.yo +1 -0
  154. package/std/sys/externs.yo +38 -37
  155. package/std/sys/file.yo +17 -16
  156. package/std/sys/future.yo +4 -3
  157. package/std/sys/iov.yo +1 -0
  158. package/std/sys/mmap.yo +1 -0
  159. package/std/sys/path.yo +1 -0
  160. package/std/sys/perm.yo +2 -1
  161. package/std/sys/pipe.yo +1 -0
  162. package/std/sys/process.yo +5 -4
  163. package/std/sys/signal.yo +1 -0
  164. package/std/sys/socketpair.yo +1 -0
  165. package/std/sys/sockinfo.yo +1 -0
  166. package/std/sys/statfs.yo +2 -1
  167. package/std/sys/statx.yo +1 -0
  168. package/std/sys/sysinfo.yo +1 -0
  169. package/std/sys/tcp.yo +15 -14
  170. package/std/sys/temp.yo +1 -0
  171. package/std/sys/time.yo +2 -1
  172. package/std/sys/timer.yo +6 -6
  173. package/std/sys/tty.yo +2 -1
  174. package/std/sys/udp.yo +13 -12
  175. package/std/sys/unix.yo +12 -11
  176. package/std/testing/bench.yo +4 -3
  177. package/std/thread.yo +7 -6
  178. package/std/time/datetime.yo +18 -15
  179. package/std/time/duration.yo +11 -10
  180. package/std/time/instant.yo +4 -4
  181. package/std/time/sleep.yo +1 -0
  182. package/std/url/index.yo +5 -5
  183. package/std/worker.yo +4 -3
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yo-async-effects
3
- description: Write Yo async code and algebraic effect handlers. Use this when working with IO, Future, JoinHandle, using/given, io.async, io.await, io.spawn, return, and escape.
3
+ description: Write Yo async code and algebraic effect handlers. Use this when working with Io, Future, JoinHandle, ctl/fn handlers, io.async, io.await, io.spawn, return, and unwind.
4
4
  argument-hint: "[async task, effect, or API]"
5
5
  ---
6
6
 
@@ -14,33 +14,33 @@ If a repository wraps these primitives, keep the same semantics and verify wheth
14
14
 
15
15
  Use this skill when you need to:
16
16
 
17
- - write functions with `using(io : IO)`
17
+ - write functions that take an `io : Io` parameter
18
18
  - return or consume `Future(...)` values
19
19
  - run tasks with `io.async`, `io.await`, or `io.spawn`
20
- - define or handle effects via `using(...)` and `given(...)`
21
- - reason about `return` versus `escape` in handlers
20
+ - define handlers using `ctl(args) -> R` and install them as plain local bindings
21
+ - reason about `return` versus `unwind` in handlers
22
22
 
23
23
  ## Workflow
24
24
 
25
25
  1. Decide whether the task needs sequential async, concurrent async on one thread, or true parallelism.
26
- 2. Add the necessary `using(...)` parameters to function signatures and call sites.
26
+ 2. Add the effect parameters (`io : Io`, `raise : Raise`, …) to function signatures and call sites. There is no implicit injection — pass them explicitly.
27
27
  3. Use the [async and effects recipes](./async-effects-recipes.md) for working patterns.
28
28
  4. Re-check handler semantics before finalizing:
29
29
  - `return(value)` resumes the continuation
30
- - `escape(expr)` discards it
30
+ - `unwind(expr)` discards it (only valid inside a `ctl(...) -> R` body)
31
31
 
32
32
  ## High-signal rules
33
33
 
34
34
  - `io.async(fn)` creates a lazy future; it does not start until awaited or spawned.
35
- - `io.await(future)` runs or waits for the future and returns its result.
36
- - `io.spawn(future)` starts it without waiting and returns `JoinHandle(T)`.
37
- - `handle.await(using(io))` returns `Option(T)`; `.None` means the task aborted via `escape`.
38
- - Future types include their effects: `Future(ResultType, IO, Effect...)`.
39
- - Effects are matched by type, not variable name.
40
- - `using(name : Type)` declares an implicit effect parameter; `given(name) := Type(...)` installs the handler.
41
- - `return(value)` inside a handler resumes the continuation; `escape(expr)` discards it.
42
- - `Exception` — non-resumable; handler calls `escape(...)` to exit. `ResumableException(T)` — handler calls `return(...)` to resume.
43
- - Effect handlers are standalone, not closures; pass state explicitly.
35
+ - `io.await(future, e)` runs or waits for the future and returns its result. `e` is the effect bundle the future expects.
36
+ - `io.spawn(future, e)` starts it without waiting and returns `JoinHandle(T)`.
37
+ - `handle.await(io)` returns `Option(T)`; `.None` means the task aborted via `unwind`.
38
+ - Future types are `Future(T)` or `Future(T, E)` where `E` is a single effect bundle (typically a struct). Pack multiple effects into one struct rather than passing them as separate type arguments.
39
+ - Effects are passed as explicit parameters — pass them by name at call sites.
40
+ - A handler whose body may `unwind` must be typed `ctl(args) -> R`; otherwise type it `fn(args) -> R`. Subtyping is one-way: `fn(T) -> R <: ctl(T) -> R`.
41
+ - `return(value)` inside a handler resumes the continuation; `unwind(expr)` discards it and exits the install frame.
42
+ - `Exception` — non-resumable; handler calls `unwind(...)` to exit. `ResumableException(T)` — handler calls `return(...)` to resume.
43
+ - Closures cannot be `ctl` and cannot capture `ctl` values. Handlers are bare (non-capturing) anonymous functions.
44
44
  - Yo async is single-threaded concurrency, not multithreaded parallelism.
45
45
 
46
46
  ## Resource
@@ -4,52 +4,42 @@ These patterns cover normal Yo async code and algebraic effects.
4
4
 
5
5
  ## Pick the right execution model
6
6
 
7
- | Need | Pattern |
8
- | -------------------------- | --------------------------------------------------------- |
9
- | Sequential async work | `result := io.await(task)` |
10
- | Start work and wait later | `handle := io.spawn(task)` then `handle.await(using(io))` |
11
- | Yield to other ready tasks | `io.await(yield())` |
12
- | True multithreading | Use thread or parallelism APIs, not `io.async` alone |
7
+ | Need | Pattern |
8
+ | -------------------------- | ------------------------------------------------------ |
9
+ | Sequential async work | `result := io.await(task, io)` |
10
+ | Start work and wait later | `handle := io.spawn(task, io)` then `handle.await(io)` |
11
+ | Yield to other ready tasks | `io.await(yield(), io)` |
12
+ | True multithreading | Use thread or parallelism APIs, not `io.async` alone |
13
13
 
14
14
  ## Minimal async function
15
15
 
16
16
  ```rust
17
17
  { yield } :: import("std/async");
18
18
 
19
- pause_then_answer :: (fn(using(io : IO)) -> Impl(Future(i32, IO)))(
20
- io.async((using(io : IO)) => {
21
- io.await(yield());
19
+ pause_then_answer :: (fn(io : Io) -> Impl(Future(i32, Io)))(
20
+ io.async((io : Io) => {
21
+ io.await(yield(), io);
22
22
  i32(42)
23
23
  })
24
24
  );
25
25
  ```
26
26
 
27
- - `io.async(...)` is lazy
28
- - If a function uses `using(io : IO)` and returns a future, include `IO` in the `Future(...)` type
29
- - **Type annotations can be omitted** in the lambda's `using` params — the compiler infers types from the `Future(T, IO, Raise, ...)` return type. You can even rename the params:
30
-
31
- ```rust
32
- // Equivalent — types inferred from Future(i32, IO, Raise) in the return type
33
- work :: (fn(using(io : IO, raise : Raise)) -> Impl(Future(i32, IO, Raise)))(
34
- io.async((using(my_io, my_raise)) => { // ← any names, no type annotations needed
35
- my_io.await(yield());
36
- my_raise("error")
37
- })
38
- );
39
- ```
27
+ - `io.async(...)` is lazy.
28
+ - The closure's parameter is the effect bundle. The simplest bundle is just `Io`.
29
+ - The `Future(T, E)` return type names the same bundle type that the closure consumes.
40
30
 
41
31
  ## Sequential await
42
32
 
43
33
  ```rust
44
34
  { yield } :: import("std/async");
45
35
 
46
- main :: (fn(using(io : IO)) -> unit)({
47
- task := io.async((using(io : IO)) => {
48
- io.await(yield());
36
+ main :: (fn(io : Io) -> unit)({
37
+ task := io.async((io : Io) => {
38
+ io.await(yield(), io);
49
39
  i32(1)
50
40
  });
51
41
 
52
- result := io.await(task);
42
+ result := io.await(task, io);
53
43
  assert((result == i32(1)), "unexpected result");
54
44
  });
55
45
 
@@ -61,38 +51,42 @@ export(main);
61
51
  ```rust
62
52
  { yield } :: import("std/async");
63
53
 
64
- main :: (fn(using(io : IO)) -> unit)({
65
- task1 := io.async((using(io : IO)) => {
66
- io.await(yield());
54
+ main :: (fn(io : Io) -> unit)({
55
+ task1 := io.async((io : Io) => {
56
+ io.await(yield(), io);
67
57
  i32(1)
68
58
  });
69
- task2 := io.async((using(io : IO)) => {
70
- io.await(yield());
59
+ task2 := io.async((io : Io) => {
60
+ io.await(yield(), io);
71
61
  i32(2)
72
62
  });
73
63
 
74
- handle1 := io.spawn(task1);
75
- handle2 := io.spawn(task2);
64
+ handle1 := io.spawn(task1, io);
65
+ handle2 := io.spawn(task2, io);
76
66
 
77
- result1 := handle1.await(using(io));
78
- result2 := handle2.await(using(io));
67
+ result1 := handle1.await(io);
68
+ result2 := handle2.await(io);
79
69
  });
80
70
 
81
71
  export(main);
82
72
  ```
83
73
 
84
- - `io.spawn(...)` begins execution without waiting
85
- - `handle.await(using(io))` returns `Option(T)` because a spawned task can abort via `escape`
74
+ - `io.spawn(...)` begins execution without waiting.
75
+ - `handle.await(io)` returns `Option(T)` because a spawned task can abort via `unwind`.
86
76
 
87
77
  ## Propagating and handling effects
88
78
 
79
+ Handlers are typed `fn(...) -> R` when they only resume, and `ctl(...) -> R` when their
80
+ body may `unwind`. Use the local binding form `(name : EffectType) = ((args) -> { ... })`
81
+ to install a handler; lambdas on the RHS of `=` need outer parens.
82
+
89
83
  ```rust
90
84
  open(import("std/fmt"));
91
85
  open(import("std/string"));
92
86
 
93
- Raise :: (fn(msg : String) -> i32);
87
+ Raise :: (ctl(msg : String) -> i32);
94
88
 
95
- safe_divide :: (fn(x : i32, y : i32, using(raise : Raise)) -> i32)(
89
+ safe_divide :: (fn(x : i32, y : i32, raise : Raise) -> i32)(
96
90
  cond(
97
91
  (y == i32(0)) => raise(`divide by zero`),
98
92
  true => (x / y)
@@ -100,62 +94,75 @@ safe_divide :: (fn(x : i32, y : i32, using(raise : Raise)) -> i32)(
100
94
  );
101
95
 
102
96
  resume_example :: (fn() -> i32)({
103
- (given(raise) : Raise) = (fn(msg : String) -> i32)({
97
+ // No `unwind` in this body — type the binding as the same Raise (a `ctl` is also a `fn`-compatible value when not unwinding).
98
+ // Use plain `fn(...) -> i32` if you want to forbid unwind altogether at this site.
99
+ (raise : Raise) = ((msg) -> {
104
100
  println(msg);
105
101
  return(i32(0));
106
102
  });
107
103
 
108
- safe_divide(i32(8), i32(0))
104
+ safe_divide(i32(8), i32(0), raise)
109
105
  });
110
106
 
111
107
  escape_example :: (fn() -> i32)({
112
- (given(raise) : Raise) = (fn(msg : String) -> i32)({
108
+ (raise : Raise) = ((msg) -> {
113
109
  println(msg);
114
- escape(i32(-1));
110
+ unwind(i32(-1));
115
111
  });
116
112
 
117
- safe_divide(i32(8), i32(0))
113
+ safe_divide(i32(8), i32(0), raise)
118
114
  });
119
115
  ```
120
116
 
121
- | Handler action | Meaning |
122
- | --------------- | -------------------------------------------- |
123
- | `return(value)` | Resume the continuation with `value` |
124
- | `escape(expr)` | Exit the function that installed the handler |
117
+ | Handler action | Meaning |
118
+ | --------------- | --------------------------------------------------------------------------------------- |
119
+ | `return(value)` | Resume the continuation with `value` |
120
+ | `unwind(expr)` | Exit the function that installed the handler. Only valid inside a `ctl(...) -> R` body. |
125
121
 
126
- ## Futures with multiple effects
122
+ ## Futures with multiple effects — bundle them in a struct
123
+
124
+ `Future(T, E)` accepts a single effect type `E`. To carry several effects, declare a
125
+ bundle struct and pass that.
127
126
 
128
127
  ```rust
129
128
  { yield } :: import("std/async");
130
129
 
131
- work :: (fn(using(io : IO, raise : Raise)) -> Impl(Future(i32, IO, Raise)))(
132
- io.async((using(io : IO, raise : Raise)) => {
133
- io.await(yield());
134
- safe_divide(i32(10), i32(2), using(raise))
130
+ Raise :: (ctl(msg : String) -> i32);
131
+ TaskCtx :: struct(io : Io, raise : Raise);
132
+
133
+ work :: (fn(ctx : TaskCtx) -> Impl(Future(i32, TaskCtx)))(
134
+ io.async((ctx : TaskCtx) => {
135
+ ctx.io.await(yield(), ctx.io);
136
+ safe_divide(i32(10), i32(2), ctx.raise)
135
137
  })
136
138
  );
137
139
  ```
138
140
 
139
- - Every effect used by the future should appear in the `Future(...)` type
140
- - Effects propagate through `using(...)` just like other contextual parameters
141
+ - The closure takes a single bundle parameter (e.g. `ctx : TaskCtx`).
142
+ - Inside the body, fields are accessed via dot (`ctx.io`, `ctx.raise`).
143
+ - The Future type names the same bundle struct: `Future(i32, TaskCtx)`.
144
+ - Build the bundle at the call site (`ctx := TaskCtx(io: io, raise: raise)`) and
145
+ pass it to `io.await` / `io.spawn`.
141
146
 
142
147
  ## Async recursion — use an iterative worklist instead
143
148
 
144
- `recur` does **not** work inside an `io.async` lambda — it refers to the lambda's own signature, not the outer function, so the argument types will not match. Calling the outer function by name is also forbidden in Yo. Attempting either will produce a compile-time error.
149
+ `recur` does **not** work inside an `io.async` lambda — it refers to the lambda's own signature, not the outer function. Calling the outer function by name is also forbidden in Yo. Attempting either will produce a compile-time error.
145
150
 
146
151
  **Solution**: replace async recursion with an iterative worklist using `ArrayList` as a stack:
147
152
 
148
153
  ```rust
149
154
  { read_dir, DirEntry } :: import("std/fs/dir");
150
155
 
151
- process_dir :: (fn(root: Path, using(io: IO, exn: Exception)) -> Impl(Future(unit, IO, Exception)))(
152
- io.async((using(io, exn)) => {
156
+ WalkCtx :: struct(io : Io, exn : Exception);
157
+
158
+ process_dir :: (fn(root: Path, ctx : WalkCtx) -> Impl(Future(unit, WalkCtx)))(
159
+ io.async((ctx : WalkCtx) => {
153
160
  stack := ArrayList(Path).new();
154
161
  { stack.push(root); };
155
162
 
156
163
  while(runtime((stack.len() > usize(0))), {
157
164
  cur := match(stack.pop(), .Some(p) => p, .None => return());
158
- entries := io.await(read_dir(cur));
165
+ entries := ctx.io.await(read_dir(cur, ctx.io), ctx.io);
159
166
  // process `entries`, push subdirectories to `stack`
160
167
  n := entries.len();
161
168
  i := usize(0);
@@ -178,15 +185,26 @@ process_dir :: (fn(root: Path, using(io: IO, exn: Exception)) -> Impl(Future(uni
178
185
 
179
186
  ## Common pitfalls
180
187
 
181
- - `io.async(...)` does not run immediately
182
- - `escape` inside async aborts the future instead of completing it normally
183
- - `io.await(...)` on an aborted future can panic; `JoinHandle.await(...)` converts abort into `.None`
184
- - Handler functions cannot capture outer variables like closures; pass required state explicitly
185
- - **`recur` inside `io.async` calls the lambda, not the outer function** use an iterative worklist for async recursion
188
+ - `io.async(...)` does not run immediately.
189
+ - `unwind` is only valid inside a `ctl(...) -> R` body. From any other position
190
+ (match arm, `cond` branch, `begin` block, plain `fn` body), use `return`.
191
+ - `unwind` inside an async task aborts the future instead of completing it normally.
192
+ - `io.await(...)` on an already-aborted future can panic; `JoinHandle.await(...)` converts abort into `.None`.
193
+ - Closures cannot be `ctl`, and they cannot capture a `ctl`-typed value. Handlers are bare (non-capturing) anonymous functions. If you need to use a `ctl` handler from inside a closure body, pass it in as an explicit parameter instead of capturing it.
194
+ - Pointers and references to `ctl` types (or structs containing them) are rejected.
195
+ - **`recur` inside `io.async` calls the lambda, not the outer function** — use an iterative worklist for async recursion.
196
+ - **Closure params bound to a generic `E : Type.Struct` need an explicit annotation.** When a generic function takes `callback : Impl(Fn(v : V, e : E) -> R)` and you pass a closure, the closure's `e` parameter type cannot be inferred from the call's bundle argument — `E` is still unbound at the closure body's evaluation site. Define a concrete struct first and annotate the closure parameter:
197
+ ```rust
198
+ // ✗ fails with "Cannot infer the type of anonymous closure parameter `e`"
199
+ traverse(arr, (v, e) => { e.log(v); ... }, { yield, log });
200
+ // ✓ annotate via a named bundle struct
201
+ Eff :: struct(yield : Yield, log : Log);
202
+ traverse(arr, (v : i32, e : Eff) => { e.log(v); ... }, Eff(yield, log));
203
+ ```
186
204
 
187
205
  ## Exception (non-resumable)
188
206
 
189
- `Exception` is a built-in struct-record effect for non-resumable error handling. When the handler calls `escape`, the continuation is discarded:
207
+ `Exception` is a built-in struct-record effect for non-resumable error handling. When the handler calls `unwind`, the continuation is discarded:
190
208
 
191
209
  ```rust
192
210
  open(import("std/error"));
@@ -196,7 +214,7 @@ DivError :: enum(DivByZero);
196
214
  impl(DivError, ToString(to_string : ((self) -> `division by zero`)));
197
215
  impl(DivError, Error());
198
216
 
199
- safe_divide :: (fn(x : i32, y : i32, using(exn : Exception)) -> i32)(
217
+ safe_divide :: (fn(x : i32, y : i32, exn : Exception) -> i32)(
200
218
  cond(
201
219
  (y == i32(0)) => exn.throw(dyn(DivError.DivByZero)),
202
220
  true => (x / y)
@@ -204,52 +222,51 @@ safe_divide :: (fn(x : i32, y : i32, using(exn : Exception)) -> i32)(
204
222
  );
205
223
 
206
224
  main :: (fn() -> unit)({
207
- given(exn) := Exception(
225
+ exn := Exception(
208
226
  throw : ((err) -> {
209
227
  println(`Error: ${err}`);
210
- escape();
228
+ unwind(());
211
229
  })
212
230
  );
213
231
 
214
- result := safe_divide(i32(10), i32(2));
232
+ result := safe_divide(i32(10), i32(2), exn);
215
233
  println(`result: ${result}`);
216
234
 
217
- safe_divide(i32(10), i32(0));
235
+ safe_divide(i32(10), i32(0), exn);
218
236
  });
219
237
 
220
238
  export(main);
221
239
  ```
222
240
 
223
- - `Exception` has a single field `throw : (fn(error : AnyError) -> T)`
224
- - `exn.throw(dyn(error))` calls the handler with a type-erased error
225
- - Handler uses `escape` to discard the continuation and exit the enclosing function
226
- - Code after the escaped call is never reached
241
+ - The struct constructor `Exception(...)` already pins the binding's type, so a plain `exn := Exception(...)` is enough — no `(exn : Exception) = ...` annotation needed. The annotation form is only required when the RHS is a raw lambda that has to commit to `ctl(...) -> R`.
242
+ - `Exception` has a single field `throw : (ctl(error : AnyError) -> T)`.
243
+ - `exn.throw(dyn(error))` calls the handler with a type-erased error.
244
+ - Handler uses `unwind` to discard the continuation and exit the enclosing function.
245
+ - Code after the escaped call is never reached.
227
246
 
228
247
  ### Swallowing exceptions with a fallback value (return in Exception handler)
229
248
 
230
- When an exception is thrown inside an async operation (e.g., `cmd.status()` or `cmd.output()`), you can **swallow the error and resume with a fallback value** by using `return` in the handler (not `escape`). The `ResumeType` is the return type of the operation that would have thrown.
249
+ When an exception is thrown inside an async operation (e.g., `cmd.status()` or `cmd.output()`), you can **swallow the error and resume with a fallback value** by using `return` in the handler (not `unwind`). The `ResumeType` is the return type of the operation that would have thrown.
231
250
 
232
251
  ```rust
233
252
  { Command, ExitStatus, Output } :: import("std/process/command");
234
253
 
235
254
  // Check if a tool is available — returns false if it throws (e.g., not found)
236
- given(try_exn) := Exception(throw: ((err) -> {
255
+ try_exn := Exception(throw: ((err) -> {
237
256
  return(ExitStatus(raw: i32(1))); // resume with "failed" exit status
238
257
  }));
239
- status := io.await(cmd.status(using(io, try_exn)));
258
+ status := io.await(cmd.status(io, try_exn), io);
240
259
  available := status.success(); // false if exception was swallowed
241
260
 
242
261
  // For cmd.output(), resume with a failed Output:
243
- given(out_exn) := Exception(throw: ((err) -> {
262
+ out_exn := Exception(throw: ((err) -> {
244
263
  return(Output(status: ExitStatus(raw: i32(1)), stdout: ArrayList(u8).new(), stderr: ArrayList(u8).new()));
245
264
  }));
246
- out := io.await(cmd.output(using(io, out_exn)));
265
+ out := io.await(cmd.output(io, out_exn), io);
247
266
  if((!(out.status.success())), { return(); }); // handle failure
248
267
  ```
249
268
 
250
- Key: the `return` inside the handler resumes the _effect invocation site_ with the provided value. The calling code then sees the fallback as if the operation returned normally. Use `escape` only when the enclosing function returns `unit` (e.g., test bodies).
251
-
252
- **`escape(T_value)` constraint**: `escape(T_value)` inside an `Exception` handler requires that the enclosing `io.async` closure's return type matches `T_value`. Due to forward type inference, the evaluator may not know the closure's return type at the point where `given` is declared. This causes a "Expected: unit" error when `escape(non_unit)` is used in a handler declared before the final return expression. Prefer `return(fallback_value)` (resume) when possible.
269
+ Key: the `return` inside the handler resumes the _effect invocation site_ with the provided value. The calling code then sees the fallback as if the operation returned normally. Use `unwind` only when the enclosing function returns `unit` (e.g., test bodies).
253
270
 
254
271
  `ResumableException(ResumeType)` is a struct-record effect for resumable error handling. The handler uses `return` to resume with a recovery value:
255
272
 
@@ -257,7 +274,7 @@ Key: the `return` inside the handler resumes the _effect invocation site_ with t
257
274
  open(import("std/error"));
258
275
  open(import("std/fmt"));
259
276
 
260
- safe_divide :: (fn(x : i32, y : i32, using(exn : ResumableException(i32))) -> i32)(
277
+ safe_divide :: (fn(x : i32, y : i32, exn : ResumableException(i32)) -> i32)(
261
278
  cond(
262
279
  (y == i32(0)) => exn.throw(dyn(`division by zero`)),
263
280
  true => (x / y)
@@ -265,71 +282,51 @@ safe_divide :: (fn(x : i32, y : i32, using(exn : ResumableException(i32))) -> i3
265
282
  );
266
283
 
267
284
  main :: (fn() -> unit)({
268
- given(exn) := ResumableException(i32)(
285
+ exn := ResumableException(i32)(
269
286
  throw : ((err) -> {
270
287
  println(`Recovering from: ${err}`);
271
288
  return(i32(0));
272
289
  })
273
290
  );
274
291
 
275
- result := safe_divide(i32(10), i32(0));
292
+ result := safe_divide(i32(10), i32(0), exn);
276
293
  assert((result == i32(0)), "recovered with 0");
277
294
  });
278
295
 
279
296
  export(main);
280
297
  ```
281
298
 
282
- - Handler uses `return(value)` to resume the continuation with the recovery value
283
- - The call site receives the returned value and continues normally
299
+ - Handler uses `return(value)` to resume the continuation with the recovery value.
300
+ - The call site receives the returned value and continues normally.
284
301
 
285
302
  ## Struct-record effects vs function-type effects
286
303
 
287
- Effects in Yo can be plain function types or struct-record types:
304
+ Effects in Yo can be plain function/ctl types or struct-record types that group several
305
+ operations:
288
306
 
289
307
  ```rust
290
- Raise :: (fn(msg : String) -> i32);
308
+ Raise :: (ctl(msg : String) -> i32);
291
309
 
292
310
  Logger :: struct(
293
311
  log : (fn(level : i32, msg : String) -> unit)
294
312
  );
295
313
  ```
296
314
 
297
- Both kinds use `using(...)` / `given(...)` with the same semantics — they compile to evidence passing (function pointers as implicit C parameters). Struct-record effects group related operations under a single nominal type.
298
-
299
- ## Async with effects
300
-
301
- When an async future uses effects, include them in the `Future` type:
302
-
303
- ```rust
304
- work :: (fn(using(io : IO, exn : Exception)) -> Impl(Future(i32, IO, Exception)))(
305
- io.async((using(io : IO, exn : Exception)) => {
306
- io.await(yield());
307
- safe_divide(i32(10), i32(2), using(exn))
308
- })
309
- );
310
- ```
311
-
312
- To spawn a task with effects, pass them explicitly via `using`:
313
-
314
- ```rust
315
- handle := io.spawn(task, using(io, exn));
316
- result := handle.await(using(io));
317
- match(result,
318
- .Some(value) => println(`got: ${value}`),
319
- .None => println("task aborted via escape")
320
- );
321
- ```
315
+ Both kinds are passed as explicit parameters. Struct-record effects group related
316
+ operations under a single nominal type — that pattern composes naturally with the
317
+ "single bundle struct" Future contract.
322
318
 
323
- ## Effect row variables (advanced)
319
+ ## Effect-bundle polymorphism (advanced)
324
320
 
325
- Functions can be polymorphic over their effects using spread parameters:
321
+ A function can be polymorphic over the effect bundle a Future carries by quantifying
322
+ over `E : Type.Struct`:
326
323
 
327
324
  ```rust
328
- wrapper :: (fn(forall(...(E)), x : i32, using(...(E))) -> i32)(
329
- safe_divide(x, i32(2))
325
+ wait_then :: (fn(forall(T : Type, E : Type.Struct), fut : Impl(Future(T, E)), e : E) -> T)(
326
+ io.await(fut, e)
330
327
  );
331
328
  ```
332
329
 
333
- - `forall(...(E))` introduces an effect row variable
334
- - `using(...(E))` forwards whatever effects the caller provides
335
- - See [ALGEBRAIC_EFFECTS.md](https://github.com/shd101wyy/Yo/blob/develop/docs/en-US/ALGEBRAIC_EFFECTS.md) for the full design
330
+ - `forall(E : Type.Struct)` constrains `E` to be a struct (so its fields can be looked
331
+ up at call sites and injected into the underlying state machine).
332
+ - See [ALGEBRAIC_EFFECTS.md](https://github.com/shd101wyy/Yo/blob/develop/docs/en-US/ALGEBRAIC_EFFECTS.md) for the full design.
@@ -105,6 +105,12 @@ match(numbers.get(usize(0)),
105
105
  .None => ()
106
106
  );
107
107
 
108
+ // Do NOT write the verbose forms when X(i) works:
109
+ // ✗ (&(numbers)).index(usize(0)).* = i32(99); // needs pragma, scans as raw-ptr code
110
+ // ✗ v := numbers.get(usize(0)).unwrap(); // panic-on-OOB anyway — just use call syntax
111
+ // ✓ numbers(usize(0)) = i32(99);
112
+ // ✓ v := numbers(usize(0));
113
+
108
114
  counts := HashMap(String, i32).new();
109
115
  counts.set(`yo`, i32(1));
110
116
  ```
@@ -123,7 +129,7 @@ counts.set(`yo`, i32(1));
123
129
  ```rust
124
130
  Iterator :: trait(
125
131
  Item : Type,
126
- next : (fn(self : *(Self)) -> Option(Self.Item))
132
+ next : (fn(ref(self) : Self) -> Option(Self.Item))
127
133
  );
128
134
  ```
129
135
 
@@ -193,6 +199,15 @@ TcpStream :: object(fd : i32, buffer : ArrayList(u8));
193
199
 
194
200
  - Use `newtype(...)` when the type has exactly one field
195
201
  - Use `object(...)` for types that need shared ownership
202
+ - **Parameter form by type kind:**
203
+ - `object(...)`: plain `name : Type` (reference semantics — no pointer or ref needed).
204
+ `foo :: (fn(ctx : EvalContext) -> unit)(ctx.do_stuff());`
205
+ - `struct(...)` / `enum(...)` / primitive, read-only: plain `name : Type`.
206
+ - `struct(...)` / `enum(...)` / primitive, need mutation: `ref(name) : Type`.
207
+ `swap :: (fn(ref(a) : i32, ref(b) : i32) -> unit)(...);`
208
+ - Method receiver on `object`: plain `self : Self`.
209
+ - Method receiver on value type (traits + inherent mutators): `ref(self) : Self`.
210
+ - Raw FFI pointer: `name : *(T)` (requires `pragma(Pragma.AllowUnsafe);`).
196
211
  - Source-file imports are namespace structs. The old `module(...)`, `Module`,
197
212
  and `SelfModule` syntax is gone; use `struct(...)`, `Type`, and normal `Self`.
198
213
  - Fields written as `name :: value` or `comptime(name) : Type` are compile-time-only static fields/methods and have no runtime layout
@@ -361,8 +376,8 @@ safe_div :: (fn(a : i32, b : i32) -> Result(i32, DivError))(
361
376
  result := inc(i32(5));
362
377
 
363
378
  transform :: (fn(values : ArrayList(i32), f : Impl(Fn(x : i32) -> i32)) -> unit)({
364
- for(values.iter(), (ptr) => {
365
- ptr.* = f(ptr.*);
379
+ for(values, ref(x) => {
380
+ x = f(x);
366
381
  });
367
382
  });
368
383
  ```
@@ -381,22 +396,27 @@ list := ArrayList(i32).new();
381
396
  list.push(i32(1));
382
397
  list.push(i32(2));
383
398
 
384
- for(list.iter(), (ptr) => {
385
- println(ptr.*);
399
+ // Value form — implicit .into_iter().
400
+ for(list, (value) => {
401
+ println(value);
386
402
  });
387
403
 
388
- for(list.into_iter(), (value) => {
389
- println(value);
404
+ // Borrow form — implicit .iter() + .project(pos). `x` is a writable
405
+ // binding into the collection; assignments propagate back.
406
+ for(list, ref(x) => {
407
+ x = (x + i32(10));
390
408
  });
391
409
  ```
392
410
 
393
- | Method | Yields | Semantics |
394
- | -------------- | ------ | ----------------------------------- |
395
- | `.iter()` | `*(T)` | Borrow via pointer; yields pointers |
396
- | `.into_iter()` | `T` | Takes ownership; yields values |
411
+ | Form | Expansion | When to use |
412
+ | ----------------------------- | ------------------------------------------ | --------------------------------------------- |
413
+ | `for(coll, (x) => …)` | `coll.into_iter()`, yields `T` by value | Read-only iteration; combinator chains |
414
+ | `for(coll, ref(x) => …)` | `coll.iter()` + `coll.project(pos)` borrow | Mutation in place; writes propagate to `coll` |
415
+ | `for(chain.map(f), (x) => …)` | Treats chain as the iterator (value form) | Computed values; chains support only value |
397
416
 
398
- - Implement `Iterator` trait to make custom types iterable
399
- - Implement `IntoIterator` trait for collection-style iteration
417
+ - `Iterator` trait defines `next() -> Option(Item)`. Custom iterables impl this.
418
+ - `IntoIterator` trait — defines `into_iter() -> IntoIter`. Collections impl this so `for(coll, ...)` works.
419
+ - `Indexable(Position)` trait — defines `project(pos) -> ref(Element)`. Collections impl this to support the borrow form.
400
420
 
401
421
  ## Module-level mutable variables
402
422
 
@@ -146,7 +146,7 @@ test("Async test", {
146
146
  });
147
147
  ```
148
148
 
149
- - `test("name", { body })` defines a test — `io : IO` is automatically available
149
+ - `test("name", { body })` defines a test — `io : Io` is automatically available
150
150
  - All tests can use `io.async(...)`, `io.await(...)`, etc. without a `using` clause
151
151
  - `assert(condition, "message")` — always include a message string
152
152
  - `comptime_assert(expr)` — verified at compile time
@@ -32,11 +32,11 @@ Use this skill when you need to:
32
32
  - `{ expr }` is a struct literal; `{ expr; }` is a begin block.
33
33
  - Yo has no operator precedence. Parenthesize every binary operation.
34
34
  - Use `func(arg)` with no space before `(` for every call; `func arg` and `func (arg)` are invalid.
35
- - Use `return(value)` / `return()` and `escape(value)` / `escape()`; bare control-flow arguments are invalid.
35
+ - Use `return(value)` / `return()` and `unwind(value)` / `unwind()`; bare control-flow arguments are invalid.
36
36
  - Use `recur(...)` for self-recursion instead of the function name.
37
37
  - Use `forall(T : Type)` for generic type parameters, `comptime(x) : T` for compile-time parameters.
38
38
  - Use `where(T <: Trait)` to constrain type parameters.
39
- - Use `using(name : Type)` for implicit/effect parameters and `given(name) := Type(...)` to install handlers.
39
+ - Effect parameters are explicit: name them in the function signature (e.g. `raise : Raise`, `exn : Exception`) and pass them at the call site. Install a handler locally with `name := Constructor(...)` for struct effects, or `(name : EffectType) = ((args) -> { ... })` when the RHS is a bare lambda that needs the `ctl(...) -> R` annotation.
40
40
  - Use `(params) => expr` for closures; `Impl(Fn(...) -> T)` for the closure type.
41
41
  - Every executable needs `export(main);`.
42
42
  - Import sibling modules with relative paths like `./file.yo`.