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