main.mx raw
1 package main
2
3 import (
4 "os"
5 "runtime"
6 "unsafe"
7
8 "git.smesh.lol/moxie/pkg/mxutil"
9 . "git.smesh.lol/moxie/pkg/types"
10 )
11
12 // buildState holds all build-configuration and persistent state that
13 // outlives individual package compilations. Allocated once at init and
14 // never reassigned; fields are mutated through the pointer.
15 type buildState struct {
16 selfHash string
17 globalModPrefix string
18 globalModDir string
19 globalReplaces map[string]string
20 buildJobs int32
21 quietDiscover bool
22 wasmRtPkgs map[string]bool
23 builtinOnly []string
24 pkgKeyMemo map[string]string
25 tlsProbeVar int32
26 }
27
28 var bst *buildState
29
30 func initBuildState() {
31 if bst == nil {
32 bst = &buildState{}
33 }
34 }
35
36 //export mxc_run
37 func cRun(argv unsafe.Pointer, argc int32) (n int32)
38
39 //export mxc_run_async
40 func cRunAsync(argv unsafe.Pointer, argc int32) (n int32)
41
42 //export mxc_fork
43 func cFork() (n int32)
44
45 //export mxc_wait_any
46 func cWaitAny() (n int32)
47
48 //export mxc_getenv
49 func cGetenv(name unsafe.Pointer, nameLen int32, buf unsafe.Pointer, bufCap int32) (n int32)
50
51 //export mxc_listdir
52 func cListdir(dir unsafe.Pointer, dirLen int32, buf unsafe.Pointer, bufCap int32) (n int32)
53
54 //export mxc_writefile
55 func cWritefile(path unsafe.Pointer, pathLen int32, data unsafe.Pointer, dataLen int32, mode uint32) (n int32)
56
57 //export mxc_mkdir
58 func cMkdir(path unsafe.Pointer, pathLen int32, mode uint32) (n int32)
59
60 // Ring buffer FFI
61 //export mxc_ring_create
62 func cRingCreate(dataSize int32) (p unsafe.Pointer)
63 //export mxc_ring_destroy
64 func cRingDestroy(ring unsafe.Pointer)
65 //export mxc_ring_close
66 func cRingClose(ring unsafe.Pointer)
67 //export mxc_ring_closed
68 func cRingClosed(ring unsafe.Pointer) (n int32)
69 //export mxc_ring_send
70 func cRingSend(ring unsafe.Pointer, data unsafe.Pointer, dlen int32) (n int32)
71 //export mxc_ring_peek_len
72 func cRingPeekLen(ring unsafe.Pointer) (n int32)
73 //export mxc_ring_recv
74 func cRingRecv(ring unsafe.Pointer, buf unsafe.Pointer, bufcap int32) (n int32)
75
76 // Thread FFI
77 //export mxc_spawn_thread
78 func cSpawnThread(fn unsafe.Pointer, arg unsafe.Pointer, handleOut unsafe.Pointer, statusOut unsafe.Pointer) (n int32)
79 //export mxc_thread_alive
80 func cThreadAlive(statusPtr unsafe.Pointer) (n int32)
81 //export mxc_thread_join
82 func cThreadJoin(handlePtr unsafe.Pointer, statusPtr unsafe.Pointer)
83 //export mxc_usleep
84 func cUsleep(us int32)
85 //export mxc_spawn_worker
86 func cSpawnWorker(arg unsafe.Pointer, handleOut unsafe.Pointer, statusOut unsafe.Pointer) (n int32)
87 //export mxc_detect_tls
88 func cDetectTLS() (n int32)
89
90 // TLS probe: the C side spawns a thread that calls mxc_tls_probe_write,
91 // then the parent calls mxc_tls_probe_read. If the global is TLS, the
92 // child's write is invisible to the parent.
93
94 //export mxc_tls_probe_write
95 func tlsProbeWrite() {
96 bst.tlsProbeVar = 1
97 }
98
99 //export mxc_tls_probe_read
100 func tlsProbeRead() (n int32) {
101 return bst.tlsProbeVar
102 }
103
104 func writeFile(path string, data []byte, mode uint32) {
105 if len(path) == 0 {
106 return
107 }
108 cWritefile(unsafe.Pointer(unsafe.SliceData([]byte(path))), int32(len(path)),
109 unsafe.Pointer(unsafe.SliceData([]byte(data))), int32(len(data)), mode)
110 }
111
112 func mkdirAll(path string, mode uint32) {
113 if len(path) == 0 {
114 return
115 }
116 cMkdir(unsafe.Pointer(unsafe.SliceData([]byte(path))), int32(len(path)), mode)
117 }
118
119 func fatal(msg string) {
120 mxutil.WriteStr(2, "mxc: " | msg | "\n")
121 os.Exit(1)
122 }
123
124 func clangCompileToObj(clang, triple, bcFile string) (s string) {
125 oFile := bcFile | ".o"
126 rc := run([]string{clang, "--target=" | triple, "-c", bcFile, "-o", oFile})
127 if rc != 0 {
128 fatal("clang -c failed for " | bcFile)
129 }
130 return oFile
131 }
132
133 func getenv(name string) (s string) {
134 buf := []byte{:4096}
135 n := cGetenv(unsafe.Pointer(unsafe.SliceData([]byte(name))), int32(len(name)), unsafe.Pointer(unsafe.SliceData(buf)), int32(len(buf)))
136 if n <= 0 {
137 return ""
138 }
139 return string(buf[:n])
140 }
141
142
143 func listFiles(dir, ext string) (ss []string) {
144 buf := []byte{:65536}
145 n := cListdir(unsafe.Pointer(unsafe.SliceData([]byte(dir))), int32(len(dir)),
146 unsafe.Pointer(unsafe.SliceData(buf)), int32(len(buf)))
147 if n <= 0 {
148 return nil
149 }
150 // Count null-separated entries matching ext.
151 nc := int32(0)
152 start := int32(0)
153 for i := int32(0); i < n; i++ {
154 if buf[i] == 0 {
155 if mxutil.HasSuffix(string(buf[start:i]), ext) {
156 nc++
157 }
158 start = i + 1
159 }
160 }
161 if nc == 0 {
162 return nil
163 }
164 result := []string{:0:nc}
165 start = 0
166 for i := int32(0); i < n; i++ {
167 if buf[i] == 0 {
168 name := string(buf[start:i])
169 if mxutil.HasSuffix(name, ext) {
170 push(result, name)
171 }
172 start = i + 1
173 }
174 }
175 return result
176 }
177
178 func run(args []string) (n int32) {
179 argv := []unsafe.Pointer{:len(args) + 1}
180 cstrs := [][]byte{:len(args)}
181 for i, a := range args {
182 b := []byte{:len(a) + 1}
183 copy(b, a)
184 b[len(a)] = 0
185 cstrs[i] = b
186 argv[i] = unsafe.Pointer(unsafe.SliceData(b))
187 }
188 return cRun(unsafe.Pointer(unsafe.SliceData(argv)), int32(len(args)))
189 }
190
191 // runAsync forks+execs args without waiting. Returns child pid, or -1 on
192 // fork failure. The forked child copies the argv buffers, so parent-side
193 // lifetimes end at return.
194 func runAsync(args []string) (pid int32) {
195 argv := []unsafe.Pointer{:len(args) + 1}
196 cstrs := [][]byte{:len(args)}
197 for i, a := range args {
198 b := []byte{:len(a) + 1}
199 copy(b, a)
200 b[len(a)] = 0
201 cstrs[i] = b
202 argv[i] = unsafe.Pointer(unsafe.SliceData(b))
203 }
204 return cRunAsync(unsafe.Pointer(unsafe.SliceData(argv)), int32(len(args)))
205 }
206
207 // runPool runs cmds with at most jobs concurrent children. Returns false if
208 // any command exits nonzero (remaining in-flight children are drained, no
209 // new ones are dispatched).
210 func runPool(cmds [][]string, jobs int32) (ok bool) {
211 if jobs < 1 {
212 jobs = 1
213 }
214 next := int32(0)
215 running := int32(0)
216 failed := false
217 for {
218 for !failed && running < jobs && next < int32(len(cmds)) {
219 pid := runAsync(cmds[next])
220 if pid < 0 {
221 failed = true
222 break
223 }
224 running++
225 next++
226 }
227 if running == 0 {
228 break
229 }
230 r := cWaitAny()
231 if r == -1 {
232 return false
233 }
234 running--
235 if r < 0 {
236 failed = true
237 }
238 }
239 return !failed
240 }
241
242 func parseInt32(s string) (n int32) {
243 v := int32(0)
244 for i := int32(0); i < int32(len(s)); i++ {
245 if s[i] < '0' || s[i] > '9' {
246 return 0
247 }
248 v = v*10 + int32(s[i]-'0')
249 }
250 return v
251 }
252
253 func pathBase(path string) (s string) {
254 for i := int32(len(path)) - 1; i >= 0; i-- {
255 if path[i] == '/' {
256 return path[i+1:]
257 }
258 }
259 return path
260 }
261
262 func pathDir(path string) (s string) {
263 for i := int32(len(path)) - 1; i >= 0; i-- {
264 if path[i] == '/' {
265 if i == 0 {
266 return "/"
267 }
268 return path[:i]
269 }
270 }
271 return "."
272 }
273
274 func splitSlash(s string) (ss []string) {
275 var parts []string
276 start := int32(0)
277 for i := int32(0); i < int32(len(s)); i++ {
278 if s[i] == '/' {
279 push(parts, s[start:i])
280 start = i + 1
281 }
282 }
283 push(parts, s[start:])
284 return parts
285 }
286
287 func cleanPath(path string) (s string) {
288 parts := splitSlash(path)
289 var out []string
290 for _, p := range parts {
291 if p == "." || p == "" {
292 continue
293 }
294 if p == ".." && len(out) > 0 && out[len(out)-1] != ".." {
295 out = out[:len(out)-1]
296 } else {
297 push(out, p)
298 }
299 }
300 if len(out) == 0 {
301 return "."
302 }
303 result := out[0]
304 for i := 1; i < len(out); i++ {
305 result = result | "/" | out[i]
306 }
307 if len(path) > 0 && path[0] == '/' {
308 result = "/" | result
309 }
310 return result
311 }
312
313 func getMoxiePath() (s string) {
314 p := getenv("MOXIEPATH")
315 if p != "" {
316 return p
317 }
318 home := getenv("HOME")
319 if home == "" {
320 return "/tmp/moxie"
321 }
322 return mxutil.JoinPath(home, "moxie")
323 }
324
325 func mxcVersion() (s string) { return "1.9.10" }
326
327
328 func compilerHash() (s string) {
329 if bst.selfHash != "" {
330 return bst.selfHash
331 }
332 bin, ok := mxutil.ReadFile("/proc/self/exe")
333 if !ok {
334 bst.selfHash = "unknown"
335 return bst.selfHash
336 }
337 bst.selfHash = fnvHash(bin)
338 return bst.selfHash
339 }
340
341 func moxieCacheDir() (s string) {
342 return mxutil.JoinPath(mxutil.JoinPath(getMoxiePath(), "cache"), "mxc-" | mxcVersion() | "-" | compilerHash())
343 }
344
345
346 func parseReplaceLine(line string, baseDir string) (from string, to string, ok bool) {
347 arrow := int32(-1)
348 for k := int32(0); k < int32(len(line))-1; k++ {
349 if line[k] == '=' && line[k+1] == '>' {
350 arrow = k
351 break
352 }
353 }
354 if arrow < 0 {
355 return "", "", false
356 }
357 from := mxutil.TrimSpace(line[:arrow])
358 to := mxutil.TrimSpace(line[arrow+2:])
359 for fi := int32(0); fi < int32(len(from)); fi++ {
360 if from[fi] == ' ' {
361 from = from[:fi]
362 break
363 }
364 }
365 if mxutil.HasPrefix(to, "./") || mxutil.HasPrefix(to, "../") || mxutil.HasPrefix(to, "/") {
366 to = cleanPath(mxutil.JoinPath(baseDir, to))
367 }
368 return from, to, true
369 }
370
371 func findModuleRoot(startDir string) (modName string, dir string) {
372 dir = startDir
373 for {
374 data, ok := mxutil.ReadFile(mxutil.JoinPath(dir, "moxie.mod"))
375 if ok {
376 modName = ""
377 bst.globalReplaces = map[string]string{}
378 lines := mxutil.SplitLines(string(data))
379 inReplace := false
380 for _, line := range lines {
381 line = mxutil.TrimSpace(line)
382 if mxutil.HasPrefix(line, "module ") {
383 modName = mxutil.TrimSpace(line[7:])
384 continue
385 }
386 if line == "replace (" {
387 inReplace = true
388 continue
389 }
390 if inReplace {
391 if line == ")" {
392 inReplace = false
393 continue
394 }
395 from, to, ok2 := parseReplaceLine(line, dir)
396 if ok2 {
397 bst.globalReplaces[from] = to
398 }
399 continue
400 }
401 if mxutil.HasPrefix(line, "replace ") {
402 from, to, ok2 := parseReplaceLine(line[8:], dir)
403 if ok2 {
404 bst.globalReplaces[from] = to
405 }
406 }
407 }
408 if modName != "" {
409 return modName, dir
410 }
411 }
412 parent := pathDir(dir)
413 if parent == dir || parent == "" || parent == "." {
414 break
415 }
416 dir = parent
417 }
418 return "", ""
419 }
420
421 func mergeModReplaces(dir string) {
422 data, ok := mxutil.ReadFile(mxutil.JoinPath(dir, "moxie.mod"))
423 if !ok {
424 return
425 }
426 if bst.globalReplaces == nil {
427 bst.globalReplaces = map[string]string{}
428 }
429 inReplace := false
430 for _, line := range mxutil.SplitLines(string(data)) {
431 line = mxutil.TrimSpace(line)
432 if line == "replace (" {
433 inReplace = true
434 continue
435 }
436 if inReplace {
437 if line == ")" {
438 inReplace = false
439 continue
440 }
441 from, to, ok2 := parseReplaceLine(line, dir)
442 if ok2 {
443 if _, exists := bst.globalReplaces[from]; !exists {
444 bst.globalReplaces[from] = to
445 }
446 }
447 continue
448 }
449 if mxutil.HasPrefix(line, "replace ") {
450 from, to, ok2 := parseReplaceLine(line[8:], dir)
451 if ok2 {
452 if _, exists := bst.globalReplaces[from]; !exists {
453 bst.globalReplaces[from] = to
454 }
455 }
456 }
457 }
458 }
459
460 type modRequire struct {
461 path string
462 version string
463 }
464
465 func parseModRequires(dir string) (modName string, reqs []modRequire, repls map[string]string) {
466 data, ok := mxutil.ReadFile(mxutil.JoinPath(dir, "moxie.mod"))
467 if !ok {
468 return "", nil, nil
469 }
470 modName = ""
471 repls = map[string]string{}
472 inRequire := false
473 inReplace := false
474 for _, line := range mxutil.SplitLines(string(data)) {
475 line = mxutil.TrimSpace(line)
476 if mxutil.HasPrefix(line, "module ") {
477 modName = mxutil.TrimSpace(line[7:])
478 continue
479 }
480 if line == "require (" {
481 inRequire = true
482 continue
483 }
484 if inRequire {
485 if line == ")" {
486 inRequire = false
487 continue
488 }
489 parts := splitBySpace(line)
490 if len(parts) >= 2 {
491 push(reqs, modRequire{path: parts[0], version: parts[1]})
492 } else if len(parts) == 1 {
493 push(reqs, modRequire{path: parts[0], version: ""})
494 }
495 continue
496 }
497 if mxutil.HasPrefix(line, "require ") {
498 parts := splitBySpace(line[8:])
499 if len(parts) >= 2 {
500 push(reqs, modRequire{path: parts[0], version: parts[1]})
501 } else if len(parts) == 1 {
502 push(reqs, modRequire{path: parts[0], version: ""})
503 }
504 continue
505 }
506 if line == "replace (" {
507 inReplace = true
508 continue
509 }
510 if inReplace {
511 if line == ")" {
512 inReplace = false
513 continue
514 }
515 from, to, ok2 := parseReplaceLine(line, dir)
516 if ok2 {
517 repls[from] = to
518 }
519 continue
520 }
521 if mxutil.HasPrefix(line, "replace ") {
522 from, to, ok2 := parseReplaceLine(line[8:], dir)
523 if ok2 {
524 repls[from] = to
525 }
526 }
527 }
528 return modName, reqs, repls
529 }
530
531 func isRemotePath(path string) (ok bool) {
532 slash := int32(-1)
533 for i := int32(0); i < int32(len(path)); i++ {
534 if path[i] == '/' {
535 slash = i
536 break
537 }
538 }
539 if slash < 0 {
540 return false
541 }
542 host := path[:slash]
543 for i := int32(0); i < int32(len(host)); i++ {
544 if host[i] == '.' {
545 return true
546 }
547 }
548 return false
549 }
550
551 func repoLockPath(mpath, repoPath string) (s string) {
552 return mxutil.JoinPath(mpath, repoPath | ".lock")
553 }
554
555 func acquireRepoLock(mpath, repoPath string) (ok bool) {
556 lp := repoLockPath(mpath, repoPath)
557 _, locked := mxutil.ReadFile(lp)
558 if locked {
559 return false
560 }
561 mkdirAll(pathDir(lp), 0755)
562 writeFile(lp, []byte("1"), 0644)
563 return true
564 }
565
566 func releaseRepoLock(mpath, repoPath string) {
567 os.Remove(repoLockPath(mpath, repoPath))
568 }
569
570 func autoFetchRepo(pkgPath, version string) (s string) {
571 mpath := getMoxiePath()
572 repoPath := pkgPath
573 for {
574 d := mxutil.JoinPath(mpath, repoPath)
575 gh := mxutil.JoinPath(d, ".git/HEAD")
576 _, ex := mxutil.ReadFile(gh)
577 if ex {
578 break
579 }
580 parent := pathDir(repoPath)
581 if parent == repoPath || parent == "." || parent == "" {
582 break
583 }
584 parentDest := mxutil.JoinPath(mpath, parent)
585 parentGit := mxutil.JoinPath(parentDest, ".git/HEAD")
586 _, parentExists := mxutil.ReadFile(parentGit)
587 if parentExists {
588 repoPath = parent
589 break
590 }
591 repoPath = parent
592 }
593
594 if !acquireRepoLock(mpath, repoPath) {
595 mxutil.WriteStr(2, " waiting for lock: " | repoPath | "\n")
596 fatal("repo locked: " | repoPath)
597 }
598
599 dest := mxutil.JoinPath(mpath, repoPath)
600 gitHead := mxutil.JoinPath(dest, ".git/HEAD")
601 _, exists := mxutil.ReadFile(gitHead)
602
603 if !exists {
604 mkdirAll(pathDir(dest), 0755)
605 url := "https://" | repoPath
606 mxutil.WriteStr(2, " auto-fetch " | repoPath | "\n")
607 if run([]string{"git", "clone", url, dest}) != 0 {
608 releaseRepoLock(mpath, repoPath)
609 return ""
610 }
611 }
612
613 if version != "" && version != "v0.0.0" {
614 if run([]string{"git", "-C", dest, "checkout", version}) != 0 {
615 run([]string{"git", "-C", dest, "fetch", "--tags"})
616 run([]string{"git", "-C", dest, "pull"})
617 run([]string{"git", "-C", dest, "checkout", version})
618 }
619 }
620
621 releaseRepoLock(mpath, repoPath)
622 return mxutil.JoinPath(mpath, pkgPath)
623 }
624
625 func gitURLToModPath(url string) (s string) {
626 u := url
627 // strip scheme
628 for _, scheme := range []string{"ssh://", "https://", "http://", "git://"} {
629 if mxutil.HasPrefix(u, scheme) {
630 u = u[len(scheme):]
631 break
632 }
633 }
634 // strip user@
635 for i := int32(0); i < int32(len(u)); i++ {
636 if u[i] == '@' {
637 u = u[i+1:]
638 break
639 }
640 if u[i] == '/' {
641 break
642 }
643 }
644 // host:port/path or host:path (scp-style) or host/path
645 // extract host, skip port if numeric, keep path
646 host := ""
647 rest := ""
648 for i := int32(0); i < int32(len(u)); i++ {
649 if u[i] == ':' {
650 host = u[:i]
651 after := u[i+1:]
652 // skip port (digits until /)
653 j := int32(0)
654 for j < int32(len(after)) && after[j] >= '0' && after[j] <= '9' {
655 j++
656 }
657 if j > 0 && j < int32(len(after)) && after[j] == '/' {
658 rest = after[j+1:]
659 } else if j > 0 && j == int32(len(after)) {
660 rest = ""
661 } else {
662 rest = after
663 }
664 break
665 }
666 if u[i] == '/' {
667 host = u[:i]
668 rest = u[i+1:]
669 break
670 }
671 }
672 if host == "" {
673 return ""
674 }
675 // strip ~/ prefix from path
676 if mxutil.HasPrefix(rest, "~/") {
677 rest = rest[2:]
678 }
679 // strip .git suffix
680 if mxutil.HasSuffix(rest, ".git") {
681 rest = rest[:len(rest)-4]
682 }
683 if rest == "" {
684 return ""
685 }
686 return host | "/" | rest
687 }
688
689 func currentBranch(dest string) (s string) {
690 data, ok := mxutil.ReadFile(mxutil.JoinPath(dest, ".git/HEAD"))
691 if !ok {
692 return ""
693 }
694 line := mxutil.TrimSpace(string(data))
695 if !mxutil.HasPrefix(line, "ref: refs/heads/") {
696 return ""
697 }
698 return line[len("ref: refs/heads/"):]
699 }
700
701 func cmdFetch(url string) {
702 mpath := getMoxiePath()
703 mkdirAll(mpath, 0755)
704
705 rootDir := "."
706 if url != "" {
707 modPath := gitURLToModPath(url)
708 if modPath == "" {
709 fatal("cannot derive module path from URL: " | url)
710 }
711 dest := mxutil.JoinPath(mpath, modPath)
712 _, exists := mxutil.ReadFile(mxutil.JoinPath(dest, ".git/HEAD"))
713 if exists {
714 mxutil.WriteStr(2, modPath | ": fetch\n")
715 run([]string{"git", "-C", dest, "remote", "set-url", "origin", url})
716 run([]string{"git", "-C", dest, "fetch", "origin"})
717 // older clones lack the origin/HEAD symbolic ref; resolve it
718 // from the remote before resetting to it
719 run([]string{"git", "-C", dest, "remote", "set-head", "origin", "-a"})
720 if run([]string{"git", "-C", dest, "reset", "--hard", "origin/HEAD"}) != 0 {
721 // server advertises no HEAD (default branch unset);
722 // fall back to the upstream of the current branch
723 br := currentBranch(dest)
724 if br != "" {
725 run([]string{"git", "-C", dest, "reset", "--hard", "origin/" | br})
726 }
727 }
728 } else {
729 mxutil.WriteStr(2, modPath | ": clone\n")
730 mkdirAll(pathDir(dest), 0755)
731 if run([]string{"git", "clone", url, dest}) != 0 {
732 fatal("clone failed: " | url)
733 }
734 }
735 rootDir = dest
736 }
737
738 modName, reqs, repls := parseModRequires(rootDir)
739 if modName == "" {
740 fatal("no moxie.mod in " | rootDir)
741 }
742 if url == "" {
743 mxutil.WriteStr(2, "module: " | modName | "\n")
744 }
745
746 queue := []modRequire{:0:len(reqs)}
747 for _, r := range reqs {
748 if _, ok := repls[r.path]; ok {
749 mxutil.WriteStr(2, " skip (replaced): " | r.path | "\n")
750 continue
751 }
752 push(queue, r)
753 }
754
755 seen := map[string]bool{}
756 for len(queue) > 0 {
757 req := queue[0]
758 queue = queue[1:]
759 if seen[req.path] {
760 continue
761 }
762 seen[req.path] = true
763
764 if !acquireRepoLock(mpath, req.path) {
765 mxutil.WriteStr(2, " locked: " | req.path | " (another build in progress)\n")
766 fatal("repo locked: " | req.path)
767 }
768
769 dest := mxutil.JoinPath(mpath, req.path)
770 gitHead := mxutil.JoinPath(dest, ".git/HEAD")
771 _, exists := mxutil.ReadFile(gitHead)
772
773 if !exists {
774 mkdirAll(pathDir(dest), 0755)
775 cloneURL := "https://" | req.path
776 mxutil.WriteStr(2, " clone " | req.path | "\n")
777 if run([]string{"git", "clone", cloneURL, dest}) != 0 {
778 releaseRepoLock(mpath, req.path)
779 mxutil.WriteStr(2, " FAIL: clone " | req.path | "\n")
780 continue
781 }
782 }
783
784 if req.version != "" && req.version != "v0.0.0" {
785 if run([]string{"git", "-C", dest, "checkout", req.version}) != 0 {
786 mxutil.WriteStr(2, " tag " | req.version | " not found, pulling\n")
787 run([]string{"git", "-C", dest, "pull"})
788 if run([]string{"git", "-C", dest, "checkout", req.version}) != 0 {
789 mxutil.WriteStr(2, " WARN: " | req.version | " unavailable for " | req.path | "\n")
790 }
791 } else {
792 mxutil.WriteStr(2, " " | req.path | " @ " | req.version | "\n")
793 }
794 } else if exists {
795 mxutil.WriteStr(2, " pull " | req.path | "\n")
796 run([]string{"git", "-C", dest, "pull"})
797 } else {
798 mxutil.WriteStr(2, " fetched " | req.path | "\n")
799 }
800
801 releaseRepoLock(mpath, req.path)
802
803 _, subReqs, _ := parseModRequires(dest)
804 for _, sr := range subReqs {
805 if !seen[sr.path] {
806 push(queue, sr)
807 }
808 }
809 }
810
811 mxutil.WriteStr(2, "fetch done\n")
812 }
813
814 func joinStrings(ss []string, sep string) (s string) {
815 if len(ss) == 0 {
816 return ""
817 }
818 n := int32(0)
819 for _, v := range ss {
820 n += int32(len(v))
821 }
822 n += int32(len(sep)) * (int32(len(ss)) - 1)
823 buf := []byte{:0:n}
824 for i, v := range ss {
825 if i > 0 {
826 buf = buf | sep
827 }
828 buf = buf | v
829 }
830 return string(buf)
831 }
832
833 func appendUniq(ss []string, s string) (ss2 []string) {
834 for _, x := range ss {
835 if x == s {
836 return ss
837 }
838 }
839 push(ss, s)
840 return ss
841 }
842
843 func fnvHash(data []byte) (s string) {
844 h := uint32(2166136261)
845 for i := int32(0); i < int32(len(data)); i++ {
846 h ^= uint32(data[i])
847 h *= uint32(16777619)
848 }
849 hex := "0123456789abcdef"
850 buf := []byte{:8}
851 for i := int32(7); i >= 0; i-- {
852 buf[i] = hex[h&0xf]
853 h >>= 4
854 }
855 return string(buf)
856 }
857
858 func pkgCacheDir() (s string) {
859 if mxutil.TargetArch == "wasm" {
860 return mxutil.JoinPath(moxieCacheDir(), "pkg-wasm")
861 }
862 return mxutil.JoinPath(moxieCacheDir(), "pkg")
863 }
864
865 func pkgCacheKey(dir string, files []string) (s string) {
866 var sorted []string
867 for _, f := range files {
868 push(sorted, f)
869 }
870 for i := int32(1); i < int32(len(sorted)); i++ {
871 for j := i; j > 0 && sorted[j] < sorted[j-1]; j-- {
872 sorted[j], sorted[j-1] = sorted[j-1], sorted[j]
873 }
874 }
875 var all []byte
876 for _, f := range sorted {
877 p := mxutil.JoinPath(dir, f)
878 data, ok := mxutil.ReadFile(p)
879 if !ok {
880 continue
881 }
882 all = all | data
883 push(all, 0)
884 }
885 return fnvHash(all)
886 }
887
888 // pkgKeyMemo caches each package's transitive hash by import path. Filled
889 // in dependency order (resolver emits deps before dependents), so a
890 // dependent's key always sees its deps' final hashes. Without dep hashes in
891 // the key, a dependency's layout change would leave a dependent's cached .bc
892 // reusable with a stale ABI - silent struct-offset corruption at link time.
893
894 func pkgHash(p *pkgInfo) (s string) {
895 if bst.pkgKeyMemo == nil {
896 bst.pkgKeyMemo = map[string]string{}
897 }
898 if h, ok := bst.pkgKeyMemo[p.path]; ok {
899 return h
900 }
901 var sorted []string
902 for _, imp := range p.imports {
903 push(sorted, imp)
904 }
905 for i := int32(1); i < int32(len(sorted)); i++ {
906 for j := i; j > 0 && sorted[j] < sorted[j-1]; j-- {
907 sorted[j], sorted[j-1] = sorted[j-1], sorted[j]
908 }
909 }
910 acc := []byte(pkgCacheKey(p.dir, p.files))
911 for _, imp := range sorted {
912 if dh, ok := bst.pkgKeyMemo[imp]; ok {
913 push(acc, '|')
914 acc = acc | dh
915 }
916 }
917 hv := fnvHash(acc)
918 bst.pkgKeyMemo[p.path] = hv
919 return hv
920 }
921
922 func versionedCacheName(hash, version, ext string) (s string) {
923 if version != "" {
924 return hash | "_" | version | ext
925 }
926 return hash | ext
927 }
928
929 func cachedBcPath(cacheBase, pkgPath, hash, version string) (s string) {
930 return mxutil.JoinPath(mxutil.JoinPath(cacheBase, safeName(pkgPath)), versionedCacheName(hash, version, ".bc"))
931 }
932
933 func isStdlib(path string) (ok bool) {
934 return !mxutil.HasPrefix(path, "/") && !mxutil.HasPrefix(path, "./") && !mxutil.HasPrefix(path, "../")
935 }
936
937
938 func isBuiltinOnly(path string) (ok bool) {
939 for _, b := range bst.builtinOnly {
940 if path == b {
941 return true
942 }
943 }
944 return false
945 }
946
947 func loadSysroot(path string) {
948 data, ok := mxutil.ReadFile(path)
949 if !ok {
950 return
951 }
952 for _, line := range mxutil.SplitLines(string(data)) {
953 line = mxutil.TrimSpace(line)
954 if len(line) == 0 || line[0] == '#' {
955 continue
956 }
957 eq := int32(-1)
958 for i := int32(0); i < int32(len(line)); i++ {
959 if line[i] == '=' {
960 eq = i
961 break
962 }
963 }
964 if eq < 0 {
965 continue
966 }
967 key := line[:eq]
968 val := line[eq+1:]
969 os.Setenv(key, val)
970 }
971 }
972
973 func splitBySpace(s string) (ss []string) {
974 var result []string
975 start := int32(0)
976 inWord := false
977 for i := int32(0); i < int32(len(s)); i++ {
978 if s[i] == ' ' || s[i] == '\t' {
979 if inWord {
980 push(result, s[start:i])
981 inWord = false
982 }
983 } else {
984 if !inWord {
985 start = i
986 inWord = true
987 }
988 }
989 }
990 if inWord {
991 push(result, s[start:])
992 }
993 return result
994 }
995
996 func safeName(pkg string) (s string) {
997 b := []byte{:len(pkg)}
998 for i := int32(0); i < int32(len(pkg)); i++ {
999 if pkg[i] == '/' {
1000 b[i] = '_'
1001 } else {
1002 b[i] = pkg[i]
1003 }
1004 }
1005 return string(b)
1006 }
1007
1008 func extractPkgName(data []byte) (s string) {
1009 for i := int32(0); i < int32(len(data)); i++ {
1010 if i+8 < int32(len(data)) && string(data[i:i+8]) == "package " {
1011 j := i + 8
1012 for j < int32(len(data)) && data[j] != '\n' && data[j] != '\r' && data[j] != ' ' {
1013 j++
1014 }
1015 return string(data[i+8 : j])
1016 }
1017 if data[i] == '\n' {
1018 continue
1019 }
1020 if data[i] == '/' && i+1 < int32(len(data)) && data[i+1] == '/' {
1021 for i < int32(len(data)) && data[i] != '\n' {
1022 i++
1023 }
1024 continue
1025 }
1026 // Block comment: skip to closing */ (fmt/doc.mx has its package
1027 // clause below a /* ... */ doc comment).
1028 if data[i] == '/' && i+1 < int32(len(data)) && data[i+1] == '*' {
1029 i += 2
1030 for i+1 < int32(len(data)) && !(data[i] == '*' && data[i+1] == '/') {
1031 i++
1032 }
1033 i++ // lands on '/', loop increment moves past it
1034 continue
1035 }
1036 if data[i] == ' ' || data[i] == '\t' || data[i] == '\r' {
1037 continue
1038 }
1039 break
1040 }
1041 return "main"
1042 }
1043
1044 func extractImports(data []byte) (ss []string) {
1045 imports := []string{:0:32}
1046 off := int32(0)
1047 for off < int32(len(data)) {
1048 ls := off
1049 for off < int32(len(data)) && data[off] != '\n' {
1050 off++
1051 }
1052 le := off
1053 if off < int32(len(data)) {
1054 off++
1055 }
1056 // Trim
1057 for ls < le && (data[ls] == ' ' || data[ls] == '\t') {
1058 ls++
1059 }
1060 for le > ls && (data[le-1] == ' ' || data[le-1] == '\t' || data[le-1] == '\r') {
1061 le--
1062 }
1063 line := string(data[ls:le])
1064 if line == "import (" {
1065 // Scan block
1066 for off < int32(len(data)) {
1067 bls := off
1068 for off < int32(len(data)) && data[off] != '\n' {
1069 off++
1070 }
1071 ble := off
1072 if off < int32(len(data)) {
1073 off++
1074 }
1075 for bls < ble && (data[bls] == ' ' || data[bls] == '\t') {
1076 bls++
1077 }
1078 for ble > bls && (data[ble-1] == ' ' || data[ble-1] == '\t' || data[ble-1] == '\r') {
1079 ble--
1080 }
1081 bt := string(data[bls:ble])
1082 if bt == ")" {
1083 break
1084 }
1085 imp := mxutil.ExtractQuoted(bt)
1086 if imp != "" {
1087 imports = appendUniq(imports, imp)
1088 }
1089 }
1090 continue
1091 }
1092 if mxutil.HasPrefix(line, "import ") {
1093 imp := mxutil.ExtractQuoted(line[7:])
1094 if imp != "" {
1095 imports = appendUniq(imports, imp)
1096 }
1097 }
1098 }
1099 return imports
1100 }
1101
1102 type srcSpan struct {
1103 file string
1104 outStart int32
1105 srcLine int32
1106 }
1107
1108
1109 func concatLineToFile(concatLine int32) (file string, srcLine int32) {
1110 if len(cctx.concatSourceMap) == 0 {
1111 return "", concatLine
1112 }
1113 best := int32(0)
1114 for i, s := range cctx.concatSourceMap {
1115 if s.outStart <= concatLine {
1116 best = int32(i)
1117 }
1118 }
1119 sp := cctx.concatSourceMap[best]
1120 return sp.file, sp.srcLine + (concatLine - sp.outStart)
1121 }
1122
1123 // scanFileRegions scans file bytes to find the body start offset and
1124 // extract import specs. No SplitLines, no per-line copies. Returns
1125 // bodyStart (byte offset of first non-package/import line) and the
1126 // source line number of that offset (1-based).
1127 func scanFileRegions(data []byte, imports map[string]bool) (bodyStart int32, bodyLine int32, fimps []string) {
1128 off := int32(0)
1129 lineNum := int32(1)
1130 bodyStart = -1
1131 for off < int32(len(data)) {
1132 // Find line extent
1133 lineStart := off
1134 for off < int32(len(data)) && data[off] != '\n' {
1135 off++
1136 }
1137 lineEnd := off
1138 if off < int32(len(data)) {
1139 off++ // skip \n
1140 }
1141 // Trim whitespace for classification
1142 ls := lineStart
1143 for ls < lineEnd && (data[ls] == ' ' || data[ls] == '\t') {
1144 ls++
1145 }
1146 le := lineEnd
1147 for le > ls && (data[le-1] == ' ' || data[le-1] == '\t' || data[le-1] == '\r') {
1148 le--
1149 }
1150 trimmed := data[ls:le]
1151 if len(trimmed) >= 8 && string(trimmed[:8]) == "package " {
1152 lineNum++
1153 continue
1154 }
1155 if string(trimmed) == "import (" {
1156 lineNum++
1157 // Scan import block
1158 for off < int32(len(data)) {
1159 bls := off
1160 for off < int32(len(data)) && data[off] != '\n' {
1161 off++
1162 }
1163 ble := off
1164 if off < int32(len(data)) {
1165 off++
1166 }
1167 // Trim
1168 for bls < ble && (data[bls] == ' ' || data[bls] == '\t') {
1169 bls++
1170 }
1171 for ble > bls && (data[ble-1] == ' ' || data[ble-1] == '\t' || data[ble-1] == '\r') {
1172 ble--
1173 }
1174 bt := data[bls:ble]
1175 lineNum++
1176 if string(bt) == ")" {
1177 break
1178 }
1179 if len(bt) > 0 {
1180 spec := string(bt)
1181 imports[spec] = true
1182 push(fimps, spec)
1183 }
1184 }
1185 continue
1186 }
1187 if len(trimmed) >= 7 && string(trimmed[:7]) == "import " && (len(trimmed) < 8 || trimmed[7] != '(') {
1188 spec := string(trimmed[7:])
1189 imports[spec] = true
1190 push(fimps, spec)
1191 lineNum++
1192 continue
1193 }
1194 // First body line
1195 if bodyStart < 0 {
1196 bodyStart = lineStart
1197 bodyLine = lineNum
1198 }
1199 lineNum++
1200 }
1201 if bodyStart < 0 {
1202 bodyStart = int32(len(data))
1203 bodyLine = lineNum
1204 }
1205 return bodyStart, bodyLine, fimps
1206 }
1207
1208 func concatSources(dir string, files []string) (buf []byte) {
1209 imports := map[string]bool{}
1210 pkgName := ""
1211 // First pass: scan each file for imports and body boundaries.
1212 // Store body as a byte slice reference into the file data.
1213 type fileBody struct {
1214 name string
1215 data []byte // full file data (freed after copy into output)
1216 bodyStart int32
1217 bodyLine int32
1218 fimps []string
1219 }
1220 var fb []fileBody
1221 for _, f := range files {
1222 data, ok := mxutil.ReadFile(mxutil.JoinPath(dir, f))
1223 if !ok {
1224 continue
1225 }
1226 if pkgName == "" {
1227 pkgName = extractPkgName(data)
1228 }
1229 bs, bl, fimps := scanFileRegions(data, imports)
1230 push(fb, fileBody{name: f, data: data, bodyStart: bs, bodyLine: bl, fimps: fimps})
1231 }
1232 // Estimate total size for pre-allocation
1233 totalSize := int32(64) // header
1234 for _, f := range fb {
1235 totalSize += int32(len(f.data)) - f.bodyStart + 1
1236 }
1237 for imp := range imports {
1238 totalSize += int32(len(imp)) + 2
1239 }
1240 var out []byte
1241 out = out | ("package " | pkgName | "\n")
1242 if len(imports) > 0 {
1243 var impKeys []string
1244 for imp := range imports {
1245 push(impKeys, imp)
1246 }
1247 for i := int32(1); i < int32(len(impKeys)); i++ {
1248 for j := i; j > 0 && impKeys[j] < impKeys[j-1]; j-- {
1249 impKeys[j], impKeys[j-1] = impKeys[j-1], impKeys[j]
1250 }
1251 }
1252 out = out | "import (\n"
1253 for _, imp := range impKeys {
1254 push(out, '\t')
1255 out = out | imp
1256 push(out, '\n')
1257 }
1258 out = out | ")\n"
1259 }
1260 cctx.concatSourceMap = nil
1261 mxutil.ResetConcatImportSegs()
1262 outLine := int32(1)
1263 for j := int32(0); j < int32(len(out)); j++ {
1264 if out[j] == '\n' {
1265 outLine++
1266 }
1267 }
1268 var bodyChunks [][]byte
1269 push(bodyChunks, out)
1270 for i := range fb {
1271 body := fb[i].data[fb[i].bodyStart:]
1272 push(cctx.concatSourceMap, srcSpan{
1273 file: fb[i].name,
1274 outStart: outLine,
1275 srcLine: fb[i].bodyLine,
1276 })
1277 mxutil.AppendConcatImportSeg(mxutil.ImportSeg{
1278 OutStart: outLine,
1279 Specs: fb[i].fimps,
1280 })
1281 for j := int32(0); j < int32(len(body)); j++ {
1282 if body[j] == '\n' {
1283 outLine++
1284 }
1285 }
1286 outLine++
1287 push(bodyChunks, body)
1288 push(bodyChunks, []byte("\n"))
1289 }
1290 return joinChunks(bodyChunks)
1291 }
1292
1293 func fileContainsPart(base string, part string) (ok bool) {
1294 plen := len(part)
1295 for i := 0; i <= len(base)-plen; i++ {
1296 if base[i:i+plen] == part {
1297 before := i == 0 || base[i-1] == '_'
1298 after := i+plen == len(base) || base[i+plen] == '_'
1299 if before && after {
1300 return true
1301 }
1302 }
1303 }
1304 return false
1305 }
1306
1307 func shouldSkipFile(name string) (ok bool) {
1308 if mxutil.HasSuffix(name, "_test.mx") {
1309 return true
1310 }
1311 base := name
1312 if mxutil.HasSuffix(base, ".mx") {
1313 base = base[:len(base)-3]
1314 }
1315 if mxutil.TargetArch == "wasm" {
1316 for _, a := range []string{"amd64", "arm64", "386"} {
1317 if fileContainsPart(base, a) {
1318 return true
1319 }
1320 }
1321 for _, s := range []string{"_linux", "_unix", "_darwin", "_posix"} {
1322 if mxutil.HasSuffix(base, s) {
1323 return true
1324 }
1325 }
1326 } else {
1327 for _, a := range []string{"arm64"} {
1328 if fileContainsPart(base, a) {
1329 return true
1330 }
1331 }
1332 if mxutil.HasSuffix(base, "_wasm") {
1333 return true
1334 }
1335 }
1336 return false
1337 }
1338
1339 func isTagChar(c byte) (ok bool) {
1340 return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' || c == '.'
1341 }
1342
1343 func constraintContainsPositive(line string, word string) (ok bool) {
1344 wl := int32(len(word))
1345 for i := int32(0); i <= int32(len(line))-wl; i++ {
1346 if string(line[i:i+wl]) == word {
1347 if i > 0 && line[i-1] == '!' {
1348 continue
1349 }
1350 after := i + wl
1351 if after < int32(len(line)) {
1352 if isTagChar(line[after]) {
1353 continue
1354 }
1355 }
1356 if i > 0 {
1357 if isTagChar(line[i-1]) {
1358 continue
1359 }
1360 }
1361 // check if inside a negated group !(...)
1362 if isInsideNegatedGroup(line, i) {
1363 continue
1364 }
1365 return true
1366 }
1367 }
1368 return false
1369 }
1370
1371 func isInsideNegatedGroup(line string, pos int32) (result bool) {
1372 // scan backwards from pos for !(, counting parens
1373 for j := pos - 1; j >= 1; j-- {
1374 if line[j] == '(' && line[j-1] == '!' {
1375 // found !(, now check the matching ) is after pos
1376 depth := int32(1)
1377 k := j + 1
1378 for k < int32(len(line)) && depth > 0 {
1379 if line[k] == '(' { depth++ }
1380 if line[k] == ')' { depth-- }
1381 k++
1382 }
1383 if k-1 > pos { // closing ) is after our position
1384 return true
1385 }
1386 }
1387 }
1388 return false
1389 }
1390
1391 func constraintContainsNegated(line string, word string) (ok bool) {
1392 wl := int32(len(word))
1393 // check direct negation: !word
1394 for i := int32(1); i <= int32(len(line))-wl; i++ {
1395 if string(line[i:i+wl]) == word && line[i-1] == '!' {
1396 after := i + wl
1397 if after < int32(len(line)) {
1398 if isTagChar(line[after]) {
1399 continue
1400 }
1401 }
1402 return true
1403 }
1404 }
1405 // check parenthesized negation: !(... word ...)
1406 for i := int32(0); i+1 < int32(len(line)); i++ {
1407 if line[i] == '!' && line[i+1] == '(' {
1408 depth := int32(1)
1409 j := i + 2
1410 for j < int32(len(line)) && depth > 0 {
1411 if line[j] == '(' { depth++ }
1412 if line[j] == ')' { depth-- }
1413 j++
1414 }
1415 group := string(line[i+2 : j-1])
1416 if constraintContainsPositive(group, word) {
1417 return true
1418 }
1419 }
1420 }
1421 return false
1422 }
1423
1424 func hasBuildConstraint(data []byte, forbidden []string) (ok bool) {
1425 for i := int32(0); i < int32(len(data)) && i < 512; i++ {
1426 if data[i] == 'p' {
1427 return false
1428 }
1429 skip := int32(0)
1430 if i+9 < int32(len(data)) && string(data[i:i+9]) == "//:build " {
1431 skip = 9
1432 }
1433 if skip > 0 {
1434 var buf []byte
1435 for j := i + skip; j < int32(len(data)) && data[j] != '\n'; j++ {
1436 push(buf, data[j])
1437 }
1438 line := string(buf)
1439 if mxutil.TargetArch == "wasm" {
1440 // Tags true for all wasm: wasm, moxie.wasm, go1.23, !linux, !baremetal
1441 // js tag only true when mxutil.TargetOS == "js" (js/wasm target)
1442 if constraintContainsPositive(line, "moxie.wasm") ||
1443 constraintContainsPositive(line, "wasm") ||
1444 constraintContainsPositive(line, "go1.23") {
1445 // Only reject js requirement when it's conjunctive (js && wasm)
1446 // not disjunctive (js || wasm)
1447 if constraintContainsPositive(line, "js") && mxutil.TargetOS != "js" {
1448 // Check if js and wasm are in a conjunction
1449 isConjunction := false
1450 for ci := int32(0); ci < int32(len(line))-1; ci++ {
1451 if line[ci] == '&' && line[ci+1] == '&' {
1452 isConjunction = true
1453 break
1454 }
1455 }
1456 if isConjunction {
1457 return true
1458 }
1459 }
1460 return false
1461 }
1462 if mxutil.TargetOS == "js" && constraintContainsPositive(line, "js") {
1463 return false
1464 }
1465 if constraintContainsNegated(line, "js") ||
1466 constraintContainsNegated(line, "wasm") ||
1467 constraintContainsNegated(line, "moxie.wasm") ||
1468 constraintContainsNegated(line, "go1.23") {
1469 return true
1470 }
1471 // Architecture tags: amd64, arm64, 386 are false on wasm.
1472 for _, arch := range []string{"amd64", "arm64", "386"} {
1473 if constraintContainsPositive(line, arch) {
1474 return true
1475 }
1476 }
1477 // !linux is true for wasm - include files that need it
1478 if constraintContainsNegated(line, "linux") {
1479 return false
1480 }
1481 // linux-only constraints don't apply to wasm
1482 if constraintContainsPositive(line, "linux") {
1483 return true
1484 }
1485 }
1486 for _, f := range forbidden {
1487 if line == f || mxutil.HasPrefix(line, f | " ") || mxutil.HasPrefix(line, f | "\t") || mxutil.HasPrefix(line, f | "(") {
1488 return true
1489 }
1490 }
1491 return false
1492 }
1493 if data[i] == '\n' {
1494 continue
1495 }
1496 if data[i] == '/' && i+1 < int32(len(data)) && data[i+1] == '/' {
1497 for i < int32(len(data)) && data[i] != '\n' {
1498 i++
1499 }
1500 continue
1501 }
1502 }
1503 return false
1504 }
1505
1506 func hasImport(data []byte, pkg string) (ok bool) {
1507 target := "\"" | pkg | "\""
1508 for i := int32(0); i <= int32(len(data))-int32(len(target)); i++ {
1509 if string(data[i:i+int32(len(target))]) == target {
1510 return true
1511 }
1512 }
1513 return false
1514 }
1515
1516 // quietDiscover suppresses per-file discovery chatter. Set by build-pkg
1517 // workers, which re-run discovery the orchestrator already printed.
1518
1519 // buildJobs is the max number of concurrent child processes (workers and
1520 // clang) used by build/test. Set by the -j flag; 1 forces the serial path.
1521
1522 // Packages baked into sysroot/wasm/runtime.bc (compiled by the old
1523 // TinyGo-based compiler). Stage4 type-checks these but does not compile
1524 // them - the prebuilt versions are used instead. This avoids ABI
1525 // mismatches between old-compiler and stage4-compiled code (different
1526 // context-pointer conventions).
1527
1528 type pkgInfo struct {
1529 path string
1530 name string
1531 dir string
1532 files []string
1533 cfiles []string
1534 bcfiles []string
1535 imports []string
1536 version string
1537 }
1538
1539 func discoverPkg(pkgPath, root string) (p *pkgInfo) {
1540 var dir string
1541 if pkgPath == "." || pkgPath == ".." || mxutil.HasPrefix(pkgPath, "/") || mxutil.HasPrefix(pkgPath, "./") || mxutil.HasPrefix(pkgPath, "../") {
1542 dir = pkgPath
1543 } else if bst.globalReplaces != nil {
1544 if rdir, ok := bst.globalReplaces[pkgPath]; ok {
1545 dir = rdir
1546 }
1547 if dir == "" {
1548 for from, to := range bst.globalReplaces {
1549 if mxutil.HasPrefix(pkgPath, from|"/") {
1550 rel := pkgPath[len(from)+1:]
1551 dir = mxutil.JoinPath(to, rel)
1552 break
1553 }
1554 }
1555 }
1556 }
1557 if dir == "" && bst.globalModPrefix != "" && pkgPath == bst.globalModPrefix {
1558 dir = bst.globalModDir
1559 }
1560 if dir == "" && bst.globalModPrefix != "" && mxutil.HasPrefix(pkgPath, bst.globalModPrefix|"/") {
1561 rel := pkgPath[len(bst.globalModPrefix)+1:]
1562 dir = mxutil.JoinPath(bst.globalModDir, rel)
1563 }
1564 if dir == "" {
1565 candidate := mxutil.JoinPath(getMoxiePath(), pkgPath)
1566 mxF := listFiles(candidate, ".mx")
1567 if len(mxF) > 0 {
1568 dir = candidate
1569 }
1570 }
1571 if dir == "" && isRemotePath(pkgPath) {
1572 ver := ""
1573 if bst.globalModDir != "" {
1574 _, reqs, _ := parseModRequires(bst.globalModDir)
1575 for _, r := range reqs {
1576 if r.path == pkgPath {
1577 ver = r.version
1578 break
1579 }
1580 }
1581 }
1582 fetched := autoFetchRepo(pkgPath, ver)
1583 if fetched != "" {
1584 mxF := listFiles(fetched, ".mx")
1585 if len(mxF) > 0 {
1586 dir = fetched
1587 }
1588 }
1589 }
1590 if dir != "" && dir != pkgPath {
1591 mergeModReplaces(dir)
1592 }
1593 if dir == "" {
1594 dir = mxutil.JoinPath(root, mxutil.JoinPath("src", pkgPath))
1595 mxf := listFiles(dir, ".mx")
1596 if len(mxf) == 0 {
1597 dir = mxutil.JoinPath(root, mxutil.JoinPath("src/vendor", pkgPath))
1598 }
1599 }
1600 mxFiles := listFiles(dir, ".mx")
1601 bcFiles := listFiles(dir, ".bc")
1602 allMx := mxFiles
1603 var forbidden []string
1604 if mxutil.TargetArch == "wasm" {
1605 forbidden = []string{"none", "linux", "unix", "darwin", "amd64", "arm64",
1606 "ignore", "compiler_bootstrap",
1607 "scheduler.tasks", "scheduler.cores", "scheduler.asyncify",
1608 "faketime", "baremetal", "nintendoswitch",
1609 "boringcrypto", "cgo",
1610 "runtime_memhash_leveldb", "runtime_memhash_tsip",
1611 "runtime_asserts", "moxie.stringer"}
1612 } else {
1613 forbidden = []string{"none", "wasm", "js", "ignore", "compiler_bootstrap",
1614 "scheduler.tasks", "scheduler.cores", "scheduler.asyncify",
1615 "faketime", "baremetal", "nintendoswitch", "boringcrypto", "gc"}
1616 }
1617 cFilesAll := listFiles(dir, ".c")
1618 cFiles := []string{:0:len(cFilesAll)}
1619 for _, cf := range cFilesAll {
1620 if mxutil.HasSuffix(cf, "_test.c") {
1621 continue
1622 }
1623 if shouldSkipFile(cf) {
1624 continue
1625 }
1626 cdata, cok := mxutil.ReadFile(mxutil.JoinPath(dir, cf))
1627 if cok && hasBuildConstraint(cdata, forbidden) {
1628 continue
1629 }
1630 push(cFiles, cf)
1631 }
1632 files := []string{:0:len(allMx)}
1633 for _, f := range allMx {
1634 if shouldSkipFile(f) {
1635 continue
1636 }
1637 data, ok := mxutil.ReadFile(mxutil.JoinPath(dir, f))
1638 if !ok {
1639 continue
1640 }
1641 bc := hasBuildConstraint(data, forbidden)
1642 if bc {
1643 if !bst.quietDiscover {
1644 mxutil.WriteStr(2, " skip-constraint: " | f | "\n")
1645 }
1646 continue
1647 }
1648 fileImps := extractImports(data)
1649 skipFile := ""
1650 for _, imp := range fileImps {
1651 if imp == "iter" || imp == "weak" || imp == "crypto/internal/fips140cache" {
1652 skipFile = imp
1653 break
1654 }
1655 }
1656 if skipFile != "" {
1657 if !bst.quietDiscover {
1658 mxutil.WriteStr(2, " skip-import(" | skipFile | "): " | f | "\n")
1659 }
1660 continue
1661 }
1662 if !bst.quietDiscover {
1663 mxutil.WriteStr(2, " include: " | f | "\n")
1664 }
1665 push(files, f)
1666 }
1667 if len(files) == 0 {
1668 return nil
1669 }
1670 allImports := []string{:0:len(files) * 4}
1671 pkgName := ""
1672 for _, f := range files {
1673 data, ok := mxutil.ReadFile(mxutil.JoinPath(dir, f))
1674 if !ok {
1675 continue
1676 }
1677 if pkgName == "" {
1678 pkgName = extractPkgName(data)
1679 }
1680 for _, imp := range extractImports(data) {
1681 allImports = appendUniq(allImports, imp)
1682 }
1683 }
1684 if !bst.quietDiscover {
1685 mxutil.WriteStr(2, " discover " | pkgPath | " [")
1686 for fi, ff := range files {
1687 if fi > 0 {
1688 mxutil.WriteStr(2, ", ")
1689 }
1690 mxutil.WriteStr(2, ff)
1691 }
1692 mxutil.WriteStr(2, "]\n")
1693 }
1694 version := ""
1695 if isRemotePath(pkgPath) && bst.globalModDir != "" {
1696 _, reqs, _ := parseModRequires(bst.globalModDir)
1697 for _, r := range reqs {
1698 if r.path == pkgPath || mxutil.HasPrefix(pkgPath, r.path|"/") {
1699 version = r.version
1700 break
1701 }
1702 }
1703 }
1704 return &pkgInfo{
1705 path: pkgPath,
1706 name: pkgName,
1707 dir: dir,
1708 files: files,
1709 cfiles: cFiles,
1710 bcfiles: bcFiles,
1711 imports: allImports,
1712 version: version,
1713 }
1714 }
1715
1716 type resolver struct {
1717 visited map[string]bool
1718 order []*pkgInfo
1719 root string
1720 }
1721
1722 func (r *resolver) walk(path string) {
1723 if r.visited[path] {
1724 return
1725 }
1726 r.visited[path] = true
1727 if isBuiltinOnly(path) {
1728 return
1729 }
1730 pkg := discoverPkg(path, r.root)
1731 if pkg == nil {
1732 return
1733 }
1734 for _, imp := range pkg.imports {
1735 r.walk(imp)
1736 }
1737 push(r.order, pkg)
1738 }
1739
1740 func resolveAll(rootPkg string, root string) (ss []*pkgInfo) {
1741 r := &resolver{
1742 visited: map[string]bool{},
1743 order: []*pkgInfo{:0:64},
1744 root: root,
1745 }
1746 r.walk(rootPkg)
1747 return r.order
1748 }
1749
1750 func registerBuiltins() {
1751 for k := range cctx.universe.Registry {
1752 delete(cctx.universe.Registry, k)
1753 }
1754
1755 // unsafe - compiler builtin, no source
1756 unsafePkg := NewTCPackage("unsafe", "unsafe")
1757 unsafePkg.Scope.Insert(NewTypeName(unsafePkg, "Pointer", Typ[UnsafePointer]))
1758 unsafePkg.Scope.Insert(NewTCFunc(unsafePkg, "Sizeof", parseSignatureDesc("interface{}->int32")))
1759 unsafePkg.Scope.Insert(NewTCFunc(unsafePkg, "Offsetof", parseSignatureDesc("interface{}->int32")))
1760 unsafePkg.Scope.Insert(NewTCFunc(unsafePkg, "Alignof", parseSignatureDesc("interface{}->int32")))
1761 unsafePkg.Scope.Insert(NewTCFunc(unsafePkg, "Slice", parseSignatureDesc("ptr,int32->[]uint8")))
1762 unsafePkg.Scope.Insert(NewTCFunc(unsafePkg, "SliceData", parseSignatureDesc("[]uint8->ptr")))
1763 cctx.universe.Registry["unsafe"] = unsafePkg
1764
1765 // moxie - spawn/codec runtime, types and constants from protocol header
1766 mroot := getenv("MOXIEROOT")
1767 if mroot == "" {
1768 mroot = "."
1769 }
1770 if !loadMxh(mxutil.JoinPath(mxutil.JoinPath(mxutil.JoinPath(mroot, "src"), "moxie"), "moxie.mxh")) {
1771 fatal("cannot load src/moxie/moxie.mxh (MOXIEROOT=" | mroot | ")")
1772 }
1773
1774 // runtime - assembly + C, pre-compiled in runtime.bc
1775 rtPkg := NewTCPackage("runtime", "runtime")
1776 rtPkg.Scope.Insert(NewTCFunc(rtPkg, "InitCShared", parseSignatureDesc("")))
1777 rtPkg.Scope.Insert(NewTCFunc(rtPkg, "LastSpawnedParentFd", parseSignatureDesc("->int32")))
1778 rtPkg.Scope.Insert(NewTCFunc(rtPkg, "PipeClosed", parseSignatureDesc("int32->bool")))
1779 rtPkg.Scope.Insert(NewTCFunc(rtPkg, "PipeFDCanSend", parseSignatureDesc("int32->bool")))
1780 rtPkg.Scope.Insert(NewTCFunc(rtPkg, "LockOSThread", parseSignatureDesc("")))
1781 rtPkg.Scope.Insert(NewTCFunc(rtPkg, "UnlockOSThread", parseSignatureDesc("")))
1782 rtPkg.Scope.Insert(NewTCFunc(rtPkg, "KeepAlive", parseSignatureDesc("interface{}")))
1783 rtPkg.Scope.Insert(NewTCFunc(rtPkg, "GOROOT", parseSignatureDesc("->string")))
1784 rtPkg.Scope.Insert(NewTCFunc(rtPkg, "Gosched", parseSignatureDesc("")))
1785 rtPkg.Scope.Insert(NewTCFunc(rtPkg, "Version", parseSignatureDesc("->string")))
1786 rtPkg.Scope.Insert(NewTCVar(rtPkg, "GOOS", Typ[TCString]))
1787 rtPkg.Scope.Insert(NewTCVar(rtPkg, "GOARCH", Typ[TCString]))
1788 rtPkg.Scope.Insert(NewTCVar(rtPkg, "ChildPipeFd", Typ[Int32]))
1789 cctx.universe.Registry["runtime"] = rtPkg
1790 }
1791
1792 func addPtrMethod(pkg *TCPackage, named *Named, name, sigDesc string) {
1793 var sig *Signature
1794 if sigDesc == "" {
1795 sig = NewSignature(nil, nil, nil, false)
1796 } else {
1797 sig = parseSignatureDesc(sigDesc)
1798 }
1799 recvType := NewPointer(named)
1800 sig = NewSignature(NewTCVar(pkg, "", recvType), sig.Params, sig.Results, sig.Variadic)
1801 fn := NewTCFunc(pkg, name, sig)
1802 fn.PtrRecv = true
1803 named.AddMethod(fn)
1804 }
1805
1806 func remapErrorLines(msg string) (s string) {
1807 if len(cctx.concatSourceMap) == 0 {
1808 return msg
1809 }
1810 lines := mxutil.SplitLines(msg)
1811 var out []string
1812 for _, line := range lines {
1813 push(out, remapOneLine(line))
1814 }
1815 return joinStrings(out, "\n")
1816 }
1817
1818 func remapOneLine(line string) (s string) {
1819 if len(cctx.concatSourceMap) == 0 {
1820 return line
1821 }
1822 colonCount := int32(0)
1823 firstColon := int32(-1)
1824 secondColon := int32(-1)
1825 for i := int32(0); i < int32(len(line)); i++ {
1826 if line[i] == ':' {
1827 colonCount++
1828 if colonCount == 1 {
1829 firstColon = i
1830 } else if colonCount == 2 {
1831 secondColon = i
1832 break
1833 }
1834 }
1835 }
1836 if secondColon < 0 {
1837 return line
1838 }
1839 numStr := line[firstColon+1 : secondColon]
1840 n := int32(0)
1841 valid := len(numStr) > 0
1842 for i := int32(0); i < int32(len(numStr)); i++ {
1843 if numStr[i] < '0' || numStr[i] > '9' {
1844 valid = false
1845 break
1846 }
1847 n = n*10 + int32(numStr[i]-'0')
1848 }
1849 if !valid {
1850 return line
1851 }
1852 file, srcLine := concatLineToFile(n)
1853 if file == "" {
1854 return line
1855 }
1856 return file | ":" | simpleItoa(srcLine) | line[secondColon:]
1857 }
1858
1859 func compileSource(src []byte, pkgName, triple, pkgDir string) (s string) {
1860 cctx.compilePkgDir = pkgDir
1861 ir := CompileToIR(src, pkgName, triple)
1862 if len(ir) > 0 && ir[0] == ';' {
1863 mxutil.WriteStr(2, remapErrorLines(ir))
1864 fatal("compile error for " | pkgName)
1865 }
1866 // When streaming (irOutputFile set), IR is written to file directly.
1867 // Empty ir string is expected - the file has the content.
1868 if len(ir) == 0 && cctx.irOutputFile == "" {
1869 fatal("empty IR for " | pkgName)
1870 }
1871 return ir
1872 }
1873
1874
1875 // mxhBuf accumulates mxh output. Pre-sized buffer with offset write.
1876 // No growth, no abandoned arrays under arena allocation.
1877 type mxhBuf struct {
1878 data []byte
1879 off int32
1880 }
1881
1882 func (b *mxhBuf) w(s string) {
1883 need := b.off + len(s)
1884 if need > len(b.data) {
1885 newCap := int32(len(b.data)) * 2
1886 if newCap < need {
1887 newCap = need + 4096
1888 }
1889 nd := []byte{:newCap}
1890 for i := int32(0); i < b.off; i++ {
1891 nd[i] = b.data[i]
1892 }
1893 b.data = nd
1894 }
1895 for j := int32(0); j < len(s); j++ {
1896 b.data[b.off+j] = s[j]
1897 }
1898 b.off += len(s)
1899 }
1900
1901 func (b *mxhBuf) nl() {
1902 if b.off >= len(b.data) {
1903 push(b.data, '\n')
1904 b.off++
1905 return
1906 }
1907 b.data[b.off] = '\n'
1908 b.off++
1909 }
1910
1911 // typeDescStr returns the type descriptor for simple/leaf types.
1912 // For compound types that recurse, use typeDescWrite.
1913 func typeDescStr(t Type) (s string) {
1914 var b mxhBuf
1915 b.data = []byte{:256}
1916 typeDescWrite(&b, t)
1917 return string(b.data[:b.off])
1918 }
1919
1920 func typeDescWrite(b *mxhBuf, t Type) {
1921 if t == nil {
1922 b.w("interface{}")
1923 return
1924 }
1925 switch v := t.(type) {
1926 case *Basic:
1927 switch v.Kind {
1928 case Bool:
1929 b.w("bool")
1930 case Int8:
1931 b.w("int8")
1932 case Int16:
1933 b.w("int16")
1934 case Int32:
1935 b.w("int32")
1936 case Int64:
1937 b.w("int64")
1938 case Uint8:
1939 b.w("uint8")
1940 case Uint16:
1941 b.w("uint16")
1942 case Uint32:
1943 b.w("uint32")
1944 case Uint64:
1945 b.w("uint64")
1946 case Float32:
1947 b.w("float32")
1948 case Float64:
1949 b.w("float64")
1950 case TCString:
1951 b.w("string")
1952 case UnsafePointer:
1953 b.w("ptr")
1954 default:
1955 b.w("int32")
1956 }
1957 case *Slice:
1958 b.w("[]")
1959 typeDescWrite(b, v.Elem)
1960 case *Array:
1961 b.w("[")
1962 b.w(simpleItoa64(v.Len))
1963 b.w("]")
1964 typeDescWrite(b, v.Elem)
1965 case *Pointer:
1966 b.w("*")
1967 typeDescWrite(b, v.Base)
1968 case *TCStruct:
1969 b.w("struct{")
1970 for i := int32(0); i < v.NumFields(); i++ {
1971 if i > 0 {
1972 b.w(",")
1973 }
1974 f := v.Field(i)
1975 if f.Anonymous {
1976 b.w("@")
1977 }
1978 b.w(f.Name)
1979 b.w(":")
1980 typeDescWrite(b, f.Typ)
1981 }
1982 b.w("}")
1983 case *TCInterface:
1984 if v.IsEmpty() {
1985 b.w("interface{}")
1986 return
1987 }
1988 hasError := false
1989 for mi := int32(0); mi < v.NumMethods(); mi++ {
1990 if v.Method(mi).Name == "Error" {
1991 hasError = true
1992 break
1993 }
1994 }
1995 if hasError {
1996 b.w("error")
1997 } else {
1998 b.w("interface{}")
1999 }
2000 case *TCChan:
2001 switch v.Dir {
2002 case TCSendOnly:
2003 b.w("chan<-:")
2004 case TCRecvOnly:
2005 b.w("<-chan:")
2006 default:
2007 b.w("chan:")
2008 }
2009 typeDescWrite(b, v.Elem)
2010 case *TCMap:
2011 b.w("map[")
2012 typeDescWrite(b, v.Key)
2013 b.w("]")
2014 typeDescWrite(b, v.Elem)
2015 case *Signature:
2016 b.w("func(")
2017 sigDescWrite(b, v)
2018 b.w(")")
2019 case *TypeParam:
2020 b.w("$")
2021 b.w(v.String())
2022 case *Named:
2023 if v.Obj != nil {
2024 if cctx.typeDescPkgPath != "" && v.Obj.Pkg != nil && v.Obj.Pkg.Path != cctx.typeDescPkgPath {
2025 b.w(v.Obj.Pkg.Path)
2026 b.w(".")
2027 }
2028 b.w(v.Obj.Name)
2029 return
2030 }
2031 typeDescWrite(b, v.Under)
2032 default:
2033 b.w("interface{}")
2034 }
2035 }
2036
2037 func sigDescWrite(b *mxhBuf, sig *Signature) {
2038 if sig.Params != nil {
2039 for i := int32(0); i < sig.Params.Len(); i++ {
2040 if i > 0 {
2041 b.w(",")
2042 }
2043 pt := sig.Params.At(i).Typ
2044 if sig.Variadic && i == sig.Params.Len()-1 {
2045 if sl, ok := pt.(*Slice); ok {
2046 b.w("...")
2047 typeDescWrite(b, sl.Elem)
2048 continue
2049 }
2050 }
2051 typeDescWrite(b, pt)
2052 }
2053 }
2054 if sig.Results != nil && sig.Results.Len() > 0 {
2055 b.w("->")
2056 for i := int32(0); i < sig.Results.Len(); i++ {
2057 if i > 0 {
2058 b.w(",")
2059 }
2060 typeDescWrite(b, sig.Results.At(i).Typ)
2061 }
2062 }
2063 }
2064
2065 // sigDescStr kept for callers that need a string.
2066 func sigDescStr(sig *Signature) (s string) {
2067 var b mxhBuf
2068 b.data = []byte{:256}
2069 sigDescWrite(&b, sig)
2070 return string(b.data[:b.off])
2071 }
2072
2073 func generateMxh(pkg *TCPackage) (s string) {
2074 path := pkg.Path
2075 name := pkg.Name
2076 cctx.typeDescPkgPath = path
2077 var b mxhBuf
2078 b.data = []byte{:int32(len(pkg.Scope.Names())) * 80 + 4096}
2079 b.w("package ") ; b.w(path) ; b.w(" ") ; b.w(name) ; b.nl()
2080 for _, ip := range cctx.universe.DirectImportPaths(pkg) {
2081 b.w("import ") ; b.w(ip) ; b.nl()
2082 }
2083 for _, objName := range pkg.Scope.Names() {
2084 if len(objName) == 0 {
2085 continue
2086 }
2087 exported := objName[0] >= 'A' && objName[0] <= 'Z'
2088 obj := pkg.Scope.Lookup(objName)
2089 if obj == nil {
2090 continue
2091 }
2092 if !exported {
2093 if tn, ok := obj.(*TypeName); ok {
2094 if named, ok2 := tn.Typ.(*Named); ok2 {
2095 _, isStruct := named.Under.(*TCStruct)
2096 if named.NumMethods() == 0 && !isStruct {
2097 continue
2098 }
2099 } else {
2100 continue
2101 }
2102 } else {
2103 continue
2104 }
2105 }
2106 if fn, ok := obj.(*TCFunc); ok {
2107 sig := fn.Signature()
2108 if sig != nil {
2109 b.w("func ") ; b.w(objName) ; b.w(" ")
2110 sigDescWrite(&b, sig) ; b.nl()
2111 }
2112 } else if vr, ok2 := obj.(*TCVar); ok2 {
2113 if vr == nil || vr.Typ == nil {
2114 continue
2115 }
2116 b.w("var ") ; b.w(objName) ; b.w(" ")
2117 typeDescWrite(&b, vr.Typ) ; b.nl()
2118 } else if cn, ok3 := obj.(*TCConst); ok3 {
2119 if cn == nil || cn.Typ == nil {
2120 continue
2121 }
2122 b.w("const ") ; b.w(objName) ; b.w(" ")
2123 typeDescWrite(&b, cn.Typ) ; b.w(" ")
2124 if cn.Val != nil {
2125 b.w(cn.Val.String())
2126 }
2127 b.nl()
2128 } else if tn2, ok4 := obj.(*TypeName); ok4 {
2129 mxhWriteTypeName(&b, objName, tn2)
2130 }
2131 }
2132 return string(b.data[:b.off])
2133 }
2134
2135 func mxhWriteTypeName(b *mxhBuf, objName string, v *TypeName) {
2136 if v == nil {
2137 return
2138 }
2139 typ := v.Typ
2140 if named, ok := typ.(*Named); ok {
2141 if ui, ok2 := named.Under.(*TCInterface); ok2 && !ui.IsEmpty() {
2142 complete := true
2143 for mi := int32(0); mi < ui.NumMethods(); mi++ {
2144 if ui.Method(mi).Sig == nil {
2145 complete = false
2146 break
2147 }
2148 }
2149 if complete {
2150 b.w("iface ") ; b.w(objName)
2151 mxhWriteTParams(b, named.TParams)
2152 b.w(" ")
2153 mxhWriteIfaceMethods(b, ui)
2154 b.nl()
2155 }
2156 } else {
2157 b.w("type ") ; b.w(objName)
2158 mxhWriteTParams(b, named.TParams)
2159 b.w(" ")
2160 if named.Under != nil {
2161 typeDescWrite(b, named.Under)
2162 }
2163 b.nl()
2164 if len(named.TParams) == 0 {
2165 for _, m := range named.Methods {
2166 msig := m.Signature()
2167 if msig != nil {
2168 ptrRecv := "0"
2169 if m.PtrRecv {
2170 ptrRecv = "1"
2171 }
2172 b.w("method ") ; b.w(objName) ; b.w(" ")
2173 b.w(m.Name) ; b.w(" ")
2174 sigDescWrite(b, msig) ; b.w(" ")
2175 b.w(ptrRecv) ; b.nl()
2176 }
2177 }
2178 }
2179 }
2180 } else if iface, ok3 := typ.(*TCInterface); ok3 {
2181 complete := true
2182 for mi := int32(0); mi < iface.NumMethods(); mi++ {
2183 if iface.Method(mi).Sig == nil {
2184 complete = false
2185 break
2186 }
2187 }
2188 if complete {
2189 b.w("iface ") ; b.w(objName) ; b.w(" ")
2190 mxhWriteIfaceMethods(b, iface)
2191 b.nl()
2192 }
2193 }
2194 }
2195
2196 func mxhWriteTParams(b *mxhBuf, tparams []*TypeParam) {
2197 if len(tparams) == 0 {
2198 return
2199 }
2200 b.w("[")
2201 for i, tp := range tparams {
2202 if i > 0 {
2203 b.w(",")
2204 }
2205 b.w("$")
2206 b.w(tp.String())
2207 }
2208 b.w("]")
2209 }
2210
2211 func mxhWriteIfaceMethods(b *mxhBuf, ui *TCInterface) {
2212 for mi := int32(0); mi < ui.NumMethods(); mi++ {
2213 m := ui.Method(mi)
2214 if mi > 0 {
2215 b.w(";")
2216 }
2217 b.w(m.Name)
2218 b.w("=")
2219 sigDescWrite(b, m.Sig)
2220 }
2221 }
2222
2223 // parseMxhTParams splits "Curve[$P,$Q]" into ("Curve", [TypeParam P, TypeParam Q]).
2224 // For plain names like "Foo" it returns ("Foo", nil).
2225 func parseMxhTParams(raw string) (name string, tparams []*TypeParam) {
2226 bracketIdx := int32(-1)
2227 for i := int32(0); i < int32(len(raw)); i++ {
2228 if raw[i] == '[' {
2229 bracketIdx = i
2230 break
2231 }
2232 }
2233 if bracketIdx < 0 {
2234 return raw, nil
2235 }
2236 name = raw[:bracketIdx]
2237 inner := raw[bracketIdx+1 : int32(len(raw))-1]
2238 parts := splitCommaBalanced(inner)
2239 for _, p := range parts {
2240 if len(p) > 1 && p[0] == '$' {
2241 tpn := p[1:]
2242 push(tparams, NewTypeParam(NewTypeName(nil, tpn, nil), nil))
2243 }
2244 }
2245 return name, tparams
2246 }
2247
2248 func loadMxh(path string) (ok bool) {
2249 data, ok := mxutil.ReadFile(path)
2250 if !ok {
2251 return false
2252 }
2253 return loadMxhData(data)
2254 }
2255
2256 // mxh index-walking primitives. All return index pairs into the original
2257 // buffer d. No intermediate string/slice allocations.
2258
2259 // mxhNextLine returns the start and end of the next line at or after pos,
2260 // and the position after the newline. end == start when no content remains.
2261 func mxhNextLine(d string, pos int32) (ls int32, le int32, next int32) {
2262 ls = pos
2263 i := pos
2264 for i < int32(len(d)) && d[i] != '\n' {
2265 i++
2266 }
2267 le = i
2268 next = i
2269 if next < int32(len(d)) {
2270 next++
2271 }
2272 return ls, le, next
2273 }
2274
2275 // mxhNextWord returns the start and end of the next whitespace-delimited
2276 // word within d[from:limit]. Returns (limit, limit) when no word found.
2277 func mxhNextWord(d string, from, limit int32) (ws, we int32) {
2278 i := from
2279 for i < limit && (d[i] == ' ' || d[i] == '\t') {
2280 i++
2281 }
2282 ws = i
2283 for i < limit && d[i] != ' ' && d[i] != '\t' {
2284 i++
2285 }
2286 we = i
2287 return ws, we
2288 }
2289
2290 // mxhHasPrefix checks whether d[start:end] begins with prefix.
2291 func mxhHasPrefix(d string, start, end int32, prefix string) (ok bool) {
2292 plen := int32(len(prefix))
2293 if end-start < plen {
2294 return false
2295 }
2296 for i := int32(0); i < plen; i++ {
2297 if d[start+i] != prefix[i] {
2298 return false
2299 }
2300 }
2301 return true
2302 }
2303
2304 // mxhEq checks whether d[start:end] equals s.
2305 func mxhEq(d string, start, end int32, s string) (eq bool) {
2306 if end-start != int32(len(s)) {
2307 return false
2308 }
2309 for i := int32(0); i < end-start; i++ {
2310 if d[start+i] != s[i] {
2311 return false
2312 }
2313 }
2314 return true
2315 }
2316
2317 // mxhParseIfaceMethods parses "name1=sig1;name2=sig2" from d[start:end]
2318 // without allocating a []string for the semicolon split.
2319 func mxhParseIfaceMethods(d string, start, end int32) (methods []*IfaceMethod) {
2320 if start >= end {
2321 return nil
2322 }
2323 // Count methods (semicolon-separated).
2324 n := int32(1)
2325 for i := start; i < end; i++ {
2326 if d[i] == ';' {
2327 n++
2328 }
2329 }
2330 methods = []*IfaceMethod{:0:n}
2331 seg := start
2332 for i := start; i <= end; i++ {
2333 if i == end || d[i] == ';' {
2334 eqIdx := int32(-1)
2335 for k := seg; k < i; k++ {
2336 if d[k] == '=' {
2337 eqIdx = k
2338 break
2339 }
2340 }
2341 if eqIdx > seg {
2342 mname := string(d[seg:eqIdx])
2343 msig := parseSignatureDesc(string(d[eqIdx+1 : i]))
2344 push(methods, NewTCIfaceMethod(mname, msig))
2345 }
2346 seg = i + 1
2347 }
2348 }
2349 return methods
2350 }
2351
2352 // mxhCountWords counts space-delimited words in d[from:limit].
2353 func mxhCountWords(d string, from, limit int32) (n int32) {
2354 i := from
2355 for i < limit {
2356 for i < limit && (d[i] == ' ' || d[i] == '\t') {
2357 i++
2358 }
2359 if i >= limit {
2360 break
2361 }
2362 n++
2363 for i < limit && d[i] != ' ' && d[i] != '\t' {
2364 i++
2365 }
2366 }
2367 return n
2368 }
2369
2370 func loadMxhData(data []byte) (ok bool) {
2371 // Types created here must survive on the root arena - they're stored
2372 // in cctx.universe.Registry which outlives any compile/typecheck arena.
2373 mxhSavedArena := runtime.CurrentArena()
2374 runtime.SetCurrentArena(runtime.RootArena())
2375
2376 d := string(data)
2377 dlen := int32(len(d))
2378 if dlen == 0 {
2379 runtime.SetCurrentArena(mxhSavedArena)
2380 return false
2381 }
2382
2383 // Parse header: "package <path> <name>"
2384 ls, le, pos := mxhNextLine(d, 0)
2385 if !mxhHasPrefix(d, ls, le, "package ") {
2386 return false
2387 }
2388 ws, we := mxhNextWord(d, ls+8, le)
2389 if ws == we {
2390 return false
2391 }
2392 pkgPath := string(d[ws:we])
2393 ns, ne := mxhNextWord(d, we, le)
2394 if ns == ne {
2395 return false
2396 }
2397 pkgName := string(d[ns:ne])
2398 pkg := NewTCPackage(pkgPath, pkgName)
2399
2400 // Collect direct imports.
2401 var dimps []string
2402 scanPos := pos
2403 for scanPos < dlen {
2404 ls2, le2, next2 := mxhNextLine(d, scanPos)
2405 if mxhHasPrefix(d, ls2, le2, "import ") {
2406 push(dimps, string(d[ls2+7:le2]))
2407 }
2408 scanPos = next2
2409 }
2410 cctx.universe.DirectImports[pkgPath] = dimps
2411
2412 oldScope := cctx.parseTypePkgScope
2413 cctx.parseTypePkgScope = pkg.Scope
2414
2415 // Pass 1a: register all type and interface names with placeholder types.
2416 scanPos = pos
2417 for scanPos < dlen {
2418 ls2, le2, next2 := mxhNextLine(d, scanPos)
2419 scanPos = next2
2420 if mxhHasPrefix(d, ls2, le2, "type ") {
2421 ws2, we2 := mxhNextWord(d, ls2+5, le2)
2422 if ws2 < we2 {
2423 actualName, tparams := parseMxhTParams(string(d[ws2:we2]))
2424 tn := NewTypeName(pkg, actualName, nil)
2425 named := NewNamed(tn, nil)
2426 named.TParams = tparams
2427 pkg.Scope.Insert(tn)
2428 }
2429 } else if mxhHasPrefix(d, ls2, le2, "iface ") {
2430 ws2, we2 := mxhNextWord(d, ls2+6, le2)
2431 if ws2 < we2 {
2432 actualName, tparams := parseMxhTParams(string(d[ws2:we2]))
2433 placeholder := NewTCInterface(nil, nil)
2434 placeholder.Complete()
2435 tn := NewTypeName(pkg, actualName, nil)
2436 named := NewNamed(tn, placeholder)
2437 named.TParams = tparams
2438 pkg.Scope.Insert(tn)
2439 }
2440 }
2441 }
2442
2443 // Pass 1b: resolve underlying types and interface methods.
2444 scanPos = pos
2445 for scanPos < dlen {
2446 ls2, le2, next2 := mxhNextLine(d, scanPos)
2447 scanPos = next2
2448 if mxhHasPrefix(d, ls2, le2, "type ") {
2449 ws2, we2 := mxhNextWord(d, ls2+5, le2)
2450 if ws2 >= we2 {
2451 continue
2452 }
2453 ts, te := mxhNextWord(d, we2, le2)
2454 actualName, _ := parseMxhTParams(string(d[ws2:we2]))
2455 obj := pkg.Scope.Lookup(actualName)
2456 if obj == nil {
2457 continue
2458 }
2459 tn, istn := obj.(*TypeName)
2460 if !istn {
2461 continue
2462 }
2463 named, isn := tn.Typ.(*Named)
2464 if !isn {
2465 continue
2466 }
2467 if ts >= te {
2468 named.Under = NewTCStruct(nil, nil)
2469 } else {
2470 named.Under = parseTypeDesc(string(d[ts:te]))
2471 }
2472 } else if mxhHasPrefix(d, ls2, le2, "iface ") {
2473 ws2, we2 := mxhNextWord(d, ls2+6, le2)
2474 if ws2 >= we2 {
2475 continue
2476 }
2477 ifaceName, _ := parseMxhTParams(string(d[ws2:we2]))
2478 // Methods descriptor is the second word (may contain semicolons).
2479 ms, me := mxhNextWord(d, we2, le2)
2480 methods := mxhParseIfaceMethods(d, ms, me)
2481 iface := NewTCInterface(methods, nil)
2482 iface.Complete()
2483 obj := pkg.Scope.Lookup(ifaceName)
2484 if obj == nil {
2485 continue
2486 }
2487 if tn, istn := obj.(*TypeName); istn {
2488 if named, isn := tn.Typ.(*Named); isn {
2489 named.Under = iface
2490 } else {
2491 tn.Typ = iface
2492 }
2493 }
2494 }
2495 }
2496
2497 // Pass 2: funcs, methods, vars, consts.
2498 scanPos = pos
2499 for scanPos < dlen {
2500 ls2, le2, next2 := mxhNextLine(d, scanPos)
2501 scanPos = next2
2502 if mxhHasPrefix(d, ls2, le2, "func ") {
2503 ws2, we2 := mxhNextWord(d, ls2+5, le2)
2504 if ws2 >= we2 {
2505 continue
2506 }
2507 fname := string(d[ws2:we2])
2508 ss, se := mxhNextWord(d, we2, le2)
2509 if ss < se {
2510 sig := parseSignatureDesc(string(d[ss:se]))
2511 pkg.Scope.Insert(NewTCFunc(pkg, fname, sig))
2512 } else {
2513 sig := parseSignatureDesc("")
2514 pkg.Scope.Insert(NewTCFunc(pkg, fname, sig))
2515 }
2516 } else if mxhHasPrefix(d, ls2, le2, "var ") {
2517 ws2, we2 := mxhNextWord(d, ls2+4, le2)
2518 if ws2 >= we2 {
2519 continue
2520 }
2521 vname := string(d[ws2:we2])
2522 ts, te := mxhNextWord(d, we2, le2)
2523 if ts < te {
2524 typ := parseTypeDesc(string(d[ts:te]))
2525 pkg.Scope.Insert(NewTCVar(pkg, vname, typ))
2526 }
2527 } else if mxhHasPrefix(d, ls2, le2, "const ") {
2528 ws2, we2 := mxhNextWord(d, ls2+6, le2)
2529 if ws2 >= we2 {
2530 continue
2531 }
2532 cname := string(d[ws2:we2])
2533 ts, te := mxhNextWord(d, we2, le2)
2534 if ts >= te {
2535 continue
2536 }
2537 typ := parseTypeDesc(string(d[ts:te]))
2538 // Remaining words after type are the const value.
2539 vs, ve := mxhNextWord(d, te, le2)
2540 var val ConstVal
2541 if vs < ve {
2542 // Reassemble value from remaining words. Const values
2543 // may contain spaces (string literals). Walk remaining
2544 // words and join with space - but use index range to
2545 // avoid per-word | concat.
2546 valStart := vs
2547 valEnd := le2
2548 // trim trailing whitespace
2549 for valEnd > valStart && (d[valEnd-1] == ' ' || d[valEnd-1] == '\t') {
2550 valEnd--
2551 }
2552 cval := string(d[valStart:valEnd])
2553 isNum := true
2554 for ci := int32(0); ci < int32(len(cval)); ci++ {
2555 ch := cval[ci]
2556 if !((ch >= '0' && ch <= '9') || ch == '-' || ch == '+' || ch == 'x' || ch == 'X' || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F') || ch == ' ') {
2557 isNum = false
2558 break
2559 }
2560 }
2561 if isNum {
2562 n := ssaParseInt64(cval)
2563 val = &ConstInt{V: n}
2564 } else {
2565 if b, isb := typ.(*Basic); isb && (b.Kind == TCString || b.Kind == UntypedString) {
2566 val = &ConstStr{S: cval}
2567 } else {
2568 val = &ConstInt{V: 0}
2569 }
2570 }
2571 } else {
2572 val = &ConstInt{V: 0}
2573 }
2574 pkg.Scope.Insert(NewTCConst(pkg, cname, typ, val))
2575 } else if mxhHasPrefix(d, ls2, le2, "method ") {
2576 nwords := mxhCountWords(d, ls2+7, le2)
2577 if nwords < 3 {
2578 continue
2579 }
2580 w1s, w1e := mxhNextWord(d, ls2+7, le2)
2581 w2s, w2e := mxhNextWord(d, w1e, le2)
2582 w3s, w3e := mxhNextWord(d, w2e, le2)
2583 typeName := string(d[w1s:w1e])
2584 methodName := string(d[w2s:w2e])
2585 sigDesc := string(d[w3s:w3e])
2586 ptrRecv := false
2587 if nwords >= 4 {
2588 w4s, w4e := mxhNextWord(d, w3e, le2)
2589 if mxhEq(d, w4s, w4e, "1") {
2590 ptrRecv = true
2591 }
2592 }
2593 if nwords == 3 && (sigDesc == "0" || sigDesc == "1") {
2594 ptrRecv = sigDesc == "1"
2595 sigDesc = ""
2596 }
2597 obj := pkg.Scope.Lookup(typeName)
2598 if obj == nil {
2599 continue
2600 }
2601 tn, istn := obj.(*TypeName)
2602 if !istn {
2603 continue
2604 }
2605 named, isn := tn.Typ.(*Named)
2606 if !isn {
2607 continue
2608 }
2609 sig := parseSignatureDesc(sigDesc)
2610 var recvType Type = named
2611 if ptrRecv {
2612 recvType = NewPointer(named)
2613 }
2614 sig = NewSignature(NewTCVar(pkg, "", recvType), sig.Params, sig.Results, sig.Variadic)
2615 fn := NewTCFunc(pkg, methodName, sig)
2616 fn.PtrRecv = ptrRecv
2617 named.AddMethod(fn)
2618 }
2619 }
2620
2621 cctx.parseTypePkgScope = oldScope
2622 cctx.universe.Registry[pkgPath] = pkg
2623 runtime.SetCurrentArena(mxhSavedArena)
2624 return true
2625 }
2626
2627 func cachedMxhPath(cacheBase, pkgPath, hash, version string) (s string) {
2628 return mxutil.JoinPath(mxutil.JoinPath(cacheBase, safeName(pkgPath)), versionedCacheName(hash, version, ".mxh"))
2629 }
2630
2631 func removeAll(path string) {
2632 entries := listFiles(path, "")
2633 if entries == nil {
2634 return
2635 }
2636 for _, e := range entries {
2637 os.Remove(mxutil.JoinPath(path, e))
2638 }
2639 os.Remove(path)
2640 }
2641
2642 func walkStdlibPkgs(srcDir, prefix string, out []string) (result []string) {
2643 entries := listFiles(srcDir, "")
2644 if entries == nil {
2645 return out
2646 }
2647 mxFiles := listFiles(srcDir, ".mx")
2648 if len(mxFiles) > 0 && prefix != "" {
2649 push(out, prefix)
2650 }
2651 for _, e := range entries {
2652 if e[0] == '.' || e[0] == '_' {
2653 continue
2654 }
2655 sub := mxutil.JoinPath(srcDir, e)
2656 subEntries := listFiles(sub, "")
2657 if subEntries != nil {
2658 var subPrefix string
2659 if prefix == "" {
2660 subPrefix = e
2661 } else {
2662 subPrefix = prefix | "/" | e
2663 }
2664 out = walkStdlibPkgs(sub, subPrefix, out)
2665 }
2666 }
2667 return out
2668 }
2669
2670 func cmdCacheWarm() {
2671 root := getenv("MOXIEROOT")
2672 if root == "" {
2673 root = "."
2674 }
2675 srcDir := mxutil.JoinPath(root, "src")
2676 mxutil.WriteStr(2, "warming stdlib cache from " | srcDir | " ...\n")
2677 pkgPaths := walkStdlibPkgs(srcDir, "", nil)
2678 if len(pkgPaths) == 0 {
2679 mxutil.WriteStr(2, " no stdlib packages found\n")
2680 return
2681 }
2682 triple := "x86_64-unknown-linux-musleabihf"
2683 clang := "clang-22"
2684 tmpdir := "/tmp/mxc-build"
2685 mkdirAll(tmpdir, 0755)
2686 cacheBase := pkgCacheDir()
2687 registerBuiltins()
2688 compiled := int32(0)
2689 skipped := int32(0)
2690 failed := int32(0)
2691 for _, pkgPath := range pkgPaths {
2692 if isBuiltinOnly(pkgPath) {
2693 continue
2694 }
2695 pkg := discoverPkg(pkgPath, root)
2696 if pkg == nil {
2697 skipped++
2698 continue
2699 }
2700 hash := pkgHash(pkg)
2701 cached := cachedBcPath(cacheBase, pkg.path, hash, pkg.version)
2702 cachedMxh := cachedMxhPath(cacheBase, pkg.path, hash, pkg.version)
2703 bcOk := mxutil.FileExists(cached)
2704 mxhOk := mxutil.FileExists(cachedMxh)
2705 if bcOk && mxhOk {
2706 skipped++
2707 continue
2708 }
2709 compilePath := pkg.path
2710 if mxutil.HasPrefix(compilePath, "/") || mxutil.HasPrefix(compilePath, "./") || mxutil.HasPrefix(compilePath, "../") {
2711 compilePath = pkg.name
2712 }
2713 src := concatSources(pkg.dir, pkg.files)
2714 ir := compileSource(src, compilePath, triple, pkg.dir)
2715 llFile := mxutil.JoinPath(tmpdir, safeName(pkg.path) | ".ll")
2716 writeFile(llFile, []byte(ir), 0644)
2717 bcFile := mxutil.JoinPath(tmpdir, safeName(pkg.path) | ".bc")
2718 rc := run([]string{clang, "--target=" | triple, "-emit-llvm", "-c", llFile, "-o", bcFile})
2719 if rc != 0 {
2720 mxutil.WriteStr(2, " FAIL: " | pkg.path | "\n")
2721 failed++
2722 continue
2723 }
2724 cdir := mxutil.JoinPath(cacheBase, safeName(pkg.path))
2725 mkdirAll(cdir, 0755)
2726 data, rok := mxutil.ReadFile(bcFile)
2727 if rok {
2728 writeFile(cached, data, 0644)
2729 }
2730 if len(cctx.lastMxhData) > 0 {
2731 writeFile(cachedMxh, []byte(cctx.lastMxhData), 0644)
2732 cctx.lastMxhData = ""
2733 }
2734 compiled++
2735 mxutil.WriteStr(2, " " | pkg.path | "\n")
2736 }
2737 mxutil.WriteStr(2, "warm done: " | simpleItoa(compiled) | " compiled, " | simpleItoa(skipped) | " cached, " | simpleItoa(failed) | " failed\n")
2738 }
2739
2740 func cacheClean() {
2741 base := pkgCacheDir()
2742 mxutil.WriteStr(2, "cleaning cache: " | base | "\n")
2743 entries := listFiles(base, "")
2744 if entries == nil {
2745 mxutil.WriteStr(2, " (empty)\n")
2746 return
2747 }
2748 for _, e := range entries {
2749 removeAll(mxutil.JoinPath(base, e))
2750 }
2751 os.Remove(base)
2752 mxutil.WriteStr(2, " done\n")
2753 }
2754
2755 func cmdUpdate(rebuildAll bool) {
2756 cacheRoot := mxutil.JoinPath(getMoxiePath(), "cache")
2757 currentDir := "mxc-" | mxcVersion()
2758 entries := listFiles(cacheRoot, "")
2759 if entries == nil {
2760 mxutil.WriteStr(2, "cache is empty\n")
2761 return
2762 }
2763 for _, e := range entries {
2764 if mxutil.HasPrefix(e, "mxc-") && e != currentDir {
2765 mxutil.WriteStr(2, " remove old: " | e | "\n")
2766 fullPath := mxutil.JoinPath(cacheRoot, e)
2767 pkgEntries := listFiles(fullPath, "")
2768 if pkgEntries != nil {
2769 for _, pe := range pkgEntries {
2770 removeAll(mxutil.JoinPath(fullPath, pe))
2771 }
2772 }
2773 os.Remove(fullPath)
2774 }
2775 }
2776 if !rebuildAll {
2777 mxutil.WriteStr(2, "update done\n")
2778 return
2779 }
2780 mxutil.WriteStr(2, "rebuilding all cached packages...\n")
2781 mpath := getMoxiePath()
2782 root := getenv("MOXIEROOT")
2783 if root == "" {
2784 root = "."
2785 }
2786 cacheBase := pkgCacheDir()
2787 pkgDirs := listFiles(cacheBase, "")
2788 if pkgDirs == nil {
2789 mxutil.WriteStr(2, " no packages to rebuild\n")
2790 return
2791 }
2792 triple := "x86_64-unknown-linux-musleabihf"
2793 clang := "clang-22"
2794 tmpdir := "/tmp/mxc-build"
2795 mkdirAll(tmpdir, 0755)
2796 registerBuiltins()
2797 for _, pkgDir := range pkgDirs {
2798 pkgPath := ""
2799 for i := 0; i < len(pkgDir); i++ {
2800 if pkgDir[i] == '_' {
2801 pkgPath = pkgPath | "/"
2802 } else {
2803 pkgPath = pkgPath | string([]byte{pkgDir[i]})
2804 }
2805 }
2806 if len(pkgPath) > 0 && pkgPath[0] == '/' {
2807 continue
2808 }
2809 mxutil.WriteStr(2, " rebuild " | pkgPath | "\n")
2810 pkg := discoverPkg(pkgPath, root)
2811 if pkg == nil {
2812 candidate := mxutil.JoinPath(mpath, pkgPath)
2813 mxF := listFiles(candidate, ".mx")
2814 if len(mxF) > 0 {
2815 pkg = discoverPkg(pkgPath, root)
2816 }
2817 }
2818 if pkg == nil {
2819 mxutil.WriteStr(2, " skip (source not found)\n")
2820 continue
2821 }
2822 compilePath := pkg.path
2823 if compilePath == "." || compilePath == ".." || mxutil.HasPrefix(compilePath, "/") || mxutil.HasPrefix(compilePath, "./") || mxutil.HasPrefix(compilePath, "../") {
2824 compilePath = pkg.name
2825 }
2826 src := concatSources(pkg.dir, pkg.files)
2827 ir := compileSource(src, compilePath, triple, pkg.dir)
2828 llFile := mxutil.JoinPath(tmpdir, safeName(pkg.path) | ".ll")
2829 writeFile(llFile, []byte(ir), 0644)
2830 bcFile := mxutil.JoinPath(tmpdir, safeName(pkg.path) | ".bc")
2831 rc := run([]string{clang, "--target=" | triple, "-emit-llvm", "-c", llFile, "-o", bcFile})
2832 if rc != 0 {
2833 mxutil.WriteStr(2, " FAIL: clang for " | pkg.path | "\n")
2834 continue
2835 }
2836 hash := pkgHash(pkg)
2837 cached := cachedBcPath(cacheBase, pkg.path, hash, pkg.version)
2838 cdir := mxutil.JoinPath(cacheBase, safeName(pkg.path))
2839 mkdirAll(cdir, 0755)
2840 data, rok := mxutil.ReadFile(bcFile)
2841 if rok {
2842 writeFile(cached, data, 0644)
2843 }
2844 if len(cctx.lastMxhData) > 0 {
2845 cachedMxh := cachedMxhPath(cacheBase, pkg.path, hash, pkg.version)
2846 writeFile(cachedMxh, []byte(cctx.lastMxhData), 0644)
2847 cctx.lastMxhData = ""
2848 }
2849 }
2850 mxutil.WriteStr(2, "rebuild done\n")
2851 }
2852
2853 func scanTestFuncs(data []byte) (ss []string) {
2854 var funcs []string
2855 lines := mxutil.SplitLines(string(data))
2856 for _, line := range lines {
2857 line = mxutil.TrimSpace(line)
2858 if !mxutil.HasPrefix(line, "func Test") {
2859 continue
2860 }
2861 paren := int32(-1)
2862 for i := int32(5); i < int32(len(line)); i++ {
2863 if line[i] == '(' {
2864 paren = i
2865 break
2866 }
2867 }
2868 if paren < 0 {
2869 continue
2870 }
2871 name := line[5:paren] // "func " is 5 chars, so name starts at "Test..."
2872 if len(name) == 0 {
2873 continue
2874 }
2875 if name[0] < 'A' || name[0] > 'Z' {
2876 continue
2877 }
2878 rest := line[paren:]
2879 if mxutil.HasPrefix(rest, "(t *testing.T)") {
2880 push(funcs, name)
2881 }
2882 }
2883 return funcs
2884 }
2885
2886 func generateTestMain(pkgName string, testFuncs []string) (buf []byte) {
2887 var out []byte
2888 out = out | ("package " | pkgName | "\n\nimport \"testing\"\n\nfunc main() {\n\ttests := []testing.InternalTest{\n")
2889 for _, name := range testFuncs {
2890 out = out | ("\t\t{Name: \"" | name | "\", F: " | name | "},\n")
2891 }
2892 out = out | "\t}\n\ttesting.Main(tests)\n}\n"
2893 return out
2894 }
2895
2896 func cmdTest(args []string) {
2897 pkgPath := ""
2898 forceRebuild := false
2899 verbose := false
2900 i := int32(2)
2901 for i < int32(len(args)) {
2902 if args[i] == "-a" {
2903 forceRebuild = true
2904 i++
2905 } else if args[i] == "-v" {
2906 verbose = true
2907 i++
2908 } else {
2909 pkgPath = args[i]
2910 i++
2911 }
2912 }
2913 if pkgPath == "" {
2914 pkgPath = "."
2915 }
2916 _ = verbose
2917
2918 root := getenv("MOXIEROOT")
2919 if root == "" {
2920 root = "."
2921 }
2922 triple := "x86_64-unknown-linux-musleabihf"
2923 clang := "clang-22"
2924
2925 if mxutil.HasPrefix(pkgPath, "./") || mxutil.HasPrefix(pkgPath, "../") || pkgPath == "." || mxutil.HasPrefix(pkgPath, "/") {
2926 absDir := pkgPath
2927 if absDir == "." {
2928 absDir = ""
2929 }
2930 bst.globalModPrefix, bst.globalModDir = findModuleRoot(absDir)
2931 }
2932
2933 registerBuiltins()
2934
2935 var testPkgDir string
2936 if pkgPath == "." || pkgPath == ".." || mxutil.HasPrefix(pkgPath, "/") || mxutil.HasPrefix(pkgPath, "./") || mxutil.HasPrefix(pkgPath, "../") {
2937 testPkgDir = pkgPath
2938 } else {
2939 testPkgDir = mxutil.JoinPath(root, mxutil.JoinPath("src", pkgPath))
2940 }
2941
2942 testFiles := listFiles(testPkgDir, "_test.mx")
2943 if len(testFiles) == 0 {
2944 mxutil.WriteStr(2, "no test files found in " | testPkgDir | "\n")
2945 return
2946 }
2947
2948 var allTestFuncs []string
2949 for _, tf := range testFiles {
2950 data, ok := mxutil.ReadFile(mxutil.JoinPath(testPkgDir, tf))
2951 if !ok {
2952 continue
2953 }
2954 funcs := scanTestFuncs(data)
2955 for _, fn := range funcs {
2956 push(allTestFuncs, fn)
2957 }
2958 }
2959 if len(allTestFuncs) == 0 {
2960 mxutil.WriteStr(2, "no Test* functions found\n")
2961 return
2962 }
2963
2964 mxutil.WriteStr(2, "=== test: " | pkgPath | " ===\n")
2965 for _, fn := range allTestFuncs {
2966 mxutil.WriteStr(2, " found " | fn | "\n")
2967 }
2968
2969 pkgName := ""
2970 for _, tf := range testFiles {
2971 data, ok := mxutil.ReadFile(mxutil.JoinPath(testPkgDir, tf))
2972 if ok && pkgName == "" {
2973 pkgName = extractPkgName(data)
2974 }
2975 }
2976 if pkgName == "" {
2977 pkgName = "main"
2978 }
2979
2980 tmpdir := "/tmp/mxc-testbuild"
2981 mkdirAll(tmpdir, 0755)
2982
2983 testMainSrc := generateTestMain(pkgName, allTestFuncs)
2984 testMainFile := mxutil.JoinPath(tmpdir, "_testmain.mx")
2985 writeFile(testMainFile, testMainSrc, 0644)
2986
2987 pkg := discoverPkg(pkgPath, root)
2988 if pkg == nil {
2989 fatal("cannot find package " | pkgPath)
2990 }
2991
2992 for _, tf := range testFiles {
2993 push(pkg.files, tf)
2994 }
2995 push(pkg.files, testMainFile)
2996 pkg.name = "main"
2997
2998 if !hasImportStr(pkg.imports, "testing") {
2999 push(pkg.imports, "testing")
3000 }
3001
3002 pkgs := resolveTestDeps(pkg, root)
3003
3004 cacheBase := pkgCacheDir()
3005
3006 for _, dp := range pkgs {
3007 compilePath := dp.path
3008 if compilePath == "." || compilePath == ".." || mxutil.HasPrefix(compilePath, "/") || mxutil.HasPrefix(compilePath, "./") || mxutil.HasPrefix(compilePath, "../") {
3009 compilePath = dp.name
3010 }
3011 if dp == pkg {
3012 compilePath = "main"
3013 }
3014 hash := pkgHash(dp)
3015 cachedMxh := cachedMxhPath(cacheBase, dp.path, hash, dp.version)
3016 if mxhOk := mxutil.FileExists(cachedMxh); mxhOk && !forceRebuild && dp != pkg {
3017 loadMxh(cachedMxh)
3018 src := concatSources(dp.dir, dp.files)
3019 ScanGenericDecls(src, compilePath)
3020 } else {
3021 var src []byte
3022 if dp == pkg {
3023 src = concatTestSources(dp.dir, dp.files, testMainFile)
3024 } else {
3025 src = concatSources(dp.dir, dp.files)
3026 }
3027 TypeCheckOnly(src, compilePath, "")
3028 }
3029 }
3030
3031 var allBcFiles []string
3032 hasRuntimePkg := false
3033
3034 for _, dp := range pkgs {
3035 if dp.path == "runtime" {
3036 hasRuntimePkg = true
3037 }
3038 compilePath := dp.path
3039 if compilePath == "." || compilePath == ".." || mxutil.HasPrefix(compilePath, "/") || mxutil.HasPrefix(compilePath, "./") || mxutil.HasPrefix(compilePath, "../") {
3040 compilePath = dp.name
3041 }
3042 if dp == pkg {
3043 compilePath = "main"
3044 }
3045
3046 hash := pkgHash(dp)
3047 cached := cachedBcPath(cacheBase, dp.path, hash, dp.version)
3048 cachedMxh := cachedMxhPath(cacheBase, dp.path, hash, dp.version)
3049
3050 if dp != pkg {
3051 bcOk := mxutil.FileExists(cached)
3052 mxhOk := mxutil.FileExists(cachedMxh)
3053 if bcOk && mxhOk && !forceRebuild {
3054 mxutil.WriteStr(2, " cached " | dp.path | "\n")
3055 loadMxh(cachedMxh)
3056 push(allBcFiles, cached)
3057 for _, cf := range dp.cfiles {
3058 cPath := mxutil.JoinPath(dp.dir, cf)
3059 cbcFile := mxutil.JoinPath(tmpdir, safeName(dp.path) | "_" | cf | ".bc")
3060 crc := run([]string{clang, "--target=" | triple, "-emit-llvm", "-c", cPath, "-o", cbcFile})
3061 if crc == 0 {
3062 push(allBcFiles, cbcFile)
3063 }
3064 }
3065 for _, bf := range dp.bcfiles {
3066 push(allBcFiles, mxutil.JoinPath(dp.dir, bf))
3067 }
3068 continue
3069 }
3070
3071 if isStdlib(dp.path) && bcOk && !forceRebuild {
3072 mxutil.WriteStr(2, " cached " | dp.path | "\n")
3073 csrc := concatSources(dp.dir, dp.files)
3074 cdir := mxutil.JoinPath(cacheBase, safeName(dp.path))
3075 mkdirAll(cdir, 0755)
3076 TypeCheckOnly(csrc, compilePath, cachedMxh)
3077 push(allBcFiles, cached)
3078 continue
3079 }
3080 }
3081
3082 mxutil.WriteStr(2, " compile " | dp.path | "\n")
3083 var src []byte
3084 if dp == pkg {
3085 src = concatTestSources(dp.dir, dp.files, testMainFile)
3086 } else {
3087 src = concatSources(dp.dir, dp.files)
3088 }
3089 ir := compileSource(src, compilePath, triple, dp.dir)
3090 llFile := mxutil.JoinPath(tmpdir, safeName(dp.path) | ".ll")
3091 writeFile(llFile, []byte(ir), 0644)
3092 bcFile := mxutil.JoinPath(tmpdir, safeName(dp.path) | ".bc")
3093 drc := run([]string{clang, "--target=" | triple, "-emit-llvm", "-c", llFile, "-o", bcFile})
3094 if drc != 0 {
3095 fatal("clang failed for " | dp.path)
3096 }
3097 if dp != pkg {
3098 cacheDir := mxutil.JoinPath(cacheBase, safeName(dp.path))
3099 mkdirAll(cacheDir, 0755)
3100 data, rok := mxutil.ReadFile(bcFile)
3101 if rok {
3102 writeFile(cached, data, 0644)
3103 }
3104 if len(cctx.lastMxhData) > 0 {
3105 writeFile(cachedMxh, []byte(cctx.lastMxhData), 0644)
3106 cctx.lastMxhData = ""
3107 }
3108 }
3109 push(allBcFiles, bcFile)
3110 for _, cf := range dp.cfiles {
3111 cPath := mxutil.JoinPath(dp.dir, cf)
3112 cbcFile := mxutil.JoinPath(tmpdir, safeName(dp.path) | "_" | cf | ".bc")
3113 drc = run([]string{clang, "--target=" | triple, "-emit-llvm", "-c", cPath, "-o", cbcFile})
3114 if drc != 0 {
3115 fatal("clang failed for " | cf)
3116 }
3117 push(allBcFiles, cbcFile)
3118 }
3119 for _, bf := range dp.bcfiles {
3120 push(allBcFiles, mxutil.JoinPath(dp.dir, bf))
3121 }
3122 }
3123
3124 loadSysroot(mxutil.JoinPath(root, "_sysroot.env"))
3125
3126 sysroot := mxutil.JoinPath(root, "sysroot")
3127 runtimeBc := getenv("RUNTIME_BC")
3128 if runtimeBc == "" {
3129 runtimeBc = mxutil.JoinPath(sysroot, "runtime.bc")
3130 }
3131 muslDir := getenv("MUSL_DIR")
3132 if muslDir == "" {
3133 muslDir = mxutil.JoinPath(sysroot, "musl")
3134 }
3135 comprtLib := getenv("COMPRT_LIB")
3136 if comprtLib == "" {
3137 comprtLib = mxutil.JoinPath(mxutil.JoinPath(sysroot, "compiler-rt"), "lib.a")
3138 }
3139 muslLib := getenv("MUSL_LIB")
3140 if muslLib == "" {
3141 muslLib = mxutil.JoinPath(mxutil.JoinPath(sysroot, "musl"), "lib.a")
3142 }
3143
3144 mxutil.WriteStr(2, " generate initAll\n")
3145 var initCalls string
3146 for _, dp := range pkgs {
3147 compilePath := dp.path
3148 if compilePath == "." || compilePath == ".." || mxutil.HasPrefix(compilePath, "/") || mxutil.HasPrefix(compilePath, "./") || mxutil.HasPrefix(compilePath, "../") {
3149 compilePath = dp.name
3150 }
3151 if dp == pkg {
3152 continue
3153 }
3154 initSym := irGlobalSymbol(compilePath, "main")
3155 initCalls = initCalls | " call void " | initSym | "(ptr null)\n"
3156 }
3157 initAllIR := "\ndefine dso_local void @runtime.initAll(ptr %_ctx) {\nentry:\n" | initCalls | " ret void\n}\n" |
3158 "declare ptr @llvm.stacksave.p0()\n" |
3159 "define ptr @runtime.stacksave(ptr %_ctx) {\n" |
3160 " %1 = call ptr @llvm.stacksave.p0()\n" |
3161 " ret ptr %1\n" |
3162 "}\n" |
3163 "define void @runtime.relocCallFixup(i64 %fn, ptr %obj, ptr %rs, ptr %ctx) {\n" |
3164 " %fp = inttoptr i64 %fn to ptr\n" |
3165 " call void %fp(ptr %obj, ptr %rs, ptr null)\n" |
3166 " ret void\n" |
3167 "}\n"
3168 declared := map[string]bool{}
3169 for _, dp := range pkgs {
3170 compilePath := dp.path
3171 if compilePath == "." || compilePath == ".." || mxutil.HasPrefix(compilePath, "/") || mxutil.HasPrefix(compilePath, "./") || mxutil.HasPrefix(compilePath, "../") {
3172 compilePath = dp.name
3173 }
3174 if dp == pkg {
3175 continue
3176 }
3177 declSym := irGlobalSymbol(compilePath, "main")
3178 if declared[declSym] {
3179 continue
3180 }
3181 declared[declSym] = true
3182 initAllIR = initAllIR | "declare void " | declSym | "(ptr)\n"
3183 }
3184 initAllFile := mxutil.JoinPath(tmpdir, "initall.ll")
3185 writeFile(initAllFile, []byte(initAllIR), 0644)
3186 initAllBc := mxutil.JoinPath(tmpdir, "initall.bc")
3187 rc := run([]string{clang, "--target=" | triple, "-emit-llvm", "-c", initAllFile, "-o", initAllBc})
3188 if rc != 0 {
3189 fatal("clang failed for initall")
3190 }
3191
3192 mxutil.WriteStr(2, " compile objects\n")
3193 var objList []string
3194 if !hasRuntimePkg {
3195 runtimeGoBc := mxutil.JoinPath(sysroot, "runtime_go.bc")
3196 if _, gok := mxutil.ReadFile(runtimeGoBc); gok {
3197 push(objList, clangCompileToObj(clang, triple,runtimeGoBc))
3198 } else {
3199 push(objList, clangCompileToObj(clang, triple,runtimeBc))
3200 }
3201 }
3202 if !hasRuntimePkg {
3203 rtShims := mxutil.JoinPath(sysroot, "rt_shims.bc")
3204 if _, rsk := mxutil.ReadFile(rtShims); rsk {
3205 push(objList, clangCompileToObj(clang, triple,rtShims))
3206 }
3207 }
3208 if bst.buildJobs > 1 {
3209 var objCmds [][]string
3210 for _, bf := range allBcFiles {
3211 push(objCmds, []string{clang, "--target=" | triple,
3212 "-c", "-O0", bf, "-o", bf | ".o"})
3213 }
3214 if !runPool(objCmds, bst.buildJobs) {
3215 fatal("clang -c failed")
3216 }
3217 for _, bf := range allBcFiles {
3218 push(objList, bf|".o")
3219 }
3220 } else {
3221 for _, bf := range allBcFiles {
3222 push(objList, clangCompileToObj(clang, triple,bf))
3223 }
3224 }
3225 push(objList, clangCompileToObj(clang, triple,initAllBc))
3226 if !hasRuntimePkg {
3227 cstubDir := mxutil.JoinPath(root, "src/runtime")
3228 cstubFiles := listFiles(cstubDir, ".c")
3229 for _, cf := range cstubFiles {
3230 cPath := mxutil.JoinPath(cstubDir, cf)
3231 coFile := mxutil.JoinPath(tmpdir, "cstub_" | cf | ".o")
3232 rc = run([]string{clang, "--target=" | triple, "-c", "-O0", cPath, "-o", coFile})
3233 if rc == 0 {
3234 push(objList, coFile)
3235 }
3236 }
3237 }
3238 objDir := mxutil.JoinPath(sysroot, "obj")
3239 objEntries := listFiles(objDir, ".bc")
3240 if objEntries != nil {
3241 for _, e := range objEntries {
3242 push(objList, clangCompileToObj(clang, triple,mxutil.JoinPath(objDir, e)))
3243 }
3244 }
3245 oEntries := listFiles(objDir, ".o")
3246 if oEntries != nil {
3247 for _, e := range oEntries {
3248 push(objList, mxutil.JoinPath(objDir, e))
3249 }
3250 }
3251 outpath := mxutil.JoinPath(tmpdir, "test.bin")
3252 mxutil.WriteStr(2, " link\n")
3253 linker := "ld.lld-22"
3254 ldArgs := []string{linker, "--gc-sections", "-o", outpath,
3255 mxutil.JoinPath(muslDir, "crt1.o")}
3256 for _, o := range objList {
3257 push(ldArgs, o)
3258 }
3259 push(ldArgs, comprtLib, muslLib, "-z", "stack-size=67108864")
3260 rc = run(ldArgs)
3261 if rc != 0 {
3262 fatal("linker failed")
3263 }
3264
3265 mxutil.WriteStr(2, " run tests\n")
3266 rc = run([]string{outpath})
3267 os.Exit(int32(rc))
3268 }
3269
3270 func hasImportStr(imports []string, pkg string) (ok bool) {
3271 for _, imp := range imports {
3272 if imp == pkg {
3273 return true
3274 }
3275 }
3276 return false
3277 }
3278
3279 func resolveTestDeps(testPkg *pkgInfo, root string) (ss []*pkgInfo) {
3280 r := &resolver{
3281 visited: map[string]bool{},
3282 root: root,
3283 }
3284 for _, imp := range testPkg.imports {
3285 r.walk(imp)
3286 }
3287 push(r.order, testPkg)
3288 return r.order
3289 }
3290
3291 func concatTestSources(dir string, files []string, testMainFile string) (buf []byte) {
3292 imports := map[string]bool{}
3293 var bodies [][]byte
3294 var bodyFiles []string
3295 var bodyStartLines []int32
3296 var bodyImports [][]string
3297 pkgName := "main"
3298
3299 allFiles := []string{:0:len(files) + 1}
3300 for _, f := range files {
3301 if f == testMainFile {
3302 continue
3303 }
3304 push(allFiles, f)
3305 }
3306
3307 for _, f := range allFiles {
3308 var data []byte
3309 var ok bool
3310 if mxutil.HasPrefix(f, "/") {
3311 data, ok = mxutil.ReadFile(f)
3312 } else {
3313 data, ok = mxutil.ReadFile(mxutil.JoinPath(dir, f))
3314 }
3315 if !ok {
3316 continue
3317 }
3318 lines := mxutil.SplitLines(string(data))
3319 var body []string
3320 var fimps []string
3321 i := int32(0)
3322 firstBodyLine := int32(0)
3323 for i < int32(len(lines)) {
3324 line := mxutil.TrimSpace(lines[i])
3325 if mxutil.HasPrefix(line, "package ") {
3326 i++
3327 continue
3328 }
3329 if line == "import (" {
3330 i++
3331 for i < int32(len(lines)) {
3332 imp := mxutil.TrimSpace(lines[i])
3333 i++
3334 if imp == ")" {
3335 break
3336 }
3337 if imp != "" {
3338 imports[imp] = true
3339 push(fimps, imp)
3340 }
3341 }
3342 continue
3343 }
3344 if mxutil.HasPrefix(line, "import ") && !mxutil.HasPrefix(line, "import (") {
3345 imp := line[7:]
3346 imports[imp] = true
3347 push(fimps, imp)
3348 i++
3349 continue
3350 }
3351 if len(body) == 0 {
3352 firstBodyLine = i + 1
3353 }
3354 push(body, lines[i])
3355 i++
3356 }
3357 push(bodies, []byte(joinStrings(body, "\n")))
3358 push(bodyFiles, f)
3359 push(bodyStartLines, firstBodyLine)
3360 push(bodyImports, fimps)
3361 }
3362
3363 tmData, tmOk := mxutil.ReadFile(testMainFile)
3364 if tmOk {
3365 lines := mxutil.SplitLines(string(tmData))
3366 var body []string
3367 var fimps []string
3368 i := int32(0)
3369 firstBodyLine := int32(0)
3370 for i < int32(len(lines)) {
3371 line := mxutil.TrimSpace(lines[i])
3372 if mxutil.HasPrefix(line, "package ") {
3373 i++
3374 continue
3375 }
3376 if line == "import (" {
3377 i++
3378 for i < int32(len(lines)) {
3379 imp := mxutil.TrimSpace(lines[i])
3380 i++
3381 if imp == ")" {
3382 break
3383 }
3384 if imp != "" {
3385 imports[imp] = true
3386 push(fimps, imp)
3387 }
3388 }
3389 continue
3390 }
3391 if mxutil.HasPrefix(line, "import ") && !mxutil.HasPrefix(line, "import (") {
3392 imp := line[7:]
3393 imports[imp] = true
3394 push(fimps, imp)
3395 i++
3396 continue
3397 }
3398 if len(body) == 0 {
3399 firstBodyLine = i + 1
3400 }
3401 push(body, lines[i])
3402 i++
3403 }
3404 push(bodies, []byte(joinStrings(body, "\n")))
3405 push(bodyFiles, "_testmain.mx")
3406 push(bodyStartLines, firstBodyLine)
3407 push(bodyImports, fimps)
3408 }
3409
3410 var out []byte
3411 out = out | ("package " | pkgName | "\n")
3412 if len(imports) > 0 {
3413 var impKeys []string
3414 for imp := range imports {
3415 push(impKeys, imp)
3416 }
3417 for i := int32(1); i < int32(len(impKeys)); i++ {
3418 for j := i; j > 0 && impKeys[j] < impKeys[j-1]; j-- {
3419 impKeys[j], impKeys[j-1] = impKeys[j-1], impKeys[j]
3420 }
3421 }
3422 out = out | "import (\n"
3423 for _, imp := range impKeys {
3424 push(out, '\t')
3425 out = out | imp
3426 push(out, '\n')
3427 }
3428 out = out | ")\n"
3429 }
3430 cctx.concatSourceMap = nil
3431 mxutil.ResetConcatImportSegs()
3432 outLine := int32(1)
3433 for j := int32(0); j < int32(len(out)); j++ {
3434 if out[j] == '\n' {
3435 outLine++
3436 }
3437 }
3438 for i, b := range bodies {
3439 push(cctx.concatSourceMap, srcSpan{
3440 file: bodyFiles[i],
3441 outStart: outLine,
3442 srcLine: bodyStartLines[i],
3443 })
3444 mxutil.AppendConcatImportSeg(mxutil.ImportSeg{
3445 OutStart: outLine,
3446 Specs: bodyImports[i],
3447 })
3448 for j := int32(0); j < int32(len(b)); j++ {
3449 if b[j] == '\n' {
3450 outLine++
3451 }
3452 }
3453 outLine++
3454 out = out | b
3455 push(out, '\n')
3456 bodies[i] = nil
3457 }
3458 bodies = nil
3459 imports = nil
3460 return out
3461 }
3462
3463 func main() {
3464 // Root arena is the domain root's arena, set up by the runtime.
3465 // 2GB virtual - pages committed on touch by the MMU.
3466 initCompileCtx()
3467 cctx.universe = InitUniverse()
3468 SetUniverseGlobals(cctx.universe)
3469 initBuildState()
3470 bst.buildJobs = int32(4)
3471 bst.wasmRtPkgs = map[string]bool{
3472 "runtime": true, "runtime/interrupt": true,
3473 "internal/task": true, "internal/itoa": true,
3474 "math": true, "math/bits": true,
3475 "unicode/utf8": true, "strconv": true,
3476 "errors": true, "io": true, "io/fs": true,
3477 "os": true, "syscall": true, "time": true,
3478 "fmt": true, "path": true,
3479 "internal/bytealg": true, "internal/byteorder": true,
3480 "internal/goarch": true, "internal/binary": true,
3481 "internal/gclayout": true, "internal/oserror": true,
3482 "internal/stringslite": true, "internal/testlog": true,
3483 }
3484 bst.builtinOnly = []string{"unsafe"}
3485 args := os.Args
3486 if len(args) < 2 {
3487 mxutil.WriteStr(2, "usage: mxc <build|test|version|cache|fetch|update> ...\n")
3488 os.Exit(1)
3489 }
3490
3491 cmd := args[1]
3492 if cmd == "version" {
3493 mxutil.WriteStr(1, "mxc " | mxcVersion() | " (" | compilerHash() | ")\n")
3494 return
3495 }
3496 if cmd == "cache" {
3497 if len(args) >= 3 && args[2] == "clean" {
3498 cacheClean()
3499 return
3500 }
3501 if len(args) >= 3 && args[2] == "warm" {
3502 cmdCacheWarm()
3503 return
3504 }
3505 mxutil.WriteStr(2, "usage: mxc cache <clean|warm>\n")
3506 os.Exit(1)
3507 }
3508 if cmd == "fetch" {
3509 fetchURL := ""
3510 if len(args) >= 3 {
3511 fetchURL = args[2]
3512 }
3513 cmdFetch(fetchURL)
3514 return
3515 }
3516 if cmd == "update" {
3517 rebuildAll := false
3518 for ai := int32(2); ai < int32(len(args)); ai++ {
3519 if args[ai] == "--all" {
3520 rebuildAll = true
3521 }
3522 }
3523 cmdUpdate(rebuildAll)
3524 return
3525 }
3526 if cmd == "test" {
3527 cmdTest(args)
3528 return
3529 }
3530 if cmd == "build-runtime" {
3531 cmdBuildRuntime()
3532 return
3533 }
3534 if cmd != "build" {
3535 mxutil.WriteStr(2, "usage: mxc <build|test|version|cache|fetch|update|build-runtime> ...\n")
3536 os.Exit(1)
3537 }
3538
3539 outpath := ""
3540 pkgPath := ""
3541 printIR := false
3542 forceRebuild := false
3543 targetFlag := ""
3544 i := int32(2)
3545 for i < int32(len(args)) {
3546 if args[i] == "-o" && i+1 < int32(len(args)) {
3547 outpath = args[i+1]
3548 i += 2
3549 } else if args[i] == "-target" && i+1 < int32(len(args)) {
3550 targetFlag = args[i+1]
3551 i += 2
3552 } else if args[i] == "-print-ir" {
3553 printIR = true
3554 i++
3555 } else if args[i] == "-a" {
3556 forceRebuild = true
3557 i++
3558 } else if args[i] == "-j" && i+1 < int32(len(args)) {
3559 bst.buildJobs = parseInt32(args[i+1])
3560 if bst.buildJobs < 1 {
3561 bst.buildJobs = 1
3562 }
3563 i += 2
3564 } else if args[i] == "-gc=dealloc" || args[i] == "-buildmode=c-shared" || args[i] == "-x" || args[i] == "-work" {
3565 i++
3566 } else {
3567 pkgPath = args[i]
3568 i++
3569 }
3570 }
3571 if printIR {
3572 // -print-ir needs the in-process compile path.
3573 bst.buildJobs = 1
3574 }
3575 if pkgPath == "" {
3576 fatal("no package specified")
3577 }
3578 if outpath == "" {
3579 outpath = pathBase(pkgPath)
3580 }
3581
3582 root := getenv("MOXIEROOT")
3583 if root == "" {
3584 root = "."
3585 }
3586 mxutil.SetTarget("linux", "amd64")
3587 triple := "x86_64-unknown-linux-musleabihf"
3588 if targetFlag == "js/wasm" || targetFlag == "wasm" {
3589 mxutil.SetTarget("js", "wasm")
3590 triple = "wasm32-unknown-wasi"
3591 } else if targetFlag == "wasi/wasm" {
3592 mxutil.SetTarget("wasi", "wasm")
3593 triple = "wasm32-unknown-wasi"
3594 }
3595 isWasm := mxutil.TargetArch == "wasm"
3596 clang := "clang-22"
3597
3598 if mxutil.HasPrefix(pkgPath, "./") || mxutil.HasPrefix(pkgPath, "../") || pkgPath == "." || mxutil.HasPrefix(pkgPath, "/") {
3599 absDir := pkgPath
3600 if absDir == "." {
3601 absDir = ""
3602 }
3603 bst.globalModPrefix, bst.globalModDir = findModuleRoot(absDir)
3604 }
3605
3606 registerBuiltins()
3607 pkgs := resolveAll(pkgPath, root)
3608 // wasm: runtime must be compiled from source (no prebuilt runtime.bc)
3609 if isWasm {
3610 hasRT := false
3611 for _, p := range pkgs {
3612 if p.path == "runtime" {
3613 hasRT = true
3614 break
3615 }
3616 }
3617 if !hasRT {
3618 rtPkgs := resolveAll("runtime", root)
3619 // prepend runtime deps before user packages
3620 var merged []*pkgInfo
3621 seen := map[string]bool{}
3622 for _, p := range rtPkgs {
3623 if !seen[p.path] {
3624 seen[p.path] = true
3625 push(merged, p)
3626 }
3627 }
3628 for _, p := range pkgs {
3629 if !seen[p.path] {
3630 seen[p.path] = true
3631 push(merged, p)
3632 }
3633 }
3634 pkgs = merged
3635 }
3636 }
3637 if len(pkgs) == 0 {
3638 fatal("no packages to compile")
3639 }
3640
3641 tmpdir := "/tmp/mxc-build"
3642 mkdirAll(tmpdir, 0755)
3643
3644 cacheBase := pkgCacheDir()
3645 // TLS probe: if this binary's globals aren't thread_local, the parallel
3646 // path would corrupt shared state. Fall back to serial.
3647 if bst.buildJobs > 1 && cDetectTLS() == 0 {
3648 mxutil.WriteStr(2, " note: globals not TLS-isolated, forcing -j 1 (rebuild with self-compiled mxc for parallel)\n")
3649 bst.buildJobs = 1
3650 }
3651 var allBcFiles []string
3652 var hasRuntimePkg bool
3653 if bst.buildJobs > 1 {
3654 a, h := buildPkgsParallel(pkgs, triple, clang, tmpdir, cacheBase, forceRebuild, bst.buildJobs)
3655 allBcFiles = a
3656 hasRuntimePkg = h
3657 } else {
3658 a, h := buildPkgsSerial(pkgs, triple, clang, tmpdir, cacheBase, forceRebuild, printIR)
3659 allBcFiles = a
3660 hasRuntimePkg = h
3661 }
3662 loadSysroot(mxutil.JoinPath(root, "_sysroot.env"))
3663
3664 sysroot := mxutil.JoinPath(root, "sysroot")
3665 runtimeBc := ""
3666 muslDir := ""
3667 comprtLib := ""
3668 muslLib := ""
3669 if !isWasm {
3670 runtimeBc = getenv("RUNTIME_BC")
3671 if runtimeBc == "" {
3672 runtimeBc = mxutil.JoinPath(sysroot, "runtime.bc")
3673 }
3674 muslDir = getenv("MUSL_DIR")
3675 if muslDir == "" {
3676 muslDir = mxutil.JoinPath(sysroot, "musl")
3677 }
3678 comprtLib = getenv("COMPRT_LIB")
3679 if comprtLib == "" {
3680 comprtLib = mxutil.JoinPath(mxutil.JoinPath(sysroot, "compiler-rt"), "lib.a")
3681 }
3682 muslLib = getenv("MUSL_LIB")
3683 if muslLib == "" {
3684 muslLib = mxutil.JoinPath(mxutil.JoinPath(sysroot, "musl"), "lib.a")
3685 }
3686 }
3687
3688 mxutil.WriteStr(2, " generate initAll\n")
3689 var initCalls string
3690 for _, pkg := range pkgs {
3691 compilePath := pkg.path
3692 if compilePath == "." || compilePath == ".." || mxutil.HasPrefix(compilePath, "/") || mxutil.HasPrefix(compilePath, "./") || mxutil.HasPrefix(compilePath, "../") {
3693 compilePath = pkg.name
3694 }
3695 if compilePath == "main" {
3696 continue
3697 }
3698 initSym := irGlobalSymbol(compilePath, "main")
3699 initCalls = initCalls | " call void " | initSym | "(ptr null)\n"
3700 }
3701 initAllIR := "\ndefine dso_local void @runtime.initAll(ptr %_ctx) {\nentry:\n" | initCalls | " ret void\n}\n" |
3702 "declare ptr @llvm.stacksave.p0()\n" |
3703 "define ptr @runtime.stacksave(ptr %_ctx) {\n" |
3704 " %1 = call ptr @llvm.stacksave.p0()\n" |
3705 " ret ptr %1\n" |
3706 "}\n" |
3707 "define void @runtime.relocCallFixup(i64 %fn, ptr %obj, ptr %rs, ptr %ctx) {\n" |
3708 " %fp = inttoptr i64 %fn to ptr\n" |
3709 " call void %fp(ptr %obj, ptr %rs, ptr null)\n" |
3710 " ret void\n" |
3711 "}\n"
3712 declared := map[string]bool{}
3713 for _, pkg := range pkgs {
3714 compilePath := pkg.path
3715 if compilePath == "." || compilePath == ".." || mxutil.HasPrefix(compilePath, "/") || mxutil.HasPrefix(compilePath, "./") || mxutil.HasPrefix(compilePath, "../") {
3716 compilePath = pkg.name
3717 }
3718 if compilePath == "main" {
3719 continue
3720 }
3721 declSym := irGlobalSymbol(compilePath, "main")
3722 if declared[declSym] {
3723 continue
3724 }
3725 declared[declSym] = true
3726 initAllIR = initAllIR | "declare void " | declSym | "(ptr)\n"
3727 }
3728 initAllFile := mxutil.JoinPath(tmpdir, "initall.ll")
3729 writeFile(initAllFile, []byte(initAllIR), 0644)
3730 initAllBc := mxutil.JoinPath(tmpdir, "initall.bc")
3731 rc := run([]string{clang, "--target=" | triple, "-emit-llvm", "-c", initAllFile, "-o", initAllBc})
3732 if rc != 0 {
3733 fatal("clang failed for initall")
3734 }
3735
3736 if isWasm {
3737 // wasm: compile each .bc to .o individually, then wasm-ld
3738 mxutil.WriteStr(2, " compile objects\n")
3739 wasmTriple := "wasm32-unknown-wasi"
3740 var objList []string
3741 if bst.buildJobs > 1 {
3742 var objCmds [][]string
3743 for _, bf := range allBcFiles {
3744 push(objCmds, []string{clang, "--target=" | wasmTriple,
3745 "-c", bf, "-o", bf | ".o"})
3746 }
3747 if !runPool(objCmds, bst.buildJobs) {
3748 fatal("clang -c failed")
3749 }
3750 for _, bf := range allBcFiles {
3751 push(objList, bf|".o")
3752 }
3753 } else {
3754 for _, bf := range allBcFiles {
3755 push(objList, clangCompileToObj(clang, wasmTriple,bf))
3756 }
3757 }
3758 push(objList, clangCompileToObj(clang, wasmTriple,initAllBc))
3759 // Stubs for symbols not provided by stage4-compiled runtime
3760 wasiStubs := mxutil.JoinPath(tmpdir, "wasi_stubs.ll")
3761 writeFile(wasiStubs, []byte(
3762 "target datalayout = \"e-m:e-p:32:32-p10:8:8-p20:8:8-i64:64-n32:64-S128-ni:1:10:20\"\n" |
3763 "target triple = \"wasm32-unknown-wasi\"\n" |
3764 "declare ptr @llvm.stacksave.p0()\n" |
3765 "define ptr @runtime.stacksave(ptr %_ctx) {\n" |
3766 " %1 = call ptr @llvm.stacksave.p0()\n" |
3767 " ret ptr %1\n" |
3768 "}\n" |
3769 "define void @moxie_longjmp(ptr %0, ptr %1) {\n" |
3770 " call void @llvm.trap()\n" |
3771 " unreachable\n" |
3772 "}\n" |
3773 "define i32 @\"time.timezoneOffsetMinutes\"() {\n" |
3774 " ret i32 0\n" |
3775 "}\n" |
3776 "declare ptr @runtime.alloc(i32, ptr, ptr)\n" |
3777 "define ptr @malloc(i32 %size, ptr %_ctx) {\n" |
3778 " %p = call ptr @runtime.alloc(i32 %size, ptr null, ptr null)\n" |
3779 " ret ptr %p\n" |
3780 "}\n" |
3781 "define i32 @strlen(ptr %s, ptr %_ctx) {\n" |
3782 "entry:\n" |
3783 " br label %loop\n" |
3784 "loop:\n" |
3785 " %i = phi i32 [0, %entry], [%i1, %next]\n" |
3786 " %cp = getelementptr i8, ptr %s, i32 %i\n" |
3787 " %c = load i8, ptr %cp\n" |
3788 " %z = icmp eq i8 %c, 0\n" |
3789 " br i1 %z, label %done, label %next\n" |
3790 "next:\n" |
3791 " %i1 = add i32 %i, 1\n" |
3792 " br label %loop\n" |
3793 "done:\n" |
3794 " ret i32 %i\n" |
3795 "}\n" |
3796 "declare void @llvm.trap()\n"), 0644)
3797 push(objList, clangCompileToObj(clang, wasmTriple,wasiStubs))
3798 mxutil.WriteStr(2, " link\n")
3799 ldArgs := []string{"wasm-ld", "--export-dynamic", "--allow-undefined",
3800 "--gc-sections", "-o", outpath}
3801 for _, o := range objList {
3802 push(ldArgs, o)
3803 }
3804 push(ldArgs, "-z", "stack-size=67108864")
3805 rc = run(ldArgs)
3806 if rc != 0 {
3807 fatal("linker failed")
3808 }
3809 } else {
3810 // native: compile each .bc to .o, then ld.lld
3811 mxutil.WriteStr(2, " compile objects\n")
3812 var objList []string
3813 if !hasRuntimePkg {
3814 runtimeGoBc := mxutil.JoinPath(sysroot, "runtime_go.bc")
3815 if _, gok := mxutil.ReadFile(runtimeGoBc); gok {
3816 push(objList, clangCompileToObj(clang, triple,runtimeGoBc))
3817 } else {
3818 push(objList, clangCompileToObj(clang, triple,runtimeBc))
3819 }
3820 }
3821 rtShims := mxutil.JoinPath(sysroot, "rt_shims.bc")
3822 if _, rsk := mxutil.ReadFile(rtShims); rsk {
3823 push(objList, clangCompileToObj(clang, triple,rtShims))
3824 }
3825 if bst.buildJobs > 1 {
3826 var objCmds [][]string
3827 for _, bf := range allBcFiles {
3828 push(objCmds, []string{clang, "--target=" | triple,
3829 "-c", "-O0", bf, "-o", bf | ".o"})
3830 }
3831 if !runPool(objCmds, bst.buildJobs) {
3832 fatal("clang -c failed")
3833 }
3834 for _, bf := range allBcFiles {
3835 push(objList, bf|".o")
3836 }
3837 } else {
3838 for _, bf := range allBcFiles {
3839 push(objList, clangCompileToObj(clang, triple,bf))
3840 }
3841 }
3842 push(objList, clangCompileToObj(clang, triple,initAllBc))
3843 if !hasRuntimePkg {
3844 cstubDir := mxutil.JoinPath(root, "src/runtime")
3845 cstubFiles := listFiles(cstubDir, ".c")
3846 for _, cf := range cstubFiles {
3847 cPath := mxutil.JoinPath(cstubDir, cf)
3848 coFile := mxutil.JoinPath(tmpdir, "cstub_" | cf | ".o")
3849 rc = run([]string{clang, "--target=" | triple, "-c", "-O0", cPath, "-o", coFile})
3850 if rc == 0 {
3851 push(objList, coFile)
3852 }
3853 }
3854 }
3855 objDir := mxutil.JoinPath(sysroot, "obj")
3856 objEntries := listFiles(objDir, ".bc")
3857 if objEntries != nil {
3858 for _, e := range objEntries {
3859 push(objList, clangCompileToObj(clang, triple,mxutil.JoinPath(objDir, e)))
3860 }
3861 }
3862 oEntries := listFiles(objDir, ".o")
3863 if oEntries != nil {
3864 for _, e := range oEntries {
3865 push(objList, mxutil.JoinPath(objDir, e))
3866 }
3867 }
3868 mxutil.WriteStr(2, " link\n")
3869 ldArgs := []string{"ld.lld-22", "--gc-sections", "-o", outpath,
3870 mxutil.JoinPath(muslDir, "crt1.o")}
3871 for _, o := range objList {
3872 push(ldArgs, o)
3873 }
3874 push(ldArgs, comprtLib, muslLib, "-z", "stack-size=67108864")
3875 rc = run(ldArgs)
3876 if rc != 0 {
3877 fatal("linker failed")
3878 }
3879 }
3880 mxutil.WriteStr(2, " -> " | outpath | "\n")
3881 }
3882
3883 // buildPkgsSerial type-checks and compiles each package in dependency order
3884 // in a single pass. Each package's working set dies before the next begins.
3885 func buildPkgsSerial(pkgs []*pkgInfo, triple, clang, tmpdir, cacheBase string, forceRebuild, printIR bool) (bcs []string, hasRT bool) {
3886 var deferredClang [][]string
3887 isWasmTarget := mxutil.TargetArch == "wasm"
3888 for _, pkg := range pkgs {
3889 if pkg.path == "runtime" {
3890 if !isWasmTarget {
3891 hasRT = true
3892 }
3893 }
3894 compilePath := pkg.path
3895 if compilePath == "." || compilePath == ".." || mxutil.HasPrefix(compilePath, "/") || mxutil.HasPrefix(compilePath, "./") || mxutil.HasPrefix(compilePath, "../") {
3896 compilePath = pkg.name
3897 }
3898
3899 hash := pkgHash(pkg)
3900 cached := cachedBcPath(cacheBase, pkg.path, hash, pkg.version)
3901 cachedMxh := cachedMxhPath(cacheBase, pkg.path, hash, pkg.version)
3902
3903 bcOk := mxutil.FileExists(cached)
3904 mxhOk := mxutil.FileExists(cachedMxh)
3905
3906 if bcOk && mxhOk && !forceRebuild {
3907 mxutil.WriteStr(2, " cached " | pkg.path)
3908 if pkg.version != "" {
3909 mxutil.WriteStr(2, " @ " | pkg.version)
3910 }
3911 mxutil.WriteStr(2, "\n")
3912 loadMxh(cachedMxh)
3913 gsrc := concatSources(pkg.dir, pkg.files)
3914 ScanGenericDecls(gsrc, compilePath)
3915 push(bcs, cached)
3916 for _, cf := range pkg.cfiles {
3917 cPath := mxutil.JoinPath(pkg.dir, cf)
3918 cbcFile := mxutil.JoinPath(tmpdir, safeName(pkg.path) | "_" | cf | ".bc")
3919 crc := run([]string{clang, "--target=" | triple, "-emit-llvm", "-c", cPath, "-o", cbcFile})
3920 if crc == 0 {
3921 push(bcs, cbcFile)
3922 }
3923 }
3924 for _, bf := range pkg.bcfiles {
3925 push(bcs, mxutil.JoinPath(pkg.dir, bf))
3926 }
3927 continue
3928 }
3929
3930 if isStdlib(pkg.path) && bcOk && !forceRebuild {
3931 mxutil.WriteStr(2, " cached " | pkg.path | "\n")
3932 csrc := concatSources(pkg.dir, pkg.files)
3933 cdir := mxutil.JoinPath(cacheBase, safeName(pkg.path))
3934 mkdirAll(cdir, 0755)
3935 TypeCheckOnly(csrc, compilePath, cachedMxh)
3936 if cctx.compileArena != nil {
3937 runtime.ArenaFree(cctx.compileArena)
3938 cctx.compileArena = nil
3939 }
3940 push(bcs, cached)
3941 continue
3942 }
3943
3944 mxutil.WriteStr(2, " compile " | pkg.path | "\n")
3945 src := concatSources(pkg.dir, pkg.files)
3946 llFile := mxutil.JoinPath(tmpdir, safeName(pkg.path) | ".ll")
3947 // Stream IR directly to .ll file to avoid accumulating entire
3948 // package IR in memory (unicode/tables.mx produces ~10MB IR).
3949 cctx.irOutputFile = llFile
3950 ir := compileSource(src, compilePath, triple, pkg.dir)
3951 cctx.irOutputFile = ""
3952 if printIR && len(ir) > 0 {
3953 mxutil.WriteStr(1, ir)
3954 }
3955 if len(ir) > 0 {
3956 // Non-streaming fallback (error messages start with ';')
3957 writeFile(llFile, []byte(ir), 0644)
3958 }
3959 bcFile := mxutil.JoinPath(tmpdir, safeName(pkg.path) | ".bc")
3960 // Defer clang call - collect for parallel execution after all IR emitted.
3961 push(deferredClang, []string{clang, "--target=" | triple, "-emit-llvm", "-c", llFile, "-o", bcFile})
3962 // mxh written inside CompileToIR's arena before it dies.
3963 if len(cctx.lastMxhData) > 0 {
3964 cdir := mxutil.JoinPath(cacheBase, safeName(pkg.path))
3965 mkdirAll(cdir, 0755)
3966 writeFile(cachedMxh, []byte(cctx.lastMxhData), 0644)
3967 cctx.lastMxhData = ""
3968 }
3969 push(bcs, bcFile)
3970 for _, cf := range pkg.cfiles {
3971 cPath := mxutil.JoinPath(pkg.dir, cf)
3972 cbcFile := mxutil.JoinPath(tmpdir, safeName(pkg.path) | "_" | cf | ".bc")
3973 push(deferredClang, []string{clang, "--target=" | triple, "-emit-llvm", "-c", cPath, "-o", cbcFile})
3974 // rc = run(...) -- deferred
3975 if false {
3976 fatal("clang failed for " | cf)
3977 }
3978 push(bcs, cbcFile)
3979 }
3980 for _, bf := range pkg.bcfiles {
3981 push(bcs, mxutil.JoinPath(pkg.dir, bf))
3982 }
3983 }
3984 // Run all deferred clang calls in parallel.
3985 if len(deferredClang) > 0 {
3986 nproc := int32(8)
3987 if int32(len(deferredClang)) < nproc {
3988 nproc = int32(len(deferredClang))
3989 }
3990 mxutil.WriteStr(2, " clang: " | simpleItoa(len(deferredClang)) | " files, " | simpleItoa(nproc) | " parallel\n")
3991 if !runPool(deferredClang, nproc) {
3992 fatal("clang failed")
3993 }
3994 // Cache the .bc files
3995 for _, pkg := range pkgs {
3996 compilePath := pkg.path
3997 if compilePath == "." || compilePath == ".." || mxutil.HasPrefix(compilePath, "/") || mxutil.HasPrefix(compilePath, "./") || mxutil.HasPrefix(compilePath, "../") {
3998 compilePath = pkg.name
3999 }
4000 hash := pkgHash(pkg)
4001 cached := cachedBcPath(cacheBase, compilePath, hash, pkg.version)
4002 bcFile := mxutil.JoinPath(tmpdir, safeName(pkg.path) | ".bc")
4003 if mxutil.FileExists(bcFile) {
4004 cacheDir := mxutil.JoinPath(cacheBase, safeName(pkg.path))
4005 mkdirAll(cacheDir, 0755)
4006 data, rok := mxutil.ReadFile(bcFile)
4007 if rok {
4008 writeFile(cached, data, 0644)
4009 }
4010 }
4011 }
4012 }
4013 return bcs, hasRT
4014 }
4015
4016 // --- Ring buffer helpers ---
4017
4018 func ringSend(ring unsafe.Pointer, data []byte) (ok bool) {
4019 var dp unsafe.Pointer
4020 if len(data) > 0 {
4021 dp = unsafe.Pointer(unsafe.SliceData(data))
4022 }
4023 for {
4024 rc := cRingSend(ring, dp, int32(len(data)))
4025 if rc == 0 {
4026 return true
4027 }
4028 if rc < 0 {
4029 return false
4030 }
4031 cUsleep(100) // backpressure
4032 }
4033 }
4034
4035 func ringRecv(ring unsafe.Pointer) (data []byte, ok bool) {
4036 for {
4037 pn := cRingPeekLen(ring)
4038 if pn > 0 {
4039 buf := []byte{:pn}
4040 n := cRingRecv(ring, unsafe.Pointer(unsafe.SliceData(buf)), pn)
4041 if n > 0 {
4042 return buf[:n], true
4043 }
4044 if n < 0 {
4045 return nil, false
4046 }
4047 }
4048 if pn < 0 {
4049 return nil, false
4050 }
4051 cUsleep(100)
4052 }
4053 }
4054
4055 // ringRecvNonblock polls once. Returns (data, true) on message, (nil, true)
4056 // if empty but open, (nil, false) if closed+empty.
4057 // Allocates a buffer of exactly the right size for the message.
4058 func ringRecvNonblock(ring unsafe.Pointer) (data []byte, ok bool) {
4059 pn := cRingPeekLen(ring)
4060 if pn == 0 {
4061 return nil, true // empty but open
4062 }
4063 if pn < 0 {
4064 return nil, false // closed+empty
4065 }
4066 buf := []byte{:pn}
4067 n := cRingRecv(ring, unsafe.Pointer(unsafe.SliceData(buf)), pn)
4068 if n > 0 {
4069 return buf[:n], true
4070 }
4071 if n < 0 {
4072 return nil, false
4073 }
4074 return nil, true
4075 }
4076
4077 func ringSendInt32(ring unsafe.Pointer, v int32) (ok bool) {
4078 var b [4]byte
4079 b[0] = byte(v >> 24)
4080 b[1] = byte(v >> 16)
4081 b[2] = byte(v >> 8)
4082 b[3] = byte(v)
4083 return ringSend(ring, b[:])
4084 }
4085
4086 func ringRecvInt32(ring unsafe.Pointer) (v int32, ok bool) {
4087 d, rok := ringRecv(ring)
4088 if !rok || len(d) < 4 {
4089 return -1, false
4090 }
4091 v = int32(d[0])<<24 | int32(d[1])<<16 | int32(d[2])<<8 | int32(d[3])
4092 return v, true
4093 }
4094
4095 // --- Package metadata serialization ---
4096 // Text format: newline-separated fields. All strings are raw UTF-8.
4097 // Layout:
4098 // numPkgs\n
4099 // triple\n clang\n tmpdir\n cacheBase\n root\n modPrefix\n modDir\n
4100 // for each pkg: path\n name\n dir\n version\n
4101 // numFiles\n file1\n ... numCfiles\n cf1\n ... numBcfiles\n bf1\n ...
4102 // numImports\n imp1\n ...
4103
4104 func serializePkgMeta(pkgs []*pkgInfo, triple, clang, tmpdir, cacheBase, root, modPrefix, modDir string) (data []byte) {
4105 var b []byte
4106 b = b | simpleItoa(len(pkgs))
4107 push(b, '\n')
4108 b = b | triple
4109 push(b, '\n')
4110 b = b | clang
4111 push(b, '\n')
4112 b = b | tmpdir
4113 push(b, '\n')
4114 b = b | cacheBase
4115 push(b, '\n')
4116 b = b | root
4117 push(b, '\n')
4118 b = b | modPrefix
4119 push(b, '\n')
4120 b = b | modDir
4121 push(b, '\n')
4122 for _, p := range pkgs {
4123 b = b | p.path
4124 push(b, '\n')
4125 b = b | p.name
4126 push(b, '\n')
4127 b = b | p.dir
4128 push(b, '\n')
4129 b = b | p.version
4130 push(b, '\n')
4131 b = b | simpleItoa(len(p.files))
4132 push(b, '\n')
4133 for _, f := range p.files {
4134 b = b | f
4135 push(b, '\n')
4136 }
4137 b = b | simpleItoa(len(p.cfiles))
4138 push(b, '\n')
4139 for _, f := range p.cfiles {
4140 b = b | f
4141 push(b, '\n')
4142 }
4143 b = b | simpleItoa(len(p.bcfiles))
4144 push(b, '\n')
4145 for _, f := range p.bcfiles {
4146 b = b | f
4147 push(b, '\n')
4148 }
4149 b = b | simpleItoa(len(p.imports))
4150 push(b, '\n')
4151 for _, im := range p.imports {
4152 b = b | im
4153 push(b, '\n')
4154 }
4155 }
4156 return b
4157 }
4158
4159 // deserLine reads the next line from data[pos:], returns it and advances pos.
4160 func deserLine(data []byte, pos int32) (line string, next int32) {
4161 start := pos
4162 for pos < int32(len(data)) && data[pos] != '\n' {
4163 pos++
4164 }
4165 line = string(data[start:pos])
4166 if pos < int32(len(data)) {
4167 pos++ // skip \n
4168 }
4169 return line, pos
4170 }
4171
4172 func deserInt(data []byte, pos int32) (v int32, next int32) {
4173 s, np := deserLine(data, pos)
4174 return parseInt32(s), np
4175 }
4176
4177 type workerMeta struct {
4178 pkgs []*pkgInfo
4179 triple string
4180 clang string
4181 tmpdir string
4182 cacheBase string
4183 root string
4184 modPrefix string
4185 modDir string
4186 }
4187
4188 func deserializePkgMeta(data []byte) (m *workerMeta) {
4189 pos := int32(0)
4190 numPkgs, pos := deserInt(data, pos)
4191 m = &workerMeta{}
4192 m.triple, pos = deserLine(data, pos)
4193 m.clang, pos = deserLine(data, pos)
4194 m.tmpdir, pos = deserLine(data, pos)
4195 m.cacheBase, pos = deserLine(data, pos)
4196 m.root, pos = deserLine(data, pos)
4197 m.modPrefix, pos = deserLine(data, pos)
4198 m.modDir, pos = deserLine(data, pos)
4199 m.pkgs = []*pkgInfo{:numPkgs}
4200 for i := int32(0); i < numPkgs; i++ {
4201 p := &pkgInfo{}
4202 p.path, pos = deserLine(data, pos)
4203 p.name, pos = deserLine(data, pos)
4204 p.dir, pos = deserLine(data, pos)
4205 p.version, pos = deserLine(data, pos)
4206 nf, np := deserInt(data, pos)
4207 pos = np
4208 if nf > 0 {
4209 p.files = []string{:nf}
4210 for j := int32(0); j < nf; j++ {
4211 p.files[j], pos = deserLine(data, pos)
4212 }
4213 }
4214 nc, npc := deserInt(data, pos)
4215 pos = npc
4216 if nc > 0 {
4217 p.cfiles = []string{:nc}
4218 for j := int32(0); j < nc; j++ {
4219 p.cfiles[j], pos = deserLine(data, pos)
4220 }
4221 }
4222 nb, npb := deserInt(data, pos)
4223 pos = npb
4224 if nb > 0 {
4225 p.bcfiles = []string{:nb}
4226 for j := int32(0); j < nb; j++ {
4227 p.bcfiles[j], pos = deserLine(data, pos)
4228 }
4229 }
4230 ni, npi := deserInt(data, pos)
4231 pos = npi
4232 if ni > 0 {
4233 p.imports = []string{:ni}
4234 for j := int32(0); j < ni; j++ {
4235 p.imports[j], pos = deserLine(data, pos)
4236 }
4237 }
4238 m.pkgs[i] = p
4239 }
4240 return m
4241 }
4242
4243 // --- Worker entry (runs in a new thread) ---
4244 // Receives metadata via ringA, then receives job indices. Compiles each
4245 // package, writes .bc + .mxh to cache, sends result on ringB.
4246 // ringA/ringB pointers are passed as a 2-element unsafe.Pointer array.
4247
4248 type workerRings struct {
4249 ringA unsafe.Pointer // parent -> worker (jobs)
4250 ringB unsafe.Pointer // worker -> parent (results)
4251 }
4252
4253 //export mxc_worker_entry
4254 func workerEntryExport(arg unsafe.Pointer) {
4255 // Initialize this thread's heap and package state (TLS globals start zeroed).
4256 // Must happen before any Moxie allocation. allocateHeap() mmaps a fresh
4257 // region, initHeap() sets the bump pointer, initAll() runs package inits.
4258 runtime.InitCShared()
4259 initBuildState()
4260 initCompileCtx()
4261
4262 wr := (*workerRings)(arg)
4263 ringA := wr.ringA
4264 ringB := wr.ringB
4265
4266 metaData, metaOk := ringRecv(ringA)
4267 if !metaOk {
4268 return
4269 }
4270 meta := deserializePkgMeta(metaData)
4271 metaData = nil
4272
4273 // Set up module context from metadata
4274 bst.globalModPrefix = meta.modPrefix
4275 bst.globalModDir = meta.modDir
4276 bst.quietDiscover = true
4277
4278 // Set target globals from triple (needed by registerBuiltins for uintptr sizing)
4279 if len(meta.triple) >= 4 && meta.triple[:4] == "wasm" {
4280 mxutil.SetTarget("js", "wasm")
4281 } else {
4282 mxutil.SetTarget("linux", "amd64")
4283 }
4284
4285 // Initialize this thread's compiler state (TLS globals are zeroed)
4286 registerBuiltins()
4287
4288 // Build idxByPath for this thread's copy
4289 idxByPath := map[string]int32{}
4290 for i := int32(0); i < int32(len(meta.pkgs)); i++ {
4291 idxByPath[meta.pkgs[i].path] = i
4292 }
4293
4294 // Compute hashes for all packages (worker's own bst.pkgKeyMemo)
4295 hashes := []string{:len(meta.pkgs)}
4296 for i := int32(0); i < int32(len(meta.pkgs)); i++ {
4297 hashes[i] = pkgHash(meta.pkgs[i])
4298 }
4299
4300 // Work loop: receive job index, compile, send result
4301 for {
4302 idx, ok := ringRecvInt32(ringA)
4303 if !ok {
4304 break // ring closed, shutdown
4305 }
4306 if idx < 0 || idx >= int32(len(meta.pkgs)) {
4307 workerSendResult(ringB, idx, "invalid package index")
4308 continue
4309 }
4310 pkg := meta.pkgs[idx]
4311 errMsg := workerCompilePkg(pkg, hashes[idx], meta, idxByPath, hashes)
4312 workerSendResult(ringB, idx, errMsg)
4313 }
4314 }
4315
4316 // workerSendResult sends idx + error text on ringB.
4317 // Empty errMsg = success. Non-empty = error description.
4318 // Wire format: [idx:4 BE] [errlen:4 BE] [error text]
4319 func workerSendResult(ring unsafe.Pointer, idx int32, errMsg string) {
4320 elen := int32(len(errMsg))
4321 msg := []byte{:8 + elen}
4322 msg[0] = byte(idx >> 24)
4323 msg[1] = byte(idx >> 16)
4324 msg[2] = byte(idx >> 8)
4325 msg[3] = byte(idx)
4326 msg[4] = byte(elen >> 24)
4327 msg[5] = byte(elen >> 16)
4328 msg[6] = byte(elen >> 8)
4329 msg[7] = byte(elen)
4330 for i := int32(0); i < elen; i++ {
4331 msg[8+i] = errMsg[i]
4332 }
4333 ringSend(ring, msg)
4334 }
4335
4336 // workerLoadDeps recursively loads .mxh files for a package's imports,
4337 // ensuring transitive deps are loaded before the package that references them.
4338 func workerLoadDeps(pkg *pkgInfo, meta *workerMeta, idxByPath map[string]int32, hashes []string) (errMsg string) {
4339 for _, imp := range pkg.imports {
4340 di, ok := idxByPath[imp]
4341 if !ok {
4342 continue
4343 }
4344 dep := meta.pkgs[di]
4345 compilePath := dep.path
4346 if compilePath == "." || compilePath == ".." || mxutil.HasPrefix(compilePath, "/") || mxutil.HasPrefix(compilePath, "./") || mxutil.HasPrefix(compilePath, "../") {
4347 compilePath = dep.name
4348 }
4349 if cctx.universe.Registry[compilePath] != nil {
4350 continue
4351 }
4352 // Recursively load this dep's deps first
4353 if depErr := workerLoadDeps(dep, meta, idxByPath, hashes); depErr != "" {
4354 return depErr
4355 }
4356 depHash := hashes[di]
4357 cachedMxh := cachedMxhPath(meta.cacheBase, dep.path, depHash, dep.version)
4358 if !loadMxh(cachedMxh) {
4359 return "missing dep .mxh for " | dep.path
4360 }
4361 src := concatSources(dep.dir, dep.files)
4362 ScanGenericDecls(src, compilePath)
4363 }
4364 return ""
4365 }
4366
4367 // workerCompilePkg compiles one package in the worker thread. All deps must
4368 // already have .mxh in cache. Returns empty string on success, error text on failure.
4369 func workerCompilePkg(pkg *pkgInfo, hash string, meta *workerMeta, idxByPath map[string]int32, hashes []string) (errMsg string) {
4370 // Load all dependency .mxh from cache (recursively, transitive deps first)
4371 if loadErr := workerLoadDeps(pkg, meta, idxByPath, hashes); loadErr != "" {
4372 return loadErr
4373 }
4374
4375 compilePath := pkg.path
4376 if compilePath == "." || compilePath == ".." || mxutil.HasPrefix(compilePath, "/") || mxutil.HasPrefix(compilePath, "./") || mxutil.HasPrefix(compilePath, "../") {
4377 compilePath = pkg.name
4378 }
4379
4380 mxutil.WriteStr(2, " compile " | pkg.path | "\n")
4381 src := concatSources(pkg.dir, pkg.files)
4382 cctx.compilePkgDir = pkg.dir
4383 ir := CompileToIR(src, compilePath, meta.triple)
4384 if len(ir) > 0 && ir[0] == ';' {
4385 return remapErrorLines(ir)
4386 }
4387 if len(ir) == 0 {
4388 return "empty IR for " | pkg.path
4389 }
4390
4391 llFile := mxutil.JoinPath(meta.tmpdir, safeName(pkg.path) | ".ll")
4392 writeFile(llFile, []byte(ir), 0644)
4393
4394 bcFile := mxutil.JoinPath(meta.tmpdir, safeName(pkg.path) | ".bc")
4395 rc := run([]string{meta.clang, "--target=" | meta.triple, "-emit-llvm", "-c", llFile, "-o", bcFile})
4396 if rc != 0 {
4397 return "clang failed for " | pkg.path
4398 }
4399
4400 // Cache .bc
4401 cacheDir := mxutil.JoinPath(meta.cacheBase, safeName(pkg.path))
4402 mkdirAll(cacheDir, 0755)
4403 cached := cachedBcPath(meta.cacheBase, pkg.path, hash, pkg.version)
4404 bcData, rok := mxutil.ReadFile(bcFile)
4405 if rok {
4406 writeFile(cached, bcData, 0644)
4407 }
4408
4409 // Cache .mxh (already generated inside CompileToIR's arena)
4410 if len(cctx.lastMxhData) > 0 {
4411 cachedMxh := cachedMxhPath(meta.cacheBase, pkg.path, hash, pkg.version)
4412 writeFile(cachedMxh, []byte(cctx.lastMxhData), 0644)
4413 cctx.lastMxhData = ""
4414 }
4415
4416 // Compile C files
4417 for _, cf := range pkg.cfiles {
4418 cPath := mxutil.JoinPath(pkg.dir, cf)
4419 cbcFile := mxutil.JoinPath(meta.tmpdir, safeName(pkg.path) | "_" | cf | ".bc")
4420 rc = run([]string{meta.clang, "--target=" | meta.triple, "-emit-llvm", "-c", cPath, "-o", cbcFile})
4421 if rc != 0 {
4422 return "clang failed for " | cf
4423 }
4424 }
4425
4426 return ""
4427 }
4428
4429 // --- Worker info for the orchestrator ---
4430
4431 type workerInfo struct {
4432 ringA unsafe.Pointer // parent -> worker
4433 ringB unsafe.Pointer // worker -> parent
4434 handle uint64
4435 status uint64
4436 busy bool
4437 busyIdx int32
4438 }
4439
4440 // buildPkgsParallel spawns worker threads with ring buffer IPC. Workers
4441 // receive package metadata at startup, then job indices. Each worker
4442 // independently loads deps from cache, compiles, writes .bc + .mxh to cache.
4443 // No shared mutable state - this IS the Moxie spawn model.
4444 func buildPkgsParallel(pkgs []*pkgInfo, triple, clang, tmpdir, cacheBase string, forceRebuild bool, jobs int32) (bcs []string, hasRT bool) {
4445 n := int32(len(pkgs))
4446 done := []bool{:n}
4447 needBuild := []bool{:n}
4448 idxByPath := map[string]int32{}
4449 for i := int32(0); i < n; i++ {
4450 idxByPath[pkgs[i].path] = i
4451 }
4452
4453 // Phase 1: check cache
4454 for i := int32(0); i < n; i++ {
4455 pkg := pkgs[i]
4456 hash := pkgHash(pkg)
4457 cached := cachedBcPath(cacheBase, pkg.path, hash, pkg.version)
4458 cachedMxh := cachedMxhPath(cacheBase, pkg.path, hash, pkg.version)
4459 bcOk := mxutil.FileExists(cached)
4460 mxhOk := mxutil.FileExists(cachedMxh)
4461 if bcOk && mxhOk && !forceRebuild {
4462 done[i] = true
4463 mxutil.WriteStr(2, " cached " | pkg.path)
4464 if pkg.version != "" {
4465 mxutil.WriteStr(2, " @ " | pkg.version)
4466 }
4467 mxutil.WriteStr(2, "\n")
4468 } else {
4469 needBuild[i] = true
4470 }
4471 }
4472
4473 // Phase 1b: compute transitive dep indices for dispatch readiness.
4474 // A package is ready when ALL transitive deps (not just direct) are done.
4475 transDeps := [][]int32{:n}
4476 for i := int32(0); i < n; i++ {
4477 visited := map[int32]bool{}
4478 stack := []int32{:0:8}
4479 for _, imp := range pkgs[i].imports {
4480 if j, ok := idxByPath[imp]; ok {
4481 push(stack, j)
4482 }
4483 }
4484 for len(stack) > 0 {
4485 top := stack[len(stack)-1]
4486 stack = stack[:len(stack)-1]
4487 if visited[top] {
4488 continue
4489 }
4490 visited[top] = true
4491 for _, imp := range pkgs[top].imports {
4492 if j, ok := idxByPath[imp]; ok && !visited[j] {
4493 push(stack, j)
4494 }
4495 }
4496 }
4497 td := []int32{:0:len(visited)}
4498 for j := range visited {
4499 push(td, j)
4500 }
4501 transDeps[i] = td
4502 }
4503
4504 // Phase 2: serialize metadata and spawn workers
4505 root := getenv("MOXIEROOT")
4506 if root == "" {
4507 root = "."
4508 }
4509 meta := serializePkgMeta(pkgs, triple, clang, tmpdir, cacheBase, root,
4510 bst.globalModPrefix, bst.globalModDir)
4511
4512 workers := []*workerInfo{:jobs}
4513 for wi := int32(0); wi < jobs; wi++ {
4514 w := &workerInfo{}
4515 w.ringA = cRingCreate(262144) // 256KB jobs+metadata
4516 w.ringB = cRingCreate(262144) // 256KB results+errors
4517 if w.ringA == nil || w.ringB == nil {
4518 fatal("ring_create failed for worker " | simpleItoa(wi))
4519 }
4520
4521 // Pack rings into arg struct (lives until thread reads it)
4522 wr := &workerRings{ringA: w.ringA, ringB: w.ringB}
4523
4524 rc := cSpawnWorker(unsafe.Pointer(wr), unsafe.Pointer(&w.handle), unsafe.Pointer(&w.status))
4525 if rc != 0 {
4526 fatal("spawn_worker failed for worker " | simpleItoa(wi))
4527 }
4528
4529 // Send metadata to worker via ringA
4530 if !ringSend(w.ringA, meta) {
4531 fatal("failed to send metadata to worker " | simpleItoa(wi))
4532 }
4533
4534 workers[wi] = w
4535 }
4536 meta = nil
4537
4538 // Phase 3: three-check dispatch loop
4539 failed := false
4540 for {
4541 allDone := true
4542 for i := int32(0); i < n; i++ {
4543 if needBuild[i] && !done[i] {
4544 allDone = false
4545 break
4546 }
4547 }
4548 if allDone {
4549 break
4550 }
4551
4552 // On failure: close all ringAs to signal workers to stop, drain busy
4553 if failed {
4554 for wi := int32(0); wi < int32(len(workers)); wi++ {
4555 w := workers[wi]
4556 if w != nil {
4557 cRingClose(w.ringA) // idempotent
4558 }
4559 }
4560 anyBusy := false
4561 for wi := int32(0); wi < int32(len(workers)); wi++ {
4562 w := workers[wi]
4563 if w != nil && w.busy {
4564 anyBusy = true
4565 break
4566 }
4567 }
4568 if !anyBusy {
4569 break
4570 }
4571 // Fall through to collection to drain in-flight results
4572 }
4573
4574 if !failed {
4575 // Check 1: death check
4576 for wi := int32(0); wi < int32(len(workers)); wi++ {
4577 w := workers[wi]
4578 if w == nil {
4579 continue
4580 }
4581 alive := cThreadAlive(unsafe.Pointer(&w.status))
4582 if alive < 0 {
4583 if w.busy {
4584 mxutil.WriteStr(2, " CRASHED worker " | simpleItoa(wi) | " compiling " | pkgs[w.busyIdx].path | "\n")
4585 } else {
4586 mxutil.WriteStr(2, " CRASHED worker " | simpleItoa(wi) | "\n")
4587 }
4588 failed = true
4589 workers[wi] = nil
4590 }
4591 }
4592
4593 // Check 2: dispatch jobs to idle workers
4594 if !failed {
4595 for wi := int32(0); wi < int32(len(workers)); wi++ {
4596 w := workers[wi]
4597 if w == nil || w.busy {
4598 continue
4599 }
4600 // Find a dispatchable package
4601 di := int32(-1)
4602 for i := int32(0); i < n; i++ {
4603 if done[i] || !needBuild[i] {
4604 continue
4605 }
4606 // Check not already in flight
4607 inFlight := false
4608 for wj := int32(0); wj < int32(len(workers)); wj++ {
4609 wk := workers[wj]
4610 if wk != nil && wk.busy && wk.busyIdx == i {
4611 inFlight = true
4612 break
4613 }
4614 }
4615 if inFlight {
4616 continue
4617 }
4618 // Check all transitive deps done
4619 ready := true
4620 for _, j := range transDeps[i] {
4621 if !done[j] {
4622 ready = false
4623 break
4624 }
4625 }
4626 if ready {
4627 di = i
4628 break
4629 }
4630 }
4631 if di < 0 {
4632 continue
4633 }
4634 // Dispatch
4635 if !ringSendInt32(w.ringA, di) {
4636 mxutil.WriteStr(2, " dispatch failed for " | pkgs[di].path | "\n")
4637 failed = true
4638 break
4639 }
4640 w.busy = true
4641 w.busyIdx = di
4642 }
4643 }
4644 }
4645
4646 // Check 3: collect results from busy workers (always runs - drains on failure)
4647 progress := false
4648 for wi := int32(0); wi < int32(len(workers)); wi++ {
4649 w := workers[wi]
4650 if w == nil || !w.busy {
4651 continue
4652 }
4653 rd, rok := ringRecvNonblock(w.ringB)
4654 if rd == nil {
4655 if !rok {
4656 mxutil.WriteStr(2, " worker " | simpleItoa(wi) | " ringB closed\n")
4657 failed = true
4658 }
4659 continue
4660 }
4661 if len(rd) < 8 {
4662 continue
4663 }
4664 ridx := int32(rd[0])<<24 | int32(rd[1])<<16 | int32(rd[2])<<8 | int32(rd[3])
4665 elen := int32(rd[4])<<24 | int32(rd[5])<<16 | int32(rd[6])<<8 | int32(rd[7])
4666 w.busy = false
4667 progress = true
4668 if elen > 0 && int32(len(rd)) >= 8+elen {
4669 errText := string(rd[8 : 8+elen])
4670 mxutil.WriteStr(2, errText)
4671 mxutil.WriteStr(2, "mxc: compile error for " | pkgs[ridx].path | "\n")
4672 failed = true
4673 } else if elen > 0 {
4674 mxutil.WriteStr(2, "mxc: compile error for " | pkgs[ridx].path | "\n")
4675 failed = true
4676 } else {
4677 done[ridx] = true
4678 }
4679 }
4680
4681 // Join point
4682 if !progress {
4683 cUsleep(1000)
4684 }
4685 }
4686
4687 // Phase 4: shutdown - close rings, join threads
4688 for wi := int32(0); wi < int32(len(workers)); wi++ {
4689 w := workers[wi]
4690 if w == nil {
4691 continue
4692 }
4693 cRingClose(w.ringA)
4694 cThreadJoin(unsafe.Pointer(&w.handle), unsafe.Pointer(&w.status))
4695 cRingDestroy(w.ringA)
4696 cRingDestroy(w.ringB)
4697 }
4698 workers = nil
4699
4700 if failed {
4701 fatal("parallel build failed")
4702 }
4703 for i := int32(0); i < n; i++ {
4704 if needBuild[i] && !done[i] {
4705 fatal("dependency cycle: cannot schedule " | pkgs[i].path)
4706 }
4707 }
4708
4709 // Phase 5: assemble .bc list
4710 for i := int32(0); i < n; i++ {
4711 pkg := pkgs[i]
4712 if pkg.path == "runtime" {
4713 hasRT = true
4714 }
4715 if needBuild[i] {
4716 push(bcs, mxutil.JoinPath(tmpdir, safeName(pkg.path) | ".bc"))
4717 for _, cf := range pkg.cfiles {
4718 push(bcs, mxutil.JoinPath(tmpdir, safeName(pkg.path) | "_" | cf | ".bc"))
4719 }
4720 } else {
4721 hash := pkgHash(pkg)
4722 push(bcs, cachedBcPath(cacheBase, pkg.path, hash, pkg.version))
4723 for _, cf := range pkg.cfiles {
4724 cPath := mxutil.JoinPath(pkg.dir, cf)
4725 cbcFile := mxutil.JoinPath(tmpdir, safeName(pkg.path) | "_" | cf | ".bc")
4726 rc := run([]string{clang, "--target=" | triple, "-emit-llvm", "-c", cPath, "-o", cbcFile})
4727 if rc == 0 {
4728 push(bcs, cbcFile)
4729 }
4730 }
4731 }
4732 for _, bf := range pkg.bcfiles {
4733 push(bcs, mxutil.JoinPath(pkg.dir, bf))
4734 }
4735 }
4736 return bcs, hasRT
4737 }
4738
4739 func cmdBuildRuntime() {
4740 root := getenv("MOXIEROOT")
4741 if root == "" {
4742 root = "."
4743 }
4744 triple := "x86_64-unknown-linux-musleabihf"
4745 clang := "clang-22"
4746 sysroot := mxutil.JoinPath(root, "sysroot")
4747 tmpdir := "/tmp/mxc-rt-build"
4748 mkdirAll(tmpdir, 0755)
4749
4750 registerBuiltins()
4751
4752 rtPkgs := []string{"internal/task", "runtime"}
4753 var bcFiles []string
4754 for _, pkg := range rtPkgs {
4755 dir := mxutil.JoinPath(root, mxutil.JoinPath("src", pkg))
4756 info := discoverPkg(pkg, root)
4757 if info == nil || len(info.files) == 0 {
4758 mxutil.WriteStr(2, " skip " | pkg | " (no files)\n")
4759 continue
4760 }
4761 mxutil.WriteStr(2, " compile " | pkg | "\n")
4762 src := concatSources(dir, info.files)
4763 ir := compileSource(src, pkg, triple, dir)
4764 llFile := mxutil.JoinPath(tmpdir, safeName(pkg) | ".ll")
4765 writeFile(llFile, []byte(ir), 0644)
4766 bcFile := mxutil.JoinPath(tmpdir, safeName(pkg) | ".bc")
4767 rc := run([]string{clang, "--target=" | triple, "-emit-llvm", "-c", llFile, "-o", bcFile})
4768 if rc != 0 {
4769 fatal("clang failed for " | pkg)
4770 }
4771 push(bcFiles, bcFile)
4772 }
4773 if len(bcFiles) == 0 {
4774 fatal("no runtime packages compiled")
4775 }
4776 outBc := mxutil.JoinPath(sysroot, "runtime_go.bc")
4777 if len(bcFiles) == 1 {
4778 data, ok := mxutil.ReadFile(bcFiles[0])
4779 if !ok {
4780 fatal("cannot read " | bcFiles[0])
4781 }
4782 writeFile(outBc, data, 0644)
4783 } else {
4784 linkArgs := []string{"llvm-link-22", "-o", outBc}
4785 for _, bf := range bcFiles {
4786 push(linkArgs, bf)
4787 }
4788 rc := run(linkArgs)
4789 if rc != 0 {
4790 fatal("llvm-link failed merging runtime")
4791 }
4792 }
4793 mxutil.WriteStr(2, " -> " | outBc | "\n")
4794 }
4795