@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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shd101wyy/yo",
3
3
  "displayName": "Yo",
4
- "version": "0.1.27",
4
+ "version": "0.1.29",
5
5
  "main": "./out/cjs/index.cjs",
6
6
  "module": "./out/esm/index.mjs",
7
7
  "types": "./out/types/src/index.d.ts",
@@ -0,0 +1,61 @@
1
+ // Probe: for each .yo file in tests/ and yo-self/, parse via TS and check
2
+ // if yo-self's parser via `yo-self compile --emit-c --skip-c-compiler` also
3
+ // succeeds (or fails for the same reason).
4
+ //
5
+ // We don't actually compile — we just lex+parse+evaluate enough to detect
6
+ // parser errors. But yo-self's `compile` subcommand runs the full pipeline
7
+ // which includes evaluation. So we use a lighter probe: try compiling
8
+ // from a tiny stub `main.yo` that imports each file.
9
+ //
10
+ // Simpler: just use the TS parser. If any file fails to TS-parse, log it.
11
+ // Then optionally compare with yo-self by running yo-self-bin and seeing
12
+ // if it also fails.
13
+ import Parser from "../src/parser";
14
+ import * as fs from "fs";
15
+ import * as path from "path";
16
+
17
+ function walk(dir: string, files: string[] = []): string[] {
18
+ if (!fs.existsSync(dir)) return files;
19
+ const stat = fs.statSync(dir);
20
+ if (stat.isFile() && dir.endsWith(".yo")) {
21
+ files.push(dir);
22
+ return files;
23
+ }
24
+ if (stat.isDirectory()) {
25
+ for (const entry of fs.readdirSync(dir)) {
26
+ if ([".git", "node_modules", "out", "yo-out"].includes(entry)) continue;
27
+ walk(path.join(dir, entry), files);
28
+ }
29
+ }
30
+ return files;
31
+ }
32
+
33
+ const files = [
34
+ ...walk("tests"),
35
+ ...walk("yo-self"),
36
+ ];
37
+
38
+ console.log(`Found ${files.length} .yo files\n`);
39
+ let tsOk = 0, tsFail = 0;
40
+ const tsFailures: { file: string; err: string }[] = [];
41
+
42
+ for (const f of files) {
43
+ const src = fs.readFileSync(f, "utf-8");
44
+ try {
45
+ const p = new Parser({ inputString: src, modulePath: f });
46
+ p.getProgram();
47
+ tsOk++;
48
+ } catch (e) {
49
+ tsFail++;
50
+ tsFailures.push({ file: f, err: String(e).split("\n")[0].slice(0, 120) });
51
+ }
52
+ }
53
+
54
+ console.log(`TS parser: ${tsOk} OK, ${tsFail} fail`);
55
+ if (tsFailures.length > 0 && tsFailures.length < 30) {
56
+ console.log("\nTS failures:");
57
+ for (const f of tsFailures.slice(0, 20)) {
58
+ console.log(` ${f.file}`);
59
+ console.log(` ${f.err}`);
60
+ }
61
+ }
@@ -0,0 +1,33 @@
1
+ #!/bin/bash
2
+ # Run yo-self-bin fmt on every .yo file, count fail/success.
3
+ BIN=/tmp/yo-self-fmt-bin
4
+ OK=0; FAIL=0; FAILS=()
5
+
6
+ while IFS= read -r f; do
7
+ # Copy to a temp so original isn't modified.
8
+ tmp="/tmp/yo_parse_probe_$$.yo"
9
+ cp "$f" "$tmp"
10
+ if "$BIN" fmt --check "$tmp" >/dev/null 2>&1 ; then
11
+ OK=$((OK+1))
12
+ else
13
+ # --check may exit 1 for "would format" — that's OK. Check stderr.
14
+ out=$("$BIN" fmt --check "$tmp" 2>&1)
15
+ rc=$?
16
+ if [ "$rc" = "0" ] || [ "$rc" = "1" ]; then
17
+ OK=$((OK+1))
18
+ else
19
+ FAIL=$((FAIL+1))
20
+ FAILS+=("$f: $out")
21
+ fi
22
+ fi
23
+ rm -f "$tmp"
24
+ done < <(find tests yo-self -name "*.yo" 2>/dev/null)
25
+
26
+ echo "OK: $OK FAIL: $FAIL"
27
+ if [ "$FAIL" -gt 0 ]; then
28
+ echo
29
+ echo "Failures (first 10):"
30
+ for f in "${FAILS[@]:0:10}"; do
31
+ echo " - $f"
32
+ done
33
+ fi
@@ -0,0 +1,184 @@
1
+ // One-off validation: extract each test() block from src/tests/formatter.test.ts,
2
+ // run yo-self-bin fmt on the source, compare to the inline expected output.
3
+ //
4
+ // Usage: bun /tmp/validate-yo-self-fmt.ts
5
+ import { spawnSync } from "child_process";
6
+ import * as fs from "fs";
7
+
8
+ const BIN = "/tmp/yo-self-fmt-bin";
9
+ const TEST_FILE = "src/tests/formatter.test.ts";
10
+
11
+ const source = fs.readFileSync(TEST_FILE, "utf-8");
12
+
13
+ // Match each `test("name", () => { ... });` block at the top level.
14
+ // Within: extract `const source = (template-literal);` and the final
15
+ // `expect(formatYoSource(source)).toBe(template-literal);`.
16
+ //
17
+ // Template literals may contain newlines, embedded ${...}, and escaped backticks.
18
+ // We use a state-machine over the source to find balanced backtick-delimited
19
+ // template literals starting at a given position.
20
+ function findTemplateLiteral(
21
+ s: string,
22
+ start: number
23
+ ): { value: string; end: number } | null {
24
+ if (s[start] !== "`") return null;
25
+ let i = start + 1;
26
+ while (i < s.length) {
27
+ const c = s[i];
28
+ if (c === "\\") {
29
+ i += 2;
30
+ continue;
31
+ }
32
+ if (c === "`") {
33
+ // Closing backtick.
34
+ return { value: s.slice(start, i + 1), end: i + 1 };
35
+ }
36
+ if (c === "$" && s[i + 1] === "{") {
37
+ // Skip the ${...} interpolation balanced by curlies.
38
+ let depth = 1;
39
+ i += 2;
40
+ while (i < s.length && depth > 0) {
41
+ if (s[i] === "{") depth++;
42
+ else if (s[i] === "}") depth--;
43
+ i++;
44
+ }
45
+ continue;
46
+ }
47
+ i++;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ function findStringLiteral(
53
+ s: string,
54
+ start: number
55
+ ): { value: string; end: number } | null {
56
+ if (s[start] !== '"' && s[start] !== "'" && s[start] !== "`") return null;
57
+ if (s[start] === "`") return findTemplateLiteral(s, start);
58
+ const quote = s[start];
59
+ let i = start + 1;
60
+ while (i < s.length) {
61
+ if (s[i] === "\\") {
62
+ i += 2;
63
+ continue;
64
+ }
65
+ if (s[i] === quote) return { value: s.slice(start, i + 1), end: i + 1 };
66
+ i++;
67
+ }
68
+ return null;
69
+ }
70
+
71
+ // Parse all test("name", () => { ... }); blocks.
72
+ //
73
+ // Anchor matching on `test("name", () => {` so we don't mistake
74
+ // `test("name", { … })` substrings that appear inside test source/expected
75
+ // template literals (e.g. test 7's source includes a literal
76
+ // `test("format effect", { … })`). The `() =>` arrow distinguishes a real
77
+ // top-level `test()` call from the inner content.
78
+ type TestCase = { name: string; source: string; expected: string };
79
+ const cases: TestCase[] = [];
80
+ const skipped: { name: string; reason: string }[] = [];
81
+
82
+ const testStartRe = /test\("([^"]+)",\s*\(\s*\)\s*=>\s*\{/g;
83
+ let m: RegExpExecArray | null;
84
+ while ((m = testStartRe.exec(source)) !== null) {
85
+ const name = m[1];
86
+ const p = m.index + m[0].length;
87
+
88
+ // Find `const source = `...`;`
89
+ const srcMarker = "const source = ";
90
+ const srcIdx = source.indexOf(srcMarker, p);
91
+ if (srcIdx === -1) {
92
+ skipped.push({ name, reason: "no `const source = `" });
93
+ continue;
94
+ }
95
+ const srcLitStart = srcIdx + srcMarker.length;
96
+ const srcLit = findStringLiteral(source, srcLitStart);
97
+ if (!srcLit) {
98
+ skipped.push({ name, reason: "source is not a string literal" });
99
+ continue;
100
+ }
101
+
102
+ // Find the first `.toBe(` after the source declaration. Most tests use
103
+ // `expect(formatYoSource(source)).toBe(`<literal>`)` but some use the
104
+ // intermediate `const once = formatYoSource(source); expect(once).toBe(`<literal>`)`
105
+ // form — both are caught here because the first `.toBe(` carries the
106
+ // literal. Tests that compare two identifiers (e.g. `expect(twice).toBe(once)`
107
+ // for idempotency-only checks) have no literal to compare and are skipped.
108
+ const expMarker = ".toBe(";
109
+ const expIdx = source.indexOf(expMarker, srcLit.end);
110
+ if (expIdx === -1) {
111
+ skipped.push({ name, reason: "no `.toBe(` after source" });
112
+ continue;
113
+ }
114
+ let expLitStart = expIdx + expMarker.length;
115
+ while (/\s/.test(source[expLitStart])) expLitStart++;
116
+ const expLit = findStringLiteral(source, expLitStart);
117
+ if (!expLit) {
118
+ skipped.push({
119
+ name,
120
+ reason:
121
+ "`.toBe(` argument is not a string literal (idempotency-only test?)",
122
+ });
123
+ continue;
124
+ }
125
+
126
+ // eval the literals (template-strings with embedded vars: those aren't used
127
+ // in our test cases except for backslash-escaped backticks)
128
+ let srcValue: string, expectedValue: string;
129
+ try {
130
+ srcValue = eval(srcLit.value);
131
+ expectedValue = eval(expLit.value);
132
+ } catch (e) {
133
+ skipped.push({ name, reason: `eval failed: ${e}` });
134
+ continue;
135
+ }
136
+ cases.push({ name, source: srcValue, expected: expectedValue });
137
+ }
138
+
139
+ console.log(
140
+ `Extracted ${cases.length} test cases; skipped ${skipped.length}.\n`
141
+ );
142
+ if (skipped.length > 0) {
143
+ console.log("Skipped:");
144
+ for (const s of skipped) {
145
+ console.log(` - ${s.name}: ${s.reason}`);
146
+ }
147
+ console.log();
148
+ }
149
+
150
+ // Run each through yo-self-bin
151
+ let pass = 0,
152
+ fail = 0;
153
+ const failures: { name: string; expected: string; actual: string }[] = [];
154
+
155
+ for (const tc of cases) {
156
+ const tmpPath = `/tmp/yo_fmt_${Date.now()}_${Math.floor(Math.random() * 1e9)}.yo`;
157
+ fs.writeFileSync(tmpPath, tc.source);
158
+ spawnSync(BIN, ["fmt", tmpPath], { encoding: "utf-8" });
159
+ const actual = fs.readFileSync(tmpPath, "utf-8");
160
+ fs.unlinkSync(tmpPath);
161
+
162
+ if (actual === tc.expected) {
163
+ console.log(`✓ ${tc.name}`);
164
+ pass++;
165
+ } else {
166
+ console.log(`✗ ${tc.name}`);
167
+ failures.push({ name: tc.name, expected: tc.expected, actual });
168
+ fail++;
169
+ }
170
+ }
171
+
172
+ console.log(`\n${pass} passed, ${fail} failed, ${pass + fail} total\n`);
173
+
174
+ if (failures.length > 0) {
175
+ console.log("=== first 5 failures ===\n");
176
+ for (const f of failures.slice(0, 5)) {
177
+ console.log(`--- ${f.name} ---`);
178
+ console.log("expected:");
179
+ console.log(JSON.stringify(f.expected));
180
+ console.log("actual:");
181
+ console.log(JSON.stringify(f.actual));
182
+ console.log();
183
+ }
184
+ }
package/std/async.yo CHANGED
@@ -1,7 +1,7 @@
1
1
  //! Async utilities for cooperative task scheduling.
2
2
  /// Suspend the current async task, allowing other tasks to run on the event loop.
3
3
  /// Implemented as an immediately-completing async block that forces a suspension point.
4
- yield :: (fn(using(io : IO)) -> Impl(Future(unit)))(
4
+ yield :: (fn(io : IO) -> Impl(Future(unit)))(
5
5
  io.async(() => {
6
6
  return(());
7
7
  })
@@ -54,7 +54,7 @@ extern(
54
54
  // ============================================================================
55
55
  // Fill a buffer with cryptographically secure random bytes.
56
56
  // ============================================================================
57
- random_bytes :: (fn(buf : *(u8), size : usize, using(exn : Exception)) -> unit)(
57
+ random_bytes :: (fn(buf : *(u8), size : usize, exn : Exception) -> unit)(
58
58
  cond(
59
59
  (platform == Platform.Macos) => {
60
60
  __yo_arc4random_buf(buf, size);
@@ -104,7 +104,7 @@ export(random_bytes);
104
104
  // ============================================================================
105
105
  // Typed helpers
106
106
  // ============================================================================
107
- random_u32 :: (fn(using(exn : Exception)) -> u32)({
107
+ random_u32 :: (fn(exn : Exception) -> u32)({
108
108
  buf := Array(u8, usize(4)).fill(u8(0));
109
109
  buf_ptr := &(buf(usize(0)));
110
110
  random_bytes(buf_ptr, usize(4));
@@ -113,16 +113,16 @@ random_u32 :: (fn(using(exn : Exception)) -> u32)({
113
113
  (u32(buf(usize(2))) << u32(16)) |
114
114
  (u32(buf(usize(3))) << u32(24))
115
115
  });
116
- random_u64 :: (fn(using(exn : Exception)) -> u64)({
116
+ random_u64 :: (fn(exn : Exception) -> u64)({
117
117
  lo := u64(random_u32());
118
118
  hi := u64(random_u32());
119
119
  lo | (hi << u64(32))
120
120
  });
121
- random_f64 :: (fn(using(exn : Exception)) -> f64)(
121
+ random_f64 :: (fn(exn : Exception) -> f64)(
122
122
  f64(random_u64()) / f64(18446744073709551615.0)
123
123
  );
124
124
  // Return a random integer in the half-open range [min, max).
125
- random_range :: (fn(min : i64, max : i64, using(exn : Exception)) -> i64)({
125
+ random_range :: (fn(min : i64, max : i64, exn : Exception) -> i64)({
126
126
  span := (max - min);
127
127
  cond(
128
128
  (span <= i64(0)) => min,
@@ -137,7 +137,7 @@ export(random_u32, random_u64, random_f64, random_range);
137
137
  // UUID v4
138
138
  // ============================================================================
139
139
  // Generate a random UUID v4 string: "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
140
- uuid_v4 :: (fn(using(exn : Exception)) -> String)({
140
+ uuid_v4 :: (fn(exn : Exception) -> String)({
141
141
  bytes := ArrayList(u8).with_capacity(usize(16));
142
142
  buf := Array(u8, usize(16)).fill(u8(0));
143
143
  buf_ptr := &(buf(usize(0)));
@@ -64,7 +64,7 @@ export(base64_encode, base64_encode_url);
64
64
  // ============================================================================
65
65
  // Decoding
66
66
  // ============================================================================
67
- _decode_char :: (fn(c : u8, alpha : str, using(exn : Exception)) -> u8)({
67
+ _decode_char :: (fn(c : u8, alpha : str, exn : Exception) -> u8)({
68
68
  i := usize(0);
69
69
  while(i < usize(64), i = (i + usize(1)), {
70
70
  cond(
@@ -76,7 +76,7 @@ _decode_char :: (fn(c : u8, alpha : str, using(exn : Exception)) -> u8)({
76
76
  });
77
77
  exn.throw(dyn(EncodingError.InvalidChar(c)))
78
78
  });
79
- _decode_with :: (fn(s : str, alpha : str, using(exn : Exception)) -> ArrayList(u8))({
79
+ _decode_with :: (fn(s : str, alpha : str, exn : Exception) -> ArrayList(u8))({
80
80
  // Strip trailing padding
81
81
  len := s.len();
82
82
  while((len > usize(0)) && (s.bytes(len - usize(1)) == _PAD), len = (len - usize(1)), ());
@@ -107,11 +107,11 @@ _decode_with :: (fn(s : str, alpha : str, using(exn : Exception)) -> ArrayList(u
107
107
  out
108
108
  });
109
109
  /// Decode a standard base64 string to bytes. Throws via `Exception` on invalid input.
110
- base64_decode :: (fn(s : str, using(exn : Exception)) -> ArrayList(u8))(
110
+ base64_decode :: (fn(s : str, exn : Exception) -> ArrayList(u8))(
111
111
  _decode_with(s, _STD_ALPHA)
112
112
  );
113
113
  /// Decode a URL-safe base64 string to bytes. Throws via `Exception` on invalid input.
114
- base64_decode_url :: (fn(s : str, using(exn : Exception)) -> ArrayList(u8))(
114
+ base64_decode_url :: (fn(s : str, exn : Exception) -> ArrayList(u8))(
115
115
  _decode_with(s, _URL_ALPHA)
116
116
  );
117
117
  export(base64_decode, base64_decode_url);
@@ -64,7 +64,7 @@ export(hex_encode);
64
64
  // ============================================================================
65
65
  // Decoding
66
66
  // ============================================================================
67
- _hex_nibble :: (fn(c : u8, using(exn : Exception)) -> u8)(
67
+ _hex_nibble :: (fn(c : u8, exn : Exception) -> u8)(
68
68
  cond(
69
69
  ((c >= u8(48)) && (c <= u8(57))) => (c - u8(48)),
70
70
  // '0'-'9'
@@ -76,7 +76,7 @@ _hex_nibble :: (fn(c : u8, using(exn : Exception)) -> u8)(
76
76
  )
77
77
  );
78
78
  /// Decode a hexadecimal string to bytes. Throws via `Exception` on invalid input.
79
- hex_decode :: (fn(s : str, using(exn : Exception)) -> ArrayList(u8))({
79
+ hex_decode :: (fn(s : str, exn : Exception) -> ArrayList(u8))({
80
80
  cond(
81
81
  ((s.len() % usize(2)) != usize(0)) => {
82
82
  exn.throw(dyn(EncodingError.OddLength));
@@ -8,7 +8,7 @@
8
8
  //! { json_parse, json_stringify, JsonValue } :: import "std/encoding/json";
9
9
  //! { Exception } :: import "std/error";
10
10
  //!
11
- //! given(exn) := Exception(throw : ((err) -> { escape (); }));
11
+ //! exn := Exception(throw : ((err) -> { unwind (); }));
12
12
  //! v := json_parse(`{"x": 1}`);
13
13
  //! println(json_stringify(v));
14
14
  //! ```
@@ -30,7 +30,7 @@ JsonError :: enum(
30
30
  InvalidNumber,
31
31
  /// Invalid escape sequence in a string.
32
32
  InvalidEscape,
33
- /// Invalid Unicode escape in a string.
33
+ /// Invalid Unicode unwind in a string.
34
34
  InvalidUnicode,
35
35
  /// Other error with a descriptive message.
36
36
  Other(msg : String)
@@ -553,7 +553,7 @@ _parse_value :: (fn(p : _Parser) -> Result(JsonValue, JsonError))({
553
553
  // Public API
554
554
  // ============================================================================
555
555
  /// Parse a JSON string into a `JsonValue` tree. Throws via `Exception` on invalid input.
556
- json_parse :: (fn(s : str, using(exn : Exception)) -> JsonValue)({
556
+ json_parse :: (fn(s : str, exn : Exception) -> JsonValue)({
557
557
  p := _Parser.new(s);
558
558
  match(
559
559
  _parse_value(p),
@@ -68,7 +68,7 @@ export(utf8_to_utf16);
68
68
  ///
69
69
  /// Decodes surrogate pairs back into supplementary code points.
70
70
  /// Throws via `Exception` on unpaired surrogates.
71
- utf16_to_utf8 :: (fn(data : ArrayList(u16), using(exn : Exception)) -> String)({
71
+ utf16_to_utf8 :: (fn(data : ArrayList(u16), exn : Exception) -> String)({
72
72
  out := ArrayList(u8).new();
73
73
  i := usize(0);
74
74
  while(i < data.len(), {
package/std/error.yo CHANGED
@@ -15,15 +15,27 @@ export(AnyError);
15
15
  impl(String, Error());
16
16
  /// Non-resumable exception handling effect record.
17
17
  /// Use `throw` to raise an `AnyError` and abort the current computation.
18
+ /// `throw` is `ctl(...)` — handler body may contain `unwind`, and the
19
+ /// handler value is frame-bound (see plans/EXPLICIT_EFFECTS.md §4).
18
20
  Exception :: struct(
19
- throw : (fn(forall(ResumeType : Type), error : AnyError) -> ResumeType)
21
+ throw : (ctl(forall(ResumeType : Type), error : AnyError) -> ResumeType)
20
22
  );
21
23
  export(Exception);
22
24
  /// Creates a resumable exception effect record parameterized by the resume type.
23
25
  /// Unlike `Exception`, the handler can return a value to resume the computation.
26
+ /// `throw` is still `ctl(...)` because the handler MAY choose to unwind;
27
+ /// regular `fn` returns are also valid via subtyping `fn <: ctl`.
24
28
  ResumableException :: (fn(comptime(ResumeType) : Type) -> comptime(Type))(
25
29
  struct(
26
- throw : (fn(error : AnyError) -> ResumeType)
30
+ throw : (ctl(error : AnyError) -> ResumeType)
27
31
  )
28
32
  );
29
33
  export(ResumableException);
34
+ /// Common effect bundle: IO + Exception.
35
+ /// Used as the effect parameter for Futures that perform IO and may throw.
36
+ /// Construct with `{ io, exn }`; project with `e.io` or `e.exn`.
37
+ IOErr :: struct(
38
+ io : IO,
39
+ exn : Exception
40
+ );
41
+ export(IOErr);