@shd101wyy/yo 0.1.28 → 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.
- package/.github/skills/yo-async-effects/SKILL.md +15 -15
- package/.github/skills/yo-async-effects/async-effects-recipes.md +110 -121
- package/.github/skills/yo-syntax/SKILL.md +2 -2
- package/.github/skills/yo-syntax/syntax-cheatsheet.md +49 -75
- package/README.md +2 -0
- package/out/cjs/index.cjs +622 -611
- package/out/cjs/yo-cli.cjs +727 -716
- package/out/cjs/yo-lsp.cjs +637 -626
- package/out/esm/index.mjs +515 -504
- package/out/types/src/codegen/functions/declarations.d.ts +1 -1
- package/out/types/src/doc/model.d.ts +0 -1
- package/out/types/src/env.d.ts +0 -2
- package/out/types/src/evaluator/context.d.ts +1 -1
- package/out/types/src/evaluator/exprs/{escape.d.ts → unwind.d.ts} +1 -1
- package/out/types/src/evaluator/types/function.d.ts +1 -2
- package/out/types/src/evaluator/utils.d.ts +0 -1
- package/out/types/src/expr.d.ts +5 -6
- package/out/types/src/types/creators.d.ts +4 -6
- package/out/types/src/types/definitions.d.ts +7 -16
- package/out/types/src/types/guards.d.ts +1 -2
- package/out/types/src/types/tags.d.ts +0 -1
- package/out/types/src/types/utils.d.ts +1 -0
- package/out/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/std/async.yo +1 -1
- package/std/crypto/random.yo +6 -6
- package/std/encoding/base64.yo +4 -4
- package/std/encoding/hex.yo +2 -2
- package/std/encoding/json.yo +3 -3
- package/std/encoding/utf16.yo +1 -1
- package/std/error.yo +14 -2
- package/std/fs/dir.yo +56 -62
- package/std/fs/file.yo +118 -124
- package/std/fs/metadata.yo +11 -17
- package/std/fs/temp.yo +21 -27
- package/std/fs/walker.yo +10 -16
- package/std/http/client.yo +25 -29
- package/std/http/index.yo +4 -4
- package/std/io/reader.yo +1 -1
- package/std/io/writer.yo +2 -2
- package/std/net/addr.yo +1 -1
- package/std/net/dns.yo +10 -14
- package/std/net/errors.yo +1 -1
- package/std/net/tcp.yo +67 -71
- package/std/net/udp.yo +36 -40
- package/std/os/signal.yo +2 -2
- package/std/prelude.yo +27 -21
- package/std/process/command.yo +32 -38
- package/std/regex/parser.yo +10 -10
- package/std/sys/bufio/buf_reader.yo +14 -14
- package/std/sys/bufio/buf_writer.yo +17 -17
- package/std/sys/errors.yo +1 -1
- package/std/thread.yo +2 -2
- package/std/url/index.yo +2 -2
- 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,
|
|
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
|
|
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
|
|
21
|
-
- reason about `return` versus `
|
|
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
|
|
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
|
-
- `
|
|
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(
|
|
38
|
-
- Future types
|
|
39
|
-
- Effects are
|
|
40
|
-
- `
|
|
41
|
-
- `return(value)` inside a handler resumes the continuation; `
|
|
42
|
-
- `Exception` — non-resumable; handler calls `
|
|
43
|
-
-
|
|
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(
|
|
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(
|
|
20
|
-
io.async((
|
|
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
|
-
-
|
|
29
|
-
-
|
|
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(
|
|
47
|
-
task := io.async((
|
|
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(
|
|
65
|
-
task1 := io.async((
|
|
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((
|
|
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(
|
|
78
|
-
result2 := handle2.await(
|
|
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(
|
|
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 :: (
|
|
87
|
+
Raise :: (ctl(msg : String) -> i32);
|
|
94
88
|
|
|
95
|
-
safe_divide :: (fn(x : i32, y : 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
|
-
|
|
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
|
-
(
|
|
108
|
+
(raise : Raise) = ((msg) -> {
|
|
113
109
|
println(msg);
|
|
114
|
-
|
|
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
|
-
| `
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
-
|
|
140
|
-
-
|
|
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
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
- `
|
|
183
|
-
|
|
184
|
-
-
|
|
185
|
-
-
|
|
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 `
|
|
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,
|
|
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
|
-
|
|
217
|
+
exn := Exception(
|
|
208
218
|
throw : ((err) -> {
|
|
209
219
|
println(`Error: ${err}`);
|
|
210
|
-
|
|
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`
|
|
224
|
-
- `
|
|
225
|
-
-
|
|
226
|
-
-
|
|
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 `
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 `
|
|
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,
|
|
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
|
-
|
|
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 :: (
|
|
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
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
311
|
+
## Effect-bundle polymorphism (advanced)
|
|
324
312
|
|
|
325
|
-
|
|
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
|
-
|
|
329
|
-
|
|
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(
|
|
334
|
-
|
|
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.
|
|
@@ -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 `
|
|
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
|
-
-
|
|
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`.
|