@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
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Migrate `(self : *(Self))` method signatures to `(inout(self) : Self)`
3
+ * inside `impl(...)` blocks, and rewrite `self.*` references in the
4
+ * matching method bodies to `self`.
5
+ *
6
+ * Operates on a single file (passed as argv[2]). The script is
7
+ * intentionally narrow:
8
+ * - Only rewrites function signatures of the exact shape
9
+ * `(self : *(Self), …) -> …` or `(self : *(Self)) -> …`.
10
+ * - Only rewrites `self.*` *inside the body* of one of those
11
+ * methods, found by walking balanced parens after the signature.
12
+ * - `self.*` outside any such method body — for example in helper
13
+ * functions that operate on a `*(SomeType)` parameter — is left
14
+ * alone.
15
+ *
16
+ * Patterns deliberately NOT rewritten (need manual review):
17
+ * - `(&(self.*.field))…` array-element mutation patterns
18
+ * - `(other : *(SomeType))` non-self pointer parameters
19
+ * - `self.*.field = …` writes (these are valid `self.field = …`
20
+ * after migration, but worth eyeballing the surrounding context)
21
+ *
22
+ * Usage:
23
+ * bun run scripts/migrate-self-ptr.ts <file> # dry-run
24
+ * bun run scripts/migrate-self-ptr.ts <file> --write # apply
25
+ */
26
+
27
+ import { readFileSync, writeFileSync } from "node:fs";
28
+ import * as path from "node:path";
29
+
30
+ interface RewriteResult {
31
+ result: string;
32
+ sigsRewritten: number;
33
+ derefRewrites: number;
34
+ }
35
+
36
+ function migrate(content: string): RewriteResult {
37
+ const out: string[] = [];
38
+ let i = 0;
39
+ let sigsRewritten = 0;
40
+ let derefRewrites = 0;
41
+
42
+ // Match `(self : *(Self)` or `(self : *(Self),` — the signature
43
+ // start. We need to be inside a function-type expression, but it's
44
+ // hard to verify that without a real parser; rely on the literal
45
+ // shape, which is uncommon outside this context.
46
+ const sigStart = /\(self : \*\(Self\)/g;
47
+
48
+ // Heuristic: skip `*(Self)` matches inside Iterator or Index trait
49
+ // impls. Those trait declarations specify `*(Self)` and changing
50
+ // the impl alone would break trait conformance. We detect the
51
+ // enclosing trait by scanning backwards from each match for the
52
+ // nearest unmatched `Iterator(` or `Index(...)(` opener.
53
+ const isInsideTraitImpl = (matchIdx: number): boolean => {
54
+ // Scan backwards looking for `Iterator(` or `Index(...)(` at a
55
+ // depth where its `(` is still open at matchIdx. We use a
56
+ // simple paren-balance check.
57
+ let depth = 0;
58
+ for (let j = matchIdx - 1; j >= 0; j--) {
59
+ const ch = content[j]!;
60
+ if (ch === ")") depth++;
61
+ else if (ch === "(") {
62
+ if (depth === 0) {
63
+ // This `(` is one whose scope contains matchIdx. Check
64
+ // what immediately precedes it for a trait name.
65
+ const before = content.slice(Math.max(0, j - 30), j);
66
+ if (/\bIterator\s*$/.test(before)) return true;
67
+ if (/\bIndex\s*\([^()]*\)\s*$/.test(before)) return true;
68
+ // Not Iterator/Index — keep scanning outward.
69
+ } else {
70
+ depth--;
71
+ }
72
+ }
73
+ }
74
+ return false;
75
+ };
76
+
77
+ while (i < content.length) {
78
+ sigStart.lastIndex = i;
79
+ const m = sigStart.exec(content);
80
+ if (!m) {
81
+ out.push(content.slice(i));
82
+ break;
83
+ }
84
+ const startIdx = m.index;
85
+
86
+ // Skip Iterator/Index trait impl methods — they're declared with
87
+ // `*(Self)` in the trait and migrating only the impl would break
88
+ // conformance. The trait declarations themselves stay verbatim.
89
+ if (isInsideTraitImpl(startIdx)) {
90
+ out.push(content.slice(i, startIdx + m[0].length));
91
+ i = startIdx + m[0].length;
92
+ continue;
93
+ }
94
+
95
+ out.push(content.slice(i, startIdx));
96
+
97
+ // The match starts with `(` of the param list. Walk balanced parens
98
+ // to find the end of the param-list `)`. We DON'T rewrite the
99
+ // body's self.* until we find the method body, which is the
100
+ // *following* parenthesized expression.
101
+ let j = startIdx;
102
+ let depth = 0;
103
+ while (j < content.length) {
104
+ const ch = content[j]!;
105
+ if (ch === "(") depth++;
106
+ else if (ch === ")") {
107
+ depth--;
108
+ if (depth === 0) {
109
+ j++;
110
+ break;
111
+ }
112
+ }
113
+ j++;
114
+ }
115
+ // We've now consumed the `(self : *(Self), …)` param list. Emit
116
+ // the rewritten signature.
117
+ const sigText = content.slice(startIdx, j);
118
+ const newSig = sigText.replace("(self : *(Self)", "(inout(self) : Self");
119
+ out.push(newSig);
120
+ sigsRewritten++;
121
+
122
+ // After the param list, we expect ` -> ReturnType)( body )`.
123
+ // We don't care to fully parse — we just keep walking until we
124
+ // find the next top-level `)`, which closes the `fn(...)` type,
125
+ // then the next `(` opens the body. Inside the body (balanced
126
+ // parens) we rewrite `self.*` → `self`.
127
+ //
128
+ // Practically: keep emitting content unchanged until we hit a
129
+ // matching close-paren depth at the level of the `fn` type
130
+ // expression, then walk into the body.
131
+ //
132
+ // Simpler approach: find the next `)` from current position that
133
+ // closes the surrounding fn-type expression. We know the
134
+ // pattern: `(fn(self : *(Self), …) -> RETURN_TYPE)`. We've consumed
135
+ // up through the params; now expect ` -> RETURN_TYPE)`.
136
+ let k = j;
137
+ // Walk past ` -> ReturnType)` — handle nested parens in the
138
+ // return type.
139
+ let returnDepth = 0;
140
+ while (k < content.length) {
141
+ const ch = content[k]!;
142
+ if (ch === "(") returnDepth++;
143
+ else if (ch === ")") {
144
+ if (returnDepth === 0) {
145
+ k++;
146
+ break;
147
+ }
148
+ returnDepth--;
149
+ }
150
+ k++;
151
+ }
152
+ // Emit ` -> RETURN_TYPE)` unchanged.
153
+ out.push(content.slice(j, k));
154
+
155
+ // Now the body. Skip whitespace.
156
+ while (k < content.length && /\s/.test(content[k]!)) {
157
+ out.push(content[k]!);
158
+ k++;
159
+ }
160
+ // The body should start with `(`. If not, give up rewriting this
161
+ // method's `self.*` references (signature still flipped, but
162
+ // body left alone — manual review needed).
163
+ if (content[k] !== "(") {
164
+ i = k;
165
+ continue;
166
+ }
167
+ // Walk balanced parens for the body.
168
+ const bodyStart = k;
169
+ let bodyDepth = 0;
170
+ while (k < content.length) {
171
+ const ch = content[k]!;
172
+ if (ch === "(") bodyDepth++;
173
+ else if (ch === ")") {
174
+ bodyDepth--;
175
+ if (bodyDepth === 0) {
176
+ k++;
177
+ break;
178
+ }
179
+ }
180
+ k++;
181
+ }
182
+ const body = content.slice(bodyStart, k);
183
+ // Rewrite `self.*` → `self` inside this method's body only.
184
+ const rewrittenBody = body.replace(/self\.\*/g, () => {
185
+ derefRewrites++;
186
+ return "self";
187
+ });
188
+ out.push(rewrittenBody);
189
+
190
+ i = k;
191
+ }
192
+
193
+ return { result: out.join(""), sigsRewritten, derefRewrites };
194
+ }
195
+
196
+ function main(): void {
197
+ const args = process.argv.slice(2);
198
+ const write = args.includes("--write");
199
+ const file = args.find((a) => !a.startsWith("--"));
200
+ if (!file) {
201
+ console.error(
202
+ "Usage: bun run scripts/migrate-self-ptr.ts <file> [--write]"
203
+ );
204
+ process.exit(1);
205
+ }
206
+ const abs = path.resolve(file);
207
+ const content = readFileSync(abs, "utf-8");
208
+ const { result, sigsRewritten, derefRewrites } = migrate(content);
209
+ if (result === content) {
210
+ console.log(`no changes: ${file}`);
211
+ return;
212
+ }
213
+ console.log(
214
+ `${write ? "migrated" : "would migrate"}: ${file} ` +
215
+ `(${sigsRewritten} signatures, ${derefRewrites} self.* rewrites)`
216
+ );
217
+ if (write) writeFileSync(abs, result, "utf-8");
218
+ }
219
+
220
+ main();
@@ -0,0 +1,109 @@
1
+ /**
2
+ * One-shot migration: convert `// @skip_*` comment directives to
3
+ * `pragma(Pragma.Skip*);` calls.
4
+ *
5
+ * Only line-leading comment directives are migrated — directives
6
+ * inside string literals or doc-comments are left alone. Each
7
+ * matching line is replaced by the corresponding pragma call,
8
+ * preserving any trailing "— rationale" text as a leading comment.
9
+ *
10
+ * Usage:
11
+ * bun run scripts/migrate-skip-pragmas.ts # dry-run
12
+ * bun run scripts/migrate-skip-pragmas.ts --write # apply
13
+ */
14
+
15
+ import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
16
+ import * as path from "node:path";
17
+
18
+ const ROOTS = ["tests", "std", "yo-self", "src/tests"];
19
+
20
+ const MAPPING: Array<{ pattern: RegExp; variant: string }> = [
21
+ // Order matters: longer/more-specific first so the broader
22
+ // `@skip_wasm` doesn't shadow `@skip_wasm32-...`.
23
+ {
24
+ pattern: /^(\s*)\/\/\s*@skip_wasm32-emscripten\b(.*)$/,
25
+ variant: "SkipWasm32Emscripten",
26
+ },
27
+ {
28
+ pattern: /^(\s*)\/\/\s*@skip_wasm32-wasi\b(.*)$/,
29
+ variant: "SkipWasm32Wasi",
30
+ },
31
+ { pattern: /^(\s*)\/\/\s*@skip_wasm\b(.*)$/, variant: "SkipWasm" },
32
+ { pattern: /^(\s*)\/\/\s*@skip_prelude\b(.*)$/, variant: "SkipPrelude" },
33
+ ];
34
+
35
+ function* walk(dir: string): Generator<string> {
36
+ for (const entry of readdirSync(dir)) {
37
+ const fullPath = path.join(dir, entry);
38
+ const st = statSync(fullPath);
39
+ if (st.isDirectory()) {
40
+ yield* walk(fullPath);
41
+ } else if (st.isFile() && entry.endsWith(".yo")) {
42
+ yield fullPath;
43
+ }
44
+ }
45
+ }
46
+
47
+ function migrateFile(
48
+ filePath: string,
49
+ write: boolean
50
+ ): {
51
+ changed: boolean;
52
+ matches: number;
53
+ } {
54
+ const original = readFileSync(filePath, "utf-8");
55
+ const lines = original.split("\n");
56
+ let matches = 0;
57
+ const out: string[] = [];
58
+ for (const line of lines) {
59
+ let migrated: string | null = null;
60
+ for (const { pattern, variant } of MAPPING) {
61
+ const m = line.match(pattern);
62
+ if (m) {
63
+ const leading = m[1] ?? "";
64
+ const trailing = (m[2] ?? "").trim();
65
+ const rationale = trailing
66
+ ? ` // ${trailing.replace(/^[—-]\s*/, "")}`
67
+ : "";
68
+ migrated = `${leading}pragma(Pragma.${variant});${rationale}`;
69
+ matches++;
70
+ break;
71
+ }
72
+ }
73
+ out.push(migrated ?? line);
74
+ }
75
+ if (matches === 0) return { changed: false, matches: 0 };
76
+ const result = out.join("\n");
77
+ if (write && result !== original) {
78
+ writeFileSync(filePath, result, "utf-8");
79
+ }
80
+ return { changed: result !== original, matches };
81
+ }
82
+
83
+ function main(): void {
84
+ const write = process.argv.includes("--write");
85
+ let totalFiles = 0;
86
+ let totalMatches = 0;
87
+ for (const root of ROOTS) {
88
+ const absRoot = path.resolve(root);
89
+ for (const filePath of walk(absRoot)) {
90
+ const { changed, matches } = migrateFile(filePath, write);
91
+ if (changed) {
92
+ totalFiles++;
93
+ totalMatches += matches;
94
+ console.log(
95
+ `${write ? "migrated" : "would migrate"}: ${path.relative(
96
+ process.cwd(),
97
+ filePath
98
+ )} (${matches} match${matches === 1 ? "" : "es"})`
99
+ );
100
+ }
101
+ }
102
+ }
103
+ console.log(
104
+ `\n${write ? "Migrated" : "Would migrate"} ${totalMatches} directives across ${totalFiles} file(s).`
105
+ );
106
+ if (!write) console.log("(dry-run — pass --write to apply)");
107
+ }
108
+
109
+ main();
@@ -0,0 +1,134 @@
1
+ /**
2
+ * One-shot migration: ToString trait impls from `(self : *(Self))` to
3
+ * `(inout(self) : Self)`. Phase D continuation of plans/MEMORY_SAFETY.md.
4
+ *
5
+ * Two pattern rewrites:
6
+ *
7
+ * 1. The impl signature:
8
+ * to_string : (fn(self : *(Self)) -> String)({ ... self.* ... })
9
+ * →
10
+ * to_string : (fn(inout(self) : Self) -> String)({ ... self ... })
11
+ *
12
+ * Replaces `self.*` inside ToString impl bodies with `self`. Outside
13
+ * ToString impls (or in any other context), `self.*` is left alone.
14
+ *
15
+ * 2. Explicit caller patterns `(&(x)).to_string()` → `x.to_string()`.
16
+ *
17
+ * Usage:
18
+ * bun run scripts/migrate-tostring.ts # dry-run
19
+ * bun run scripts/migrate-tostring.ts --write # apply
20
+ */
21
+
22
+ import { readFileSync, writeFileSync } from "node:fs";
23
+ import * as path from "node:path";
24
+
25
+ const FILES = [
26
+ "std/log.yo",
27
+ "std/fmt/to_string.yo",
28
+ "std/time/datetime.yo",
29
+ "std/time/duration.yo",
30
+ "std/testing/bench.yo",
31
+ "std/string/string_builder.yo",
32
+ "yo-self/parser.yo",
33
+ "yo-self/lexer.yo",
34
+ "yo-self/error.yo",
35
+ ];
36
+
37
+ /**
38
+ * Find all to_string impl bodies in the file and rewrite each from
39
+ * `(self : *(Self)) -> String)({ ...body... })` to `(inout(self) :
40
+ * Self) -> String)({ ...body... })`, where `body` has `self.*`
41
+ * replaced by `self`. Only rewrites bodies that begin with the exact
42
+ * to_string signature we care about; everything else is preserved
43
+ * byte-for-byte.
44
+ */
45
+ function migrateContent(content: string): { result: string; changed: boolean } {
46
+ let changed = false;
47
+ // Pattern: `to_string : (fn(self : *(Self)) -> String)` followed by a
48
+ // body. We need to find the balanced `(...)` of the body and rewrite
49
+ // `self.*` → `self` inside it.
50
+ const signature = "to_string : (fn(self : *(Self)) -> String)";
51
+ const newSignature = "to_string : (fn(inout(self) : Self) -> String)";
52
+ const out: string[] = [];
53
+ let i = 0;
54
+ while (i < content.length) {
55
+ const idx = content.indexOf(signature, i);
56
+ if (idx === -1) {
57
+ out.push(content.slice(i));
58
+ break;
59
+ }
60
+ // Push everything before the match
61
+ out.push(content.slice(i, idx));
62
+ out.push(newSignature);
63
+ let j = idx + signature.length;
64
+ // Find the body — could be `({...})` or `(expr)` immediately after.
65
+ // Skip whitespace.
66
+ while (j < content.length && /\s/.test(content[j]!)) {
67
+ out.push(content[j]!);
68
+ j++;
69
+ }
70
+ if (content[j] !== "(") {
71
+ // Unexpected shape; bail out for this match — preserve the rest verbatim.
72
+ out.push(content.slice(j));
73
+ changed = true;
74
+ break;
75
+ }
76
+ // Walk matching parens; rewrite self.* → self inside the body.
77
+ let depth = 0;
78
+ const bodyStart = j;
79
+ while (j < content.length) {
80
+ const ch = content[j]!;
81
+ if (ch === "(") depth++;
82
+ else if (ch === ")") {
83
+ depth--;
84
+ if (depth === 0) {
85
+ j++;
86
+ break;
87
+ }
88
+ }
89
+ j++;
90
+ }
91
+ const body = content.slice(bodyStart, j);
92
+ // Replace `self.*` with `self` in the impl body. Be careful: do
93
+ // not replace `self.*.field` patterns incorrectly. Just replace
94
+ // every occurrence — for trait impls the surrounding code has
95
+ // already been audited.
96
+ const rewrittenBody = body.replace(/self\.\*/g, "self");
97
+ out.push(rewrittenBody);
98
+ i = j;
99
+ changed = true;
100
+ }
101
+
102
+ let result = out.join("");
103
+
104
+ // Pattern 2: `(&(x)).to_string()` → `x.to_string()`. Conservative —
105
+ // only matches when `x` is a single identifier (possibly chained
106
+ // via `.`).
107
+ const callerPattern = /\(&\(([^()]+)\)\)\.to_string\(\)/g;
108
+ const before = result;
109
+ result = result.replace(callerPattern, "$1.to_string()");
110
+ if (result !== before) changed = true;
111
+
112
+ return { result, changed };
113
+ }
114
+
115
+ function main(): void {
116
+ const write = process.argv.includes("--write");
117
+ let totalChanged = 0;
118
+ for (const rel of FILES) {
119
+ const abs = path.resolve(rel);
120
+ const content = readFileSync(abs, "utf-8");
121
+ const { result, changed } = migrateContent(content);
122
+ if (changed && result !== content) {
123
+ totalChanged++;
124
+ console.log(`${write ? "migrated" : "would migrate"}: ${rel}`);
125
+ if (write) writeFileSync(abs, result, "utf-8");
126
+ }
127
+ }
128
+ console.log(
129
+ `\n${write ? "Migrated" : "Would migrate"} ${totalChanged} file(s).`
130
+ );
131
+ if (!write) console.log("(dry-run — pass --write to apply)");
132
+ }
133
+
134
+ main();
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Remove `pragma(Pragma.AllowUnsafe);` from files that don't actually
4
+ * need it — i.e., files that don't contain any `unsafe(...)`,
5
+ * `asm(...)`, or `extern(...)` calls.
6
+ *
7
+ * Phase C of plans/MEMORY_SAFETY.md added the pragma to every file
8
+ * under `std/`, `yo-self/`, and `tests/` mechanically. Many tests
9
+ * (and a few stdlib files) don't actually exercise raw-pointer
10
+ * machinery; the pragma was added defensively. Removing it from
11
+ * those files makes the test suite a better demonstration of
12
+ * "safe user code by default" and shrinks the auditable surface.
13
+ *
14
+ * Safety: we only consider a file "needs pragma" if it contains a
15
+ * direct `unsafe(`, `asm(`, or `extern(` call site (matched outside
16
+ * comments/strings, same heuristic as `yo unsafe-report`). If the
17
+ * file imports a stdlib module whose public API takes `*(T)`, the
18
+ * file still compiles in safe mode (per the current Phase C
19
+ * implementation — see "Known gaps" in plans/MEMORY_SAFETY.md).
20
+ *
21
+ * Usage:
22
+ * bun scripts/trim-pragma.ts <file.yo> [more...]
23
+ * bun scripts/trim-pragma.ts --dry-run <file.yo> [more...]
24
+ */
25
+
26
+ import { readFileSync, writeFileSync, statSync } from "fs";
27
+ import { argv } from "process";
28
+
29
+ const dryRun = argv.includes("--dry-run");
30
+ const files = argv.slice(2).filter((a) => a !== "--dry-run");
31
+
32
+ const PRAGMA_LINE_RE = /^pragma\(Pragma\.AllowUnsafe\);\s*$/m;
33
+
34
+ /**
35
+ * Strip line-comments and string literals from a line so that
36
+ * matches inside `"unsafe(...)"` or `// asm(...)` don't count.
37
+ */
38
+ function strip(line: string): string {
39
+ const out: string[] = [];
40
+ let inStr: string | null = null;
41
+ let i = 0;
42
+ while (i < line.length) {
43
+ const ch = line[i]!;
44
+ if (!inStr && ch === "/" && line[i + 1] === "/") break;
45
+ if (inStr) {
46
+ if (ch === "\\") {
47
+ i += 2;
48
+ continue;
49
+ }
50
+ if (ch === inStr) inStr = null;
51
+ i++;
52
+ continue;
53
+ }
54
+ if (ch === '"' || ch === "`") {
55
+ inStr = ch;
56
+ i++;
57
+ continue;
58
+ }
59
+ out.push(ch);
60
+ i++;
61
+ }
62
+ return out.join("");
63
+ }
64
+
65
+ function fileNeedsPragma(src: string): { needs: boolean; reason?: string } {
66
+ const lines = src.split("\n");
67
+ for (let i = 0; i < lines.length; i++) {
68
+ const s = strip(lines[i]!);
69
+ if (/\bunsafe\(/.test(s))
70
+ return { needs: true, reason: `unsafe(...) at line ${i + 1}` };
71
+ if (/(?:^|[^A-Za-z0-9_])asm\(/.test(s))
72
+ return { needs: true, reason: `asm(...) at line ${i + 1}` };
73
+ if (/(?:^|[^A-Za-z0-9_])extern\(/.test(s))
74
+ return { needs: true, reason: `extern(...) at line ${i + 1}` };
75
+ // Bare pointer-op sites that would fire the Phase A gate without
76
+ // a pragma. Without these checks we'd strip pragma from files
77
+ // like tests/ptr.test.yo that use `.*` / `&+` directly without
78
+ // wrapping in unsafe(...) (the pragma currently bypasses the
79
+ // gate so those work). Files like that should keep their pragma
80
+ // until/unless the pointer ops are wrapped in unsafe(...).
81
+ if (/\.\*(?:[^A-Za-z0-9_]|$)/.test(s))
82
+ return { needs: true, reason: `bare .* deref at line ${i + 1}` };
83
+ if (/(?:^|[^A-Za-z0-9_&])&\+|&-(?!=)|&\//.test(s))
84
+ return {
85
+ needs: true,
86
+ reason: `bare pointer arithmetic (&+/&-/&/) at line ${i + 1}`,
87
+ };
88
+ }
89
+ return { needs: false };
90
+ }
91
+
92
+ function processFile(file: string): "removed" | "kept" | "no-pragma" | "skip" {
93
+ try {
94
+ if (!statSync(file).isFile()) return "skip";
95
+ } catch {
96
+ return "skip";
97
+ }
98
+ const src = readFileSync(file, "utf8");
99
+ if (!PRAGMA_LINE_RE.test(src)) return "no-pragma";
100
+
101
+ const { needs, reason } = fileNeedsPragma(src);
102
+ if (needs) {
103
+ if (dryRun) console.log(`keep: ${file} (${reason})`);
104
+ return "kept";
105
+ }
106
+
107
+ // Remove the pragma line. Also remove a trailing blank line if it
108
+ // creates an awkward double-blank.
109
+ const next = src
110
+ .replace(/^pragma\(Pragma\.AllowUnsafe\);\n/m, "")
111
+ .replace(/^pragma\(Pragma\.AllowUnsafe\);\s*\n/m, "");
112
+
113
+ if (dryRun) {
114
+ console.log(`would-remove: ${file}`);
115
+ return "removed";
116
+ }
117
+
118
+ writeFileSync(file, next);
119
+ console.log(`removed: ${file}`);
120
+ return "removed";
121
+ }
122
+
123
+ const counts = { removed: 0, kept: 0, "no-pragma": 0, skip: 0 };
124
+ for (const file of files) {
125
+ const r = processFile(file);
126
+ counts[r]++;
127
+ }
128
+ console.log(
129
+ `\nsummary: removed=${counts.removed}, kept=${counts.kept}, no-pragma=${counts["no-pragma"]}, skipped=${counts.skip}`
130
+ );