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