main.mx raw

   1  package main
   2  
   3  import (
   4  	"os"
   5  	"unsafe"
   6  )
   7  
   8  //export mxc_run
   9  func cRun(argv unsafe.Pointer, argc int32) int32
  10  
  11  //export mxc_getenv
  12  func cGetenv(name unsafe.Pointer, nameLen int32, buf unsafe.Pointer, bufCap int32) int32
  13  
  14  //export mxc_listdir
  15  func cListdir(dir unsafe.Pointer, dirLen int32, buf unsafe.Pointer, bufCap int32) int32
  16  
  17  //export write
  18  func cWrite(fd int32, buf unsafe.Pointer, count uint32) int32
  19  
  20  //export mxc_writefile
  21  func cWritefile(path unsafe.Pointer, pathLen int32, data unsafe.Pointer, dataLen int32, mode uint32) int32
  22  
  23  //export mxc_mkdir
  24  func cMkdir(path unsafe.Pointer, pathLen int32, mode uint32) int32
  25  
  26  //export mxc_readfile
  27  func cReadfile(path unsafe.Pointer, pathLen int32, buf unsafe.Pointer, bufCap int32) int32
  28  
  29  //export mxc_filesize
  30  func cFilesize(path unsafe.Pointer, pathLen int32) int32
  31  
  32  func writeStr(fd int32, s string) {
  33  	b := []byte(s)
  34  	if len(b) > 0 {
  35  		cWrite(fd, unsafe.Pointer(unsafe.SliceData(b)), uint32(len(b)))
  36  	}
  37  }
  38  
  39  func writeFile(path string, data []byte, mode uint32) {
  40  	if len(path) == 0 {
  41  		return
  42  	}
  43  	cWritefile(unsafe.Pointer(unsafe.SliceData([]byte(path))), int32(len(path)),
  44  		unsafe.Pointer(unsafe.SliceData(data)), int32(len(data)), mode)
  45  }
  46  
  47  func mkdirAll(path string, mode uint32) {
  48  	if len(path) == 0 {
  49  		return
  50  	}
  51  	cMkdir(unsafe.Pointer(unsafe.SliceData([]byte(path))), int32(len(path)), mode)
  52  }
  53  
  54  func readFile(path string) ([]byte, bool) {
  55  	if len(path) == 0 {
  56  		return nil, false
  57  	}
  58  	pb := []byte(path)
  59  	sz := cFilesize(unsafe.Pointer(unsafe.SliceData(pb)), int32(len(pb)))
  60  	if sz < 0 {
  61  		return nil, false
  62  	}
  63  	if sz == 0 {
  64  		sz = 65536
  65  	}
  66  	buf := []byte{:sz}
  67  	n := cReadfile(unsafe.Pointer(unsafe.SliceData(pb)), int32(len(pb)),
  68  		unsafe.Pointer(unsafe.SliceData(buf)), sz)
  69  	if n < 0 {
  70  		return nil, false
  71  	}
  72  	return buf[:n], true
  73  }
  74  
  75  func fatal(msg string) {
  76  	writeStr(2, "mxc: " | msg | "\n")
  77  	os.Exit(1)
  78  }
  79  
  80  func getenv(name string) string {
  81  	nb := []byte(name)
  82  	buf := []byte{:4096}
  83  	n := cGetenv(unsafe.Pointer(unsafe.SliceData(nb)), int32(len(nb)), unsafe.Pointer(unsafe.SliceData(buf)), int32(len(buf)))
  84  	if n <= 0 {
  85  		return ""
  86  	}
  87  	return string(buf[:n])
  88  }
  89  
  90  func getArgs() []string {
  91  	data, ok := readFile("/proc/self/cmdline")
  92  	if !ok {
  93  		return nil
  94  	}
  95  	var args []string
  96  	start := int32(0)
  97  	for i := int32(0); i < int32(len(data)); i++ {
  98  		if data[i] == 0 {
  99  			if i > start {
 100  				args = append(args, string(data[start:i]))
 101  			}
 102  			start = i + 1
 103  		}
 104  	}
 105  	if start < int32(len(data)) {
 106  		args = append(args, string(data[start:]))
 107  	}
 108  	return args
 109  }
 110  
 111  func listFiles(dir, ext string) []string {
 112  	db := []byte(dir)
 113  	buf := []byte{:65536}
 114  	n := cListdir(unsafe.Pointer(unsafe.SliceData(db)), int32(len(db)),
 115  		unsafe.Pointer(unsafe.SliceData(buf)), int32(len(buf)))
 116  	if n <= 0 {
 117  		return nil
 118  	}
 119  	var result []string
 120  	start := int32(0)
 121  	for i := int32(0); i < n; i++ {
 122  		if buf[i] == 0 {
 123  			name := string(buf[start:i])
 124  			if hasSuffix(name, ext) {
 125  				result = append(result, name)
 126  			}
 127  			start = i + 1
 128  		}
 129  	}
 130  	return result
 131  }
 132  
 133  func run(args []string) int32 {
 134  	argv := []unsafe.Pointer{:len(args) + 1}
 135  	cstrs := [][]byte{:len(args)}
 136  	for i, a := range args {
 137  		b := []byte{:len(a) + 1}
 138  		copy(b, a)
 139  		b[len(a)] = 0
 140  		cstrs[i] = b
 141  		argv[i] = unsafe.Pointer(unsafe.SliceData(b))
 142  	}
 143  	return cRun(unsafe.Pointer(unsafe.SliceData(argv)), int32(len(args)))
 144  }
 145  
 146  func splitLines(s string) []string {
 147  	var result []string
 148  	start := int32(0)
 149  	for i := int32(0); i < int32(len(s)); i++ {
 150  		if s[i] == '\n' {
 151  			if i > start {
 152  				result = append(result, s[start:i])
 153  			}
 154  			start = i + 1
 155  		}
 156  	}
 157  	if start < int32(len(s)) {
 158  		result = append(result, s[start:])
 159  	}
 160  	return result
 161  }
 162  
 163  func hasPrefix(s, prefix string) bool {
 164  	return len(s) >= len(prefix) && s[:len(prefix)] == prefix
 165  }
 166  
 167  func hasSuffix(s, suffix string) bool {
 168  	return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
 169  }
 170  
 171  func trimSpace(s string) string {
 172  	i := int32(0)
 173  	for i < int32(len(s)) && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r') {
 174  		i++
 175  	}
 176  	j := int32(len(s))
 177  	for j > i && (s[j-1] == ' ' || s[j-1] == '\t' || s[j-1] == '\n' || s[j-1] == '\r') {
 178  		j--
 179  	}
 180  	return s[i:j]
 181  }
 182  
 183  func joinPath(a, b string) string {
 184  	if len(a) == 0 {
 185  		return b
 186  	}
 187  	if a[len(a)-1] == '/' {
 188  		return a | b
 189  	}
 190  	return a | "/" | b
 191  }
 192  
 193  func pathBase(path string) string {
 194  	for i := int32(len(path)) - 1; i >= 0; i-- {
 195  		if path[i] == '/' {
 196  			return path[i+1:]
 197  		}
 198  	}
 199  	return path
 200  }
 201  
 202  func joinStrings(ss []string, sep string) string {
 203  	if len(ss) == 0 {
 204  		return ""
 205  	}
 206  	n := int32(0)
 207  	for _, s := range ss {
 208  		n += int32(len(s))
 209  	}
 210  	n += int32(len(sep)) * (int32(len(ss)) - 1)
 211  	buf := []byte{:0:n}
 212  	for i, s := range ss {
 213  		if i > 0 {
 214  			buf = append(buf, sep...)
 215  		}
 216  		buf = append(buf, s...)
 217  	}
 218  	return string(buf)
 219  }
 220  
 221  func appendUniq(ss []string, s string) []string {
 222  	for _, x := range ss {
 223  		if x == s {
 224  			return ss
 225  		}
 226  	}
 227  	return append(ss, s)
 228  }
 229  
 230  func fnvHash(data []byte) string {
 231  	h := uint32(2166136261)
 232  	for i := int32(0); i < int32(len(data)); i++ {
 233  		h ^= uint32(data[i])
 234  		h *= uint32(16777619)
 235  	}
 236  	hex := "0123456789abcdef"
 237  	buf := []byte{:8}
 238  	for i := int32(7); i >= 0; i-- {
 239  		buf[i] = hex[h&0xf]
 240  		h >>= 4
 241  	}
 242  	return string(buf)
 243  }
 244  
 245  func pkgCacheDir() string {
 246  	home := getenv("HOME")
 247  	if home == "" {
 248  		home = "/tmp"
 249  	}
 250  	return joinPath(home, ".cache/moxie/pkg")
 251  }
 252  
 253  func pkgCacheKey(dir string, files []string) string {
 254  	var sorted []string
 255  	for _, f := range files {
 256  		sorted = append(sorted, f)
 257  	}
 258  	for i := int32(1); i < int32(len(sorted)); i++ {
 259  		for j := i; j > 0 && sorted[j] < sorted[j-1]; j-- {
 260  			sorted[j], sorted[j-1] = sorted[j-1], sorted[j]
 261  		}
 262  	}
 263  	var all []byte
 264  	for _, f := range sorted {
 265  		p := joinPath(dir, f)
 266  		data, ok := readFile(p)
 267  		if !ok {
 268  			continue
 269  		}
 270  		all = append(all, data...)
 271  		all = append(all, 0)
 272  	}
 273  	return fnvHash(all)
 274  }
 275  
 276  func cachedBcPath(cacheBase, pkgPath, hash string) string {
 277  	return joinPath(joinPath(cacheBase, safeName(pkgPath)), hash | ".bc")
 278  }
 279  
 280  func isStdlib(path string) bool {
 281  	return !hasPrefix(path, "/") && !hasPrefix(path, "./") && !hasPrefix(path, "../")
 282  }
 283  
 284  var builtinOnly []string
 285  
 286  func isBuiltinOnly(path string) bool {
 287  	for _, b := range builtinOnly {
 288  		if path == b {
 289  			return true
 290  		}
 291  	}
 292  	return false
 293  }
 294  
 295  func loadSysroot(path string) {
 296  	data, ok := readFile(path)
 297  	if !ok {
 298  		return
 299  	}
 300  	for _, line := range splitLines(string(data)) {
 301  		line = trimSpace(line)
 302  		if len(line) == 0 || line[0] == '#' {
 303  			continue
 304  		}
 305  		eq := int32(-1)
 306  		for i := int32(0); i < int32(len(line)); i++ {
 307  			if line[i] == '=' {
 308  				eq = i
 309  				break
 310  			}
 311  		}
 312  		if eq < 0 {
 313  			continue
 314  		}
 315  		key := line[:eq]
 316  		val := line[eq+1:]
 317  		os.Setenv(key, val)
 318  	}
 319  }
 320  
 321  func splitBySpace(s string) []string {
 322  	var result []string
 323  	start := int32(0)
 324  	inWord := false
 325  	for i := int32(0); i < int32(len(s)); i++ {
 326  		if s[i] == ' ' || s[i] == '\t' {
 327  			if inWord {
 328  				result = append(result, s[start:i])
 329  				inWord = false
 330  			}
 331  		} else {
 332  			if !inWord {
 333  				start = i
 334  				inWord = true
 335  			}
 336  		}
 337  	}
 338  	if inWord {
 339  		result = append(result, s[start:])
 340  	}
 341  	return result
 342  }
 343  
 344  func safeName(pkg string) string {
 345  	b := []byte{:len(pkg)}
 346  	for i := int32(0); i < int32(len(pkg)); i++ {
 347  		if pkg[i] == '/' {
 348  			b[i] = '_'
 349  		} else {
 350  			b[i] = pkg[i]
 351  		}
 352  	}
 353  	return string(b)
 354  }
 355  
 356  func extractPkgName(data []byte) string {
 357  	for i := int32(0); i < int32(len(data)); i++ {
 358  		if i+8 < int32(len(data)) && string(data[i:i+8]) == "package " {
 359  			j := i + 8
 360  			for j < int32(len(data)) && data[j] != '\n' && data[j] != '\r' && data[j] != ' ' {
 361  				j++
 362  			}
 363  			return string(data[i+8 : j])
 364  		}
 365  		if data[i] == '\n' {
 366  			continue
 367  		}
 368  		if data[i] == '/' && i+1 < int32(len(data)) && data[i+1] == '/' {
 369  			for i < int32(len(data)) && data[i] != '\n' {
 370  				i++
 371  			}
 372  			continue
 373  		}
 374  		break
 375  	}
 376  	return "main"
 377  }
 378  
 379  func extractImports(data []byte) []string {
 380  	var imports []string
 381  	lines := splitLines(string(data))
 382  	inBlock := false
 383  	for _, line := range lines {
 384  		line = trimSpace(line)
 385  		if line == "import (" {
 386  			inBlock = true
 387  			continue
 388  		}
 389  		if inBlock && line == ")" {
 390  			inBlock = false
 391  			continue
 392  		}
 393  		if inBlock {
 394  			imp := extractQuoted(line)
 395  			if imp != "" {
 396  				imports = appendUniq(imports, imp)
 397  			}
 398  		}
 399  		if hasPrefix(line, "import \"") {
 400  			imp := extractQuoted(line[7:])
 401  			if imp != "" {
 402  				imports = appendUniq(imports, imp)
 403  			}
 404  		}
 405  	}
 406  	return imports
 407  }
 408  
 409  func extractQuoted(s string) string {
 410  	start := int32(-1)
 411  	for i := int32(0); i < int32(len(s)); i++ {
 412  		if s[i] == '"' {
 413  			if start < 0 {
 414  				start = i + 1
 415  			} else {
 416  				return s[start:i]
 417  			}
 418  		}
 419  	}
 420  	return ""
 421  }
 422  
 423  func concatSources(dir string, files []string) []byte {
 424  	imports := map[string]bool{}
 425  	var bodies [][]byte
 426  	pkgName := ""
 427  	for _, f := range files {
 428  		data, ok := readFile(joinPath(dir, f))
 429  		if !ok {
 430  			continue
 431  		}
 432  		if pkgName == "" {
 433  			pkgName = extractPkgName(data)
 434  		}
 435  		lines := splitLines(string(data))
 436  		var body []string
 437  		i := int32(0)
 438  		for i < int32(len(lines)) {
 439  			line := trimSpace(lines[i])
 440  			if hasPrefix(line, "package ") {
 441  				i++
 442  				continue
 443  			}
 444  			if line == "import (" {
 445  				i++
 446  				for i < int32(len(lines)) {
 447  					imp := trimSpace(lines[i])
 448  					i++
 449  					if imp == ")" {
 450  						break
 451  					}
 452  					if imp != "" {
 453  						imports[imp] = true
 454  					}
 455  				}
 456  				continue
 457  			}
 458  			if hasPrefix(line, "import ") && !hasPrefix(line, "import (") {
 459  				imp := line[7:]
 460  				imports[imp] = true
 461  				i++
 462  				continue
 463  			}
 464  			body = append(body, lines[i])
 465  			i++
 466  		}
 467  		bodies = append(bodies, []byte(joinStrings(body, "\n")))
 468  	}
 469  	var out []byte
 470  	out = append(out, ("package " | pkgName | "\n")...)
 471  	if len(imports) > 0 {
 472  		var impKeys []string
 473  		for imp := range imports {
 474  			impKeys = append(impKeys, imp)
 475  		}
 476  		for i := int32(1); i < int32(len(impKeys)); i++ {
 477  			for j := i; j > 0 && impKeys[j] < impKeys[j-1]; j-- {
 478  				impKeys[j], impKeys[j-1] = impKeys[j-1], impKeys[j]
 479  			}
 480  		}
 481  		out = append(out, "import (\n"...)
 482  		for _, imp := range impKeys {
 483  			out = append(out, '\t')
 484  			out = append(out, imp...)
 485  			out = append(out, '\n')
 486  		}
 487  		out = append(out, ")\n"...)
 488  	}
 489  	for _, b := range bodies {
 490  		out = append(out, b...)
 491  		out = append(out, '\n')
 492  	}
 493  	return out
 494  }
 495  
 496  func fileContainsPart(base string, part string) bool {
 497  	plen := len(part)
 498  	for i := 0; i <= len(base)-plen; i++ {
 499  		if base[i:i+plen] == part {
 500  			before := i == 0 || base[i-1] == '_'
 501  			after := i+plen == len(base) || base[i+plen] == '_'
 502  			if before && after {
 503  				return true
 504  			}
 505  		}
 506  	}
 507  	return false
 508  }
 509  
 510  func shouldSkipFile(name string) bool {
 511  	if hasSuffix(name, "_test.go") || hasSuffix(name, "_test.mx") {
 512  		return true
 513  	}
 514  	base := name
 515  	if hasSuffix(base, ".go") {
 516  		base = base[:len(base)-3]
 517  	} else if hasSuffix(base, ".mx") {
 518  		base = base[:len(base)-3]
 519  	}
 520  	archSkip := []string{"arm64"}
 521  	for _, a := range archSkip {
 522  		if fileContainsPart(base, a) {
 523  			return true
 524  		}
 525  	}
 526  	suffixSkip := []string{"_wasm"}
 527  	for _, s := range suffixSkip {
 528  		if hasSuffix(base, s) {
 529  			return true
 530  		}
 531  	}
 532  	exact := []string{"pipe", "multi", "scan", "reader", "replace", "search",
 533  		"ioutil"}
 534  	for _, s := range exact {
 535  		if base == s {
 536  			return true
 537  		}
 538  	}
 539  	return false
 540  }
 541  
 542  func hasBuildConstraint(data []byte, forbidden []string) bool {
 543  	for i := int32(0); i < int32(len(data)) && i < 512; i++ {
 544  		if data[i] == 'p' {
 545  			return false
 546  		}
 547  		if i+11 < int32(len(data)) && string(data[i:i+11]) == "//go:build " {
 548  			var buf []byte
 549  			for j := i + 11; j < int32(len(data)) && data[j] != '\n'; j++ {
 550  				buf = append(buf, data[j])
 551  			}
 552  			line := string(buf)
 553  			for _, f := range forbidden {
 554  				if line == f || hasPrefix(line, f | " ") || hasPrefix(line, f | "\t") {
 555  					return true
 556  				}
 557  			}
 558  			return false
 559  		}
 560  		if data[i] == '\n' {
 561  			continue
 562  		}
 563  		if data[i] == '/' && i+1 < int32(len(data)) && data[i+1] == '/' {
 564  			for i < int32(len(data)) && data[i] != '\n' {
 565  				i++
 566  			}
 567  			continue
 568  		}
 569  	}
 570  	return false
 571  }
 572  
 573  func hasImport(data []byte, pkg string) bool {
 574  	target := "\"" | pkg | "\""
 575  	for i := int32(0); i <= int32(len(data))-int32(len(target)); i++ {
 576  		if string(data[i:i+int32(len(target))]) == target {
 577  			return true
 578  		}
 579  	}
 580  	return false
 581  }
 582  
 583  type pkgInfo struct {
 584  	path    string
 585  	name    string
 586  	dir     string
 587  	files   []string
 588  	cfiles  []string
 589  	imports []string
 590  }
 591  
 592  func discoverPkg(pkgPath, root string) *pkgInfo {
 593  	var dir string
 594  	if hasPrefix(pkgPath, "/") || hasPrefix(pkgPath, "./") || hasPrefix(pkgPath, "../") {
 595  		dir = pkgPath
 596  	} else {
 597  		dir = joinPath(root, joinPath("src", pkgPath))
 598  	}
 599  	goFiles := listFiles(dir, ".go")
 600  	mxFiles := listFiles(dir, ".mx")
 601  	cFiles := listFiles(dir, ".c")
 602  	allMx := append(mxFiles, goFiles...)
 603  	forbidden := []string{"wasm", "js", "ignore", "compiler_bootstrap",
 604  		"scheduler.tasks", "scheduler.cores", "scheduler.asyncify",
 605  		"faketime", "baremetal", "nintendoswitch"}
 606  	var files []string
 607  	for _, f := range allMx {
 608  		if shouldSkipFile(f) {
 609  			continue
 610  		}
 611  		data, ok := readFile(joinPath(dir, f))
 612  		if !ok {
 613  			continue
 614  		}
 615  		bc := hasBuildConstraint(data, forbidden)
 616  		if bc {
 617  			writeStr(2, "    skip-constraint: " | f | "\n")
 618  			continue
 619  		}
 620  		fileImps := extractImports(data)
 621  		skipIter := false
 622  		for _, imp := range fileImps {
 623  			if imp == "iter" {
 624  				skipIter = true
 625  				break
 626  			}
 627  		}
 628  		if skipIter {
 629  			writeStr(2, "    skip-iter-import: " | f | "\n")
 630  			continue
 631  		}
 632  		writeStr(2, "    include: " | f | "\n")
 633  		files = append(files, f)
 634  	}
 635  	if len(files) == 0 {
 636  		return nil
 637  	}
 638  	var allImports []string
 639  	pkgName := ""
 640  	for _, f := range files {
 641  		data, ok := readFile(joinPath(dir, f))
 642  		if !ok {
 643  			continue
 644  		}
 645  		if pkgName == "" {
 646  			pkgName = extractPkgName(data)
 647  		}
 648  		for _, imp := range extractImports(data) {
 649  			allImports = appendUniq(allImports, imp)
 650  		}
 651  	}
 652  	writeStr(2, "  discover " | pkgPath | " [")
 653  	for fi, ff := range files {
 654  		if fi > 0 {
 655  			writeStr(2, ", ")
 656  		}
 657  		writeStr(2, ff)
 658  	}
 659  	writeStr(2, "]\n")
 660  	return &pkgInfo{
 661  		path:    pkgPath,
 662  		name:    pkgName,
 663  		dir:     dir,
 664  		files:   files,
 665  		cfiles:  cFiles,
 666  		imports: allImports,
 667  	}
 668  }
 669  
 670  type resolver struct {
 671  	visited map[string]bool
 672  	order   []*pkgInfo
 673  	root    string
 674  }
 675  
 676  func (r *resolver) walk(path string) {
 677  	if r.visited[path] {
 678  		return
 679  	}
 680  	r.visited[path] = true
 681  	if isBuiltinOnly(path) {
 682  		return
 683  	}
 684  	pkg := discoverPkg(path, r.root)
 685  	if pkg == nil {
 686  		return
 687  	}
 688  	for _, imp := range pkg.imports {
 689  		r.walk(imp)
 690  	}
 691  	r.order = append(r.order, pkg)
 692  }
 693  
 694  func resolveAll(rootPkg string, root string) []*pkgInfo {
 695  	r := &resolver{
 696  		visited: map[string]bool{},
 697  		root:    root,
 698  	}
 699  	r.walk(rootPkg)
 700  	return r.order
 701  }
 702  
 703  func registerBuiltins() {
 704  	initUniverse()
 705  	ensureImportRegistry()
 706  	importRegistry = map[string]*TCPackage{}
 707  
 708  	// unsafe - compiler builtin, no source
 709  	unsafePkg := NewTCPackage("unsafe", "unsafe")
 710  	unsafePkg.Scope().Insert(NewTypeName(unsafePkg, "Pointer", Typ[UnsafePointer]))
 711  	unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "Sizeof", parseSignatureDesc("interface{}->int32")))
 712  	unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "Offsetof", parseSignatureDesc("interface{}->int32")))
 713  	unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "Alignof", parseSignatureDesc("interface{}->int32")))
 714  	unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "Slice", parseSignatureDesc("ptr,int32->[]uint8")))
 715  	unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "SliceData", parseSignatureDesc("[]uint8->ptr")))
 716  	importRegistry["unsafe"] = unsafePkg
 717  
 718  	// moxie - spawn/codec runtime, special handling
 719  	importRegistry["moxie"] = NewTCPackage("moxie", "moxie")
 720  
 721  	// runtime - assembly + C, pre-compiled in runtime.bc
 722  	rtPkg := NewTCPackage("runtime", "runtime")
 723  	rtPkg.Scope().Insert(NewTCFunc(rtPkg, "InitCShared", parseSignatureDesc("")))
 724  	importRegistry["runtime"] = rtPkg
 725  }
 726  
 727  func addPtrMethod(pkg *TCPackage, named *Named, name, sigDesc string) {
 728  	var sig *Signature
 729  	if sigDesc == "" {
 730  		sig = NewSignature(nil, nil, nil, false)
 731  	} else {
 732  		sig = parseSignatureDesc(sigDesc)
 733  	}
 734  	fn := NewTCFunc(pkg, name, sig)
 735  	fn.hasPtrRecv = true
 736  	named.AddMethod(fn)
 737  }
 738  
 739  func compileSource(src []byte, pkgName, triple string) string {
 740  	ir := CompileToIR(src, pkgName, triple)
 741  	if len(ir) > 0 && ir[0] == ';' {
 742  		writeStr(2, ir)
 743  		fatal("compile error for " | pkgName)
 744  	}
 745  	if len(ir) == 0 {
 746  		fatal("empty IR for " | pkgName)
 747  	}
 748  	return ir
 749  }
 750  
 751  func removeAll(path string) {
 752  	entries := listFiles(path, "")
 753  	if entries == nil {
 754  		return
 755  	}
 756  	for _, e := range entries {
 757  		os.Remove(joinPath(path, e))
 758  	}
 759  	os.Remove(path)
 760  }
 761  
 762  func cacheClean() {
 763  	base := pkgCacheDir()
 764  	writeStr(2, "cleaning cache: " | base | "\n")
 765  	entries := listFiles(base, "")
 766  	if entries == nil {
 767  		writeStr(2, "  (empty)\n")
 768  		return
 769  	}
 770  	for _, e := range entries {
 771  		removeAll(joinPath(base, e))
 772  	}
 773  	os.Remove(base)
 774  	writeStr(2, "  done\n")
 775  }
 776  
 777  func main() {
 778  	builtinOnly = []string{"unsafe", "runtime", "moxie"}
 779  	args := getArgs()
 780  	if len(args) < 2 {
 781  		writeStr(2, "usage: mxc <build|version|cache> ...\n")
 782  		os.Exit(1)
 783  	}
 784  
 785  	cmd := args[1]
 786  	if cmd == "version" {
 787  		writeStr(1, "mxc 1.6.1\n")
 788  		return
 789  	}
 790  	if cmd == "cache" {
 791  		if len(args) >= 3 && args[2] == "clean" {
 792  			cacheClean()
 793  			return
 794  		}
 795  		writeStr(2, "usage: mxc cache clean\n")
 796  		os.Exit(1)
 797  	}
 798  	if cmd != "build" {
 799  		writeStr(2, "usage: mxc <build|version|cache> ...\n")
 800  		os.Exit(1)
 801  	}
 802  
 803  	outpath := ""
 804  	pkgPath := ""
 805  	printIR := false
 806  	forceRebuild := false
 807  	i := int32(2)
 808  	for i < int32(len(args)) {
 809  		if args[i] == "-o" && i+1 < int32(len(args)) {
 810  			outpath = args[i+1]
 811  			i += 2
 812  		} else if args[i] == "-print-ir" {
 813  			printIR = true
 814  			i++
 815  		} else if args[i] == "-a" {
 816  			forceRebuild = true
 817  			i++
 818  		} else {
 819  			pkgPath = args[i]
 820  			i++
 821  		}
 822  	}
 823  	if pkgPath == "" {
 824  		fatal("no package specified")
 825  	}
 826  	if outpath == "" {
 827  		outpath = pathBase(pkgPath)
 828  	}
 829  
 830  	root := getenv("MOXIEROOT")
 831  	if root == "" {
 832  		root = "."
 833  	}
 834  	triple := "x86_64-unknown-linux-musleabihf"
 835  	clang := "clang-21"
 836  
 837  	registerBuiltins()
 838  	pkgs := resolveAll(pkgPath, root)
 839  	if len(pkgs) == 0 {
 840  		fatal("no packages to compile")
 841  	}
 842  
 843  	tmpdir := "/tmp/mxc-build"
 844  	mkdirAll(tmpdir, 0755)
 845  
 846  	cacheBase := pkgCacheDir()
 847  	var allBcFiles []string
 848  
 849  	for _, pkg := range pkgs {
 850  		compilePath := pkg.path
 851  		if hasPrefix(compilePath, "/") || hasPrefix(compilePath, "./") || hasPrefix(compilePath, "../") {
 852  			compilePath = pkg.name
 853  		}
 854  
 855  		if isStdlib(pkg.path) {
 856  			hash := pkgCacheKey(pkg.dir, pkg.files)
 857  			cached := cachedBcPath(cacheBase, pkg.path, hash)
 858  			_, cacheOk := readFile(cached)
 859  			if cacheOk && !forceRebuild {
 860  				writeStr(2, "  cached " | pkg.path | "\n")
 861  				src := concatSources(pkg.dir, pkg.files)
 862  				writeStr(2, "    tc-start " | compilePath | " srcLen=" | simpleItoa(len(src)) | "\n")
 863  				TypeCheckOnly(src, compilePath)
 864  				writeStr(2, "    tc-done " | compilePath | "\n")
 865  				src = nil
 866  				allBcFiles = append(allBcFiles, cached)
 867  				continue
 868  			}
 869  			writeStr(2, "  compile " | pkg.path | "\n")
 870  			src := concatSources(pkg.dir, pkg.files)
 871  			ir := compileSource(src, compilePath, triple)
 872  			src = nil
 873  			llFile := joinPath(tmpdir, safeName(pkg.path) | ".ll")
 874  			writeFile(llFile, []byte(ir), 0644)
 875  			ir = ""
 876  			bcFile := joinPath(tmpdir, safeName(pkg.path) | ".bc")
 877  			rc := run([]string{clang, "--target=" | triple, "-emit-llvm", "-c", llFile, "-o", bcFile})
 878  			if rc != 0 {
 879  				fatal("clang failed for " | pkg.path)
 880  			}
 881  			cacheDir := joinPath(cacheBase, safeName(pkg.path))
 882  			mkdirAll(cacheDir, 0755)
 883  			data, rok := readFile(bcFile)
 884  			if rok {
 885  				writeFile(cached, data, 0644)
 886  			}
 887  			data = nil
 888  			allBcFiles = append(allBcFiles, bcFile)
 889  		} else {
 890  			writeStr(2, "  compile " | pkg.path | "\n")
 891  			src := concatSources(pkg.dir, pkg.files)
 892  			ir := compileSource(src, compilePath, triple)
 893  			src = nil
 894  			if printIR {
 895  				writeStr(1, ir)
 896  			}
 897  			llFile := joinPath(tmpdir, safeName(pkg.path) | ".ll")
 898  			writeFile(llFile, []byte(ir), 0644)
 899  			ir = ""
 900  			bcFile := joinPath(tmpdir, safeName(pkg.path) | ".bc")
 901  			rc := run([]string{clang, "--target=" | triple, "-emit-llvm", "-c", llFile, "-o", bcFile})
 902  			if rc != 0 {
 903  				fatal("clang failed for " | pkg.path)
 904  			}
 905  			allBcFiles = append(allBcFiles, bcFile)
 906  			for _, cf := range pkg.cfiles {
 907  				cPath := joinPath(pkg.dir, cf)
 908  				cbcFile := joinPath(tmpdir, safeName(pkg.path) | "_" | cf | ".bc")
 909  				rc = run([]string{clang, "--target=" | triple, "-emit-llvm", "-c", cPath, "-o", cbcFile})
 910  				if rc != 0 {
 911  					fatal("clang failed for " | cf)
 912  				}
 913  				allBcFiles = append(allBcFiles, cbcFile)
 914  			}
 915  		}
 916  	}
 917  
 918  	loadSysroot(joinPath(root, "_sysroot.env"))
 919  
 920  	sysroot := joinPath(root, "sysroot")
 921  	runtimeBc := getenv("RUNTIME_BC")
 922  	if runtimeBc == "" {
 923  		runtimeBc = joinPath(sysroot, "runtime.bc")
 924  		_, rok := readFile(runtimeBc)
 925  		if !rok {
 926  			runtimeBc = joinPath(root, "_runtime.bc")
 927  		}
 928  	}
 929  	muslDir := getenv("MUSL_DIR")
 930  	if muslDir == "" {
 931  		muslDir = joinPath(sysroot, "musl")
 932  	}
 933  	comprtLib := getenv("COMPRT_LIB")
 934  	if comprtLib == "" {
 935  		comprtLib = joinPath(joinPath(sysroot, "compiler-rt"), "lib.a")
 936  	}
 937  	bdwgcLib := getenv("BDWGC_LIB")
 938  	if bdwgcLib == "" {
 939  		bdwgcLib = joinPath(joinPath(sysroot, "bdwgc"), "lib.a")
 940  	}
 941  	muslLib := getenv("MUSL_LIB")
 942  	if muslLib == "" {
 943  		muslLib = joinPath(joinPath(sysroot, "musl"), "lib.a")
 944  	}
 945  
 946  	writeStr(2, "  generate initAll\n")
 947  	var initCalls string
 948  	for _, pkg := range pkgs {
 949  		compilePath := pkg.path
 950  		if hasPrefix(compilePath, "/") || hasPrefix(compilePath, "./") || hasPrefix(compilePath, "../") {
 951  			compilePath = pkg.name
 952  		}
 953  		if compilePath == "main" {
 954  			continue
 955  		}
 956  		initSym := irGlobalSymbol(compilePath, "main")
 957  		initCalls = initCalls | "  call void " | initSym | "(ptr %context)\n"
 958  	}
 959  	initAllIR := "\ndefine void @runtime.initAll(ptr %context) {\nentry:\n" | initCalls | "  ret void\n}\n"
 960  	declared := map[string]bool{}
 961  	for _, pkg := range pkgs {
 962  		compilePath := pkg.path
 963  		if hasPrefix(compilePath, "/") || hasPrefix(compilePath, "./") || hasPrefix(compilePath, "../") {
 964  			compilePath = pkg.name
 965  		}
 966  		if compilePath == "main" {
 967  			continue
 968  		}
 969  		declSym := irGlobalSymbol(compilePath, "main")
 970  		if declared[declSym] {
 971  			continue
 972  		}
 973  		declared[declSym] = true
 974  		initAllIR = initAllIR | "declare void " | declSym | "(ptr)\n"
 975  	}
 976  	initAllFile := joinPath(tmpdir, "initall.ll")
 977  	writeFile(initAllFile, []byte(initAllIR), 0644)
 978  	initAllBc := joinPath(tmpdir, "initall.bc")
 979  	rc := run([]string{clang, "--target=" | triple, "-emit-llvm", "-c", initAllFile, "-o", initAllBc})
 980  	if rc != 0 {
 981  		fatal("clang failed for initall")
 982  	}
 983  
 984  	writeStr(2, "  merge bitcode\n")
 985  	llvmLink := "llvm-link-21"
 986  	mergedBc := joinPath(tmpdir, "merged.bc")
 987  	linkArgs := []string{llvmLink, "-o", mergedBc, runtimeBc}
 988  	shimsBc := joinPath(sysroot, "hashmap_shims.bc")
 989  	linkArgs = append(linkArgs, shimsBc)
 990  	runtimeGoBc := joinPath(sysroot, "runtime_go.bc")
 991  	if _, gok := readFile(runtimeGoBc); gok {
 992  		linkArgs = append(linkArgs, runtimeGoBc)
 993  	}
 994  	linkArgs = append(linkArgs, allBcFiles...)
 995  	linkArgs = append(linkArgs, initAllBc)
 996  	rc = run(linkArgs)
 997  	if rc != 0 {
 998  		fatal("llvm-link failed")
 999  	}
1000  
1001  	writeStr(2, "  link\n")
1002  	objFiles := getenv("OBJ_FILES")
1003  	if objFiles == "" {
1004  		objEntries := listFiles(joinPath(sysroot, "obj"), ".bc")
1005  		if objEntries != nil {
1006  			for _, e := range objEntries {
1007  				objFiles = objFiles | joinPath(joinPath(sysroot, "obj"), e) | " "
1008  			}
1009  		}
1010  	}
1011  	linker := "ld.lld-21"
1012  	ldArgs := []string{linker, "--gc-sections", "-o", outpath,
1013  		joinPath(muslDir, "crt1.o"),
1014  		mergedBc}
1015  	for _, f := range splitBySpace(objFiles) {
1016  		if f != "" {
1017  			ldArgs = append(ldArgs, f)
1018  		}
1019  	}
1020  	ldArgs = append(ldArgs, comprtLib, bdwgcLib, muslLib, "--lto-O0")
1021  	rc = run(ldArgs)
1022  	if rc != 0 {
1023  		fatal("linker failed")
1024  	}
1025  	writeStr(2, "  -> " | outpath | "\n")
1026  }
1027