@shd101wyy/yo 0.1.27 → 0.1.29

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 (63) hide show
  1. package/.github/skills/yo-async-effects/SKILL.md +15 -15
  2. package/.github/skills/yo-async-effects/async-effects-recipes.md +110 -121
  3. package/.github/skills/yo-core-patterns/core-patterns-cheatsheet.md +3 -0
  4. package/.github/skills/yo-project-workflow/workflow-cheatsheet.md +2 -0
  5. package/.github/skills/yo-syntax/SKILL.md +2 -2
  6. package/.github/skills/yo-syntax/syntax-cheatsheet.md +195 -73
  7. package/README.md +2 -0
  8. package/out/cjs/index.cjs +624 -613
  9. package/out/cjs/yo-cli.cjs +739 -727
  10. package/out/cjs/yo-lsp.cjs +636 -625
  11. package/out/esm/index.mjs +526 -515
  12. package/out/types/src/codegen/functions/declarations.d.ts +1 -1
  13. package/out/types/src/doc/model.d.ts +0 -1
  14. package/out/types/src/env.d.ts +1 -2
  15. package/out/types/src/evaluator/calls/helper.d.ts +4 -2
  16. package/out/types/src/evaluator/context.d.ts +1 -1
  17. package/out/types/src/evaluator/exprs/{escape.d.ts → unwind.d.ts} +1 -1
  18. package/out/types/src/evaluator/types/function.d.ts +1 -2
  19. package/out/types/src/evaluator/types/synthesizer.d.ts +1 -0
  20. package/out/types/src/evaluator/utils.d.ts +0 -1
  21. package/out/types/src/expr.d.ts +5 -6
  22. package/out/types/src/test-runner.d.ts +2 -0
  23. package/out/types/src/types/creators.d.ts +4 -6
  24. package/out/types/src/types/definitions.d.ts +7 -16
  25. package/out/types/src/types/guards.d.ts +1 -2
  26. package/out/types/src/types/tags.d.ts +0 -1
  27. package/out/types/src/types/utils.d.ts +1 -0
  28. package/out/types/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +1 -1
  30. package/scripts/probe-parser-parity.ts +61 -0
  31. package/scripts/probe-yo-self-parser.sh +33 -0
  32. package/scripts/validate-yo-self-fmt.ts +184 -0
  33. package/std/async.yo +1 -1
  34. package/std/crypto/random.yo +6 -6
  35. package/std/encoding/base64.yo +4 -4
  36. package/std/encoding/hex.yo +2 -2
  37. package/std/encoding/json.yo +3 -3
  38. package/std/encoding/utf16.yo +1 -1
  39. package/std/error.yo +14 -2
  40. package/std/fs/dir.yo +56 -62
  41. package/std/fs/file.yo +118 -124
  42. package/std/fs/metadata.yo +11 -17
  43. package/std/fs/temp.yo +21 -27
  44. package/std/fs/walker.yo +10 -16
  45. package/std/http/client.yo +25 -29
  46. package/std/http/index.yo +4 -4
  47. package/std/io/reader.yo +1 -1
  48. package/std/io/writer.yo +2 -2
  49. package/std/net/addr.yo +1 -1
  50. package/std/net/dns.yo +10 -14
  51. package/std/net/errors.yo +1 -1
  52. package/std/net/tcp.yo +67 -71
  53. package/std/net/udp.yo +36 -40
  54. package/std/os/signal.yo +2 -2
  55. package/std/prelude.yo +27 -21
  56. package/std/process/command.yo +32 -38
  57. package/std/regex/parser.yo +10 -10
  58. package/std/sys/bufio/buf_reader.yo +14 -14
  59. package/std/sys/bufio/buf_writer.yo +17 -17
  60. package/std/sys/errors.yo +1 -1
  61. package/std/thread.yo +2 -2
  62. package/std/url/index.yo +2 -2
  63. package/std/worker.yo +2 -2
@@ -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,18 @@ 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.
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.
186
196
 
187
197
  ## Exception (non-resumable)
188
198
 
189
- `Exception` is a built-in struct-record effect for non-resumable error handling. When the handler calls `escape`, the continuation is discarded:
199
+ `Exception` is a built-in struct-record effect for non-resumable error handling. When the handler calls `unwind`, the continuation is discarded:
190
200
 
