@shd101wyy/yo 0.1.29 → 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 (174) hide show
  1. package/.github/skills/yo-async-effects/SKILL.md +3 -3
  2. package/.github/skills/yo-async-effects/async-effects-recipes.md +19 -11
  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/syntax-cheatsheet.md +59 -21
  6. package/README.md +4 -3
  7. package/out/cjs/index.cjs +771 -676
  8. package/out/cjs/yo-cli.cjs +1003 -898
  9. package/out/cjs/yo-lsp.cjs +834 -739
  10. package/out/esm/index.mjs +716 -621
  11. package/out/types/src/codegen/exprs/async.d.ts +2 -0
  12. package/out/types/src/codegen/exprs/await.d.ts +1 -0
  13. package/out/types/src/codegen/exprs/closures.d.ts +4 -0
  14. package/out/types/src/codegen/functions/context.d.ts +6 -0
  15. package/out/types/src/env.d.ts +2 -0
  16. package/out/types/src/evaluator/builtins/pragma.d.ts +9 -0
  17. package/out/types/src/evaluator/builtins/unsafe.d.ts +8 -0
  18. package/out/types/src/evaluator/context.d.ts +2 -0
  19. package/out/types/src/evaluator/index.d.ts +1 -1
  20. package/out/types/src/evaluator/memory-safety.d.ts +14 -0
  21. package/out/types/src/evaluator/types/flowability.d.ts +6 -0
  22. package/out/types/src/expr-traversal.d.ts +1 -0
  23. package/out/types/src/expr.d.ts +4 -1
  24. package/out/types/src/public-safe-report.d.ts +19 -0
  25. package/out/types/src/tests/comptime-ref-gate.test.d.ts +1 -0
  26. package/out/types/src/tests/pragma-validation.test.d.ts +1 -0
  27. package/out/types/src/tests/public-safe-report.test.d.ts +1 -0
  28. package/out/types/src/tests/type-representation-pointer.test.d.ts +1 -0
  29. package/out/types/src/tests/unsafe-gate.test.d.ts +1 -0
  30. package/out/types/src/tests/unsafe-report-classify.test.d.ts +1 -0
  31. package/out/types/src/types/definitions.d.ts +2 -0
  32. package/out/types/src/types/utils.d.ts +4 -0
  33. package/out/types/src/unsafe-report.d.ts +29 -0
  34. package/out/types/src/value.d.ts +1 -0
  35. package/out/types/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +1 -1
  37. package/scripts/add-pragma-for-pointer-decls.ts +134 -0
  38. package/scripts/add-pragma.ts +58 -0
  39. package/scripts/migrate-amp-method-calls.ts +186 -0
  40. package/scripts/migrate-clone-calls.ts +93 -0
  41. package/scripts/migrate-get-unwrap.ts +166 -0
  42. package/scripts/migrate-index-patterns.ts +210 -0
  43. package/scripts/migrate-index-trait.ts +142 -0
  44. package/scripts/migrate-iterator.ts +150 -0
  45. package/scripts/migrate-self-ptr.ts +220 -0
  46. package/scripts/migrate-skip-pragmas.ts +109 -0
  47. package/scripts/migrate-tostring.ts +134 -0
  48. package/scripts/trim-pragma.ts +130 -0
  49. package/scripts/wrap-extern-calls.ts +161 -0
  50. package/std/alg/hash.yo +3 -2
  51. package/std/allocator.yo +6 -5
  52. package/std/async.yo +2 -2
  53. package/std/collections/array_list.yo +59 -40
  54. package/std/collections/btree_map.yo +19 -18
  55. package/std/collections/deque.yo +9 -8
  56. package/std/collections/hash_map.yo +101 -13
  57. package/std/collections/hash_set.yo +5 -4
  58. package/std/collections/linked_list.yo +39 -4
  59. package/std/collections/ordered_map.yo +3 -3
  60. package/std/collections/priority_queue.yo +14 -13
  61. package/std/crypto/md5.yo +2 -1
  62. package/std/crypto/random.yo +16 -15
  63. package/std/crypto/sha256.yo +2 -1
  64. package/std/encoding/base64.yo +14 -14
  65. package/std/encoding/hex.yo +3 -3
  66. package/std/encoding/json.yo +59 -10
  67. package/std/encoding/punycode.yo +24 -23
  68. package/std/encoding/toml.yo +4 -3
  69. package/std/encoding/utf16.yo +2 -2
  70. package/std/env.yo +43 -28
  71. package/std/error.yo +6 -6
  72. package/std/fmt/display.yo +2 -2
  73. package/std/fmt/index.yo +6 -5
  74. package/std/fmt/to_string.yo +39 -38
  75. package/std/fmt/writer.yo +9 -8
  76. package/std/fs/dir.yo +34 -33
  77. package/std/fs/file.yo +52 -51
  78. package/std/fs/metadata.yo +10 -9
  79. package/std/fs/temp.yo +24 -13
  80. package/std/fs/walker.yo +10 -9
  81. package/std/gc.yo +1 -0
  82. package/std/glob.yo +7 -7
  83. package/std/http/client.yo +15 -14
  84. package/std/http/http.yo +6 -6
  85. package/std/http/index.yo +1 -1
  86. package/std/imm/list.yo +33 -0
  87. package/std/imm/map.yo +2 -1
  88. package/std/imm/set.yo +1 -0
  89. package/std/imm/sorted_map.yo +1 -0
  90. package/std/imm/sorted_set.yo +1 -0
  91. package/std/imm/string.yo +27 -23
  92. package/std/imm/vec.yo +18 -2
  93. package/std/io/reader.yo +2 -1
  94. package/std/io/writer.yo +3 -2
  95. package/std/libc/assert.yo +1 -0
  96. package/std/libc/ctype.yo +1 -0
  97. package/std/libc/dirent.yo +1 -0
  98. package/std/libc/errno.yo +1 -0
  99. package/std/libc/fcntl.yo +1 -0
  100. package/std/libc/float.yo +1 -0
  101. package/std/libc/limits.yo +1 -0
  102. package/std/libc/math.yo +1 -0
  103. package/std/libc/signal.yo +1 -0
  104. package/std/libc/stdatomic.yo +1 -0
  105. package/std/libc/stdint.yo +1 -0
  106. package/std/libc/stdio.yo +1 -0
  107. package/std/libc/stdlib.yo +1 -0
  108. package/std/libc/string.yo +1 -0
  109. package/std/libc/sys/stat.yo +1 -0
  110. package/std/libc/time.yo +1 -0
  111. package/std/libc/unistd.yo +1 -0
  112. package/std/libc/wctype.yo +1 -0
  113. package/std/libc/windows.yo +2 -0
  114. package/std/log.yo +7 -6
  115. package/std/net/addr.yo +5 -4
  116. package/std/net/dns.yo +7 -6
  117. package/std/net/errors.yo +8 -8
  118. package/std/net/tcp.yo +19 -18
  119. package/std/net/udp.yo +13 -12
  120. package/std/os/signal.yo +3 -3
  121. package/std/path.yo +1 -0
  122. package/std/prelude.yo +353 -182
  123. package/std/process/command.yo +40 -23
  124. package/std/process/index.yo +2 -1
  125. package/std/regex/compiler.yo +10 -9
  126. package/std/regex/index.yo +41 -41
  127. package/std/regex/match.yo +2 -2
  128. package/std/regex/parser.yo +21 -21
  129. package/std/regex/vm.yo +42 -41
  130. package/std/string/string.yo +95 -40
  131. package/std/string/string_builder.yo +9 -9
  132. package/std/string/unicode.yo +50 -49
  133. package/std/sync/channel.yo +2 -1
  134. package/std/sync/cond.yo +5 -4
  135. package/std/sync/mutex.yo +4 -3
  136. package/std/sys/advise.yo +1 -0
  137. package/std/sys/bufio/buf_reader.yo +17 -16
  138. package/std/sys/bufio/buf_writer.yo +10 -9
  139. package/std/sys/clock.yo +1 -0
  140. package/std/sys/copy.yo +1 -0
  141. package/std/sys/dir.yo +10 -9
  142. package/std/sys/dns.yo +6 -5
  143. package/std/sys/errors.yo +11 -11
  144. package/std/sys/events.yo +1 -0
  145. package/std/sys/externs.yo +38 -37
  146. package/std/sys/file.yo +17 -16
  147. package/std/sys/future.yo +4 -3
  148. package/std/sys/iov.yo +1 -0
  149. package/std/sys/mmap.yo +1 -0
  150. package/std/sys/path.yo +1 -0
  151. package/std/sys/perm.yo +2 -1
  152. package/std/sys/pipe.yo +1 -0
  153. package/std/sys/process.yo +5 -4
  154. package/std/sys/signal.yo +1 -0
  155. package/std/sys/socketpair.yo +1 -0
  156. package/std/sys/sockinfo.yo +1 -0
  157. package/std/sys/statfs.yo +2 -1
  158. package/std/sys/statx.yo +1 -0
  159. package/std/sys/sysinfo.yo +1 -0
  160. package/std/sys/tcp.yo +15 -14
  161. package/std/sys/temp.yo +1 -0
  162. package/std/sys/time.yo +2 -1
  163. package/std/sys/timer.yo +6 -6
  164. package/std/sys/tty.yo +2 -1
  165. package/std/sys/udp.yo +13 -12
  166. package/std/sys/unix.yo +12 -11
  167. package/std/testing/bench.yo +4 -3
  168. package/std/thread.yo +7 -6
  169. package/std/time/datetime.yo +18 -15
  170. package/std/time/duration.yo +11 -10
  171. package/std/time/instant.yo +4 -4
  172. package/std/time/sleep.yo +1 -0
  173. package/std/url/index.yo +3 -3
  174. package/std/worker.yo +4 -3
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shd101wyy/yo",
3
3
  "displayName": "Yo",
