main.go raw

   1  //go:build ignore
   2  
   3  package main
   4  
   5  import (
   6  	"os"
   7  	"unsafe"
   8  )
   9  
  10  //export mxc_run
  11  func cRun(argv unsafe.Pointer, argc int32) int32
  12  
  13  //export mxc_getenv
  14  func cGetenv(name unsafe.Pointer, nameLen int32, buf unsafe.Pointer, bufCap int32) int32
  15  
  16  //export write
  17  func cWrite(fd int32, buf unsafe.Pointer, count uint) int
  18  
  19  func writeStr(fd int32, s string) {
  20  	if len(s) > 0 {
  21  		cWrite(fd, unsafe.Pointer(&[]byte(s)[0]), uint(len(s)))
  22  	}
  23  }
  24  
  25  func fatal(msg string) {
  26  	writeStr(2, "mxc: ")
  27  	writeStr(2, msg)
  28  	writeStr(2, "\n")
  29  	os.Exit(1)
  30  }
  31  
  32  func getenv(name string) string {
  33  	b := []byte(name)
  34  	buf := make([]byte, 4096)
  35  	n := cGetenv(unsafe.Pointer(&b[0]), int32(len(b)), unsafe.Pointer(&buf[0]), int32(len(buf)))
  36  	if n <= 0 {
  37  		return ""
  38  	}
  39  	return string(buf[:n])
  40  }
  41  
  42  func getArgs() []string {
  43  	data, err := os.ReadFile("/proc/self/cmdline")
  44  	if err != nil {
  45  		return nil
  46  	}
  47  	var args []string
  48  	start := 0
  49  	for i := 0; i < len(data); i++ {
  50  		if data[i] == 0 {
  51  			if i > start {
  52  				args = append(args, string(data[start:i]))
  53  			}
  54  			start = i + 1
  55  		}
  56  	}
  57  	if start < len(data) {
  58  		args = append(args, string(data[start:]))
  59  	}
  60  	return args
  61  }
  62  
  63  func listFiles(dir, ext string) []string {
  64  	entries, err := os.ReadDir(dir)
  65  	if err != nil {
  66  		return nil
  67  	}
  68  	var result []string
  69  	for _, e := range entries {
  70  		name := e.Name()
  71  		if hasSuffix(name, ext) {
  72  			result = append(result, name)
  73  		}
  74  	}
  75  	return result
  76  }
  77  
  78  func run(args []string) int32 {
  79  	argv := make([]unsafe.Pointer, len(args)+1)
  80  	cstrs := make([][]byte, len(args))
  81  	for i, a := range args {
  82  		b := make([]byte, len(a)+1)
  83  		copy(b, a)
  84  		b[len(a)] = 0
  85  		cstrs[i] = b
  86  		argv[i] = unsafe.Pointer(&b[0])
  87  	}
  88  	return cRun(unsafe.Pointer(&argv[0]), int32(len(args)))
  89  }
  90  
  91  func splitLines(s string) []string {
  92  	var result []string
  93  	start := 0
  94  	for i := 0; i < len(s); i++ {
  95  		if s[i] == '\n' {
  96  			if i > start {
  97  				result = append(result, s[start:i])
  98  			}
  99  			start = i + 1
 100  		}
 101  	}
 102  	if start < len(s) {
 103  		result = append(result, s[start:])
 104  	}
 105  	return result
 106  }
 107  
 108  func hasPrefix(s, prefix string) bool {
 109  	return len(s) >= len(prefix) && s[:len(prefix)] == prefix
 110  }
 111  
 112  func hasSuffix(s, suffix string) bool {
 113  	return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
 114  }
 115  
 116  func trimSpace(s string) string {
 117  	i := 0
 118  	for i < len(s) && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r') {
 119  		i++
 120  	}
 121  	j := len(s)
 122  	for j > i && (s[j-1] == ' ' || s[j-1] == '\t' || s[j-1] == '\n' || s[j-1] == '\r') {
 123  		j--
 124  	}
 125  	return s[i:j]
 126  }
 127  
 128  func joinPath(a, b string) string {
 129  	if len(a) == 0 {
 130  		return b
 131  	}
 132  	if a[len(a)-1] == '/' {
 133  		return a + b
 134  	}
 135  	return a + "/" + b
 136  }
 137  
 138  func pathBase(path string) string {
 139  	for i := len(path) - 1; i >= 0; i-- {
 140  		if path[i] == '/' {
 141  			return path[i+1:]
 142  		}
 143  	}
 144  	return path
 145  }
 146  
 147  func joinStrings(ss []string, sep string) string {
 148  	if len(ss) == 0 {
 149  		return ""
 150  	}
 151  	n := 0
 152  	for _, s := range ss {
 153  		n += len(s)
 154  	}
 155  	n += len(sep) * (len(ss) - 1)
 156  	buf := make([]byte, 0, n)
 157  	for i, s := range ss {
 158  		if i > 0 {
 159  			buf = append(buf, sep...)
 160  		}
 161  		buf = append(buf, s...)
 162  	}
 163  	return string(buf)
 164  }
 165  
 166  func appendUniq(ss []string, s string) []string {
 167  	for _, x := range ss {
 168  		if x == s {
 169  			return ss
 170  		}
 171  	}
 172  	return append(ss, s)
 173  }
 174  
 175  func loadSysroot(path string) {
 176  	data, err := os.ReadFile(path)
 177  	if err != nil {
 178  		return
 179  	}
 180  	for _, line := range splitLines(string(data)) {
 181  		line = trimSpace(line)
 182  		if len(line) == 0 || line[0] == '#' {
 183  			continue
 184  		}
 185  		eq := -1
 186  		for i := 0; i < len(line); i++ {
 187  			if line[i] == '=' {
 188  				eq = i
 189  				break
 190  			}
 191  		}
 192  		if eq < 0 {
 193  			continue
 194  		}
 195  		key := line[:eq]
 196  		val := line[eq+1:]
 197  		os.Setenv(key, val)
 198  	}
 199  }
 200  
 201  func splitBySpace(s string) []string {
 202  	var result []string
 203  	start := 0
 204  	inWord := false
 205  	for i := 0; i < len(s); i++ {
 206  		if s[i] == ' ' || s[i] == '\t' {
 207  			if inWord {
 208  				result = append(result, s[start:i])
 209  				inWord = false
 210  			}
 211  		} else {
 212  			if !inWord {
 213  				start = i
 214  				inWord = true
 215  			}
 216  		}
 217  	}
 218  	if inWord {
 219  		result = append(result, s[start:])
 220  	}
 221  	return result
 222  }
 223  
 224  func safeName(pkg string) string {
 225  	b := make([]byte, len(pkg))
 226  	for i := 0; i < len(pkg); i++ {
 227  		if pkg[i] == '/' {
 228  			b[i] = '_'
 229  		} else {
 230  			b[i] = pkg[i]
 231  		}
 232  	}
 233  	return string(b)
 234  }
 235  
 236  func extractPkgName(data []byte) string {
 237  	for i := 0; i < len(data); i++ {
 238  		if i+8 < len(data) && string(data[i:i+8]) == "package " {
 239  			j := i + 8
 240  			for j < len(data) && data[j] != '\n' && data[j] != '\r' && data[j] != ' ' {
 241  				j++
 242  			}
 243  			return string(data[i+8 : j])
 244  		}
 245  		if data[i] == '\n' {
 246  			continue
 247  		}
 248  		if data[i] == '/' && i+1 < len(data) && data[i+1] == '/' {
 249  			for i < len(data) && data[i] != '\n' {
 250  				i++
 251  			}
 252  			continue
 253  		}
 254  		break
 255  	}
 256  	return "main"
 257  }
 258  
 259  func extractImports(data []byte) []string {
 260  	var imports []string
 261  	lines := splitLines(string(data))
 262  	inBlock := false
 263  	for _, line := range lines {
 264  		line = trimSpace(line)
 265  		if line == "import (" {
 266  			inBlock = true
 267  			continue
 268  		}
 269  		if inBlock && line == ")" {
 270  			inBlock = false
 271  			continue
 272  		}
 273  		if inBlock {
 274  			imp := extractQuoted(line)
 275  			if imp != "" {
 276  				imports = appendUniq(imports, imp)
 277  			}
 278  		}
 279  		if hasPrefix(line, "import \"") {
 280  			imp := extractQuoted(line[7:])
 281  			if imp != "" {
 282  				imports = appendUniq(imports, imp)
 283  			}
 284  		}
 285  	}
 286  	return imports
 287  }
 288  
 289  func extractQuoted(s string) string {
 290  	start := -1
 291  	for i := 0; i < len(s); i++ {
 292  		if s[i] == '"' {
 293  			if start < 0 {
 294  				start = i + 1
 295  			} else {
 296  				return s[start:i]
 297  			}
 298  		}
 299  	}
 300  	return ""
 301  }
 302  
 303  func concatSources(dir string, files []string) []byte {
 304  	imports := map[string]bool{}
 305  	var bodies [][]byte
 306  	pkgName := ""
 307  	for _, f := range files {
 308  		data, err := os.ReadFile(joinPath(dir, f))
 309  		if err != nil {
 310  			continue
 311  		}
 312  		if pkgName == "" {
 313  			pkgName = extractPkgName(data)
 314  		}
 315  		lines := splitLines(string(data))
 316  		var body []string
 317  		i := 0
 318  		for i < len(lines) {
 319  			line := trimSpace(lines[i])
 320  			if hasPrefix(line, "package ") {
 321  				i++
 322  				continue
 323  			}
 324  			if line == "import (" {
 325  				i++
 326  				for i < len(lines) {
 327  					imp := trimSpace(lines[i])
 328  					i++
 329  					if imp == ")" {
 330  						break
 331  					}
 332  					if imp != "" {
 333  						imports[imp] = true
 334  					}
 335  				}
 336  				continue
 337  			}
 338  			if hasPrefix(line, "import ") && !hasPrefix(line, "import (") {
 339  				imp := line[7:]
 340  				imports[imp] = true
 341  				i++
 342  				continue
 343  			}
 344  			body = append(body, lines[i])
 345  			i++
 346  		}
 347  		bodies = append(bodies, []byte(joinStrings(body, "\n")))
 348  	}
 349  	var out []byte
 350  	out = append(out, []byte("package "+pkgName+"\n")...)
 351  	if len(imports) > 0 {
 352  		out = append(out, []byte("import (\n")...)
 353  		for imp := range imports {
 354  			out = append(out, '\t')
 355  			out = append(out, []byte(imp)...)
 356  			out = append(out, '\n')
 357  		}
 358  		out = append(out, []byte(")\n")...)
 359  	}
 360  	for _, b := range bodies {
 361  		out = append(out, b...)
 362  		out = append(out, '\n')
 363  	}
 364  	return out
 365  }
 366  
 367  func shouldSkipFile(name string) bool {
 368  	if hasSuffix(name, "_test.go") || hasSuffix(name, "_test.mx") {
 369  		return true
 370  	}
 371  	skip := []string{"_wasm"}
 372  	base := name
 373  	if hasSuffix(base, ".go") {
 374  		base = base[:len(base)-3]
 375  	} else if hasSuffix(base, ".mx") {
 376  		base = base[:len(base)-3]
 377  	}
 378  	for _, s := range skip {
 379  		if hasSuffix(base, s) {
 380  			return true
 381  		}
 382  	}
 383  	return false
 384  }
 385  
 386  func hasBuildConstraint(data []byte, forbidden []string) bool {
 387  	for i := 0; i < len(data) && i < 512; i++ {
 388  		if data[i] == 'p' {
 389  			return false
 390  		}
 391  		if i+10 < len(data) && string(data[i:i+10]) == "//go:build " {
 392  			line := ""
 393  			for j := i + 10; j < len(data) && data[j] != '\n'; j++ {
 394  				line = line + string(data[j])
 395  			}
 396  			for _, f := range forbidden {
 397  				if line == f || hasPrefix(line, f+" ") || hasPrefix(line, f+"\t") {
 398  					return true
 399  				}
 400  			}
 401  			return false
 402  		}
 403  		if data[i] == '\n' {
 404  			continue
 405  		}
 406  		if data[i] == '/' && i+1 < len(data) && data[i+1] == '/' {
 407  			for i < len(data) && data[i] != '\n' {
 408  				i++
 409  			}
 410  			continue
 411  		}
 412  	}
 413  	return false
 414  }
 415  
 416  type pkgInfo struct {
 417  	path    string
 418  	name    string
 419  	dir     string
 420  	files   []string
 421  	cfiles  []string
 422  	imports []string
 423  }
 424  
 425  func discoverPkg(pkgPath, root string) *pkgInfo {
 426  	var dir string
 427  	if hasPrefix(pkgPath, "/") || hasPrefix(pkgPath, "./") || hasPrefix(pkgPath, "../") {
 428  		dir = pkgPath
 429  	} else {
 430  		dir = joinPath(root, joinPath("src", pkgPath))
 431  	}
 432  	goFiles := listFiles(dir, ".go")
 433  	mxFiles := listFiles(dir, ".mx")
 434  	cFiles := listFiles(dir, ".c")
 435  	allMx := append(mxFiles, goFiles...)
 436  	forbidden := []string{"wasm", "js"}
 437  	var files []string
 438  	for _, f := range allMx {
 439  		if shouldSkipFile(f) {
 440  			continue
 441  		}
 442  		data, err := os.ReadFile(joinPath(dir, f))
 443  		if err != nil {
 444  			continue
 445  		}
 446  		if hasBuildConstraint(data, forbidden) {
 447  			continue
 448  		}
 449  		files = append(files, f)
 450  	}
 451  	if len(files) == 0 {
 452  		return nil
 453  	}
 454  	var allImports []string
 455  	pkgName := ""
 456  	for _, f := range files {
 457  		data, err := os.ReadFile(joinPath(dir, f))
 458  		if err != nil {
 459  			continue
 460  		}
 461  		if pkgName == "" {
 462  			pkgName = extractPkgName(data)
 463  		}
 464  		for _, imp := range extractImports(data) {
 465  			allImports = appendUniq(allImports, imp)
 466  		}
 467  	}
 468  	return &pkgInfo{
 469  		path:    pkgPath,
 470  		name:    pkgName,
 471  		dir:     dir,
 472  		files:   files,
 473  		cfiles:  cFiles,
 474  		imports: allImports,
 475  	}
 476  }
 477  
 478  func resolveAll(rootPkg string, root string) []*pkgInfo {
 479  	visited := map[string]bool{}
 480  	var order []*pkgInfo
 481  	var walk func(string)
 482  	walk = func(path string) {
 483  		if visited[path] {
 484  			return
 485  		}
 486  		visited[path] = true
 487  		if path == "unsafe" || path == "moxie" || path == "C" {
 488  			return
 489  		}
 490  		if hasPrefix(path, "runtime/") {
 491  			return
 492  		}
 493  		pkg := discoverPkg(path, root)
 494  		if pkg == nil {
 495  			writeStr(2, "mxc: cannot find package: "+path+"\n")
 496  			return
 497  		}
 498  		for _, imp := range pkg.imports {
 499  			walk(imp)
 500  		}
 501  		order = append(order, pkg)
 502  	}
 503  	walk(rootPkg)
 504  	return order
 505  }
 506  
 507  func registerBuiltins() {
 508  	initUniverse()
 509  	ensureImportRegistry()
 510  	importRegistry = map[string]*TCPackage{}
 511  	unsafePkg := NewTCPackage("unsafe", "unsafe")
 512  	unsafePkg.Scope().Insert(NewTypeName(unsafePkg, "Pointer", Typ[UnsafePointer]))
 513  	unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "Sizeof", parseSignatureDesc("interface{}->int32")))
 514  	unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "Offsetof", parseSignatureDesc("interface{}->int32")))
 515  	unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "Alignof", parseSignatureDesc("interface{}->int32")))
 516  	unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "Slice", parseSignatureDesc("ptr,int32->[]uint8")))
 517  	unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "SliceData", parseSignatureDesc("[]uint8->ptr")))
 518  	importRegistry["unsafe"] = unsafePkg
 519  	importRegistry["moxie"] = NewTCPackage("moxie", "moxie")
 520  }
 521  
 522  func compileSource(src []byte, pkgName, triple string) string {
 523  	ir := CompileToIR(src, pkgName, triple)
 524  	if len(ir) > 0 && ir[0] == ';' {
 525  		writeStr(2, ir)
 526  		fatal("compile error for " + pkgName)
 527  	}
 528  	if len(ir) == 0 {
 529  		fatal("empty IR for " + pkgName)
 530  	}
 531  	return ir
 532  }
 533  
 534  func main() {
 535  	args := getArgs()
 536  	if len(args) >= 2 {
 537  		cmd := args[1]
 538  		if cmd == "version" {
 539  			writeStr(1, "mxc 1.6.1\n")
 540  			return
 541  		}
 542  		if cmd != "build" || len(args) < 3 {
 543  			writeStr(2, "usage: mxc build [-o output] <package>\n")
 544  			os.Exit(1)
 545  		}
 546  	} else {
 547  		writeStr(2, "usage: mxc build [-o output] <package>\n")
 548  		os.Exit(1)
 549  	}
 550  
 551  	outpath := ""
 552  	pkgPath := ""
 553  	printIR := false
 554  	i := 2
 555  	for i < len(args) {
 556  		if args[i] == "-o" && i+1 < len(args) {
 557  			outpath = args[i+1]
 558  			i += 2
 559  		} else if args[i] == "-print-ir" {
 560  			printIR = true
 561  			i++
 562  		} else {
 563  			pkgPath = args[i]
 564  			i++
 565  		}
 566  	}
 567  	if pkgPath == "" {
 568  		fatal("no package specified")
 569  	}
 570  	if outpath == "" {
 571  		outpath = pathBase(pkgPath)
 572  	}
 573  
 574  	root := getenv("MOXIEROOT")
 575  	if root == "" {
 576  		root = "."
 577  	}
 578  	triple := "x86_64-unknown-linux-musleabihf"
 579  	clang := "clang-21"
 580  
 581  	registerBuiltins()
 582  	pkgs := resolveAll(pkgPath, root)
 583  	if len(pkgs) == 0 {
 584  		fatal("no packages to compile")
 585  	}
 586  
 587  	tmpdir := "/tmp/mxc-build"
 588  	os.MkdirAll(tmpdir, 0755)
 589  
 590  	var userBcFiles []string
 591  
 592  	for _, pkg := range pkgs {
 593  		writeStr(2, "  compile "+pkg.path+"\n")
 594  		src := concatSources(pkg.dir, pkg.files)
 595  		compilePath := pkg.path
 596  		if hasPrefix(compilePath, "/") || hasPrefix(compilePath, "./") || hasPrefix(compilePath, "../") {
 597  			compilePath = pkg.name
 598  		}
 599  		ir := compileSource(src, compilePath, triple)
 600  		if printIR {
 601  			writeStr(1, ir)
 602  		}
 603  		llFile := joinPath(tmpdir, safeName(pkg.path)+".ll")
 604  		os.WriteFile(llFile, []byte(ir), 0644)
 605  		bcFile := joinPath(tmpdir, safeName(pkg.path)+".bc")
 606  		rc := run([]string{clang, "--target="+triple, "-emit-llvm", "-c", llFile, "-o", bcFile})
 607  		if rc != 0 {
 608  			fatal("clang failed for " + pkg.path)
 609  		}
 610  		userBcFiles = append(userBcFiles, bcFile)
 611  	}
 612  
 613  	loadSysroot(joinPath(root, "_sysroot.env"))
 614  
 615  	runtimeBc := getenv("RUNTIME_BC")
 616  	if runtimeBc == "" {
 617  		runtimeBc = joinPath(root, "_runtime.bc")
 618  	}
 619  	muslDir := getenv("MUSL_DIR")
 620  	if muslDir == "" {
 621  		fatal("MUSL_DIR not set - run build-self.sh first")
 622  	}
 623  
 624  	writeStr(2, "  generate initAll\n")
 625  	var initCalls string
 626  	for _, pkg := range pkgs {
 627  		compilePath := pkg.path
 628  		if hasPrefix(compilePath, "/") || hasPrefix(compilePath, "./") || hasPrefix(compilePath, "../") {
 629  			compilePath = pkg.name
 630  		}
 631  		initCalls = initCalls + "  call void @" + compilePath + ".init(ptr %context)\n"
 632  	}
 633  	initAllIR := "define void @runtime.initAll(ptr %context) {\nentry:\n" + initCalls + "  ret void\n}\n"
 634  	for _, pkg := range pkgs {
 635  		compilePath := pkg.path
 636  		if hasPrefix(compilePath, "/") || hasPrefix(compilePath, "./") || hasPrefix(compilePath, "../") {
 637  			compilePath = pkg.name
 638  		}
 639  		initAllIR = initAllIR + "declare void @" + compilePath + ".init(ptr)\n"
 640  	}
 641  	initAllFile := joinPath(tmpdir, "initall.ll")
 642  	os.WriteFile(initAllFile, []byte(initAllIR), 0644)
 643  	initAllBc := joinPath(tmpdir, "initall.bc")
 644  	rc := run([]string{clang, "--target="+triple, "-emit-llvm", "-c", initAllFile, "-o", initAllBc})
 645  	if rc != 0 {
 646  		fatal("clang failed for initall")
 647  	}
 648  
 649  	writeStr(2, "  merge bitcode\n")
 650  	llvmLink := "llvm-link-21"
 651  	mergedBc := joinPath(tmpdir, "merged.bc")
 652  	linkArgs := []string{llvmLink, "-o", mergedBc, runtimeBc}
 653  	linkArgs = append(linkArgs, userBcFiles...)
 654  	linkArgs = append(linkArgs, initAllBc)
 655  	rc = run(linkArgs)
 656  	if rc != 0 {
 657  		fatal("llvm-link failed")
 658  	}
 659  
 660  	writeStr(2, "  link\n")
 661  	comprtLib := getenv("COMPRT_LIB")
 662  	bdwgcLib := getenv("BDWGC_LIB")
 663  	muslLib := getenv("MUSL_LIB")
 664  	objFiles := getenv("OBJ_FILES")
 665  	linker := "ld.lld-21"
 666  	ldArgs := []string{linker, "--gc-sections", "-o", outpath,
 667  		joinPath(muslDir, "crt1.o"),
 668  		mergedBc}
 669  	for _, f := range splitBySpace(objFiles) {
 670  		if f != "" {
 671  			ldArgs = append(ldArgs, f)
 672  		}
 673  	}
 674  	ldArgs = append(ldArgs, comprtLib, bdwgcLib, muslLib, "--lto-O0")
 675  	rc = run(ldArgs)
 676  	if rc != 0 {
 677  		fatal("linker failed")
 678  	}
 679  	writeStr(2, "  -> "+outpath+"\n")
 680  }
 681