191
201
  ```rust
192
202
  open(import("std/error"));
@@ -196,7 +206,7 @@ DivError :: enum(DivByZero);
196
206
  impl(DivError, ToString(to_string : ((self) -> `division by zero`)));
197
207
  impl(DivError, Error());
198
208
 
199
- safe_divide :: (fn(x : i32, y : i32, using(exn : Exception)) -> i32)(
209
+ safe_divide :: (fn(x : i32, y : i32, exn : Exception) -> i32)(
200
210
  cond(
201
211
  (y == i32(0)) => exn.throw(dyn(DivError.DivByZero)),
202
212
  true => (x / y)
@@ -204,52 +214,51 @@ safe_divide :: (fn(x : i32, y : i32, using(exn : Exception)) -> i32)(
204
214
  );
205
215
 
206
216
  main :: (fn() -> unit)({
207
- given(exn) := Exception(
217
+ exn := Exception(
208
218
  throw : ((err) -> {
209
219
  println(`Error: ${err}`);
210
- escape();
220
+ unwind(());
211
221
  })
212
222
  );
213
223
 
214
- result := safe_divide(i32(10), i32(2));
224
+ result := safe_divide(i32(10), i32(2), exn);
215
225
  println(`result: ${result}`);
216
226
 
217
- safe_divide(i32(10), i32(0));
227
+ safe_divide(i32(10), i32(0), exn);
218
228
  });
219
229
 
220
230
  export(main);
221
231
  ```
222
232
 
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
233
+ - 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`.
234
+ - `Exception` has a single field `throw : (ctl(error : AnyError) -> T)`.
235
+ - `exn.throw(dyn(error))` calls the handler with a type-erased error.
236
+ - Handler uses `unwind` to discard the continuation and exit the enclosing function.
237
+ - Code after the escaped call is never reached.
227
238
 
228
239
  ### Swallowing exceptions with a fallback value (return in Exception handler)
229
240
 
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.
241
+ 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
242
 
232
243
  ```rust
233
244
  { Command, ExitStatus, Output } :: import("std/process/command");
234
245
 
235
246
  // Check if a tool is available — returns false if it throws (e.g., not found)