4
- "version": "0.1.29",
4
+ "version": "0.1.30",
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,134 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Re-add `pragma(Pragma.AllowUnsafe);` to files that mention raw
4
+ * pointer types in their declarations (e.g. `*(u8)` in a parameter,
5
+ * field, or return slot, or `&(x)` to take an address).
6
+ *
7
+ * Phase C tightened the structural gate so that `*(T)` and `&(...)`
8
+ * are themselves rejected in safe code — not just `unsafe(...)`-
9
+ * wrapped ops like `.* ` and `&+`. The previous trim pass
10
+ * (scripts/trim-pragma.ts) removed the pragma from files that used
11
+ * only declarations, which now need it back.
12
+ *
13
+ * Detection: file contains `*(<word>` or `&(<word>` outside string
14
+ * literals / comments. The heuristic is loose on purpose — better
15
+ * to over-add the pragma than to leave a file failing to compile.
16
+ *
17
+ * Usage:
18
+ * bun scripts/add-pragma-for-pointer-decls.ts <root-dir>
19
+ */
20
+
21
+ import { readFileSync, writeFileSync, statSync, readdirSync } from "fs";
22
+ import { join } from "path";
23
+ import { argv, exit } from "process";
24
+
25
+ const PRAGMA_LINE = "pragma(Pragma.AllowUnsafe);";
26
+
27
+ function stripCommentsAndStrings(src: string): string {
28
+ const out: string[] = [];
29
+ let inStr: string | null = null;
30
+ let i = 0;
31
+ while (i < src.length) {
32
+ const ch = src[i]!;
33
+ if (!inStr && ch === "/" && src[i + 1] === "/") {
34
+ while (i < src.length && src[i] !== "\n") i++;
35
+ continue;
36
+ }
37
+ if (inStr) {
38
+ if (ch === "\\") {
39
+ out.push(" ", " ");
40
+ i += 2;
41
+ continue;
42
+ }
43
+ if (ch === inStr) {
44
+ inStr = null;
45
+ out.push(" ");
46
+ i++;
47
+ continue;
48
+ }
49
+ out.push(" ");
50
+ i++;
51
+ continue;
52
+ }
53
+ if (ch === '"' || ch === "`") {
54
+ inStr = ch;
55
+ out.push(" ");
56
+ i++;
57
+ continue;
58
+ }
59
+ out.push(ch);
60
+ i++;
61
+ }
62
+ return out.join("");
63
+ }
64
+
65
+ const POINTER_TYPE_RE = /\*\(/;
66
+ const ADDRESS_OF_RE = /(?:^|[^A-Za-z0-9_])&\(/;
67
+
68
+ function fileNeedsPragma(src: string): boolean {
69
+ const cleaned = stripCommentsAndStrings(src);
70
+ return POINTER_TYPE_RE.test(cleaned) || ADDRESS_OF_RE.test(cleaned);
71
+ }
72
+
73
+ function fileHasPragma(src: string): boolean {
74
+ return src.includes("pragma(Pragma.AllowUnsafe)");
75
+ }
76
+
77
+ /**
78
+ * Insert the pragma after any leading `//!` module-doc lines and
79
+ * before the first code declaration. Empty files get the pragma at
80
+ * the top.
81
+ */
82
+ function insertPragma(src: string): string {
83
+ const lines = src.split("\n");
84
+ let insertAt = 0;
85
+ // Skip leading shebang / module-doc / blank / line-comment.
86
+ while (insertAt < lines.length) {
87
+ const trimmed = lines[insertAt]!.trim();
88
+ if (
89
+ trimmed === "" ||
90
+ trimmed.startsWith("#!") ||
91
+ trimmed.startsWith("//!") ||
92
+ trimmed.startsWith("//")
93
+ ) {
94
+ insertAt++;
95
+ continue;
96
+ }
97
+ break;
98
+ }
99
+ lines.splice(insertAt, 0, PRAGMA_LINE);
100
+ return lines.join("\n");
101
+ }
102
+
103
+ function walk(dir: string, out: string[]): void {
104
+ for (const entry of readdirSync(dir)) {
105
+ const p = join(dir, entry);
106
+ const s = statSync(p);
107
+ if (s.isDirectory()) {
108
+ if (entry === "node_modules" || entry.startsWith(".")) continue;
109
+ walk(p, out);
110
+ } else if (s.isFile() && p.endsWith(".yo")) {
111
+ out.push(p);
112
+ }
113
+ }
114
+ }
115
+
116
+ const root = argv[2];
117
+ if (!root) {
118
+ console.error(`usage: bun ${argv[1]} <root-dir>`);
119
+ exit(1);
120
+ }
121
+
122
+ const files: string[] = [];
123
+ walk(root, files);
124
+
125
+ let added = 0;
126
+ for (const file of files) {
127
+ const src = readFileSync(file, "utf8");
128
+ if (fileHasPragma(src)) continue;
129
+ if (!fileNeedsPragma(src)) continue;
130
+ writeFileSync(file, insertPragma(src));
131
+ added++;
132
+ console.log(`added pragma: ${file}`);
133
+ }
134
+ console.log(`---\nadded pragma to ${added} file(s).`);
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Add `pragma(Pragma.AllowUnsafe);` at the top of every .yo file
4
+ * passed on the command line, unless it already has one.
5
+ *
6
+ * Phase C of plans/MEMORY_SAFETY.md: explicit per-file pragma replaces
7
+ * the path-based MVP heuristic. Run this once over `std/`, `yo-self/`,
8
+ * and `tests/` during the migration.
9
+ *
10
+ * Heuristics:
11
+ * - Skip the prelude itself (it defines `Pragma`; the pragma in the
12
+ * prelude is placed by hand, mid-file, after `Pragma :: enum(...)`).
13
+ * - Skip files that already contain `pragma(Pragma.AllowUnsafe);`.
14
+ * - Insert after any leading `//` line comments at the top of the file
15
+ * so the docstring stays visually at the top.
16
+ */
17
+
18
+ import { readFileSync, writeFileSync, statSync } from "fs";
19
+ import { argv } from "process";
20
+
21
+ const PRAGMA_LINE = "pragma(Pragma.AllowUnsafe);";
22
+
23
+ function processFile(file: string): "added" | "already" | "skip" {
24
+ const src = readFileSync(file, "utf8");
25
+ if (file.endsWith("/std/prelude.yo")) return "skip";
26
+ if (src.includes(PRAGMA_LINE)) return "already";
27
+
28
+ const lines = src.split("\n");
29
+ // Find first non-comment, non-blank line; insert pragma above it.
30
+ let insertAt = 0;
31
+ while (insertAt < lines.length) {
32
+ const ln = lines[insertAt]!.trim();
33
+ if (ln === "" || ln.startsWith("//") || ln.startsWith("/*")) {
34
+ insertAt++;
35
+ } else {
36
+ break;
37
+ }
38
+ }
39
+ lines.splice(insertAt, 0, PRAGMA_LINE);
40
+ writeFileSync(file, lines.join("\n"));
41
+ return "added";
42
+ }
43
+
44
+ let added = 0;
45
+ let already = 0;
46
+ let skip = 0;
47
+ for (const file of argv.slice(2)) {
48
+ try {
49
+ if (!statSync(file).isFile()) continue;
50
+ } catch {
51
+ continue;
52
+ }
53
+ const r = processFile(file);
54
+ if (r === "added") added++;
55
+ else if (r === "already") already++;
56
+ else skip++;
57
+ }
58
+ console.log(`added: ${added}, already: ${already}, skipped: ${skip}`);
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Rewrite `(&(X)).METHOD(...)` → `X.METHOD(...)` for methods that
3
+ * have been migrated from `(self : *(Self))` to `(inout(self) :
4
+ * Self)`. Inout method dispatch auto-wraps the receiver with
5
+ * `&(...)`, so the explicit `&(X)` at the call site now produces
6
+ * `&(&(X))` = `*(*(T))`, which doesn't match the expected `*(T)`.
7
+ *
8
+ * The script takes a method-name allow-list (passed via env var
9
+ * `METHODS` or a hardcoded default list). Receivers `X` can be any
10
+ * balanced expression; we don't touch the receiver, only strip the
11
+ * `&(...)` wrap.
12
+ *
13
+ * Usage:
14
+ * bun run scripts/migrate-amp-method-calls.ts # dry-run
15
+ * bun run scripts/migrate-amp-method-calls.ts --write # apply
16
+ */
17
+
18
+ import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
19
+ import * as path from "node:path";
20
+
21
+ const ROOTS = ["std", "yo-self", "tests"];
22
+
23
+ // Methods migrated to inout(self) : Self in this session, plus the
24
+ // emitter/Clone/Hash/ToString migrations from earlier. If a method
25
+ // here was actually NOT migrated, no harm — dispatch only changes
26
+ // for the migrated ones, and the rewrite stays semantically
27
+ // correct (`X.method()` already worked for `*(Self)`-style methods
28
+ // via Yo's auto-deref).
29
+ const METHODS = [
30
+ // Array / Slice
31
+ "iter",
32
+ "len",
33
+ // String mutators
34
+ "push_str",
35
+ "push_string",
36
+ "push_byte",
37
+ "reserve",
38
+ "clear",
39
+ // StringBuilder
40
+ "is_empty",
41
+ "write_str",
42
+ "write_string",
43
+ "write_byte",
44
+ "write_rune",
45
+ "write_line",
46
+ // Time
47
+ "as_secs",
48
+ "as_millis",
49
+ "as_micros",
50
+ "as_nanos",
51
+ "to_string",
52
+ "duration_since",
53
+ "elapsed",
54
+ // Sync / process / thread
55
+ "lock",
56
+ "unlock",
57
+ "signal",
58
+ "broadcast",
59
+ "wait",
60
+ "spawn",
61
+ "join",
62
+ // Collections inherent (the few that got migrated)
63
+ "trim_to_fit",
64
+ // Existing migrated (Hash, Clone, ToString) — safe no-op if not
65
+ // explicitly wrapped at the call site
66
+ "hash",
67
+ "clone",
68
+ // Iterator
69
+ "next",
70
+ ];
71
+
72
+ function matchAmpersandWrap(
73
+ content: string,
74
+ start: number
75
+ ): { x: string; end: number } | null {
76
+ if (content.slice(start, start + 3) !== "(&(") return null;
77
+ let depth = 1;
78
+ let i = start + 3;
79
+ const xStart = i;
80
+ while (i < content.length) {
81
+ const ch = content[i]!;
82
+ if (ch === "(") depth++;
83
+ else if (ch === ")") {
84
+ depth--;
85
+ if (depth === 0) break;
86
+ }
87
+ i++;
88
+ }
89
+ if (i >= content.length) return null;
90
+ const x = content.slice(xStart, i);
91
+ if (content[i + 1] !== ")") return null;
92
+ return { x, end: i + 2 };
93
+ }
94
+
95
+ function migrate(
96
+ content: string,
97
+ methodSet: Set<string>
98
+ ): { result: string; rewrites: number } {
99
+ const out: string[] = [];
100
+ let i = 0;
101
+ let rewrites = 0;
102
+ while (i < content.length) {
103
+ if (content.slice(i, i + 3) !== "(&(") {
104
+ out.push(content[i]!);
105
+ i++;
106
+ continue;
107
+ }
108
+ const wrap = matchAmpersandWrap(content, i);
109
+ if (!wrap) {
110
+ out.push(content[i]!);
111
+ i++;
112
+ continue;
113
+ }
114
+ // After `(&(X))`, look for `.METHOD(`.
115
+ if (content[wrap.end] !== ".") {
116
+ out.push(content[i]!);
117
+ i++;
118
+ continue;
119
+ }
120
+ // Extract method name.
121
+ const methodMatch = content
122
+ .slice(wrap.end + 1)
123
+ .match(/^([A-Za-z_][A-Za-z0-9_]*)\(/);
124
+ if (!methodMatch) {
125
+ out.push(content[i]!);
126
+ i++;
127
+ continue;
128
+ }
129
+ const method = methodMatch[1]!;
130
+ if (!methodSet.has(method)) {
131
+ out.push(content[i]!);
132
+ i++;
133
+ continue;
134
+ }
135
+ // Rewrite: `(&(X))` → `X`. Keep `.method(` as-is.
136
+ out.push(wrap.x);
137
+ i = wrap.end;
138
+ rewrites++;
139
+ }
140
+ return { result: out.join(""), rewrites };
141
+ }
142
+
143
+ function* walk(dir: string): Generator<string> {
144
+ for (const entry of readdirSync(dir)) {
145
+ if (entry.startsWith(".")) continue;
146
+ const fullPath = path.join(dir, entry);
147
+ const st = statSync(fullPath);
148
+ if (st.isDirectory()) {
149
+ yield* walk(fullPath);
150
+ } else if (st.isFile() && entry.endsWith(".yo")) {
151
+ yield fullPath;
152
+ }
153
+ }
154
+ }
155
+
156
+ function main(): void {
157
+ const write = process.argv.includes("--write");
158
+ const methodsFromEnv = process.env.METHODS;
159
+ const methodList = methodsFromEnv ? methodsFromEnv.split(",") : METHODS;
160
+ const methodSet = new Set(methodList);
161
+ let total = 0;
162
+ let filesTouched = 0;
163
+ for (const root of ROOTS) {
164
+ const absRoot = path.resolve(root);
165
+ for (const filePath of walk(absRoot)) {
166
+ const content = readFileSync(filePath, "utf-8");
167
+ const { result, rewrites } = migrate(content, methodSet);
168
+ if (result === content) continue;
169
+ filesTouched++;
170
+ total += rewrites;
171
+ console.log(
172
+ `${write ? "migrated" : "would migrate"}: ${path.relative(
173
+ process.cwd(),
174
+ filePath
175
+ )} (${rewrites})`
176
+ );
177
+ if (write) writeFileSync(filePath, result, "utf-8");
178
+ }
179
+ }
180
+ console.log(
181
+ `\n${write ? "Migrated" : "Would migrate"} ${total} call sites across ${filesTouched} file(s).`
182
+ );
183
+ if (!write) console.log("(dry-run — pass --write to apply)");
184
+ }
185
+
186
+ main();
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Migrate `(&(x)).clone()` to `x.clone()` in the given files.
4
+ *
5
+ * Phase D of plans/MEMORY_SAFETY.md: Clone trait now takes
6
+ * `inout(self) : Self` instead of `(self : *(Self))`, so the old
7
+ * pattern `(&(x)).clone()` (passing a `*(T)` to a `*(Self)` receiver)
8
+ * no longer matches. The caller can just write `x.clone()` and the
9
+ * compiler auto-wraps for the inout calling convention.
10
+ *
11
+ * Match: `(&(EXPR)).clone()` — handles balanced parens inside EXPR.
12
+ * Replace with `EXPR.clone()`. Conservative: only single-line matches.
13
+ */
14
+
15
+ import { readFileSync, writeFileSync } from "fs";
16
+ import { argv } from "process";
17
+
18
+ function matchedClose(s: string, openPos: number): number {
19
+ // openPos is the position of `(`. Return position of matching `)`.
20
+ let depth = 1;
21
+ let i = openPos + 1;
22
+ let inStr: string | null = null;
23
+ while (i < s.length) {
24
+ const ch = s[i]!;
25
+ if (inStr) {
26
+ if (ch === "\\") {
27
+ i += 2;
28
+ continue;
29
+ }
30
+ if (ch === inStr) inStr = null;
31
+ i++;
32
+ continue;
33
+ }
34
+ if (ch === '"' || ch === "`") {
35
+ inStr = ch;
36
+ i++;
37
+ continue;
38
+ }
39
+ if (ch === "(") depth++;
40
+ else if (ch === ")") {
41
+ depth--;
42
+ if (depth === 0) return i;
43
+ }
44
+ i++;
45
+ }
46
+ return -1;
47
+ }
48
+
49
+ function migrate(src: string): string {
50
+ let out = "";
51
+ let i = 0;
52
+ while (i < src.length) {
53
+ // Look for `(&(` start.
54
+ if (src.slice(i, i + 3) === "(&(") {
55
+ const inner_open = i + 2; // position of inner `(`
56
+ const inner_close = matchedClose(src, inner_open);
57
+ if (inner_close < 0) {
58
+ out += src[i]!;
59
+ i++;
60
+ continue;
61
+ }
62
+ const outer_close = matchedClose(src, i);
63
+ if (outer_close < 0 || outer_close !== inner_close + 1) {
64
+ out += src[i]!;
65
+ i++;
66
+ continue;
67
+ }
68
+ // Check that the trailing chars are `.clone()`.
69
+ const trailing = src.slice(outer_close + 1, outer_close + 9);
70
+ if (trailing === ".clone()") {
71
+ const inner = src.slice(inner_open + 1, inner_close);
72
+ out += `${inner}.clone()`;
73
+ i = outer_close + 9;
74
+ continue;
75
+ }
76
+ }
77
+ out += src[i]!;
78
+ i++;
79
+ }
80
+ return out;
81
+ }
82
+
83
+ let changed = 0;
84
+ for (const file of argv.slice(2)) {
85
+ const src = readFileSync(file, "utf8");
86
+ const next = migrate(src);
87
+ if (next !== src) {
88
+ writeFileSync(file, next);
89
+ changed++;
90
+ console.log(`migrated: ${file}`);
91
+ }
92
+ }
93
+ console.log(`done: ${changed} file(s) changed`);
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Replace `X.get(IDX).unwrap()` with `X(IDX)`.
3
+ *
4
+ * `X.get(idx)` on an indexable type (ArrayList, Deque, HashMap,
5
+ * BTreeMap, String, Array, imm Vec, etc.) returns `Option(T)`.
6
+ * Calling `.unwrap()` panics on a missing element. Yo's Index-trait
7
+ * call-syntax `X(idx)` returns `T` directly and panics identically
8
+ * (via the trait's bounds assertion).
9
+ *
10
+ * The substitution is safe whenever:
11
+ * 1. `X.get(...)` has exactly ONE argument.
12
+ * 2. `X` is followed by `.get(...)` then immediately `.unwrap()`.
13
+ * 3. The receiver type implements Index — for which we have no
14
+ * type info here, but it covers the vast majority of cases
15
+ * since Option/Result don't have `.get(IDX)` shape (Option has
16
+ * no `.get` at all; Result.ok() returns Option).
17
+ *
18
+ * Skipped:
19
+ * - `X.get(K, V).unwrap()` — multi-arg get (not Index trait).
20
+ * - `X.get().unwrap()` — zero-arg get.
21
+ * - Bare `.get(...)` without `.unwrap()`.
22
+ * - Anything followed by another `.method(...)` chain after
23
+ * `.unwrap()` — we leave the result chain alone (still works
24
+ * since `X(i)` returns the same type as `X.get(i).unwrap()`).
25
+ *
26
+ * Usage:
27
+ * bun run scripts/migrate-get-unwrap.ts # dry-run
28
+ * bun run scripts/migrate-get-unwrap.ts --write # apply
29
+ */
30
+
31
+ import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
32
+ import * as path from "node:path";
33
+
34
+ const ROOTS = ["std", "yo-self", "tests"];
35
+
36
+ /**
37
+ * Walk a balanced parenthesized group starting at `start` (where
38
+ * `content[start]` is `(`). Returns the inner content and the index
39
+ * just after the closing `)`. Returns null on unbalanced input.
40
+ */
41
+ function matchBalancedParens(
42
+ content: string,
43
+ start: number
44
+ ): { inner: string; end: number } | null {
45
+ if (content[start] !== "(") return null;
46
+ let depth = 1;
47
+ let i = start + 1;
48
+ const innerStart = i;
49
+ while (i < content.length) {
50
+ const ch = content[i]!;
51
+ if (ch === "(") depth++;
52
+ else if (ch === ")") {
53
+ depth--;
54
+ if (depth === 0) {
55
+ return { inner: content.slice(innerStart, i), end: i + 1 };
56
+ }
57
+ }
58
+ i++;
59
+ }
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * Count top-level commas in `s` — useful for detecting multi-arg
65
+ * function calls. Skips commas inside nested parens / brackets / braces.
66
+ */
67
+ function topLevelCommaCount(s: string): number {
68
+ let count = 0;
69
+ let depth = 0;
70
+ for (let i = 0; i < s.length; i++) {
71
+ const ch = s[i]!;
72
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
73
+ else if (ch === ")" || ch === "]" || ch === "}") depth--;
74
+ else if (ch === "," && depth === 0) count++;
75
+ }
76
+ return count;
77
+ }
78
+
79
+ function migrate(content: string): { result: string; rewrites: number } {
80
+ let rewrites = 0;
81
+ // We scan for the literal `.get(`, then walk:
82
+ // ... `.get(` BALANCED `)` `.unwrap()`
83
+ // and rewrite the entire span back to `(` BALANCED `)`.
84
+ //
85
+ // The receiver `X` is whatever precedes `.get(`. We DO NOT rewrite
86
+ // the receiver — just leave it in place and append `(args)` after
87
+ // it. That way `foo.bar.get(i).unwrap()` becomes `foo.bar(i)`.
88
+ const out: string[] = [];
89
+ let i = 0;
90
+ while (i < content.length) {
91
+ const idx = content.indexOf(".get(", i);
92
+ if (idx === -1) {
93
+ out.push(content.slice(i));
94
+ break;
95
+ }
96
+ out.push(content.slice(i, idx));
97
+ const argStart = idx + 4;
98
+ const args = matchBalancedParens(content, argStart);
99
+ if (!args) {
100
+ out.push(content[i]!);
101
+ i++;
102
+ continue;
103
+ }
104
+ // Check for `.unwrap()` immediately after.
105
+ if (content.slice(args.end, args.end + 9) !== ".unwrap()") {
106
+ // Not a `.get(IDX).unwrap()` pair — leave alone.
107
+ out.push(content.slice(idx, args.end));
108
+ i = args.end;
109
+ continue;
110
+ }
111
+ // Require exactly one top-level arg (skip empty-arg get, skip
112
+ // 2+arg get). Allow whitespace-only emptiness check.
113
+ if (args.inner.trim().length === 0 || topLevelCommaCount(args.inner) > 0) {
114
+ out.push(content.slice(idx, args.end + 9));
115
+ i = args.end + 9;
116
+ continue;
117
+ }
118
+ // Rewrite: `.get(IDX).unwrap()` → `(IDX)`.
119
+ out.push(`(${args.inner})`);
120
+ rewrites++;
121
+ i = args.end + 9; // skip past `.unwrap()`
122
+ }
123
+ return { result: out.join(""), rewrites };
124
+ }
125
+
126
+ function* walk(dir: string): Generator<string> {
127
+ for (const entry of readdirSync(dir)) {
128
+ if (entry.startsWith(".")) continue;
129
+ const fullPath = path.join(dir, entry);
130
+ const st = statSync(fullPath);
131
+ if (st.isDirectory()) {
132
+ yield* walk(fullPath);
133
+ } else if (st.isFile() && entry.endsWith(".yo")) {
134
+ yield fullPath;
135
+ }
136
+ }
137
+ }
138
+
139
+ function main(): void {
140
+ const write = process.argv.includes("--write");
141
+ let totalRewrites = 0;
142
+ let filesTouched = 0;
143
+ for (const root of ROOTS) {
144
+ const absRoot = path.resolve(root);
145
+ for (const filePath of walk(absRoot)) {
146
+ const content = readFileSync(filePath, "utf-8");
147
+ const { result, rewrites } = migrate(content);
148
+ if (result === content) continue;
149
+ filesTouched++;
150
+ totalRewrites += rewrites;
151
+ console.log(
152
+ `${write ? "migrated" : "would migrate"}: ${path.relative(
153
+ process.cwd(),
154
+ filePath
155
+ )} (${rewrites})`
156
+ );
157
+ if (write) writeFileSync(filePath, result, "utf-8");
158
+ }
159
+ }
160
+ console.log(
161
+ `\n${write ? "Migrated" : "Would migrate"} ${totalRewrites} rewrites across ${filesTouched} file(s).`
162
+ );
163
+ if (!write) console.log("(dry-run — pass --write to apply)");
164
+ }
165
+
166
+ main();