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 patchErrorString(dstDir)
74 patchExportAccess(dstDir)
75 patchScopeHashPrefix(dstDir)
76
77 fmt.Fprintf(os.Stderr, "patched go/types in %s\n", dstDir)
78 }
79
80 func patchPredicates(dir string) {
81 f := filepath.Join(dir, "predicates.go")
82 src := mustRead(f)
83
84 // Patch 1: Add isByteSlice helper after isString.
85 src = mustReplace(f, src,
86 "func isString(t Type) bool { return isBasic(t, IsString) }\n",
87 `func isString(t Type) bool { return isBasic(t, IsString) }
88 func isByteSlice(t Type) bool {
89 if s, ok := under(t).(*Slice); ok {
90 if b, ok := s.elem.(*Basic); ok && b.kind == Byte {
91 return true
92 }
93 }
94 return false
95 }
96 `)
97
98 // Patch 2: Add slice comparability — any slice of comparable elements is comparable.
99 src = mustReplace(f, src,
100 "\tdefault:\n\t\treturn typeErrorf(\"\")\n\t}\n\n\treturn nil\n}",
101 "\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}")
102
103 mustWrite(f, src)
104 }
105
106 func patchOperand(dir string) {
107 f := filepath.Join(dir, "operand.go")
108 src := mustRead(f)
109
110 // Patch 3: string↔[]byte mutual assignability.
111 src = mustReplace(f, src,
112 "\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.",
113 "\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.")
114
115 mustWrite(f, src)
116 }
117
118 func patchExpr(dir string) {
119 f := filepath.Join(dir, "expr.go")
120 src := mustRead(f)
121
122 // Patch 4: Untyped string → []byte implicit conversion.
123 // For constants, return Typ[String] (not []byte) because slices aren't
124 // valid constant types. The assignableTo patch handles string→[]byte.
125 src = mustReplace(f, src,
126 "\tswitch u := under(target).(type) {\n\tcase *Basic:\n\t\tif x.mode == constant_ {",
127 "\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_ {")
128
129 // Patch 7: Allow | on matching slice types (slice concatenation).
130 // Also allow | when either operand is a string (string=[]byte unification).
131 // After matchTypes + patch 4, an untyped string constant paired with []byte
132 // becomes typed String (not []byte, since constants cannot be slice-typed),
133 // so we must detect slice-ness on either side.
134 src = mustReplace(f, src,
135 "\tif !check.op(binaryOpPredicates, x, op) {\n\t\tx.mode = invalid\n\t\treturn\n\t}",
136 "\t// Moxie: | on matching slice types is concatenation.\n\t_, mxSliceX := under(x.typ).(*Slice)\n\t_, mxSliceY := under(y.typ).(*Slice)\n\tmxSliceConcat := mxSliceX || mxSliceY || isString(x.typ) || isString(y.typ)\n\tif !(op == token.OR && mxSliceConcat) && !check.op(binaryOpPredicates, x, op) {\n\t\tx.mode = invalid\n\t\treturn\n\t}")
137
138 // Patch 12: allow untyped string ↔ []byte conversion in matchTypes.mayConvert.
139 // Without this, `"lit" | boundary` (where boundary is []byte) fails because
140 // mayConvert short-circuits on allString mismatch, leaving the mixed-kind
141 // operands for the binary-op predicate check which then rejects them.
142 src = mustReplace(f, src,
143 "\t\t// A string type can only convert to another string type.\n\t\tif allString(x.typ) != allString(y.typ) {\n\t\t\treturn false\n\t\t}",
144 "\t\t// A string type can only convert to another string type.\n\t\tif allString(x.typ) != allString(y.typ) {\n\t\t\t// Moxie: string and []byte unify — allow conversion in either direction.\n\t\t\tif allString(x.typ) && isByteSlice(y.typ) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif isByteSlice(x.typ) && allString(y.typ) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn false\n\t\t}")
145
146 mustWrite(f, src)
147 }
148
149 func patchSizes(dir string) {
150 f := filepath.Join(dir, "sizes.go")
151 src := mustRead(f)
152
153 // Patch 5: String size = 3 words (ptr, len, cap — matching []byte layout).
154 src = mustReplace(f, src,
155 "\t\tif k == String {\n\t\t\treturn s.WordSize * 2\n\t\t}",
156 "\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}")
157
158 mustWrite(f, src)
159 }
160
161 func patchDecl(dir string) {
162 f := filepath.Join(dir, "decl.go")
163 src := mustRead(f)
164
165 // Patch 6: Allow []byte as a valid constant type (string=[]byte unification).
166 // With string=[]byte, named types like `type X []byte` should accept string constants.
167 src = mustReplace(f, src,
168 "\t\tif !isConstType(t) {",
169 "\t\tif !isConstType(t) && !isByteSlice(t) {")
170
171 mustWrite(f, src)
172 }
173
174 func patchRecording(dir string) {
175 f := filepath.Join(dir, "recording.go")
176 src := mustRead(f)
177
178 // Patch 11: Allow []byte as a valid constant type in type recording.
179 // With string=[]byte, string constants may have []byte type.
180 src = mustReplace(f, src,
181 "assert(!isValid(typ) || allBasic(typ, IsConstType))",
182 "assert(!isValid(typ) || allBasic(typ, IsConstType) || isByteSlice(typ))")
183
184 mustWrite(f, src)
185 }
186
187 func patchIdentical(dir string) {
188 f := filepath.Join(dir, "predicates.go")
189 src := mustRead(f)
190
191 // Patch 9: string ↔ []byte identity in comparer.identical.
192 // Makes Identical(string, []byte) true, propagating through all composite
193 // types: []string = [][]byte, map[string]V = map[[]byte]V, etc.
194 src = mustReplace(f, src,
195 "\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}",
196 "\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}")
197
198 mustWrite(f, src)
199 }
200
201 func patchUnify(dir string) {
202 f := filepath.Join(dir, "unify.go")
203 src := mustRead(f)
204
205 // Patch 10: string ↔ []byte unification in the type unifier.
206 // Generic constraint satisfaction uses unify(), not Identical().
207 src = mustReplace(f, src,
208 "\t// nothing to do if x == y\n\tif x == y || Unalias(x) == Unalias(y) {\n\t\treturn true\n\t}",
209 "\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}")
210
211 mustWrite(f, src)
212 }
213
214 func patchTypeTerm(dir string) {
215 f := filepath.Join(dir, "typeterm.go")
216 src := mustRead(f)
217
218 // Patch 8: Generic constraint satisfaction — []byte satisfies ~string.
219 // term.includes checks Identical(x.typ, under(t)). With string=[]byte,
220 // []byte must satisfy ~string in constraint unions like cmp.Ordered.
221 src = mustReplace(f, src,
222 "\treturn Identical(x.typ, u)\n}",
223 "\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}")
224
225 mustWrite(f, src)
226 }
227
228 func patchSpawnBuiltin(dir string) {
229 // Patch universe.go: add _Spawn builtin ID and predeclaredFuncs entry.
230 f := filepath.Join(dir, "universe.go")
231 src := mustRead(f)
232
233 // Add _Spawn after _Recover in the builtinId enum.
234 src = mustReplace(f, src,
235 "\t_Recover\n\n\t// package unsafe",
236 "\t_Recover\n\t_Spawn\n\n\t// package unsafe")
237
238 // Add spawn entry in predeclaredFuncs after _Recover.
239 // nargs=1 (minimum: the function), variadic=true, statement-kind so the
240 // call can be used as an expression statement (its primary use) while
241 // still allowing `ch := spawn(...)` to consume the lifecycle handle.
242 src = mustReplace(f, src,
243 "\t_Recover: {\"recover\", 0, false, statement},",
244 "\t_Recover: {\"recover\", 0, false, statement},\n\t_Spawn: {\"spawn\", 1, true, statement},")
245
246 mustWrite(f, src)
247
248 // Patch builtins.go: add type-checking handler for spawn.
249 f = filepath.Join(dir, "builtins.go")
250 src = mustRead(f)
251
252 // Insert spawn handler after the _Recover case, before _Add.
253 src = mustReplace(f, src,
254 "\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:",
255 ` case _Recover:
256 // recover() interface{}
257 x.mode = value
258 x.typ = &emptyInterface
259 if check.recordTypes() {
260 check.recordBuiltinType(call.Fun, makeSig(x.typ))
261 }
262
263 case _Spawn:
264 // spawn(fn, args...) chan struct{}
265 // spawn("transport", fn, args...) chan struct{}
266 // First argument must be a function or a transport string constant.
267 // Remaining arguments are validated by the compiler at SSA level
268 // (must implement moxie.Codec, channels must have Codec element types).
269 if nargs < 1 {
270 return
271 }
272 if _, ok := under(x.typ).(*Signature); !ok {
273 if !isString(x.typ) {
274 check.errorf(x, InvalidCall, invalidOp+"first argument to spawn must be a function or transport string, got %s", x)
275 return
276 }
277 // Transport string — second arg must be a function.
278 if nargs < 2 {
279 check.errorf(x, WrongArgCount, invalidOp+"spawn with transport string requires a function argument")
280 return
281 }
282 if _, ok := under(args[1].typ).(*Signature); !ok {
283 check.errorf(args[1], InvalidCall, invalidOp+"second argument to spawn must be a function when first is a transport string, got %s", args[1])
284 return
285 }
286 }
287 x.mode = value
288 x.typ = NewChan(SendRecv, NewStruct(nil, nil))
289 // Synthesize a signature for the spawn ident so the SSA builder
290 // can construct a *Builtin with a *types.Signature when it visits
291 // the call. Without this, fn.instanceType(e).(*types.Signature)
292 // at builder.go:814 panics: the universe entry has no signature.
293 if check.recordTypes() {
294 params := make([]Type, nargs)
295 for i, a := range args {
296 params[i] = a.typ
297 }
298 check.recordBuiltinType(call.Fun, makeSig(x.typ, params...))
299 }
300
301 case _Add:`)
302
303 mustWrite(f, src)
304 }
305
306 func patchErrorString(dir string) {
307 f := filepath.Join(dir, "universe.go")
308 src := mustRead(f)
309
310 // Add String() method to the error interface alongside Error().
311 // Moxie stdlib calls err.String() everywhere; error must satisfy stringer.
312 src = mustReplace(f, src,
313 "\t\t// interface{ Error() string }\n\t\tityp := &Interface{methods: []*Func{err}, complete: true}",
314 "\t\t// Moxie: error also has String() so it satisfies stringer.\n\t\tstrFn := NewFunc(nopos, nil, \"String\", sig)\n\n\t\t// interface{ Error() string; String() string }\n\t\tityp := &Interface{methods: []*Func{err, strFn}, complete: true}")
315
316 mustWrite(f, src)
317 }
318
319 func mustRead(path string) string {
320 data, err := os.ReadFile(path)
321 if err != nil {
322 fatal("read %s: %v", path, err)
323 }
324 return string(data)
325 }
326
327 func mustWrite(path string, content string) {
328 if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
329 fatal("write %s: %v", path, err)
330 }
331 }
332
333 func mustReplace(file, src, old, new string) string {
334 if !strings.Contains(src, old) {
335 fatal("patch target not found in %s:\n---\n%s\n---", file, old)
336 }
337 count := strings.Count(src, old)
338 if count != 1 {
339 fatal("patch target appears %d times in %s (expected 1)", count, file)
340 }
341 return strings.Replace(src, old, new, 1)
342 }
343
344 func patchScopeHashPrefix(dir string) {
345 f := filepath.Join(dir, "scope.go")
346 src := mustRead(f)
347
348 // Add stripHash helper before Lookup.
349 src = mustReplace(f, src,
350 "func (s *Scope) Lookup(name string) Object {\n\tobj := resolve(name, s.elems[name])",
351 "// stripHash removes a leading # from a Moxie export-annotated name.\n// #foo and foo are the same identifier; # is an annotation, not part of the name.\nfunc stripHash(name string) string {\n\tif len(name) > 0 && name[0] == '#' {\n\t\treturn name[1:]\n\t}\n\treturn name\n}\n\nfunc (s *Scope) Lookup(name string) Object {\n\tname = stripHash(name)\n\tobj := resolve(name, s.elems[name])")
352
353 // Patch insert to strip # from key.
354 src = mustReplace(f, src,
355 "func (s *Scope) insert(name string, obj Object) {\n\tif s.elems == nil {\n\t\ts.elems = make(map[string]Object)\n\t}\n\ts.elems[name] = obj\n}",
356 "func (s *Scope) insert(name string, obj Object) {\n\tname = stripHash(name)\n\tif s.elems == nil {\n\t\ts.elems = make(map[string]Object)\n\t}\n\ts.elems[name] = obj\n}")
357
358 // Patch Insert to strip # from name for lookup.
359 src = mustReplace(f, src,
360 "func (s *Scope) Insert(obj Object) Object {\n\tname := obj.Name()\n\tif alt := s.Lookup(name); alt != nil {",
361 "func (s *Scope) Insert(obj Object) Object {\n\tname := stripHash(obj.Name())\n\tif alt := s.Lookup(name); alt != nil {")
362
363 mustWrite(f, src)
364 }
365
366 func patchExportAccess(dir string) {
367 f := filepath.Join(dir, "call.go")
368 src := mustRead(f)
369
370 // Moxie: all identifiers are accessible cross-package.
371 // Capital/# prefix marks stable API, not access control.
372 // Remove the unexported name error so pkg.lowercaseName compiles.
373 src = mustReplace(f, src,
374 "\t\t\t\tif !exp.Exported() {\n\t\t\t\t\tcheck.errorf(e.Sel, UnexportedName, \"name %s not exported by package %s\", sel, pkg.name)\n\t\t\t\t\t// ok to continue\n\t\t\t\t}",
375 "\t\t\t\t// Moxie: all identifiers accessible cross-package.\n\t\t\t\t// Capital/# prefix marks stable API contract, not access control.\n\t\t\t\t_ = exp.Exported()")
376
377 mustWrite(f, src)
378 }
379
380 func fatal(format string, args ...any) {
381 fmt.Fprintf(os.Stderr, "patch-gotypes: "+format+"\n", args...)
382 os.Exit(1)
383 }
384