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 pathDir(path string) string {
 203  	for i := int32(len(path)) - 1; i >= 0; i-- {
 204  		if path[i] == '/' {
 205  			if i == 0 {
 206  				return "/"
 207  			}
 208  			return path[:i]
 209  		}
 210  	}
 211  	return "."
 212  }
 213  
 214  func splitSlash(s string) []string {
 215  	var parts []string
 216  	start := int32(0)
 217  	for i := int32(0); i < int32(len(s)); i++ {
 218  		if s[i] == '/' {
 219  			parts = append(parts, s[start:i])
 220  			start = i + 1
 221  		}
 222  	}
 223  	parts = append(parts, s[start:])
 224  	return parts
 225  }
 226  
 227  func cleanPath(path string) string {
 228  	parts := splitSlash(path)
 229  	var out []string
 230  	for _, p := range parts {
 231  		if p == "." || p == "" {
 232  			continue
 233  		}
 234  		if p == ".." && len(out) > 0 && out[len(out)-1] != ".." {
 235  			out = out[:len(out)-1]
 236  		} else {
 237  			out = append(out, p)
 238  		}
 239  	}
 240  	if len(out) == 0 {
 241  		return "."
 242  	}
 243  	result := out[0]
 244  	for i := 1; i < len(out); i++ {
 245  		result = result | "/" | out[i]
 246  	}
 247  	if len(path) > 0 && path[0] == '/' {
 248  		result = "/" | result
 249  	}
 250  	return result
 251  }
 252  
 253  func getMoxiePath() string {
 254  	p := getenv("MOXIEPATH")
 255  	if p != "" {
 256  		return p
 257  	}
 258  	home := getenv("HOME")
 259  	if home == "" {
 260  		return "/tmp/moxie"
 261  	}
 262  	return joinPath(home, "moxie")
 263  }
 264  
 265  func mxcVersion() string { return "1.7.0" }
 266  
 267  func moxieCacheDir() string {
 268  	return joinPath(joinPath(getMoxiePath(), "cache"), "mxc-" | mxcVersion())
 269  }
 270  
 271  var globalModPrefix string
 272  var globalModDir string
 273  var globalReplaces map[string]string
 274  
 275  func parseReplaceLine(line string, baseDir string) (string, string, bool) {
 276  	arrow := int32(-1)
 277  	for k := int32(0); k < int32(len(line))-1; k++ {
 278  		if line[k] == '=' && line[k+1] == '>' {
 279  			arrow = k
 280  			break
 281  		}
 282  	}
 283  	if arrow < 0 {
 284  		return "", "", false
 285  	}
 286  	from := trimSpace(line[:arrow])
 287  	to := trimSpace(line[arrow+2:])
 288  	for fi := int32(0); fi < int32(len(from)); fi++ {
 289  		if from[fi] == ' ' {
 290  			from = from[:fi]
 291  			break
 292  		}
 293  	}
 294  	if hasPrefix(to, "./") || hasPrefix(to, "../") || hasPrefix(to, "/") {
 295  		to = cleanPath(joinPath(baseDir, to))
 296  	}
 297  	return from, to, true
 298  }
 299  
 300  func findModuleRoot(startDir string) (string, string) {
 301  	dir := startDir
 302  	for {
 303  		data, ok := readFile(joinPath(dir, "moxie.mod"))
 304  		if ok {
 305  			modName := ""
 306  			globalReplaces = map[string]string{}
 307  			lines := splitLines(string(data))
 308  			inReplace := false
 309  			for _, line := range lines {
 310  				line = trimSpace(line)
 311  				if hasPrefix(line, "module ") {
 312  					modName = trimSpace(line[7:])
 313  					continue
 314  				}
 315  				if line == "replace (" {
 316  					inReplace = true
 317  					continue
 318  				}
 319  				if inReplace {
 320  					if line == ")" {
 321  						inReplace = false
 322  						continue
 323  					}
 324  					from, to, ok2 := parseReplaceLine(line, dir)
 325  					if ok2 {
 326  						globalReplaces[from] = to
 327  					}
 328  					continue
 329  				}
 330  				if hasPrefix(line, "replace ") {
 331  					from, to, ok2 := parseReplaceLine(line[8:], dir)
 332  					if ok2 {
 333  						globalReplaces[from] = to
 334  					}
 335  				}
 336  			}
 337  			if modName != "" {
 338  				return modName, dir
 339  			}
 340  		}
 341  		parent := pathDir(dir)
 342  		if parent == dir || parent == "" || parent == "." {
 343  			break
 344  		}
 345  		dir = parent
 346  	}
 347  	return "", ""
 348  }
 349  
 350  func mergeModReplaces(dir string) {
 351  	data, ok := readFile(joinPath(dir, "moxie.mod"))
 352  	if !ok {
 353  		return
 354  	}
 355  	if globalReplaces == nil {
 356  		globalReplaces = map[string]string{}
 357  	}
 358  	inReplace := false
 359  	for _, line := range splitLines(string(data)) {
 360  		line = trimSpace(line)
 361  		if line == "replace (" {
 362  			inReplace = true
 363  			continue
 364  		}
 365  		if inReplace {
 366  			if line == ")" {
 367  				inReplace = false
 368  				continue
 369  			}
 370  			from, to, ok2 := parseReplaceLine(line, dir)
 371  			if ok2 {
 372  				if _, exists := globalReplaces[from]; !exists {
 373  					globalReplaces[from] = to
 374  				}
 375  			}
 376  			continue
 377  		}
 378  		if hasPrefix(line, "replace ") {
 379  			from, to, ok2 := parseReplaceLine(line[8:], dir)
 380  			if ok2 {
 381  				if _, exists := globalReplaces[from]; !exists {
 382  					globalReplaces[from] = to
 383  				}
 384  			}
 385  		}
 386  	}
 387  }
 388  
 389  type modRequire struct {
 390  	path    string
 391  	version string
 392  }
 393  
 394  func parseModRequires(dir string) (string, []modRequire, map[string]string) {
 395  	data, ok := readFile(joinPath(dir, "moxie.mod"))
 396  	if !ok {
 397  		return "", nil, nil
 398  	}
 399  	modName := ""
 400  	var reqs []modRequire
 401  	repls := map[string]string{}
 402  	inRequire := false
 403  	inReplace := false
 404  	for _, line := range splitLines(string(data)) {
 405  		line = trimSpace(line)
 406  		if hasPrefix(line, "module ") {
 407  			modName = trimSpace(line[7:])
 408  			continue
 409  		}
 410  		if line == "require (" {
 411  			inRequire = true
 412  			continue
 413  		}
 414  		if inRequire {
 415  			if line == ")" {
 416  				inRequire = false
 417  				continue
 418  			}
 419  			parts := splitBySpace(line)
 420  			if len(parts) >= 2 {
 421  				reqs = append(reqs, modRequire{path: parts[0], version: parts[1]})
 422  			} else if len(parts) == 1 {
 423  				reqs = append(reqs, modRequire{path: parts[0], version: ""})
 424  			}
 425  			continue
 426  		}
 427  		if hasPrefix(line, "require ") {
 428  			parts := splitBySpace(line[8:])
 429  			if len(parts) >= 2 {
 430  				reqs = append(reqs, modRequire{path: parts[0], version: parts[1]})
 431  			} else if len(parts) == 1 {
 432  				reqs = append(reqs, modRequire{path: parts[0], version: ""})
 433  			}
 434  			continue
 435  		}
 436  		if line == "replace (" {
 437  			inReplace = true
 438  			continue
 439  		}
 440  		if inReplace {
 441  			if line == ")" {
 442  				inReplace = false
 443  				continue
 444  			}
 445  			from, to, ok2 := parseReplaceLine(line, dir)
 446  			if ok2 {
 447  				repls[from] = to
 448  			}
 449  			continue
 450  		}
 451  		if hasPrefix(line, "replace ") {
 452  			from, to, ok2 := parseReplaceLine(line[8:], dir)
 453  			if ok2 {
 454  				repls[from] = to
 455  			}
 456  		}
 457  	}
 458  	return modName, reqs, repls
 459  }
 460  
 461  func isRemotePath(path string) bool {
 462  	slash := int32(-1)
 463  	for i := int32(0); i < int32(len(path)); i++ {
 464  		if path[i] == '/' {
 465  			slash = i
 466  			break
 467  		}
 468  	}
 469  	if slash < 0 {
 470  		return false
 471  	}
 472  	host := path[:slash]
 473  	for i := int32(0); i < int32(len(host)); i++ {
 474  		if host[i] == '.' {
 475  			return true
 476  		}
 477  	}
 478  	return false
 479  }
 480  
 481  func repoLockPath(mpath, repoPath string) string {
 482  	return joinPath(mpath, repoPath | ".lock")
 483  }
 484  
 485  func acquireRepoLock(mpath, repoPath string) bool {
 486  	lp := repoLockPath(mpath, repoPath)
 487  	_, locked := readFile(lp)
 488  	if locked {
 489  		return false
 490  	}
 491  	mkdirAll(pathDir(lp), 0755)
 492  	writeFile(lp, []byte("1"), 0644)
 493  	return true
 494  }
 495  
 496  func releaseRepoLock(mpath, repoPath string) {
 497  	os.Remove(repoLockPath(mpath, repoPath))
 498  }
 499  
 500  func autoFetchRepo(pkgPath, version string) string {
 501  	mpath := getMoxiePath()
 502  	repoPath := pkgPath
 503  	for {
 504  		dest := joinPath(mpath, repoPath)
 505  		gitHead := joinPath(dest, ".git/HEAD")
 506  		_, exists := readFile(gitHead)
 507  		if exists {
 508  			break
 509  		}
 510  		parent := pathDir(repoPath)
 511  		if parent == repoPath || parent == "." || parent == "" {
 512  			break
 513  		}
 514  		parentDest := joinPath(mpath, parent)
 515  		parentGit := joinPath(parentDest, ".git/HEAD")
 516  		_, parentExists := readFile(parentGit)
 517  		if parentExists {
 518  			repoPath = parent
 519  			break
 520  		}
 521  		repoPath = parent
 522  	}
 523  
 524  	if !acquireRepoLock(mpath, repoPath) {
 525  		writeStr(2, "  waiting for lock: " | repoPath | "\n")
 526  		fatal("repo locked: " | repoPath)
 527  	}
 528  
 529  	dest := joinPath(mpath, repoPath)
 530  	gitHead := joinPath(dest, ".git/HEAD")
 531  	_, exists := readFile(gitHead)
 532  
 533  	if !exists {
 534  		mkdirAll(pathDir(dest), 0755)
 535  		url := "https://" | repoPath
 536  		writeStr(2, "  auto-fetch " | repoPath | "\n")
 537  		if run([]string{"git", "clone", url, dest}) != 0 {
 538  			releaseRepoLock(mpath, repoPath)
 539  			return ""
 540  		}
 541  	}
 542  
 543  	if version != "" && version != "v0.0.0" {
 544  		if run([]string{"git", "-C", dest, "checkout", version}) != 0 {
 545  			run([]string{"git", "-C", dest, "fetch", "--tags"})
 546  			run([]string{"git", "-C", dest, "pull"})
 547  			run([]string{"git", "-C", dest, "checkout", version})
 548  		}
 549  	}
 550  
 551  	releaseRepoLock(mpath, repoPath)
 552  	return joinPath(mpath, pkgPath)
 553  }
 554  
 555  func cmdFetch() {
 556  	mpath := getMoxiePath()
 557  	mkdirAll(mpath, 0755)
 558  
 559  	modName, reqs, repls := parseModRequires(".")
 560  	if modName == "" {
 561  		fatal("no moxie.mod in current directory")
 562  	}
 563  	writeStr(2, "module: " | modName | "\n")
 564  
 565  	var queue []modRequire
 566  	for _, r := range reqs {
 567  		if _, ok := repls[r.path]; ok {
 568  			writeStr(2, "  skip (replaced): " | r.path | "\n")
 569  			continue
 570  		}
 571  		queue = append(queue, r)
 572  	}
 573  
 574  	seen := map[string]bool{}
 575  	for len(queue) > 0 {
 576  		req := queue[0]
 577  		queue = queue[1:]
 578  		if seen[req.path] {
 579  			continue
 580  		}
 581  		seen[req.path] = true
 582  
 583  		if !acquireRepoLock(mpath, req.path) {
 584  			writeStr(2, "  locked: " | req.path | " (another build in progress)\n")
 585  			fatal("repo locked: " | req.path)
 586  		}
 587  
 588  		dest := joinPath(mpath, req.path)
 589  		gitHead := joinPath(dest, ".git/HEAD")
 590  		_, exists := readFile(gitHead)
 591  
 592  		if !exists {
 593  			mkdirAll(pathDir(dest), 0755)
 594  			url := "https://" | req.path
 595  			writeStr(2, "  clone " | req.path | "\n")
 596  			if run([]string{"git", "clone", url, dest}) != 0 {
 597  				releaseRepoLock(mpath, req.path)
 598  				writeStr(2, "  FAIL: clone " | req.path | "\n")
 599  				continue
 600  			}
 601  		}
 602  
 603  		if req.version != "" && req.version != "v0.0.0" {
 604  			if run([]string{"git", "-C", dest, "checkout", req.version}) != 0 {
 605  				writeStr(2, "  tag " | req.version | " not found, pulling\n")
 606  				run([]string{"git", "-C", dest, "pull"})
 607  				if run([]string{"git", "-C", dest, "checkout", req.version}) != 0 {
 608  					writeStr(2, "  WARN: " | req.version | " unavailable for " | req.path | "\n")
 609  				}
 610  			} else {
 611  				writeStr(2, "  " | req.path | " @ " | req.version | "\n")
 612  			}
 613  		} else if exists {
 614  			writeStr(2, "  pull " | req.path | "\n")
 615  			run([]string{"git", "-C", dest, "pull"})
 616  		} else {
 617  			writeStr(2, "  fetched " | req.path | "\n")
 618  		}
 619  
 620  		releaseRepoLock(mpath, req.path)
 621  
 622  		_, subReqs, _ := parseModRequires(dest)
 623  		for _, sr := range subReqs {
 624  			if !seen[sr.path] {
 625  				queue = append(queue, sr)
 626  			}
 627  		}
 628  	}
 629  
 630  	writeStr(2, "fetch done\n")
 631  }
 632  
 633  func joinStrings(ss []string, sep string) string {
 634  	if len(ss) == 0 {
 635  		return ""
 636  	}
 637  	n := int32(0)
 638  	for _, s := range ss {
 639  		n += int32(len(s))
 640  	}
 641  	n += int32(len(sep)) * (int32(len(ss)) - 1)
 642  	buf := []byte{:0:n}
 643  	for i, s := range ss {
 644  		if i > 0 {
 645  			buf = append(buf, sep...)
 646  		}
 647  		buf = append(buf, s...)
 648  	}
 649  	return string(buf)
 650  }
 651  
 652  func appendUniq(ss []string, s string) []string {
 653  	for _, x := range ss {
 654  		if x == s {
 655  			return ss
 656  		}
 657  	}
 658  	return append(ss, s)
 659  }
 660  
 661  func fnvHash(data []byte) string {
 662  	h := uint32(2166136261)
 663  	for i := int32(0); i < int32(len(data)); i++ {
 664  		h ^= uint32(data[i])
 665  		h *= uint32(16777619)
 666  	}
 667  	hex := "0123456789abcdef"
 668  	buf := []byte{:8}
 669  	for i := int32(7); i >= 0; i-- {
 670  		buf[i] = hex[h&0xf]
 671  		h >>= 4
 672  	}
 673  	return string(buf)
 674  }
 675  
 676  func pkgCacheDir() string {
 677  	return joinPath(moxieCacheDir(), "pkg")
 678  }
 679  
 680  func pkgCacheKey(dir string, files []string) string {
 681  	var sorted []string
 682  	for _, f := range files {
 683  		sorted = append(sorted, f)
 684  	}
 685  	for i := int32(1); i < int32(len(sorted)); i++ {
 686  		for j := i; j > 0 && sorted[j] < sorted[j-1]; j-- {
 687  			sorted[j], sorted[j-1] = sorted[j-1], sorted[j]
 688  		}
 689  	}
 690  	var all []byte
 691  	for _, f := range sorted {
 692  		p := joinPath(dir, f)
 693  		data, ok := readFile(p)
 694  		if !ok {
 695  			continue
 696  		}
 697  		all = append(all, data...)
 698  		all = append(all, 0)
 699  	}
 700  	return fnvHash(all)
 701  }
 702  
 703  func versionedCacheName(hash, version, ext string) string {
 704  	if version != "" {
 705  		return hash | "_" | version | ext
 706  	}
 707  	return hash | ext
 708  }
 709  
 710  func cachedBcPath(cacheBase, pkgPath, hash, version string) string {
 711  	return joinPath(joinPath(cacheBase, safeName(pkgPath)), versionedCacheName(hash, version, ".bc"))
 712  }
 713  
 714  func isStdlib(path string) bool {
 715  	return !hasPrefix(path, "/") && !hasPrefix(path, "./") && !hasPrefix(path, "../")
 716  }
 717  
 718  var builtinOnly []string
 719  
 720  func isBuiltinOnly(path string) bool {
 721  	for _, b := range builtinOnly {
 722  		if path == b {
 723  			return true
 724  		}
 725  	}
 726  	return false
 727  }
 728  
 729  func loadSysroot(path string) {
 730  	data, ok := readFile(path)
 731  	if !ok {
 732  		return
 733  	}
 734  	for _, line := range splitLines(string(data)) {
 735  		line = trimSpace(line)
 736  		if len(line) == 0 || line[0] == '#' {
 737  			continue
 738  		}
 739  		eq := int32(-1)
 740  		for i := int32(0); i < int32(len(line)); i++ {
 741  			if line[i] == '=' {
 742  				eq = i
 743  				break
 744  			}
 745  		}
 746  		if eq < 0 {
 747  			continue
 748  		}
 749  		key := line[:eq]
 750  		val := line[eq+1:]
 751  		os.Setenv(key, val)
 752  	}
 753  }
 754  
 755  func splitBySpace(s string) []string {
 756  	var result []string
 757  	start := int32(0)
 758  	inWord := false
 759  	for i := int32(0); i < int32(len(s)); i++ {
 760  		if s[i] == ' ' || s[i] == '\t' {
 761  			if inWord {
 762  				result = append(result, s[start:i])
 763  				inWord = false
 764  			}
 765  		} else {
 766  			if !inWord {
 767  				start = i
 768  				inWord = true
 769  			}
 770  		}
 771  	}
 772  	if inWord {
 773  		result = append(result, s[start:])
 774  	}
 775  	return result
 776  }
 777  
 778  func safeName(pkg string) string {
 779  	b := []byte{:len(pkg)}
 780  	for i := int32(0); i < int32(len(pkg)); i++ {
 781  		if pkg[i] == '/' {
 782  			b[i] = '_'
 783  		} else {
 784  			b[i] = pkg[i]
 785  		}
 786  	}
 787  	return string(b)
 788  }
 789  
 790  func extractPkgName(data []byte) string {
 791  	for i := int32(0); i < int32(len(data)); i++ {
 792  		if i+8 < int32(len(data)) && string(data[i:i+8]) == "package " {
 793  			j := i + 8
 794  			for j < int32(len(data)) && data[j] != '\n' && data[j] != '\r' && data[j] != ' ' {
 795  				j++
 796  			}
 797  			return string(data[i+8 : j])
 798  		}
 799  		if data[i] == '\n' {
 800  			continue
 801  		}
 802  		if data[i] == '/' && i+1 < int32(len(data)) && data[i+1] == '/' {
 803  			for i < int32(len(data)) && data[i] != '\n' {
 804  				i++
 805  			}
 806  			continue
 807  		}
 808  		break
 809  	}
 810  	return "main"
 811  }
 812  
 813  func extractImports(data []byte) []string {
 814  	var imports []string
 815  	lines := splitLines(string(data))
 816  	inBlock := false
 817  	for _, line := range lines {
 818  		line = trimSpace(line)
 819  		if line == "import (" {
 820  			inBlock = true
 821  			continue
 822  		}
 823  		if inBlock && line == ")" {
 824  			inBlock = false
 825  			continue
 826  		}
 827  		if inBlock {
 828  			imp := extractQuoted(line)
 829  			if imp != "" {
 830  				imports = appendUniq(imports, imp)
 831  			}
 832  		}
 833  		if hasPrefix(line, "import \"") {
 834  			imp := extractQuoted(line[7:])
 835  			if imp != "" {
 836  				imports = appendUniq(imports, imp)
 837  			}
 838  		}
 839  	}
 840  	return imports
 841  }
 842  
 843  func extractQuoted(s string) string {
 844  	start := int32(-1)
 845  	for i := int32(0); i < int32(len(s)); i++ {
 846  		if s[i] == '"' {
 847  			if start < 0 {
 848  				start = i + 1
 849  			} else {
 850  				return s[start:i]
 851  			}
 852  		}
 853  	}
 854  	return ""
 855  }
 856  
 857  func concatSources(dir string, files []string) []byte {
 858  	imports := map[string]bool{}
 859  	var bodies [][]byte
 860  	pkgName := ""
 861  	for _, f := range files {
 862  		data, ok := readFile(joinPath(dir, f))
 863  		if !ok {
 864  			continue
 865  		}
 866  		if pkgName == "" {
 867  			pkgName = extractPkgName(data)
 868  		}
 869  		lines := splitLines(string(data))
 870  		var body []string
 871  		i := int32(0)
 872  		for i < int32(len(lines)) {
 873  			line := trimSpace(lines[i])
 874  			if hasPrefix(line, "package ") {
 875  				i++
 876  				continue
 877  			}
 878  			if line == "import (" {
 879  				i++
 880  				for i < int32(len(lines)) {
 881  					imp := trimSpace(lines[i])
 882  					i++
 883  					if imp == ")" {
 884  						break
 885  					}
 886  					if imp != "" {
 887  						imports[imp] = true
 888  					}
 889  				}
 890  				continue
 891  			}
 892  			if hasPrefix(line, "import ") && !hasPrefix(line, "import (") {
 893  				imp := line[7:]
 894  				imports[imp] = true
 895  				i++
 896  				continue
 897  			}
 898  			body = append(body, lines[i])
 899  			i++
 900  		}
 901  		bodies = append(bodies, []byte(joinStrings(body, "\n")))
 902  	}
 903  	var out []byte
 904  	out = append(out, ("package " | pkgName | "\n")...)
 905  	if len(imports) > 0 {
 906  		var impKeys []string
 907  		for imp := range imports {
 908  			impKeys = append(impKeys, imp)
 909  		}
 910  		for i := int32(1); i < int32(len(impKeys)); i++ {
 911  			for j := i; j > 0 && impKeys[j] < impKeys[j-1]; j-- {
 912  				impKeys[j], impKeys[j-1] = impKeys[j-1], impKeys[j]
 913  			}
 914  		}
 915  		out = append(out, "import (\n"...)
 916  		for _, imp := range impKeys {
 917  			out = append(out, '\t')
 918  			out = append(out, imp...)
 919  			out = append(out, '\n')
 920  		}
 921  		out = append(out, ")\n"...)
 922  	}
 923  	for _, b := range bodies {
 924  		out = append(out, b...)
 925  		out = append(out, '\n')
 926  	}
 927  	return out
 928  }
 929  
 930  func fileContainsPart(base string, part string) bool {
 931  	plen := len(part)
 932  	for i := 0; i <= len(base)-plen; i++ {
 933  		if base[i:i+plen] == part {
 934  			before := i == 0 || base[i-1] == '_'
 935  			after := i+plen == len(base) || base[i+plen] == '_'
 936  			if before && after {
 937  				return true
 938  			}
 939  		}
 940  	}
 941  	return false
 942  }
 943  
 944  func shouldSkipFile(name string) bool {
 945  	if hasSuffix(name, "_test.go") || hasSuffix(name, "_test.mx") {
 946  		return true
 947  	}
 948  	base := name
 949  	if hasSuffix(base, ".go") {
 950  		base = base[:len(base)-3]
 951  	} else if hasSuffix(base, ".mx") {
 952  		base = base[:len(base)-3]
 953  	}
 954  	archSkip := []string{"arm64"}
 955  	for _, a := range archSkip {
 956  		if fileContainsPart(base, a) {
 957  			return true
 958  		}
 959  	}
 960  	suffixSkip := []string{"_wasm"}
 961  	for _, s := range suffixSkip {
 962  		if hasSuffix(base, s) {
 963  			return true
 964  		}
 965  	}
 966  	exact := []string{"pipe", "multi", "scan", "reader", "replace", "search",
 967  		"ioutil"}
 968  	for _, s := range exact {
 969  		if base == s {
 970  			return true
 971  		}
 972  	}
 973  	return false
 974  }
 975  
 976  func hasBuildConstraint(data []byte, forbidden []string) bool {
 977  	for i := int32(0); i < int32(len(data)) && i < 512; i++ {
 978  		if data[i] == 'p' {
 979  			return false
 980  		}
 981  		if i+11 < int32(len(data)) && string(data[i:i+11]) == "//go:build " {
 982  			var buf []byte
 983  			for j := i + 11; j < int32(len(data)) && data[j] != '\n'; j++ {
 984  				buf = append(buf, data[j])
 985  			}
 986  			line := string(buf)
 987  			for _, f := range forbidden {
 988  				if line == f || hasPrefix(line, f | " ") || hasPrefix(line, f | "\t") {
 989  					return true
 990  				}
 991  			}
 992  			return false
 993  		}
 994  		if data[i] == '\n' {
 995  			continue
 996  		}
 997  		if data[i] == '/' && i+1 < int32(len(data)) && data[i+1] == '/' {
 998  			for i < int32(len(data)) && data[i] != '\n' {
 999  				i++
1000  			}
1001  			continue
1002  		}
1003  	}
1004  	return false
1005  }
1006  
1007  func hasImport(data []byte, pkg string) bool {
1008  	target := "\"" | pkg | "\""
1009  	for i := int32(0); i <= int32(len(data))-int32(len(target)); i++ {
1010  		if string(data[i:i+int32(len(target))]) == target {
1011  			return true
1012  		}
1013  	}
1014  	return false
1015  }
1016  
1017  type pkgInfo struct {
1018  	path    string
1019  	name    string
1020  	dir     string
1021  	files   []string
1022  	cfiles  []string
1023  	bcfiles []string
1024  	imports []string
1025  	version string
1026  }
1027  
1028  func discoverPkg(pkgPath, root string) *pkgInfo {
1029  	var dir string
1030  	if pkgPath == "." || pkgPath == ".." || hasPrefix(pkgPath, "/") || hasPrefix(pkgPath, "./") || hasPrefix(pkgPath, "../") {
1031  		dir = pkgPath
1032  	} else if globalReplaces != nil {
1033  		if rdir, ok := globalReplaces[pkgPath]; ok {
1034  			dir = rdir
1035  		}
1036  		if dir == "" {
1037  			for from, to := range globalReplaces {
1038  				if hasPrefix(pkgPath, from|"/") {
1039  					rel := pkgPath[len(from)+1:]
1040  					dir = joinPath(to, rel)
1041  					break
1042  				}
1043  			}
1044  		}
1045  	}
1046  	if dir == "" && globalModPrefix != "" && pkgPath == globalModPrefix {
1047  		dir = globalModDir
1048  	}
1049  	if dir == "" && globalModPrefix != "" && hasPrefix(pkgPath, globalModPrefix|"/") {
1050  		rel := pkgPath[len(globalModPrefix)+1:]
1051  		dir = joinPath(globalModDir, rel)
1052  	}
1053  	if dir == "" {
1054  		candidate := joinPath(getMoxiePath(), pkgPath)
1055  		mxF := listFiles(candidate, ".mx")
1056  		if len(mxF) > 0 {
1057  			dir = candidate
1058  		}
1059  	}
1060  	if dir == "" && isRemotePath(pkgPath) {
1061  		version := ""
1062  		if globalModDir != "" {
1063  			_, reqs, _ := parseModRequires(globalModDir)
1064  			for _, r := range reqs {
1065  				if r.path == pkgPath {
1066  					version = r.version
1067  					break
1068  				}
1069  			}
1070  		}
1071  		fetched := autoFetchRepo(pkgPath, version)
1072  		if fetched != "" {
1073  			mxF := listFiles(fetched, ".mx")
1074  			if len(mxF) > 0 {
1075  				dir = fetched
1076  			}
1077  		}
1078  	}
1079  	if dir != "" && dir != pkgPath {
1080  		mergeModReplaces(dir)
1081  	}
1082  	if dir == "" {
1083  		dir = joinPath(root, joinPath("src", pkgPath))
1084  		mxFiles := listFiles(dir, ".mx")
1085  		if len(mxFiles) == 0 {
1086  			dir = joinPath(root, joinPath("src/vendor", pkgPath))
1087  		}
1088  	}
1089  	goFiles := listFiles(dir, ".go")
1090  	mxFiles := listFiles(dir, ".mx")
1091  	cFiles := listFiles(dir, ".c")
1092  	bcFiles := listFiles(dir, ".bc")
1093  	allMx := append(mxFiles, goFiles...)
1094  	forbidden := []string{"wasm", "js", "ignore", "compiler_bootstrap",
1095  		"scheduler.tasks", "scheduler.cores", "scheduler.asyncify",
1096  		"faketime", "baremetal", "nintendoswitch"}
1097  	var files []string
1098  	for _, f := range allMx {
1099  		if shouldSkipFile(f) {
1100  			continue
1101  		}
1102  		data, ok := readFile(joinPath(dir, f))
1103  		if !ok {
1104  			continue
1105  		}
1106  		bc := hasBuildConstraint(data, forbidden)
1107  		if bc {
1108  			writeStr(2, "    skip-constraint: " | f | "\n")
1109  			continue
1110  		}
1111  		fileImps := extractImports(data)
1112  		skipIter := false
1113  		for _, imp := range fileImps {
1114  			if imp == "iter" {
1115  				skipIter = true
1116  				break
1117  			}
1118  		}
1119  		if skipIter {
1120  			writeStr(2, "    skip-iter-import: " | f | "\n")
1121  			continue
1122  		}
1123  		writeStr(2, "    include: " | f | "\n")
1124  		files = append(files, f)
1125  	}
1126  	if len(files) == 0 {
1127  		return nil
1128  	}
1129  	var allImports []string
1130  	pkgName := ""
1131  	for _, f := range files {
1132  		data, ok := readFile(joinPath(dir, f))
1133  		if !ok {
1134  			continue
1135  		}
1136  		if pkgName == "" {
1137  			pkgName = extractPkgName(data)
1138  		}
1139  		for _, imp := range extractImports(data) {
1140  			allImports = appendUniq(allImports, imp)
1141  		}
1142  	}
1143  	writeStr(2, "  discover " | pkgPath | " [")
1144  	for fi, ff := range files {
1145  		if fi > 0 {
1146  			writeStr(2, ", ")
1147  		}
1148  		writeStr(2, ff)
1149  	}
1150  	writeStr(2, "]\n")
1151  	version := ""
1152  	if isRemotePath(pkgPath) && globalModDir != "" {
1153  		_, reqs, _ := parseModRequires(globalModDir)
1154  		for _, r := range reqs {
1155  			if r.path == pkgPath || hasPrefix(pkgPath, r.path|"/") {
1156  				version = r.version
1157  				break
1158  			}
1159  		}
1160  	}
1161  	return &pkgInfo{
1162  		path:    pkgPath,
1163  		name:    pkgName,
1164  		dir:     dir,
1165  		files:   files,
1166  		cfiles:  cFiles,
1167  		bcfiles: bcFiles,
1168  		imports: allImports,
1169  		version: version,
1170  	}
1171  }
1172  
1173  type resolver struct {
1174  	visited map[string]bool
1175  	order   []*pkgInfo
1176  	root    string
1177  }
1178  
1179  func (r *resolver) walk(path string) {
1180  	if r.visited[path] {
1181  		return
1182  	}
1183  	r.visited[path] = true
1184  	if isBuiltinOnly(path) {
1185  		return
1186  	}
1187  	pkg := discoverPkg(path, r.root)
1188  	if pkg == nil {
1189  		return
1190  	}
1191  	for _, imp := range pkg.imports {
1192  		r.walk(imp)
1193  	}
1194  	r.order = append(r.order, pkg)
1195  }
1196  
1197  func resolveAll(rootPkg string, root string) []*pkgInfo {
1198  	r := &resolver{
1199  		visited: map[string]bool{},
1200  		root:    root,
1201  	}
1202  	r.walk(rootPkg)
1203  	return r.order
1204  }
1205  
1206  func registerBuiltins() {
1207  	initUniverse()
1208  	ensureImportRegistry()
1209  	importRegistry = map[string]*TCPackage{}
1210  
1211  	// unsafe - compiler builtin, no source
1212  	unsafePkg := NewTCPackage("unsafe", "unsafe")
1213  	unsafePkg.Scope().Insert(NewTypeName(unsafePkg, "Pointer", Typ[UnsafePointer]))
1214  	unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "Sizeof", parseSignatureDesc("interface{}->int32")))
1215  	unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "Offsetof", parseSignatureDesc("interface{}->int32")))
1216  	unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "Alignof", parseSignatureDesc("interface{}->int32")))
1217  	unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "Slice", parseSignatureDesc("ptr,int32->[]uint8")))
1218  	unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "SliceData", parseSignatureDesc("[]uint8->ptr")))
1219  	importRegistry["unsafe"] = unsafePkg
1220  
1221  	// moxie - spawn/codec runtime, special handling
1222  	importRegistry["moxie"] = NewTCPackage("moxie", "moxie")
1223  
1224  	// runtime - assembly + C, pre-compiled in runtime.bc
1225  	rtPkg := NewTCPackage("runtime", "runtime")
1226  	rtPkg.Scope().Insert(NewTCFunc(rtPkg, "InitCShared", parseSignatureDesc("")))
1227  	importRegistry["runtime"] = rtPkg
1228  }
1229  
1230  func addPtrMethod(pkg *TCPackage, named *Named, name, sigDesc string) {
1231  	var sig *Signature
1232  	if sigDesc == "" {
1233  		sig = NewSignature(nil, nil, nil, false)
1234  	} else {
1235  		sig = parseSignatureDesc(sigDesc)
1236  	}
1237  	fn := NewTCFunc(pkg, name, sig)
1238  	fn.hasPtrRecv = true
1239  	named.AddMethod(fn)
1240  }
1241  
1242  func compileSource(src []byte, pkgName, triple string) string {
1243  	ir := CompileToIR(src, pkgName, triple)
1244  	if len(ir) > 0 && ir[0] == ';' {
1245  		writeStr(2, ir)
1246  		fatal("compile error for " | pkgName)
1247  	}
1248  	if len(ir) == 0 {
1249  		fatal("empty IR for " | pkgName)
1250  	}
1251  	return ir
1252  }
1253  
1254  func typeDescStr(t Type) string {
1255  	if t == nil {
1256  		return "interface{}"
1257  	}
1258  	switch v := t.(type) {
1259  	case *Basic:
1260  		switch v.kind {
1261  		case Bool:
1262  			return "bool"
1263  		case Int8:
1264  			return "int8"
1265  		case Int16:
1266  			return "int16"
1267  		case Int32:
1268  			return "int32"
1269  		case Int64:
1270  			return "int64"
1271  		case Uint8:
1272  			return "uint8"
1273  		case Uint16:
1274  			return "uint16"
1275  		case Uint32:
1276  			return "uint32"
1277  		case Uint64:
1278  			return "uint64"
1279  		case Float32:
1280  			return "float32"
1281  		case Float64:
1282  			return "float64"
1283  		case TCString:
1284  			return "string"
1285  		case UnsafePointer:
1286  			return "ptr"
1287  		}
1288  		return "int32"
1289  	case *Slice:
1290  		return "[]" | typeDescStr(v.Elem())
1291  	case *Pointer:
1292  		return "*" | typeDescStr(v.Elem())
1293  	case *TCInterface:
1294  		if v.IsEmpty() {
1295  			return "interface{}"
1296  		}
1297  		return "interface{}"
1298  	case *Named:
1299  		if v.obj != nil {
1300  			return v.obj.name
1301  		}
1302  		return typeDescStr(v.underlying)
1303  	}
1304  	return "interface{}"
1305  }
1306  
1307  func sigDescStr(sig *Signature) string {
1308  	var out string
1309  	if sig.params != nil {
1310  		for i := int32(0); i < sig.params.Len(); i++ {
1311  			if i > 0 {
1312  				out = out | ","
1313  			}
1314  			out = out | typeDescStr(sig.params.At(i).Type())
1315  		}
1316  	}
1317  	if sig.results != nil && sig.results.Len() > 0 {
1318  		out = out | "->"
1319  		for i := int32(0); i < sig.results.Len(); i++ {
1320  			if i > 0 {
1321  				out = out | ","
1322  			}
1323  			out = out | typeDescStr(sig.results.At(i).Type())
1324  		}
1325  	}
1326  	return out
1327  }
1328  
1329  func generateMxh(pkg *TCPackage) string {
1330  	path := pkg.Path()
1331  	name := pkg.Name()
1332  	var out string
1333  	out = "package " | path | " " | name | "\n"
1334  	for _, objName := range pkg.Scope().Names() {
1335  		if len(objName) == 0 || objName[0] < 'A' || objName[0] > 'Z' {
1336  			continue
1337  		}
1338  		obj := pkg.Scope().Lookup(objName)
1339  		if obj == nil {
1340  			continue
1341  		}
1342  		switch v := obj.(type) {
1343  		case *TCFunc:
1344  			sig := v.Signature()
1345  			if sig != nil {
1346  				out = out | "func " | objName | " " | sigDescStr(sig) | "\n"
1347  			}
1348  		case *TCVar:
1349  			out = out | "var " | objName | " " | typeDescStr(v.Type()) | "\n"
1350  		case *TCConst:
1351  			td := typeDescStr(v.Type())
1352  			vs := ""
1353  			if v.val != nil {
1354  				vs = v.val.String()
1355  			}
1356  			out = out | "const " | objName | " " | td | " " | vs | "\n"
1357  		case *TypeName:
1358  			typ := v.Type()
1359  			if named, ok := typ.(*Named); ok {
1360  				ud := ""
1361  				if named.underlying != nil {
1362  					ud = typeDescStr(named.underlying)
1363  				}
1364  				out = out | "type " | objName | " " | ud | "\n"
1365  				for _, m := range named.methods {
1366  					ptrRecv := "0"
1367  					if m.hasPtrRecv {
1368  						ptrRecv = "1"
1369  					}
1370  					msig := m.Signature()
1371  					if msig != nil {
1372  						out = out | "method " | objName | " " | m.Name() | " " | sigDescStr(msig) | " " | ptrRecv | "\n"
1373  					}
1374  				}
1375  			} else if iface, ok := typ.(*TCInterface); ok {
1376  				var methods string
1377  				for mi := int32(0); mi < iface.NumMethods(); mi++ {
1378  					m := iface.Method(mi)
1379  					if mi > 0 {
1380  						methods = methods | ";"
1381  					}
1382  					methods = methods | m.name | "=" | sigDescStr(m.sig)
1383  				}
1384  				out = out | "iface " | objName | " " | methods | "\n"
1385  			}
1386  		}
1387  	}
1388  	return out
1389  }
1390  
1391  func loadMxh(path string) bool {
1392  	data, ok := readFile(path)
1393  	if !ok {
1394  		return false
1395  	}
1396  	ensureImportRegistry()
1397  	lines := splitLines(string(data))
1398  	if len(lines) == 0 {
1399  		return false
1400  	}
1401  	first := lines[0]
1402  	if !hasPrefix(first, "package ") {
1403  		return false
1404  	}
1405  	rest := first[8:]
1406  	spaceIdx := int32(-1)
1407  	for i := int32(0); i < int32(len(rest)); i++ {
1408  		if rest[i] == ' ' {
1409  			spaceIdx = i
1410  			break
1411  		}
1412  	}
1413  	if spaceIdx < 0 {
1414  		return false
1415  	}
1416  	pkgPath := rest[:spaceIdx]
1417  	pkgName := rest[spaceIdx+1:]
1418  	pkg := NewTCPackage(pkgPath, pkgName)
1419  
1420  	for li := 1; li < len(lines); li++ {
1421  		line := lines[li]
1422  		if hasPrefix(line, "func ") {
1423  			parts := splitBySpace(line[5:])
1424  			if len(parts) >= 2 {
1425  				sig := parseSignatureDesc(parts[1])
1426  				pkg.Scope().Insert(NewTCFunc(pkg, parts[0], sig))
1427  			} else if len(parts) == 1 {
1428  				sig := parseSignatureDesc("")
1429  				pkg.Scope().Insert(NewTCFunc(pkg, parts[0], sig))
1430  			}
1431  		} else if hasPrefix(line, "var ") {
1432  			parts := splitBySpace(line[4:])
1433  			if len(parts) >= 2 {
1434  				typ := parseTypeDesc(parts[1])
1435  				pkg.Scope().Insert(NewTCVar(pkg, parts[0], typ))
1436  			}
1437  		} else if hasPrefix(line, "const ") {
1438  			parts := splitBySpace(line[6:])
1439  			if len(parts) >= 2 {
1440  				typ := parseTypeDesc(parts[1])
1441  				var val ConstVal
1442  				if len(parts) >= 3 {
1443  					val = constStr{parts[2]}
1444  				} else {
1445  					val = constInt{0}
1446  				}
1447  				pkg.Scope().Insert(NewTCConst(pkg, parts[0], typ, val))
1448  			}
1449  		} else if hasPrefix(line, "type ") {
1450  			parts := splitBySpace(line[5:])
1451  			if len(parts) >= 2 {
1452  				underlying := parseTypeDesc(parts[1])
1453  				tn := NewTypeName(pkg, parts[0], nil)
1454  				NewNamed(tn, underlying)
1455  				pkg.Scope().Insert(tn)
1456  			} else if len(parts) == 1 {
1457  				underlying := NewTCStruct(nil, nil)
1458  				tn := NewTypeName(pkg, parts[0], nil)
1459  				NewNamed(tn, underlying)
1460  				pkg.Scope().Insert(tn)
1461  			}
1462  		} else if hasPrefix(line, "method ") {
1463  			parts := splitBySpace(line[7:])
1464  			if len(parts) >= 3 {
1465  				typeName := parts[0]
1466  				methodName := parts[1]
1467  				sigDesc := parts[2]
1468  				ptrRecv := false
1469  				if len(parts) >= 4 && parts[3] == "1" {
1470  					ptrRecv = true
1471  				}
1472  				obj := pkg.Scope().Lookup(typeName)
1473  				if obj != nil {
1474  					if tn, ok := obj.(*TypeName); ok {
1475  						if named, ok := tn.typ.(*Named); ok {
1476  							sig := parseSignatureDesc(sigDesc)
1477  							fn := NewTCFunc(pkg, methodName, sig)
1478  							fn.hasPtrRecv = ptrRecv
1479  							named.AddMethod(fn)
1480  						}
1481  					}
1482  				}
1483  			}
1484  		} else if hasPrefix(line, "iface ") {
1485  			parts := splitBySpace(line[6:])
1486  			if len(parts) >= 1 {
1487  				ifaceName := parts[0]
1488  				methodsDesc := ""
1489  				if len(parts) >= 2 {
1490  					methodsDesc = parts[1]
1491  				}
1492  				var methods []*IfaceMethod
1493  				if methodsDesc != "" {
1494  					mparts := splitSemicolon(methodsDesc)
1495  					for _, p := range mparts {
1496  						eqIdx := -1
1497  						for i := 0; i < len(p); i++ {
1498  							if p[i] == '=' {
1499  								eqIdx = i
1500  								break
1501  							}
1502  						}
1503  						if eqIdx < 0 {
1504  							continue
1505  						}
1506  						mname := p[:eqIdx]
1507  						msig := parseSignatureDesc(p[eqIdx+1:])
1508  						methods = append(methods, NewTCIfaceMethod(mname, msig))
1509  					}
1510  				}
1511  				iface := NewTCInterface(methods, nil)
1512  				iface.Complete()
1513  				tn := NewTypeName(pkg, ifaceName, iface)
1514  				pkg.Scope().Insert(tn)
1515  			}
1516  		}
1517  	}
1518  
1519  	importRegistry[pkgPath] = pkg
1520  	return true
1521  }
1522  
1523  func cachedMxhPath(cacheBase, pkgPath, hash, version string) string {
1524  	return joinPath(joinPath(cacheBase, safeName(pkgPath)), versionedCacheName(hash, version, ".mxh"))
1525  }
1526  
1527  func removeAll(path string) {
1528  	entries := listFiles(path, "")
1529  	if entries == nil {
1530  		return
1531  	}
1532  	for _, e := range entries {
1533  		os.Remove(joinPath(path, e))
1534  	}
1535  	os.Remove(path)
1536  }
1537  
1538  func cacheClean() {
1539  	base := pkgCacheDir()
1540  	writeStr(2, "cleaning cache: " | base | "\n")
1541  	entries := listFiles(base, "")
1542  	if entries == nil {
1543  		writeStr(2, "  (empty)\n")
1544  		return
1545  	}
1546  	for _, e := range entries {
1547  		removeAll(joinPath(base, e))
1548  	}
1549  	os.Remove(base)
1550  	writeStr(2, "  done\n")
1551  }
1552  
1553  func cmdUpdate(rebuildAll bool) {
1554  	cacheRoot := joinPath(getMoxiePath(), "cache")
1555  	currentDir := "mxc-" | mxcVersion()
1556  	entries := listFiles(cacheRoot, "")
1557  	if entries == nil {
1558  		writeStr(2, "cache is empty\n")
1559  		return
1560  	}
1561  	for _, e := range entries {
1562  		if hasPrefix(e, "mxc-") && e != currentDir {
1563  			writeStr(2, "  remove old: " | e | "\n")
1564  			fullPath := joinPath(cacheRoot, e)
1565  			pkgEntries := listFiles(fullPath, "")
1566  			if pkgEntries != nil {
1567  				for _, pe := range pkgEntries {
1568  					removeAll(joinPath(fullPath, pe))
1569  				}
1570  			}
1571  			os.Remove(fullPath)
1572  		}
1573  	}
1574  	if !rebuildAll {
1575  		writeStr(2, "update done\n")
1576  		return
1577  	}
1578  	writeStr(2, "rebuilding all cached packages...\n")
1579  	mpath := getMoxiePath()
1580  	root := getenv("MOXIEROOT")
1581  	if root == "" {
1582  		root = "."
1583  	}
1584  	cacheBase := pkgCacheDir()
1585  	pkgDirs := listFiles(cacheBase, "")
1586  	if pkgDirs == nil {
1587  		writeStr(2, "  no packages to rebuild\n")
1588  		return
1589  	}
1590  	triple := "x86_64-unknown-linux-musleabihf"
1591  	clang := "clang-21"
1592  	tmpdir := "/tmp/mxc-build"
1593  	mkdirAll(tmpdir, 0755)
1594  	registerBuiltins()
1595  	for _, pkgDir := range pkgDirs {
1596  		pkgPath := ""
1597  		for i := 0; i < len(pkgDir); i++ {
1598  			if pkgDir[i] == '_' {
1599  				pkgPath = pkgPath | "/"
1600  			} else {
1601  				pkgPath = pkgPath | string([]byte{pkgDir[i]})
1602  			}
1603  		}
1604  		writeStr(2, "  rebuild " | pkgPath | "\n")
1605  		pkg := discoverPkg(pkgPath, root)
1606  		if pkg == nil {
1607  			candidate := joinPath(mpath, pkgPath)
1608  			mxF := listFiles(candidate, ".mx")
1609  			if len(mxF) > 0 {
1610  				pkg = discoverPkg(pkgPath, root)
1611  			}
1612  		}
1613  		if pkg == nil {
1614  			writeStr(2, "    skip (source not found)\n")
1615  			continue
1616  		}
1617  		compilePath := pkg.path
1618  		if hasPrefix(compilePath, "/") || hasPrefix(compilePath, "./") || hasPrefix(compilePath, "../") {
1619  			compilePath = pkg.name
1620  		}
1621  		src := concatSources(pkg.dir, pkg.files)
1622  		ir := compileSource(src, compilePath, triple)
1623  		src = nil
1624  		llFile := joinPath(tmpdir, safeName(pkg.path) | ".ll")
1625  		writeFile(llFile, []byte(ir), 0644)
1626  		ir = ""
1627  		bcFile := joinPath(tmpdir, safeName(pkg.path) | ".bc")
1628  		rc := run([]string{clang, "--target=" | triple, "-emit-llvm", "-c", llFile, "-o", bcFile})
1629  		if rc != 0 {
1630  			writeStr(2, "    FAIL: clang for " | pkg.path | "\n")
1631  			continue
1632  		}
1633  		hash := pkgCacheKey(pkg.dir, pkg.files)
1634  		cached := cachedBcPath(cacheBase, pkg.path, hash, pkg.version)
1635  		cdir := joinPath(cacheBase, safeName(pkg.path))
1636  		mkdirAll(cdir, 0755)
1637  		data, rok := readFile(bcFile)
1638  		if rok {
1639  			writeFile(cached, data, 0644)
1640  		}
1641  		data = nil
1642  		regPkg := importRegistry[compilePath]
1643  		if regPkg != nil {
1644  			cachedMxh := cachedMxhPath(cacheBase, pkg.path, hash, pkg.version)
1645  			mxhData := generateMxh(regPkg)
1646  			writeFile(cachedMxh, []byte(mxhData), 0644)
1647  		}
1648  	}
1649  	writeStr(2, "rebuild done\n")
1650  }
1651  
1652  func main() {
1653  	builtinOnly = []string{"unsafe", "runtime", "moxie"}
1654  	args := getArgs()
1655  	if len(args) < 2 {
1656  		writeStr(2, "usage: mxc <build|version|cache|fetch|update> ...\n")
1657  		os.Exit(1)
1658  	}
1659  
1660  	cmd := args[1]
1661  	if cmd == "version" {
1662  		writeStr(1, "mxc " | mxcVersion() | "\n")
1663  		return
1664  	}
1665  	if cmd == "cache" {
1666  		if len(args) >= 3 && args[2] == "clean" {
1667  			cacheClean()
1668  			return
1669  		}
1670  		writeStr(2, "usage: mxc cache clean\n")
1671  		os.Exit(1)
1672  	}
1673  	if cmd == "fetch" {
1674  		cmdFetch()
1675  		return
1676  	}
1677  	if cmd == "update" {
1678  		rebuildAll := false
1679  		for ai := int32(2); ai < int32(len(args)); ai++ {
1680  			if args[ai] == "--all" {
1681  				rebuildAll = true
1682  			}
1683  		}
1684  		cmdUpdate(rebuildAll)
1685  		return
1686  	}
1687  	if cmd != "build" {
1688  		writeStr(2, "usage: mxc <build|version|cache|fetch|update> ...\n")
1689  		os.Exit(1)
1690  	}
1691  
1692  	outpath := ""
1693  	pkgPath := ""
1694  	printIR := false
1695  	forceRebuild := false
1696  	i := int32(2)
1697  	for i < int32(len(args)) {
1698  		if args[i] == "-o" && i+1 < int32(len(args)) {
1699  			outpath = args[i+1]
1700  			i += 2
1701  		} else if args[i] == "-print-ir" {
1702  			printIR = true
1703  			i++
1704  		} else if args[i] == "-a" {
1705  			forceRebuild = true
1706  			i++
1707  		} else {
1708  			pkgPath = args[i]
1709  			i++
1710  		}
1711  	}
1712  	if pkgPath == "" {
1713  		fatal("no package specified")
1714  	}
1715  	if outpath == "" {
1716  		outpath = pathBase(pkgPath)
1717  	}
1718  
1719  	root := getenv("MOXIEROOT")
1720  	if root == "" {
1721  		root = "."
1722  	}
1723  	triple := "x86_64-unknown-linux-musleabihf"
1724  	clang := "clang-21"
1725  
1726  	if hasPrefix(pkgPath, "./") || hasPrefix(pkgPath, "../") || pkgPath == "." {
1727  		absDir := pkgPath
1728  		if absDir == "." {
1729  			absDir = ""
1730  		}
1731  		globalModPrefix, globalModDir = findModuleRoot(absDir)
1732  	}
1733  
1734  	registerBuiltins()
1735  	pkgs := resolveAll(pkgPath, root)
1736  	if len(pkgs) == 0 {
1737  		fatal("no packages to compile")
1738  	}
1739  
1740  	tmpdir := "/tmp/mxc-build"
1741  	mkdirAll(tmpdir, 0755)
1742  
1743  	cacheBase := pkgCacheDir()
1744  	var allBcFiles []string
1745  
1746  	for _, pkg := range pkgs {
1747  		compilePath := pkg.path
1748  		if hasPrefix(compilePath, "/") || hasPrefix(compilePath, "./") || hasPrefix(compilePath, "../") {
1749  			compilePath = pkg.name
1750  		}
1751  
1752  		hash := pkgCacheKey(pkg.dir, pkg.files)
1753  		cached := cachedBcPath(cacheBase, pkg.path, hash, pkg.version)
1754  		cachedMxh := cachedMxhPath(cacheBase, pkg.path, hash, pkg.version)
1755  
1756  		_, bcOk := readFile(cached)
1757  		_, mxhOk := readFile(cachedMxh)
1758  		if bcOk && mxhOk && !forceRebuild {
1759  			writeStr(2, "  cached " | pkg.path)
1760  			if pkg.version != "" {
1761  				writeStr(2, " @ " | pkg.version)
1762  			}
1763  			writeStr(2, "\n")
1764  			loadMxh(cachedMxh)
1765  			allBcFiles = append(allBcFiles, cached)
1766  			for _, bf := range pkg.bcfiles {
1767  				allBcFiles = append(allBcFiles, joinPath(pkg.dir, bf))
1768  			}
1769  			continue
1770  		}
1771  
1772  		if isStdlib(pkg.path) && bcOk && !forceRebuild {
1773  			writeStr(2, "  cached " | pkg.path | "\n")
1774  			src := concatSources(pkg.dir, pkg.files)
1775  			TypeCheckOnly(src, compilePath)
1776  			src = nil
1777  			regPkg := importRegistry[compilePath]
1778  			if regPkg != nil {
1779  				cacheDir := joinPath(cacheBase, safeName(pkg.path))
1780  				mkdirAll(cacheDir, 0755)
1781  				mxhData := generateMxh(regPkg)
1782  				writeFile(cachedMxh, []byte(mxhData), 0644)
1783  			}
1784  			allBcFiles = append(allBcFiles, cached)
1785  			continue
1786  		}
1787  
1788  		writeStr(2, "  compile " | pkg.path | "\n")
1789  		src := concatSources(pkg.dir, pkg.files)
1790  		ir := compileSource(src, compilePath, triple)
1791  		src = nil
1792  		if printIR {
1793  			writeStr(1, ir)
1794  		}
1795  		llFile := joinPath(tmpdir, safeName(pkg.path) | ".ll")
1796  		writeFile(llFile, []byte(ir), 0644)
1797  		ir = ""
1798  		bcFile := joinPath(tmpdir, safeName(pkg.path) | ".bc")
1799  		rc := run([]string{clang, "--target=" | triple, "-emit-llvm", "-c", llFile, "-o", bcFile})
1800  		if rc != 0 {
1801  			fatal("clang failed for " | pkg.path)
1802  		}
1803  		cacheDir := joinPath(cacheBase, safeName(pkg.path))
1804  		mkdirAll(cacheDir, 0755)
1805  		data, rok := readFile(bcFile)
1806  		if rok {
1807  			writeFile(cached, data, 0644)
1808  		}
1809  		data = nil
1810  		regPkg := importRegistry[compilePath]
1811  		if regPkg != nil {
1812  			mxhData := generateMxh(regPkg)
1813  			writeFile(cachedMxh, []byte(mxhData), 0644)
1814  		}
1815  		allBcFiles = append(allBcFiles, bcFile)
1816  		for _, cf := range pkg.cfiles {
1817  			cPath := joinPath(pkg.dir, cf)
1818  			cbcFile := joinPath(tmpdir, safeName(pkg.path) | "_" | cf | ".bc")
1819  			rc = run([]string{clang, "--target=" | triple, "-emit-llvm", "-c", cPath, "-o", cbcFile})
1820  			if rc != 0 {
1821  				fatal("clang failed for " | cf)
1822  			}
1823  			allBcFiles = append(allBcFiles, cbcFile)
1824  		}
1825  		for _, bf := range pkg.bcfiles {
1826  			allBcFiles = append(allBcFiles, joinPath(pkg.dir, bf))
1827  		}
1828  	}
1829  
1830  	loadSysroot(joinPath(root, "_sysroot.env"))
1831  
1832  	sysroot := joinPath(root, "sysroot")
1833  	runtimeBc := getenv("RUNTIME_BC")
1834  	if runtimeBc == "" {
1835  		runtimeBc = joinPath(sysroot, "runtime.bc")
1836  		_, rok := readFile(runtimeBc)
1837  		if !rok {
1838  			runtimeBc = joinPath(root, "_runtime.bc")
1839  		}
1840  	}
1841  	muslDir := getenv("MUSL_DIR")
1842  	if muslDir == "" {
1843  		muslDir = joinPath(sysroot, "musl")
1844  	}
1845  	comprtLib := getenv("COMPRT_LIB")
1846  	if comprtLib == "" {
1847  		comprtLib = joinPath(joinPath(sysroot, "compiler-rt"), "lib.a")
1848  	}
1849  	bdwgcLib := getenv("BDWGC_LIB")
1850  	if bdwgcLib == "" {
1851  		bdwgcLib = joinPath(joinPath(sysroot, "bdwgc"), "lib.a")
1852  	}
1853  	muslLib := getenv("MUSL_LIB")
1854  	if muslLib == "" {
1855  		muslLib = joinPath(joinPath(sysroot, "musl"), "lib.a")
1856  	}
1857  
1858  	writeStr(2, "  generate initAll\n")
1859  	var initCalls string
1860  	for _, pkg := range pkgs {
1861  		compilePath := pkg.path
1862  		if hasPrefix(compilePath, "/") || hasPrefix(compilePath, "./") || hasPrefix(compilePath, "../") {
1863  			compilePath = pkg.name
1864  		}
1865  		if compilePath == "main" {
1866  			continue
1867  		}
1868  		initSym := irGlobalSymbol(compilePath, "main")
1869  		initCalls = initCalls | "  call void " | initSym | "(ptr %context)\n"
1870  	}
1871  	initAllIR := "\ndefine void @runtime.initAll(ptr %context) {\nentry:\n" | initCalls | "  ret void\n}\n"
1872  	declared := map[string]bool{}
1873  	for _, pkg := range pkgs {
1874  		compilePath := pkg.path
1875  		if hasPrefix(compilePath, "/") || hasPrefix(compilePath, "./") || hasPrefix(compilePath, "../") {
1876  			compilePath = pkg.name
1877  		}
1878  		if compilePath == "main" {
1879  			continue
1880  		}
1881  		declSym := irGlobalSymbol(compilePath, "main")
1882  		if declared[declSym] {
1883  			continue
1884  		}
1885  		declared[declSym] = true
1886  		initAllIR = initAllIR | "declare void " | declSym | "(ptr)\n"
1887  	}
1888  	initAllFile := joinPath(tmpdir, "initall.ll")
1889  	writeFile(initAllFile, []byte(initAllIR), 0644)
1890  	initAllBc := joinPath(tmpdir, "initall.bc")
1891  	rc := run([]string{clang, "--target=" | triple, "-emit-llvm", "-c", initAllFile, "-o", initAllBc})
1892  	if rc != 0 {
1893  		fatal("clang failed for initall")
1894  	}
1895  
1896  	writeStr(2, "  merge bitcode\n")
1897  	llvmLink := "llvm-link-21"
1898  	mergedBc := joinPath(tmpdir, "merged.bc")
1899  	linkArgs := []string{llvmLink, "-o", mergedBc, runtimeBc}
1900  	shimsBc := joinPath(sysroot, "hashmap_shims.bc")
1901  	linkArgs = append(linkArgs, shimsBc)
1902  	runtimeGoBc := joinPath(sysroot, "runtime_go.bc")
1903  	if _, gok := readFile(runtimeGoBc); gok {
1904  		overrideArg := "--override=" | runtimeGoBc
1905  		linkArgs = append(linkArgs, overrideArg)
1906  	}
1907  	linkArgs = append(linkArgs, allBcFiles...)
1908  	linkArgs = append(linkArgs, initAllBc)
1909  	rc = run(linkArgs)
1910  	if rc != 0 {
1911  		fatal("llvm-link failed")
1912  	}
1913  
1914  	writeStr(2, "  link\n")
1915  	objFiles := getenv("OBJ_FILES")
1916  	if objFiles == "" {
1917  		objEntries := listFiles(joinPath(sysroot, "obj"), ".bc")
1918  		if objEntries != nil {
1919  			for _, e := range objEntries {
1920  				objFiles = objFiles | joinPath(joinPath(sysroot, "obj"), e) | " "
1921  			}
1922  		}
1923  	}
1924  	linker := "ld.lld-21"
1925  	ldArgs := []string{linker, "--gc-sections", "-o", outpath,
1926  		joinPath(muslDir, "crt1.o"),
1927  		mergedBc}
1928  	for _, f := range splitBySpace(objFiles) {
1929  		if f != "" {
1930  			ldArgs = append(ldArgs, f)
1931  		}
1932  	}
1933  	ldArgs = append(ldArgs, comprtLib, bdwgcLib, muslLib, "--lto-O0", "-z", "stack-size=67108864")
1934  	rc = run(ldArgs)
1935  	if rc != 0 {
1936  		fatal("linker failed")
1937  	}
1938  	writeStr(2, "  -> " | outpath | "\n")
1939  }
1940