236
- given(try_exn) := Exception(throw: ((err) -> {
247
+ try_exn := Exception(throw: ((err) -> {
237
248
  return(ExitStatus(raw: i32(1))); // resume with "failed" exit status
238
249
  }));
239
- status := io.await(cmd.status(using(io, try_exn)));
250
+ status := io.await(cmd.status(io, try_exn), io);
240
251
  available := status.success(); // false if exception was swallowed
241
252
 
242
253
  // For cmd.output(), resume with a failed Output:
243
- given(out_exn) := Exception(throw: ((err) -> {
254
+ out_exn := Exception(throw: ((err) -> {
244
255
  return(Output(status: ExitStatus(raw: i32(1)), stdout: ArrayList(u8).new(), stderr: ArrayList(u8).new()));
245
256
  }));
246
- out := io.await(cmd.output(using(io, out_exn)));
257
+ out := io.await(cmd.output(io, out_exn), io);
247
258
  if((!(out.status.success())), { return(); }); // handle failure
248
259
  ```
249
260
 
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.
261
+ 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
262
 
254
263
  `ResumableException(ResumeType)` is a struct-record effect for resumable error handling. The handler uses `return` to resume with a recovery value:
255
264
 
@@ -257,7 +266,7 @@ Key: the `return` inside the handler resumes the _effect invocation site_ with t
257
266
  open(import("std/error"));
258
267
  open(import("std/fmt"));
259
268
 
260
- safe_divide :: (fn(x : i32, y : i32, using(exn : ResumableException(i32))) -> i32)(
269
+ safe_divide :: (fn(x : i32, y : i32, exn : ResumableException(i32)) -> i32)(
261
270
  cond(
262
271
  (y == i32(0)) => exn.throw(dyn(`division by zero`)),
263
272
  true => (x / y)
@@ -265,71 +274,51 @@ safe_divide :: (fn(x : i32, y : i32, using(exn : ResumableException(i32))) -> i3
265
274
  );
266
275
 
267
276
  main :: (fn() -> unit)({
268
- given(exn) := ResumableException(i32)(
277
+ exn := ResumableException(i32)(
269
278
  throw : ((err) -> {
270
279
  println(`Recovering from: ${err}`);
271
280
  return(i32(0));
272
281
  })
273
282
  );
274
283
 
275
- result := safe_divide(i32(10), i32(0));
284
+ result := safe_divide(i32(10), i32(0), exn);
276
285
  assert((result == i32(0)), "recovered with 0");
277
286
  });
278
287
 
279
288
  export(main);
280
289
  ```
281
290
 
282
- - Handler uses `return(value)` to resume the continuation with the recovery value
283
- - The call site receives the returned value and continues normally
291
+ - Handler uses `return(value)` to resume the continuation with the recovery value.
292
+ - The call site receives the returned value and continues normally.
284
293
 
285
294
  ## Struct-record effects vs function-type effects
286
295
 
287
- Effects in Yo can be plain function types or struct-record types:
296
+ Effects in Yo can be plain function/ctl types or struct-record types that group several
297
+ operations:
288
298
 
289
299
  ```rust
290
- Raise :: (fn(msg : String) -> i32);
300
+ Raise :: (ctl(msg : String) -> i32);
291
301
 
292
302
  Logger :: struct(
293
303
  log : (fn(level : i32, msg : String) -> unit)
294
304
  );
295
305
  ```
296
306
 
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
- ```
307
+ Both kinds are passed as explicit parameters. Struct-record effects group related
308
+ operations under a single nominal type — that pattern composes naturally with the
309
+ "single bundle struct" Future contract.
322
310
 
323
- ## Effect row variables (advanced)
311
+ ## Effect-bundle polymorphism (advanced)
324
312
 
325
- Functions can be polymorphic over their effects using spread parameters:
313
+ A function can be polymorphic over the effect bundle a Future carries by quantifying
314
+ over `E : Type.Struct`:
326
315
 
327
316
  ```rust
328
- wrapper :: (fn(forall(...(E)), x : i32, using(...(E))) -> i32)(
329
- safe_divide(x, i32(2))
317
+ wait_then :: (fn(forall(T : Type, E : Type.Struct), fut : Impl(Future(T, E)), e : E) -> T)(
318
+ io.await(fut, e)
330
319
  );
331
320
  ```
332
321
 
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
322
+ - `forall(E : Type.Struct)` constrains `E` to be a struct (so its fields can be looked
323
+ up at call sites and injected into the underlying state machine).
324
+ - See [ALGEBRAIC_EFFECTS.md](https://github.com/shd101wyy/Yo/blob/develop/docs/en-US/ALGEBRAIC_EFFECTS.md) for the full design.
@@ -144,6 +144,9 @@ counter.* = (counter.* + i32(1));
144
144
  - Use `*(T)` for raw pointers
145
145
  - Model nullable pointers as `Option(*(T))` or `?*(T)`, not sentinel integers
146
146
  - Constructor syntax: `Box(T)(value)` — NOT `Box(T).new(value)`
147
+ - Single-payload objects may use `(*) : T`; access the payload with `value.*`.
148
+ This is a value payload accessor for object values, while pointer dereference
149
+ still applies when the receiver has pointer type.
147
150
  - For self-referential `object` types, use `Box(Self)` to break the recursive cycle:
148
151
 
149
152
  ```rust
@@ -18,6 +18,7 @@ These commands and patterns are aimed at normal Yo projects that use the public
18
18
  | Inspect generated C | `yo compile main.yo --emit-c --skip-c-compiler` |
19
19
  | Run tests in one file | `yo test ./tests/main.test.yo --parallel 1` |
20
20
  | Filter tests by name | `yo test ./tests/main.test.yo --test-name-pattern "Name"` |
21
+ | Tune test batch size | `yo test ./tests/main.test.yo --test-batch-size 100` |
21
22
  | Format Yo source | `yo fmt ./src ./tests` |
22
23
  | Check Yo formatting | `yo fmt --check` |
23
24
  | Generate docs for project | `yo doc ./src` |
@@ -104,6 +105,7 @@ yo test ./tests/main.test.yo --bail --verbose --parallel 1
104
105
 
105
106
  - Use `--parallel 1` for focused, readable single-file runs
106
107
  - Use `--test-name-pattern` when a file contains many tests
108
+ - Use `--test-batch-size N` if a large `.test.yo` file generates C that compiles slowly or looks stuck
107
109
  - Use `yo build test` when the repository's main test workflow is defined in `build.yo`
108
110
 
109
111
  ## Formatting
@@ -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`.