patch-gotypes.go raw
1 // build/patch-gotypes.go — Patches $GOROOT/src/go/types for Moxie string=[]byte.
2 // Run: go run build/patch-gotypes.go <target-dir>
3 // Copies go/types to <target-dir>/src/go/types/ and applies 5 Moxie patches.
4 package main
5
6 import (
7 "fmt"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "strings"
12 )
13
14 func main() {
15 if len(os.Args) < 2 {
16 fmt.Fprintln(os.Stderr, "usage: go run build/patch-gotypes.go <goroot-target>")
17 os.Exit(1)
18 }
19 target := os.Args[1]
20 goroot := os.Getenv("GOROOT")
21 if goroot == "" {
22 out, err := exec.Command("go", "env", "GOROOT").Output()
23 if err != nil {
24 fatal("cannot determine GOROOT: %v", err)
25 }
26 goroot = strings.TrimSpace(string(out))
27 }
28
29 srcDir := filepath.Join(goroot, "src", "go", "types")
30 dstDir := filepath.Join(target, "src", "go", "types")
31
32 // Copy go/types source.
33 if err := os.MkdirAll(dstDir, 0o755); err != nil {
34 fatal("mkdir: %v", err)
35 }
36 entries, err := os.ReadDir(srcDir)
37 if err != nil {
38 fatal("readdir %s: %v", srcDir, err)
39 }
40 for _, e := range entries {
41 if e.IsDir() {
42 continue
43 }
44 data, err := os.ReadFile(filepath.Join(srcDir, e.Name()))
45 if err != nil {
46 fatal("read %s: %v", e.Name(), err)
47 }
48 if err := os.WriteFile(filepath.Join(dstDir, e.Name()), data, 0o644); err != nil {
49 fatal("write %s: %v", e.Name(), err)
50 }
51 }
52 // Copy testdata dir if it exists (needed for go/types to compile).
53 tdSrc := filepath.Join(srcDir, "testdata")
54 if info, err := os.Stat(tdSrc); err == nil && info.IsDir() {
55 tdDst := filepath.Join(dstDir, "testdata")
56 cpCmd := exec.Command("cp", "-a", tdSrc, tdDst)
57 if out, err := cpCmd.CombinedOutput(); err != nil {
58 fatal("copy testdata: %s: %v", out, err)
59 }
60 }
61
62 // Apply patches.
63 patchPredicates(dstDir)
64 patchOperand(dstDir)
65 patchExpr(dstDir)
66 patchSizes(dstDir)
67 patchDecl(dstDir)
68 patchRecording(dstDir)
69 patchIdentical(dstDir)
70 patchUnify(dstDir)
71 patchTypeTerm(dstDir)
72 patchSpawnBuiltin(dstDir)
73
74 fmt.Fprintf(os.Stderr, "patched go/types in %s\n", dstDir)
75 }
76
77 func patchPredicates(dir string) {
78 f := filepath.Join(dir, "predicates.go")
79 src := mustRead(f)
80
81 // Patch 1: Add isByteSlice helper after isString.
82 src = mustReplace(f, src,
83 "func isString(t Type) bool { return isBasic(t, IsString) }\n",
84 `func isString(t Type) bool { return isBasic(t, IsString) }
85 func isByteSlice(t Type) bool {
86 if s, ok := under(t).(*Slice); ok {
87 if b, ok := s.elem.(*Basic); ok && b.kind == Byte {
88 return true
89 }
90 }
91 return false
92 }
93 `)
94
95 // Patch 2: Add slice comparability — any slice of comparable elements is comparable.
96 src = mustReplace(f, src,
97 "\tdefault:\n\t\treturn typeErrorf(\"\")\n\t}\n\n\treturn nil\n}",
98 "\tcase *Slice:\n\t\t// Moxie: slices of comparable types are comparable.\n\t\t// Length check + element-wise comparison at runtime.\n\t\treturn comparableType(t.elem, dynamic, seen)\n\n\tdefault:\n\t\treturn typeErrorf(\"\")\n\t}\n\n\treturn nil\n}")
99
100 mustWrite(f, src)
101 }
102
103 func patchOperand(dir string) {
104 f := filepath.Join(dir, "operand.go")
105 src := mustRead(f)
106
107 // Patch 3: string↔[]byte mutual assignability.
108 src = mustReplace(f, src,
109 "\t// T is an interface type, but not a type parameter, and V implements T.\n\t// Also handle the case where T is a pointer to an interface so that we get\n\t// the Checker.implements error cause.",
110 "\t// Moxie: string and []byte are mutually assignable.\n\tif (isString(Vu) && isByteSlice(Tu)) || (isByteSlice(Vu) && isString(Tu)) {\n\t\treturn true, 0\n\t}\n\n\t// T is an interface type, but not a type parameter, and V implements T.\n\t// Also handle the case where T is a pointer to an interface so that we get\n\t// the Checker.implements error cause.")
111
112 mustWrite(f, src)
113 }
114
115 func patchExpr(dir string) {
116 f := filepath.Join(dir, "expr.go")
117 src := mustRead(f)
118
119 // Patch 4: Untyped string → []byte implicit conversion.
120 // For constants, return Typ[String] (not []byte) because slices aren't
121 // valid constant types. The assignableTo patch handles string→[]byte.
122 src = mustReplace(f, src,
123 "\tswitch u := under(target).(type) {\n\tcase *Basic:\n\t\tif x.mode == constant_ {",
124 "\t// Moxie: untyped string → []byte implicit conversion (string=[]byte unification).\n\t// For constants, return Typ[String] because slices aren't valid constant types.\n\tif x.typ.(*Basic).kind == UntypedString && isByteSlice(target) {\n\t\tif x.mode == constant_ {\n\t\t\treturn Typ[String], x.val, 0\n\t\t}\n\t\treturn target, nil, 0\n\t}\n\n\tswitch u := under(target).(type) {\n\tcase *Basic:\n\t\tif x.mode == constant_ {")
125
126 // Patch 7: Allow | on matching slice types (slice concatenation).
127 src = mustReplace(f, src,
128 "\tif !check.op(binaryOpPredicates, x, op) {\n\t\tx.mode = invalid\n\t\treturn\n\t}",
129 "\t// Moxie: | on matching slice types is concatenation.\n\t_, mxSliceConcat := under(x.typ).(*Slice)\n\tif !(op == token.OR && mxSliceConcat) && !check.op(binaryOpPredicates, x, op) {\n\t\tx.mode = invalid\n\t\treturn\n\t}")
130
131 mustWrite(f, src)
132 }
133
134 func patchSizes(dir string) {
135 f := filepath.Join(dir, "sizes.go")
136 src := mustRead(f)
137
138 // Patch 5: String size = 3 words (ptr, len, cap — matching []byte layout).
139 src = mustReplace(f, src,
140 "\t\tif k == String {\n\t\t\treturn s.WordSize * 2\n\t\t}",
141 "\t\tif k == String {\n\t\t\t// Moxie: string=[]byte — 3-word struct (ptr, len, cap).\n\t\t\treturn s.WordSize * 3\n\t\t}")
142
143 mustWrite(f, src)
144 }
145
146 func patchDecl(dir string) {
147 f := filepath.Join(dir, "decl.go")
148 src := mustRead(f)
149
150 // Patch 6: Allow []byte as a valid constant type (string=[]byte unification).
151 // With string=[]byte, named types like `type X []byte` should accept string constants.
152 src = mustReplace(f, src,
153 "\t\tif !isConstType(t) {",
154 "\t\tif !isConstType(t) && !isByteSlice(t) {")
155
156 mustWrite(f, src)
157 }
158
159 func patchRecording(dir string) {
160 f := filepath.Join(dir, "recording.go")
161 src := mustRead(f)
162
163 // Patch 11: Allow []byte as a valid constant type in type recording.
164 // With string=[]byte, string constants may have []byte type.
165 src = mustReplace(f, src,
166 "assert(!isValid(typ) || allBasic(typ, IsConstType))",
167 "assert(!isValid(typ) || allBasic(typ, IsConstType) || isByteSlice(typ))")
168
169 mustWrite(f, src)
170 }
171
172 func patchIdentical(dir string) {
173 f := filepath.Join(dir, "predicates.go")
174 src := mustRead(f)
175
176 // Patch 9: string ↔ []byte identity in comparer.identical.
177 // Makes Identical(string, []byte) true, propagating through all composite
178 // types: []string = [][]byte, map[string]V = map[[]byte]V, etc.
179 src = mustReplace(f, src,
180 "\tswitch x := x.(type) {\n\tcase *Basic:\n\t\t// Basic types are singletons except for the rune and byte\n\t\t// aliases, thus we cannot solely rely on the x == y check\n\t\t// above. See also comment in TypeName.IsAlias.\n\t\tif y, ok := y.(*Basic); ok {\n\t\t\treturn x.kind == y.kind\n\t\t}",
181 "\t// Moxie: string and []byte are identical types.\n\tif (isString(x) && isByteSlice(y)) || (isByteSlice(x) && isString(y)) {\n\t\treturn true\n\t}\n\n\tswitch x := x.(type) {\n\tcase *Basic:\n\t\t// Basic types are singletons except for the rune and byte\n\t\t// aliases, thus we cannot solely rely on the x == y check\n\t\t// above. See also comment in TypeName.IsAlias.\n\t\tif y, ok := y.(*Basic); ok {\n\t\t\treturn x.kind == y.kind\n\t\t}")
182
183 mustWrite(f, src)
184 }
185
186 func patchUnify(dir string) {
187 f := filepath.Join(dir, "unify.go")
188 src := mustRead(f)
189
190 // Patch 10: string ↔ []byte unification in the type unifier.
191 // Generic constraint satisfaction uses unify(), not Identical().
192 src = mustReplace(f, src,
193 "\t// nothing to do if x == y\n\tif x == y || Unalias(x) == Unalias(y) {\n\t\treturn true\n\t}",
194 "\t// nothing to do if x == y\n\tif x == y || Unalias(x) == Unalias(y) {\n\t\treturn true\n\t}\n\n\t// Moxie: string and []byte unify (string=[]byte).\n\tif (isString(x) && isByteSlice(y)) || (isByteSlice(x) && isString(y)) {\n\t\treturn true\n\t}")
195
196 mustWrite(f, src)
197 }
198
199 func patchTypeTerm(dir string) {
200 f := filepath.Join(dir, "typeterm.go")
201 src := mustRead(f)
202
203 // Patch 8: Generic constraint satisfaction — []byte satisfies ~string.
204 // term.includes checks Identical(x.typ, under(t)). With string=[]byte,
205 // []byte must satisfy ~string in constraint unions like cmp.Ordered.
206 src = mustReplace(f, src,
207 "\treturn Identical(x.typ, u)\n}",
208 "\tif Identical(x.typ, u) {\n\t\treturn true\n\t}\n\t// Moxie: []byte satisfies ~string (string=[]byte unification).\n\tif isString(x.typ) && isByteSlice(t) {\n\t\treturn true\n\t}\n\tif isByteSlice(x.typ) && isString(t) {\n\t\treturn true\n\t}\n\treturn false\n}")
209
210 mustWrite(f, src)
211 }
212
213 func patchSpawnBuiltin(dir string) {
214 // Patch universe.go: add _Spawn builtin ID and predeclaredFuncs entry.
215 f := filepath.Join(dir, "universe.go")
216 src := mustRead(f)
217
218 // Add _Spawn after _Recover in the builtinId enum.
219 src = mustReplace(f, src,
220 "\t_Recover\n\n\t// package unsafe",
221 "\t_Recover\n\t_Spawn\n\n\t// package unsafe")
222
223 // Add spawn entry in predeclaredFuncs after _Recover.
224 // nargs=1 (minimum: the function), variadic=true, statement-kind so the
225 // call can be used as an expression statement (its primary use) while
226 // still allowing `ch := spawn(...)` to consume the lifecycle handle.
227 src = mustReplace(f, src,
228 "\t_Recover: {\"recover\", 0, false, statement},",
229 "\t_Recover: {\"recover\", 0, false, statement},\n\t_Spawn: {\"spawn\", 1, true, statement},")
230
231 mustWrite(f, src)
232
233 // Patch builtins.go: add type-checking handler for spawn.
234 f = filepath.Join(dir, "builtins.go")
235 src = mustRead(f)
236
237 // Insert spawn handler after the _Recover case, before _Add.
238 src = mustReplace(f, src,
239 "\tcase _Recover:\n\t\t// recover() interface{}\n\t\tx.mode = value\n\t\tx.typ = &emptyInterface\n\t\tif check.recordTypes() {\n\t\t\tcheck.recordBuiltinType(call.Fun, makeSig(x.typ))\n\t\t}\n\n\tcase _Add:",
240 ` case _Recover:
241 // recover() interface{}
242 x.mode = value
243 x.typ = &emptyInterface
244 if check.recordTypes() {
245 check.recordBuiltinType(call.Fun, makeSig(x.typ))
246 }
247
248 case _Spawn:
249 // spawn(fn, args...) chan struct{}
250 // spawn("transport", fn, args...) chan struct{}
251 // First argument must be a function or a transport string constant.
252 // Remaining arguments are validated by the compiler at SSA level
253 // (must implement moxie.Codec, channels must have Codec element types).
254 if nargs < 1 {
255 return
256 }
257 if _, ok := under(x.typ).(*Signature); !ok {
258 if !isString(x.typ) {
259 check.errorf(x, InvalidCall, invalidOp+"first argument to spawn must be a function or transport string, got %s", x)
260 return
261 }
262 // Transport string — second arg must be a function.
263 if nargs < 2 {
264 check.errorf(x, WrongArgCount, invalidOp+"spawn with transport string requires a function argument")
265 return
266 }
267 if _, ok := under(args[1].typ).(*Signature); !ok {
268 check.errorf(args[1], InvalidCall, invalidOp+"second argument to spawn must be a function when first is a transport string, got %s", args[1])
269 return
270 }
271 }
272 x.mode = value
273 x.typ = NewChan(SendRecv, NewStruct(nil, nil))
274 // Synthesize a signature for the spawn ident so the SSA builder
275 // can construct a *Builtin with a *types.Signature when it visits
276 // the call. Without this, fn.instanceType(e).(*types.Signature)
277 // at builder.go:814 panics: the universe entry has no signature.
278 if check.recordTypes() {
279 params := make([]Type, nargs)
280 for i, a := range args {
281 params[i] = a.typ
282 }
283 check.recordBuiltinType(call.Fun, makeSig(x.typ, params...))
284 }
285
286 case _Add:`)
287
288 mustWrite(f, src)
289 }
290
291 func mustRead(path string) string {
292 data, err := os.ReadFile(path)
293 if err != nil {
294 fatal("read %s: %v", path, err)
295 }
296 return string(data)
297 }
298
299 func mustWrite(path string, content string) {
300 if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
301 fatal("write %s: %v", path, err)
302 }
303 }
304
305 func mustReplace(file, src, old, new string) string {
306 if !strings.Contains(src, old) {
307 fatal("patch target not found in %s:\n---\n%s\n---", file, old)
308 }
309 count := strings.Count(src, old)
310 if count != 1 {
311 fatal("patch target appears %d times in %s (expected 1)", count, file)
312 }
313 return strings.Replace(src, old, new, 1)
314 }
315
316 func fatal(format string, args ...any) {
317 fmt.Fprintf(os.Stderr, "patch-gotypes: "+format+"\n", args...)
318 os.Exit(1)
319 }
320