@shd101wyy/yo 0.1.28 → 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.
- package/.github/skills/yo-async-effects/SKILL.md +15 -15
- package/.github/skills/yo-async-effects/async-effects-recipes.md +118 -121
- package/.github/skills/yo-core-patterns/core-patterns-cheatsheet.md +33 -13
- package/.github/skills/yo-project-workflow/workflow-cheatsheet.md +1 -1
- package/.github/skills/yo-syntax/SKILL.md +2 -2
- package/.github/skills/yo-syntax/syntax-cheatsheet.md +108 -96
- package/README.md +6 -3
- package/out/cjs/index.cjs +812 -706
- package/out/cjs/yo-cli.cjs +1023 -907
- package/out/cjs/yo-lsp.cjs +836 -730
- package/out/esm/index.mjs +757 -651
- package/out/types/src/codegen/exprs/async.d.ts +2 -0
- package/out/types/src/codegen/exprs/await.d.ts +1 -0
- package/out/types/src/codegen/exprs/closures.d.ts +4 -0
- package/out/types/src/codegen/functions/context.d.ts +6 -0
- 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 +2 -2
- package/out/types/src/evaluator/builtins/pragma.d.ts +9 -0
- package/out/types/src/evaluator/builtins/unsafe.d.ts +8 -0
- package/out/types/src/evaluator/context.d.ts +3 -1
- package/out/types/src/evaluator/exprs/{escape.d.ts → unwind.d.ts} +1 -1
- package/out/types/src/evaluator/index.d.ts +1 -1
- package/out/types/src/evaluator/memory-safety.d.ts +14 -0
- package/out/types/src/evaluator/types/flowability.d.ts +6 -0
- 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-traversal.d.ts +1 -0
- package/out/types/src/expr.d.ts +9 -7
- package/out/types/src/public-safe-report.d.ts +19 -0
- package/out/types/src/tests/comptime-ref-gate.test.d.ts +1 -0
- package/out/types/src/tests/pragma-validation.test.d.ts +1 -0
- package/out/types/src/tests/public-safe-report.test.d.ts +1 -0
- package/out/types/src/tests/type-representation-pointer.test.d.ts +1 -0
- package/out/types/src/tests/unsafe-gate.test.d.ts +1 -0
- package/out/types/src/tests/unsafe-report-classify.test.d.ts +1 -0
- package/out/types/src/types/creators.d.ts +4 -6
- package/out/types/src/types/definitions.d.ts +9 -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 +5 -0
- package/out/types/src/unsafe-report.d.ts +29 -0
- package/out/types/src/value.d.ts +1 -0
- package/out/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/scripts/add-pragma-for-pointer-decls.ts +134 -0
- package/scripts/add-pragma.ts +58 -0
- package/scripts/migrate-amp-method-calls.ts +186 -0
- package/scripts/migrate-clone-calls.ts +93 -0
- package/scripts/migrate-get-unwrap.ts +166 -0
- package/scripts/migrate-index-patterns.ts +210 -0
- package/scripts/migrate-index-trait.ts +142 -0
- package/scripts/migrate-iterator.ts +150 -0
- package/scripts/migrate-self-ptr.ts +220 -0
- package/scripts/migrate-skip-pragmas.ts +109 -0
- package/scripts/migrate-tostring.ts +134 -0
- package/scripts/trim-pragma.ts +130 -0
- package/scripts/wrap-extern-calls.ts +161 -0
- package/std/alg/hash.yo +3 -2
- package/std/allocator.yo +6 -5
- package/std/async.yo +2 -2
- package/std/collections/array_list.yo +59 -40
- package/std/collections/btree_map.yo +19 -18
- package/std/collections/deque.yo +9 -8
- package/std/collections/hash_map.yo +101 -13
- package/std/collections/hash_set.yo +5 -4
- package/std/collections/linked_list.yo +39 -4
- package/std/collections/ordered_map.yo +3 -3
- package/std/collections/priority_queue.yo +14 -13
- package/std/crypto/md5.yo +2 -1
- package/std/crypto/random.yo +21 -20
- package/std/crypto/sha256.yo +2 -1
- package/std/encoding/base64.yo +18 -18
- package/std/encoding/hex.yo +5 -5
- package/std/encoding/json.yo +62 -13
- package/std/encoding/punycode.yo +24 -23
- package/std/encoding/toml.yo +4 -3
- package/std/encoding/utf16.yo +3 -3
- package/std/env.yo +43 -28
- package/std/error.yo +15 -3
- package/std/fmt/display.yo +2 -2
- package/std/fmt/index.yo +6 -5
- package/std/fmt/to_string.yo +39 -38
- package/std/fmt/writer.yo +9 -8
- package/std/fs/dir.yo +61 -66
- package/std/fs/file.yo +121 -126
- package/std/fs/metadata.yo +13 -18
- package/std/fs/temp.yo +35 -30
- package/std/fs/walker.yo +14 -19
- package/std/gc.yo +1 -0
- package/std/glob.yo +7 -7
- package/std/http/client.yo +33 -36
- package/std/http/http.yo +6 -6
- package/std/http/index.yo +4 -4
- package/std/imm/list.yo +33 -0
- package/std/imm/map.yo +2 -1
- package/std/imm/set.yo +1 -0
- package/std/imm/sorted_map.yo +1 -0
- package/std/imm/sorted_set.yo +1 -0
- package/std/imm/string.yo +27 -23
- package/std/imm/vec.yo +18 -2
- package/std/io/reader.yo +2 -1
- package/std/io/writer.yo +3 -2
- package/std/libc/assert.yo +1 -0
- package/std/libc/ctype.yo +1 -0
- package/std/libc/dirent.yo +1 -0
- package/std/libc/errno.yo +1 -0
- package/std/libc/fcntl.yo +1 -0
- package/std/libc/float.yo +1 -0
- package/std/libc/limits.yo +1 -0
- package/std/libc/math.yo +1 -0
- package/std/libc/signal.yo +1 -0
- package/std/libc/stdatomic.yo +1 -0
- package/std/libc/stdint.yo +1 -0
- package/std/libc/stdio.yo +1 -0
- package/std/libc/stdlib.yo +1 -0
- package/std/libc/string.yo +1 -0
- package/std/libc/sys/stat.yo +1 -0
- package/std/libc/time.yo +1 -0
- package/std/libc/unistd.yo +1 -0
- package/std/libc/wctype.yo +1 -0
- package/std/libc/windows.yo +2 -0
- package/std/log.yo +7 -6
- package/std/net/addr.yo +6 -5
- package/std/net/dns.yo +13 -16
- package/std/net/errors.yo +9 -9
- package/std/net/tcp.yo +71 -74
- package/std/net/udp.yo +40 -43
- package/std/os/signal.yo +5 -5
- package/std/path.yo +1 -0
- package/std/prelude.yo +377 -200
- package/std/process/command.yo +57 -46
- package/std/process/index.yo +2 -1
- package/std/regex/compiler.yo +10 -9
- package/std/regex/index.yo +41 -41
- package/std/regex/match.yo +2 -2
- package/std/regex/parser.yo +31 -31
- package/std/regex/vm.yo +42 -41
- package/std/string/string.yo +95 -40
- package/std/string/string_builder.yo +9 -9
- package/std/string/unicode.yo +50 -49
- package/std/sync/channel.yo +2 -1
- package/std/sync/cond.yo +5 -4
- package/std/sync/mutex.yo +4 -3
- package/std/sys/advise.yo +1 -0
- package/std/sys/bufio/buf_reader.yo +27 -26
- package/std/sys/bufio/buf_writer.yo +22 -21
- package/std/sys/clock.yo +1 -0
- package/std/sys/copy.yo +1 -0
- package/std/sys/dir.yo +10 -9
- package/std/sys/dns.yo +6 -5
- package/std/sys/errors.yo +12 -12
- package/std/sys/events.yo +1 -0
- package/std/sys/externs.yo +38 -37
- package/std/sys/file.yo +17 -16
- package/std/sys/future.yo +4 -3
- package/std/sys/iov.yo +1 -0
- package/std/sys/mmap.yo +1 -0
- package/std/sys/path.yo +1 -0
- package/std/sys/perm.yo +2 -1
- package/std/sys/pipe.yo +1 -0
- package/std/sys/process.yo +5 -4
- package/std/sys/signal.yo +1 -0
- package/std/sys/socketpair.yo +1 -0
- package/std/sys/sockinfo.yo +1 -0
- package/std/sys/statfs.yo +2 -1
- package/std/sys/statx.yo +1 -0
- package/std/sys/sysinfo.yo +1 -0
- package/std/sys/tcp.yo +15 -14
- package/std/sys/temp.yo +1 -0
- package/std/sys/time.yo +2 -1
- package/std/sys/timer.yo +6 -6
- package/std/sys/tty.yo +2 -1
- package/std/sys/udp.yo +13 -12
- package/std/sys/unix.yo +12 -11
- package/std/testing/bench.yo +4 -3
- package/std/thread.yo +7 -6
- package/std/time/datetime.yo +18 -15
- package/std/time/duration.yo +11 -10
- package/std/time/instant.yo +4 -4
- package/std/time/sleep.yo +1 -0
- package/std/url/index.yo +5 -5
- 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
|
+
);
|