main.mx raw
1 package main
2
3 import (
4 "bytes"
5 "fmt"
6 "os"
7 "path/filepath"
8 "syscall"
9
10 "git.mleku.dev/iskra"
11 )
12
13 func main() {
14 if len(os.Args) < 2 {
15 usage()
16 }
17 switch os.Args[1] {
18 case "build":
19 cmdBuild()
20 case "compile":
21 cmdCompile()
22 case "bootstrap":
23 cmdBootstrap()
24 default:
25 usage()
26 }
27 }
28
29 func usage() {
30 fmt.Fprintln(os.Stderr, "moxie-iskra - lattice-based Moxie compiler")
31 fmt.Fprintln(os.Stderr, "")
32 fmt.Fprintln(os.Stderr, " moxie-iskra build [-o output] [-mesh m.mesh] [-opt level] [-target arch] <dir>")
33 fmt.Fprintln(os.Stderr, " moxie-iskra compile [-mesh m.mesh] [-pkg prefix] [-o output.ll] <src.mx...>")
34 fmt.Fprintln(os.Stderr, " moxie-iskra bootstrap [-m moxieroot] [-mesh m.mesh]")
35 fmt.Fprintln(os.Stderr, "")
36 fmt.Fprintln(os.Stderr, "targets: x86_64 (default), wasm32")
37 os.Exit(1)
38 }
39
40 func resolveMesh(explicit string, target string) string {
41 if explicit != "" {
42 return explicit
43 }
44 env := os.Getenv("ISKRA_MESH")
45 if env != "" {
46 return env
47 }
48 home, _ := os.UserHomeDir()
49 if home != "" {
50 def := filepath.Join(home, ".local", "share", "moxie-iskra", "stdlib." | target | ".mesh")
51 if _, err := os.Stat(def); err == nil {
52 return def
53 }
54 legacy := filepath.Join(home, ".local", "share", "moxie-iskra", "stdlib.mesh")
55 if target == "x86_64" {
56 if _, err := os.Stat(legacy); err == nil {
57 return legacy
58 }
59 }
60 }
61 return ""
62 }
63
64 func loadMesh(path string) *iskra.Tree {
65 if path == "" {
66 fmt.Fprintln(os.Stderr, "no mesh file: set -mesh, $ISKRA_MESH, or install to ~/.local/share/moxie-iskra/stdlib.mesh")
67 os.Exit(1)
68 }
69 t, _, _, err := iskra.MeshLoadFile(path)
70 if err != nil {
71 fmt.Fprintln(os.Stderr, "error loading mesh: " | err.Error())
72 os.Exit(1)
73 }
74 return t
75 }
76
77 func cmdBuild() {
78 meshPath := ""
79 outPath := ""
80 optLevel := "2"
81 targetDir := ""
82 target := "x86_64"
83
84 i := 2
85 for i < len(os.Args) {
86 switch os.Args[i] {
87 case "-mesh":
88 i++
89 if i >= len(os.Args) {
90 fatal("-mesh requires argument")
91 }
92 meshPath = os.Args[i]
93 case "-o":
94 i++
95 if i >= len(os.Args) {
96 fatal("-o requires argument")
97 }
98 outPath = os.Args[i]
99 case "-opt":
100 i++
101 if i >= len(os.Args) {
102 fatal("-opt requires argument")
103 }
104 optLevel = os.Args[i]
105 case "-target":
106 i++
107 if i >= len(os.Args) {
108 fatal("-target requires argument")
109 }
110 target = os.Args[i]
111 if target != "x86_64" && target != "wasm32" {
112 fatal("unknown target: " | target | " (supported: x86_64, wasm32)")
113 }
114 default:
115 if targetDir != "" {
116 fatal("multiple target directories")
117 }
118 targetDir = os.Args[i]
119 }
120 i++
121 }
122
123 if outPath == "" {
124 if target == "wasm32" {
125 outPath = "a.wasm"
126 } else {
127 outPath = "a.out"
128 }
129 }
130
131 if targetDir == "" {
132 targetDir = "."
133 }
134
135 meshPath = resolveMesh(meshPath, target)
136 meshExists := meshPath != ""
137 if !meshExists {
138 home, _ := os.UserHomeDir()
139 if home != "" {
140 meshPath = filepath.Join(home, ".local", "share", "moxie-iskra", "stdlib." | target | ".mesh")
141 os.MkdirAll(filepath.Dir(meshPath), 0755)
142 }
143 }
144 var t *iskra.Tree
145 if meshExists {
146 t = loadMesh(meshPath)
147 } else {
148 fmt.Fprintln(os.Stderr, "no mesh file found - starting with empty lattice (will auto-expand)")
149 t = iskra.NewInMemoryTree(256)
150 }
151
152 srcFiles, err := iskra.DiscoverMxFiles(targetDir)
153 if err != nil {
154 fatal("discovering sources: " | err.Error())
155 }
156 if len(srcFiles) == 0 {
157 fatal("no .mx files found in " | targetDir)
158 }
159
160 modPath := iskra.ReadModulePath(targetDir)
161
162 pkgName := detectPackage(srcFiles)
163
164 if moxieRoot := findMoxieRoot(); moxieRoot != "" {
165 cmdBuildPerPkg(t, meshPath, outPath, optLevel, targetDir, target, moxieRoot, srcFiles, pkgName, modPath)
166 return
167 }
168
169 fmt.Fprintln(os.Stderr, "compiling " | fmt.Sprint(len(srcFiles)) | " files from " | targetDir | " (package " | pkgName | ")")
170 if modPath != "" {
171 fmt.Fprintln(os.Stderr, "module: " | modPath)
172 }
173 result := iskra.CompileFiles(t, srcFiles, "", pkgName, true, modPath)
174
175 fmt.Fprintln(os.Stderr, "matched: " | fmt.Sprint(result.Matched))
176 fmt.Fprintln(os.Stderr, "unmatched: " | fmt.Sprint(result.Unmatched))
177
178 if result.Unmatched > 0 {
179 for _, u := range result.UnmatchedList {
180 fmt.Fprintln(os.Stderr, "UNMATCHED: " | u.Name | " from " | u.SrcFile)
181 }
182 expanded := expandLattice(t, meshPath, result.UnmatchedList, targetDir, pkgName, modPath, target)
183 if !expanded {
184 fatal("lattice expansion failed - moxie not on PATH or -internal-printir failed")
185 }
186 result = iskra.CompileFiles(t, srcFiles, "", pkgName, true, modPath)
187 fmt.Fprintln(os.Stderr, "after expansion: matched=" | fmt.Sprint(result.Matched) | " unmatched=" | fmt.Sprint(result.Unmatched))
188 if result.Unmatched > 0 {
189 for _, u := range result.UnmatchedList {
190 fmt.Fprintln(os.Stderr, "STILL UNMATCHED: " | u.Name)
191 }
192 fatal("lattice expansion did not resolve all functions - algorithm bug")
193 }
194 }
195
196 if len(result.IR) == 0 {
197 fatal("no functions compiled")
198 }
199
200 result.IR = injectDepFunctions(result.IR, targetDir, target)
201
202 tmpDir, err := os.MkdirTemp("", "moxie-iskra-")
203 if err != nil {
204 fatal("creating temp dir: " | err.Error())
205 }
206
207 llFile := filepath.Join(tmpDir, "module.ll")
208 err = os.WriteFile(llFile, result.IR, 0644)
209 if err != nil {
210 fatal("writing IR: " | err.Error())
211 }
212 fmt.Fprintln(os.Stderr, "wrote IR: " | llFile | " (" | fmt.Sprint(len(result.IR)) | " bytes)")
213
214 bcFile := filepath.Join(tmpDir, "module.bc")
215 if rc := runTool("opt", "-O" | optLevel, llFile, "-o", bcFile); rc != 0 {
216 fatal("opt failed (exit " | fmt.Sprint(rc) | ")")
217 }
218
219 stdlibBC := resolveStdlibBitcode(target)
220 useLTO := stdlibBC != "" && target != "wasm32"
221
222 objFile := filepath.Join(tmpDir, "module.o")
223 if useLTO {
224 combinedBC := filepath.Join(tmpDir, "combined.bc")
225 if rc := runTool("llvm-link", stdlibBC, "--override", bcFile, "-o", combinedBC); rc != 0 {
226 fatal("llvm-link failed (exit " | fmt.Sprint(rc) | ")")
227 }
228 keepFile := filepath.Join(tmpDir, "keep.txt")
229 keepSyms := "main\n_start\n__alloc\nmoxie_handle_fatal_signal\nmoxie_runtime_bdwgc_callback\nmoxie_scanstack\nmoxie_signal_handler\n"
230 if target == "wasm32" {
231 keepSyms = keepSyms | collectWasmExports(result.IR)
232 }
233 os.WriteFile(keepFile, []byte(keepSyms), 0644)
234 optBC := filepath.Join(tmpDir, "combined-opt.bc")
235 if rc := runTool("opt", "--passes=internalize,globaldce,default<O" | optLevel | ">", "--internalize-public-api-file=" | keepFile, combinedBC, "-o", optBC); rc != 0 {
236 fatal("opt LTO failed (exit " | fmt.Sprint(rc) | ")")
237 }
238 llcArgs := llcFlags(target, optLevel, optBC, objFile)
239 if rc := runToolArgv(llcArgs); rc != 0 {
240 fatal("llc failed (exit " | fmt.Sprint(rc) | ")")
241 }
242 fmt.Fprintln(os.Stderr, "LTO: linked module + stdlib bitcode")
243 } else {
244 llcArgs := llcFlags(target, optLevel, bcFile, objFile)
245 if rc := runToolArgv(llcArgs); rc != 0 {
246 fatal("llc failed (exit " | fmt.Sprint(rc) | ")")
247 }
248 }
249
250 intrinsicsLL := generateIntrinsics(result.IR, target)
251 intrinsicsFile := filepath.Join(tmpDir, "intrinsics.ll")
252 err = os.WriteFile(intrinsicsFile, intrinsicsLL, 0644)
253 if err != nil {
254 fatal("writing intrinsics: " | err.Error())
255 }
256 intrinsicsObj := filepath.Join(tmpDir, "intrinsics.o")
257 intrinsicClangArgs := intrinsicCompileFlags(target, intrinsicsFile, intrinsicsObj)
258 if rc := runToolArgv(intrinsicClangArgs); rc != 0 {
259 fatal("compiling intrinsics failed")
260 }
261
262 home, _ := os.UserHomeDir()
263 hasDeps := len(readDepModules(targetDir)) > 0
264 linkArgs := linkFlagsLTO(target, objFile, intrinsicsObj, outPath, home, useLTO, target == "wasm32" && hasDeps)
265
266 if rc := runToolArgv(linkArgs); rc != 0 {
267 fatal("link failed (exit " | fmt.Sprint(rc) | ")")
268 }
269
270 fmt.Fprintln(os.Stderr, "built: " | outPath)
271 }
272
273 func cmdCompile() {
274 meshPath := ""
275 outPath := ""
276 pkgFilter := ""
277 var srcFiles []string
278
279 i := 2
280 for i < len(os.Args) {
281 switch os.Args[i] {
282 case "-mesh":
283 i++
284 if i >= len(os.Args) {
285 fatal("-mesh requires argument")
286 }
287 meshPath = os.Args[i]
288 case "-o":
289 i++
290 if i >= len(os.Args) {
291 fatal("-o requires argument")
292 }
293 outPath = os.Args[i]
294 case "-pkg":
295 i++
296 if i >= len(os.Args) {
297 fatal("-pkg requires argument")
298 }
299 pkgFilter = os.Args[i]
300 default:
301 srcFiles = append(srcFiles, os.Args[i])
302 }
303 i++
304 }
305
306 meshPath = resolveMesh(meshPath, "x86_64")
307 if len(srcFiles) == 0 {
308 fatal("no source files specified")
309 }
310
311 t := loadMesh(meshPath)
312 result := iskra.CompileFiles(t, srcFiles, pkgFilter, pkgFilter, false)
313
314 fmt.Fprintln(os.Stderr, "matched: " | fmt.Sprint(result.Matched))
315 fmt.Fprintln(os.Stderr, "unmatched: " | fmt.Sprint(result.Unmatched))
316
317 if len(result.IR) == 0 {
318 fatal("no functions compiled")
319 }
320
321 if outPath != "" {
322 err := os.WriteFile(outPath, result.IR, 0644)
323 if err != nil {
324 fatal("writing output: " | err.Error())
325 }
326 fmt.Fprintln(os.Stderr, "wrote: " | outPath)
327 } else {
328 os.Stdout.Write(result.IR)
329 }
330 }
331
332 func cmdBootstrap() {
333 meshPath := ""
334 moxieRoot := ""
335
336 i := 2
337 for i < len(os.Args) {
338 switch os.Args[i] {
339 case "-mesh":
340 i++
341 if i >= len(os.Args) {
342 fatal("-mesh requires argument")
343 }
344 meshPath = os.Args[i]
345 case "-m":
346 i++
347 if i >= len(os.Args) {
348 fatal("-m requires argument")
349 }
350 moxieRoot = os.Args[i]
351 default:
352 fatal("unknown argument: " | os.Args[i])
353 }
354 i++
355 }
356
357 if moxieRoot == "" {
358 moxieRoot = os.Getenv("MOXIEROOT")
359 }
360 if moxieRoot == "" {
361 fatal("specify moxie root with -m or $MOXIEROOT")
362 }
363
364 meshPath = resolveMesh(meshPath, "x86_64")
365 t := loadMesh(meshPath)
366
367 mxFiles, cFiles, sFiles := iskra.FindRuntimeSources(moxieRoot)
368 fmt.Fprintln(os.Stderr, "runtime sources: " | fmt.Sprint(len(mxFiles)) | " .mx, " | fmt.Sprint(len(cFiles)) | " .c, " | fmt.Sprint(len(sFiles)) | " .S")
369
370 if len(mxFiles) == 0 {
371 fatal("no runtime .mx files found in " | moxieRoot | "/src/runtime/")
372 }
373
374 result := iskra.CompileFiles(t, mxFiles, "runtime", "runtime", false)
375
376 fmt.Fprintln(os.Stderr, "runtime matched: " | fmt.Sprint(result.Matched))
377 fmt.Fprintln(os.Stderr, "runtime unmatched: " | fmt.Sprint(result.Unmatched))
378
379 home, _ := os.UserHomeDir()
380 iskraDir := filepath.Join(home, ".local", "share", "moxie-iskra")
381 os.MkdirAll(iskraDir, 0755)
382
383 tmpDir, err := os.MkdirTemp("", "moxie-iskra-bootstrap-")
384 if err != nil {
385 fatal("creating temp dir: " | err.Error())
386 }
387
388 if len(result.IR) > 0 {
389 llFile := filepath.Join(tmpDir, "runtime.ll")
390 err = os.WriteFile(llFile, result.IR, 0644)
391 if err != nil {
392 fatal("writing runtime IR: " | err.Error())
393 }
394 fmt.Fprintln(os.Stderr, "wrote runtime IR: " | llFile)
395
396 bcFile := filepath.Join(tmpDir, "runtime.bc")
397 if rc := runTool("opt", "-O2", llFile, "-o", bcFile); rc != 0 {
398 fatal("opt on runtime failed (exit " | fmt.Sprint(rc) | ")")
399 }
400
401 rtObj := filepath.Join(tmpDir, "runtime_mx.o")
402 if rc := runTool("llc", "-filetype=obj", "-relocation-model=pic", "-O2", bcFile, "-o", rtObj); rc != 0 {
403 fatal("llc on runtime failed (exit " | fmt.Sprint(rc) | ")")
404 }
405
406 allObjs := []string{rtObj}
407
408 for _, cf := range cFiles {
409 base := filepath.Base(cf)
410 out := filepath.Join(tmpDir, base | ".o")
411 if rc := runTool("clang", "-c", "-O2", "-o", out, cf); rc != 0 {
412 fmt.Fprintln(os.Stderr, "warning: failed to compile " | cf)
413 continue
414 }
415 allObjs = append(allObjs, out)
416 }
417 for _, sf := range sFiles {
418 base := filepath.Base(sf)
419 out := filepath.Join(tmpDir, base | ".o")
420 if rc := runTool("clang", "-c", "-o", out, sf); rc != 0 {
421 fmt.Fprintln(os.Stderr, "warning: failed to assemble " | sf)
422 continue
423 }
424 allObjs = append(allObjs, out)
425 }
426
427 runtimeOut := filepath.Join(iskraDir, "runtime.o")
428 if len(allObjs) == 1 {
429 data, err := os.ReadFile(allObjs[0])
430 if err != nil {
431 fatal("reading runtime object: " | err.Error())
432 }
433 err = os.WriteFile(runtimeOut, data, 0644)
434 if err != nil {
435 fatal("writing runtime.o: " | err.Error())
436 }
437 } else {
438 ldArgs := []string{"ld", "-r", "-o", runtimeOut}
439 ldArgs = append(ldArgs, allObjs...)
440 if rc := runToolArgv(ldArgs); rc != 0 {
441 fatal("linking runtime objects failed")
442 }
443 }
444
445 fmt.Fprintln(os.Stderr, "installed: " | runtimeOut)
446 } else {
447 fmt.Fprintln(os.Stderr, "warning: no runtime functions compiled")
448 }
449 }
450
451 func runTool(name string, args ...string) int {
452 argv := []string{name}
453 argv = append(argv, args...)
454 return runToolArgv(argv)
455 }
456
457 func runToolArgv(argv []string) int {
458 name := argv[0]
459 fullPath := which(name)
460 if fullPath == "" {
461 fmt.Fprintln(os.Stderr, "tool not found: " | name)
462 return 127
463 }
464
465 proc, err := os.StartProcess(fullPath, argv, &os.ProcAttr{
466 Env: os.Environ(),
467 })
468 if err != nil {
469 fmt.Fprintln(os.Stderr, "exec " | name | ": " | err.Error())
470 return 126
471 }
472
473 var ws syscall.WaitStatus
474 for {
475 _, err = syscall.Wait4(proc.Pid, &ws, 0, nil)
476 if err == nil {
477 break
478 }
479 if err != syscall.EINTR {
480 fmt.Fprintln(os.Stderr, "wait " | name | ": " | err.Error())
481 return 126
482 }
483 }
484
485 if ws.Exited() {
486 return ws.ExitStatus()
487 }
488 return 1
489 }
490
491 func which(name string) string {
492 if bytes.IndexByte([]byte(name), '/') >= 0 {
493 return name
494 }
495 pathEnv := os.Getenv("PATH")
496 dirs := bytes.Split([]byte(pathEnv), []byte(":"))
497 for _, d := range dirs {
498 full := string(d) | "/" | name
499 if _, err := os.Stat(full); err == nil {
500 return full
501 }
502 }
503 return ""
504 }
505
506 func detectPackage(srcFiles []string) string {
507 for _, f := range srcFiles {
508 data, err := os.ReadFile(f)
509 if err != nil {
510 continue
511 }
512 lines := bytes.Split(data, []byte("\n"))
513 for _, line := range lines {
514 line = bytes.TrimSpace(line)
515 if bytes.HasPrefix(line, []byte("package ")) {
516 return string(line[8:])
517 }
518 }
519 }
520 return "main"
521 }
522
523 func expandLattice(t *iskra.Tree, meshPath string, unmatched []iskra.UnmatchedFunc, srcDir string, pkg string, modPath string, target string) bool {
524 moxiePath := which("moxie")
525 if moxiePath == "" {
526 return false
527 }
528
529 tmpDir, err := os.MkdirTemp("", "moxie-iskra-expand-")
530 if err != nil {
531 return false
532 }
533
534 irFile := filepath.Join(tmpDir, "lowered.ll")
535 shScript := filepath.Join(tmpDir, "run.sh")
536 envPrefix := ""
537 if target == "wasm32" {
538 envPrefix = "GOOS=js GOARCH=wasm "
539 }
540 shContent := "#!/bin/sh\ncd " | srcDir | " && " | envPrefix | moxiePath | " build -internal-printir -o /dev/null . > " | irFile | " 2>/dev/null\n"
541 err = os.WriteFile(shScript, []byte(shContent), 0755)
542 if err != nil {
543 return false
544 }
545 shPath := which("sh")
546 runTool(shPath, shScript)
547
548 moduleIR, err := os.ReadFile(irFile)
549 if err != nil || len(moduleIR) == 0 {
550 fmt.Fprintln(os.Stderr, "expand: moxie -internal-printir produced no output")
551 return false
552 }
553 fmt.Fprintln(os.Stderr, "expand: got " | fmt.Sprint(len(moduleIR)) | " bytes of IR from moxie")
554
555 irBlocks := splitIRFunctions(moduleIR)
556 if len(irBlocks) == 0 {
557 return false
558 }
559
560 unmatchedNames := map[string]bool{}
561 for _, u := range unmatched {
562 unmatchedNames[u.Name] = true
563 }
564
565 srcByFile := map[string][]byte{}
566 added := 0
567
568 scaffold := iskra.ExtractModuleScaffold(moduleIR)
569 scaffold = stripAttrGroupRefsAll(scaffold)
570 if target != "wasm32" {
571 scaffold = filterScaffoldForPkg(scaffold, pkg, modPath)
572 }
573 modMetaIdx := iskra.InsertSegment(t, iskra.StageIR, iskra.KindPkg, pkg | ".__module__", scaffold)
574
575 for _, u := range unmatched {
576 src, ok := srcByFile[u.SrcFile]
577 if !ok {
578 src, err = os.ReadFile(u.SrcFile)
579 if err != nil {
580 fmt.Fprintln(os.Stderr, "expand: cannot read " | u.SrcFile | ": " | err.Error())
581 continue
582 }
583 srcByFile[u.SrcFile] = src
584 }
585
586 var decl []byte
587 for _, d := range iskra.SplitDecls(src) {
588 if iskra.DeclName(d) == u.Name {
589 decl = d
590 break
591 }
592 }
593 if len(decl) == 0 {
594 fmt.Fprintln(os.Stderr, "expand: cannot find decl for " | u.Name | " in " | u.SrcFile)
595 continue
596 }
597
598 astDump := iskra.GenAST(decl)
599 if len(astDump) == 0 {
600 fmt.Fprintln(os.Stderr, "expand: empty AST for " | u.Name)
601 continue
602 }
603
604 irBlock := findIRBlock(irBlocks, u.Name, pkg)
605 if len(irBlock) == 0 {
606 fmt.Fprintln(os.Stderr, "expand: no IR found for " | u.Name)
607 continue
608 }
609 irBlock = stripAttrGroupRefs(irBlock)
610 irBlock = iskra.StripDebugMetadata(irBlock)
611
612 funcName := pkg | "." | u.Name
613 astIdx := iskra.InsertSegment(t, iskra.StageAST, iskra.KindFunc, funcName, astDump)
614 irIdx := iskra.InsertSegment(t, iskra.StageIR, iskra.KindFunc, funcName, irBlock)
615
616 t.AddAdj(astIdx, irIdx)
617 t.AddAdj(irIdx, astIdx)
618 t.AddAdj(irIdx, modMetaIdx)
619 t.AddAdj(modMetaIdx, irIdx)
620
621 fmt.Fprintln(os.Stderr, "EXPANDED: " | funcName)
622 added++
623 }
624
625
626 if added == 0 {
627 return false
628 }
629
630 t.FinalizeAdj()
631
632 if err := iskra.MeshSaveFile(meshPath, t, iskra.StageAST, iskra.StageIR); err != nil {
633 fmt.Fprintln(os.Stderr, "expand: saving mesh: " | err.Error())
634 return false
635 }
636 fmt.Fprintln(os.Stderr, "expand: added " | fmt.Sprint(added) | " entries, mesh saved to " | meshPath)
637 return true
638 }
639
640 func splitIRFunctions(ir []byte) map[string][]byte {
641 result := map[string][]byte{}
642 lines := bytes.Split(ir, []byte("\n"))
643 i := 0
644 for i < len(lines) {
645 trimmed := bytes.TrimSpace(lines[i])
646 if !bytes.HasPrefix(trimmed, []byte("define ")) {
647 i++
648 continue
649 }
650 name := iskra.ExtractAtName(trimmed)
651 var funcLines []byte
652 depth := 0
653 for i < len(lines) {
654 funcLines = append(funcLines, lines[i]...)
655 funcLines = append(funcLines, '\n')
656 for _, ch := range string(lines[i]) {
657 if ch == '{' {
658 depth++
659 } else if ch == '}' {
660 depth--
661 }
662 }
663 i++
664 if depth == 0 {
665 break
666 }
667 }
668 if name != "" {
669 result[iskra.NormalizeLLVMName(name)] = funcLines
670 }
671 }
672 return result
673 }
674
675 func readDepModules(dir string) []string {
676 modFile := filepath.Join(dir, "moxie.mod")
677 data, err := os.ReadFile(modFile)
678 if err != nil {
679 return nil
680 }
681 var deps []string
682 lines := bytes.Split(data, []byte("\n"))
683 for _, line := range lines {
684 line = bytes.TrimSpace(line)
685 if bytes.HasPrefix(line, []byte("require ")) {
686 parts := bytes.Fields(line)
687 if len(parts) >= 2 {
688 deps = append(deps, string(parts[1]))
689 }
690 }
691 }
692 return deps
693 }
694
695 func collectDefinedNames(ir []byte) map[string]bool {
696 names := map[string]bool{}
697 lines := bytes.Split(ir, []byte("\n"))
698 for _, line := range lines {
699 trimmed := bytes.TrimSpace(line)
700 if bytes.HasPrefix(trimmed, []byte("define ")) {
701 name := iskra.ExtractAtName(trimmed)
702 if name != "" {
703 names[iskra.NormalizeLLVMName(name)] = true
704 }
705 }
706 }
707 return names
708 }
709
710 func injectDepFunctions(resultIR []byte, srcDir string, target string) []byte {
711 if target != "wasm32" {
712 return resultIR
713 }
714
715 deps := readDepModules(srcDir)
716 if len(deps) == 0 {
717 fmt.Fprintln(os.Stderr, "inject-deps: no dependency modules in moxie.mod")
718 return resultIR
719 }
720 fmt.Fprintln(os.Stderr, "inject-deps: dependency modules: " | fmt.Sprint(len(deps)))
721
722 moxiePath := which("moxie")
723 if moxiePath == "" {
724 fmt.Fprintln(os.Stderr, "inject-deps: moxie not on PATH")
725 return resultIR
726 }
727
728 tmpDir, err := os.MkdirTemp("", "moxie-iskra-deps-")
729 if err != nil {
730 return resultIR
731 }
732
733 irFile := filepath.Join(tmpDir, "full.ll")
734 shScript := filepath.Join(tmpDir, "run.sh")
735 shContent := "#!/bin/sh\ncd " | srcDir | " && GOOS=js GOARCH=wasm " | moxiePath | " build -internal-printir -o /dev/null . > " | irFile | " 2>/dev/null\n"
736 os.WriteFile(shScript, []byte(shContent), 0755)
737 shPath := which("sh")
738 runTool(shPath, shScript)
739
740 fullIR, err := os.ReadFile(irFile)
741 if err != nil || len(fullIR) == 0 {
742 fmt.Fprintln(os.Stderr, "inject-deps: moxie -internal-printir produced no output")
743 return resultIR
744 }
745
746 blocks := splitIRFunctions(fullIR)
747 defined := collectDefinedNames(resultIR)
748 injected := 0
749
750 var toInject []string
751 for name := range blocks {
752 if defined[name] {
753 continue
754 }
755 toInject = append(toInject, name)
756 }
757
758 stubNames := map[string]bool{}
759 for _, name := range toInject {
760 stubNames[name] = true
761 }
762 resultIR = removeStubDeclarations(resultIR, stubNames)
763
764 wasmExportAttrs := parseWasmExportAttrs(fullIR)
765
766 var cleanedBlocks [][]byte
767 var usedPtrs []string
768 for _, name := range toInject {
769 block := blocks[name]
770 block = inlineWasmExportAttr(block, wasmExportAttrs)
771 block = stripAttrGroupRefs(block)
772 block = iskra.StripDebugMetadata(block)
773 cleanedBlocks = append(cleanedBlocks, block)
774 if bytes.Contains([]byte(name), []byte("#wasmexport")) {
775 usedPtrs = append(usedPtrs, name)
776 }
777 }
778
779 depGlobals := extractMissingGlobals(fullIR, resultIR, cleanedBlocks)
780 if len(depGlobals) > 0 {
781 resultIR = append(resultIR, '\n')
782 resultIR = append(resultIR, depGlobals...)
783 }
784
785 if len(usedPtrs) > 0 {
786 resultIR = stripLlvmUsed(resultIR)
787 resultIR = appendLlvmUsed(resultIR, usedPtrs)
788 }
789
790 for _, block := range cleanedBlocks {
791 resultIR = append(resultIR, '\n')
792 resultIR = append(resultIR, block...)
793 injected++
794 }
795
796 depDecls := extractMissingDeclarations(fullIR, resultIR)
797 if len(depDecls) > 0 {
798 resultIR = append(resultIR, '\n')
799 resultIR = append(resultIR, depDecls...)
800 }
801
802 fmt.Fprintln(os.Stderr, "inject-deps: added " | fmt.Sprint(injected) | " dependency functions")
803 return resultIR
804 }
805
806 func extractMissingGlobals(fullIR []byte, moduleIR []byte, injectedBlocks [][]byte) []byte {
807 existing := map[string]bool{}
808 modLines := bytes.Split(moduleIR, []byte("\n"))
809 for _, line := range modLines {
810 trimmed := bytes.TrimSpace(line)
811 if len(trimmed) > 0 && trimmed[0] == '@' {
812 gname := iskra.NormalizeLLVMName(extractGlobalName(trimmed))
813 if gname != "" {
814 existing[gname] = true
815 }
816 }
817 if bytes.HasPrefix(trimmed, []byte("declare ")) || bytes.HasPrefix(trimmed, []byte("define ")) {
818 name := iskra.ExtractAtName(trimmed)
819 if name != "" {
820 existing[iskra.NormalizeLLVMName(name)] = true
821 }
822 }
823 }
824
825 needed := map[string]bool{}
826 for _, block := range injectedBlocks {
827 collectGlobalRefs(block, needed)
828 }
829
830 allGlobals := map[string][]byte{}
831 fullLines := bytes.Split(fullIR, []byte("\n"))
832 for _, line := range fullLines {
833 trimmed := bytes.TrimSpace(line)
834 if len(trimmed) == 0 || trimmed[0] != '@' {
835 continue
836 }
837 if bytes.Contains(trimmed, []byte(" = ")) && !bytes.HasPrefix(trimmed, []byte("@llvm.")) {
838 gname := extractGlobalName(trimmed)
839 if gname != "" {
840 norm := iskra.NormalizeLLVMName(gname)
841 if _, ok := allGlobals[norm]; !ok {
842 allGlobals[norm] = trimmed
843 }
844 }
845 }
846 }
847
848 var out []byte
849 added := 0
850 queue := []string{}
851 for ref := range needed {
852 if !existing[ref] {
853 queue = append(queue, ref)
854 }
855 }
856
857 for len(queue) > 0 {
858 ref := queue[len(queue)-1]
859 queue = queue[:len(queue)-1]
860 if existing[ref] {
861 continue
862 }
863 existing[ref] = true
864
865 gline, ok := allGlobals[ref]
866 if !ok {
867 continue
868 }
869 cleaned := stripAttrGroupRefs(iskra.StripDebugMetadata(gline))
870 out = append(out, cleaned...)
871 out = append(out, '\n')
872 added++
873
874 transitive := map[string]bool{}
875 collectGlobalRefs(gline, transitive)
876 for tr := range transitive {
877 if !existing[tr] {
878 queue = append(queue, tr)
879 }
880 }
881 }
882
883 if added > 0 {
884 fmt.Fprintln(os.Stderr, "inject-deps: added " | fmt.Sprint(added) | " missing globals")
885 }
886 return out
887 }
888
889 func parseWasmExportAttrs(ir []byte) map[string]string {
890 result := map[string]string{}
891 lines := bytes.Split(ir, []byte("\n"))
892 for _, line := range lines {
893 trimmed := bytes.TrimSpace(line)
894 if !bytes.HasPrefix(trimmed, []byte("attributes #")) {
895 continue
896 }
897 nameIdx := bytes.Index(trimmed, []byte("\"wasm-export-name\"=\""))
898 if nameIdx < 0 {
899 continue
900 }
901 hashStart := len("attributes ")
902 spaceIdx := bytes.IndexByte(trimmed[hashStart:], ' ')
903 if spaceIdx < 0 {
904 continue
905 }
906 attrID := string(trimmed[hashStart : hashStart+spaceIdx])
907 valStart := nameIdx + len("\"wasm-export-name\"=\"")
908 rest := trimmed[valStart:]
909 closeQuote := bytes.IndexByte(rest, '"')
910 if closeQuote < 0 {
911 continue
912 }
913 result[attrID] = string(rest[:closeQuote])
914 }
915 return result
916 }
917
918 func inlineWasmExportAttr(block []byte, attrs map[string]string) []byte {
919 lines := bytes.Split(block, []byte("\n"))
920 if len(lines) == 0 {
921 return block
922 }
923 firstLine := lines[0]
924 if !bytes.HasPrefix(bytes.TrimSpace(firstLine), []byte("define ")) {
925 return block
926 }
927 for attrID, exportName := range attrs {
928 ref := []byte(" " | attrID | " ")
929 refEnd := []byte(" " | attrID | "\n")
930 refBrace := []byte(") " | attrID | " {")
931 if bytes.Contains(firstLine, ref) || bytes.HasSuffix(firstLine, refEnd) || bytes.Contains(firstLine, refBrace) {
932 inlineAttr := []byte(" \"wasm-export-name\"=\"" | exportName | "\"")
933 braceIdx := bytes.LastIndexByte(firstLine, '{')
934 if braceIdx > 0 {
935 var newLine []byte
936 newLine = append(newLine, firstLine[:braceIdx]...)
937 newLine = append(newLine, inlineAttr...)
938 newLine = append(newLine, " {"...)
939 lines[0] = newLine
940 return bytes.Join(lines, []byte("\n"))
941 }
942 }
943 }
944 return block
945 }
946
947 func stripLlvmUsed(ir []byte) []byte {
948 prefix := []byte("@llvm.used = ")
949 lines := bytes.Split(ir, []byte("\n"))
950 var out []byte
951 for i, line := range lines {
952 if bytes.HasPrefix(bytes.TrimSpace(line), prefix) {
953 continue
954 }
955 if i > 0 {
956 out = append(out, '\n')
957 }
958 out = append(out, line...)
959 }
960 return out
961 }
962
963 func appendLlvmUsed(ir []byte, ptrs []string) []byte {
964 marker := []byte("@llvm.used = appending global [")
965 existing := bytes.Index(ir, marker)
966 if existing >= 0 {
967 lineEnd := bytes.IndexByte(ir[existing:], '\n')
968 if lineEnd < 0 {
969 lineEnd = len(ir) - existing
970 }
971 line := ir[existing : existing+lineEnd]
972 lastBracket := bytes.LastIndexByte(line, ']')
973 if lastBracket > 0 {
974 var extra []byte
975 newCount := 0
976 for _, p := range ptrs {
977 name := p
978 if len(name) > 0 && name[0] == '@' {
979 if len(name) > 1 && name[1] != '"' {
980 name = "@\"" | name[1:] | "\""
981 }
982 }
983 if !bytes.Contains(line, []byte(name)) {
984 extra = append(extra, ", ptr "...)
985 extra = append(extra, name...)
986 newCount++
987 }
988 }
989 if newCount > 0 {
990 countStart := len(marker)
991 countEnd := bytes.IndexByte(line[countStart:], ' ')
992 if countEnd > 0 {
993 oldCountStr := string(line[countStart : countStart+countEnd])
994 oldCount := 0
995 for _, ch := range oldCountStr {
996 if ch >= '0' && ch <= '9' {
997 oldCount = oldCount*10 + int(ch-'0')
998 }
999 }
1000 totalCount := oldCount + newCount
1001 newCountStr := fmt.Sprint(totalCount)
1002 var newLine []byte
1003 newLine = append(newLine, line[:countStart]...)
1004 newLine = append(newLine, newCountStr...)
1005 newLine = append(newLine, line[countStart+countEnd:]...)
1006 newLastBracket := bytes.LastIndexByte(newLine, ']')
1007 var merged []byte
1008 merged = append(merged, newLine[:newLastBracket]...)
1009 merged = append(merged, extra...)
1010 merged = append(merged, newLine[newLastBracket:]...)
1011 var result []byte
1012 result = append(result, ir[:existing]...)
1013 result = append(result, merged...)
1014 result = append(result, ir[existing+lineEnd:]...)
1015 return result
1016 }
1017 }
1018 return ir
1019 }
1020 }
1021 ir = append(ir, '\n')
1022 entry := "@llvm.used = appending global [" | fmt.Sprint(len(ptrs)) | " x ptr] ["
1023 for i, p := range ptrs {
1024 if i > 0 {
1025 entry = entry | ", "
1026 }
1027 name := p
1028 if len(name) > 0 && name[0] == '@' {
1029 if len(name) > 1 && name[1] != '"' {
1030 name = "@\"" | name[1:] | "\""
1031 }
1032 }
1033 entry = entry | "ptr " | name
1034 }
1035 entry = entry | "]\n"
1036 ir = append(ir, []byte(entry)...)
1037 return ir
1038 }
1039
1040 func collectWasmExports(ir []byte) string {
1041 var exports string
1042 lines := bytes.Split(ir, []byte("\n"))
1043 for _, line := range lines {
1044 trimmed := bytes.TrimSpace(line)
1045 if !bytes.HasPrefix(trimmed, []byte("define ")) {
1046 continue
1047 }
1048 name := iskra.ExtractAtName(trimmed)
1049 if name == "" {
1050 continue
1051 }
1052 norm := iskra.NormalizeLLVMName(name)
1053 if bytes.Contains([]byte(norm), []byte("#wasmexport")) {
1054 bare := norm
1055 if len(bare) > 0 && bare[0] == '@' {
1056 bare = bare[1:]
1057 }
1058 if len(bare) > 0 && bare[0] == '"' {
1059 bare = bare[1:]
1060 }
1061 if len(bare) > 0 && bare[len(bare)-1] == '"' {
1062 bare = bare[:len(bare)-1]
1063 }
1064 exports = exports | bare | "\n"
1065 }
1066 }
1067 return exports
1068 }
1069
1070 func defineToDecl(line []byte) []byte {
1071 braceIdx := bytes.LastIndexByte(line, '{')
1072 if braceIdx < 0 {
1073 return nil
1074 }
1075 sig := bytes.TrimSpace(line[:braceIdx])
1076 rest := sig[len("define "):]
1077 for {
1078 if bytes.HasPrefix(rest, []byte("internal ")) {
1079 rest = rest[len("internal "):]
1080 } else if bytes.HasPrefix(rest, []byte("hidden ")) {
1081 rest = rest[len("hidden "):]
1082 } else if bytes.HasPrefix(rest, []byte("linkonce_odr ")) {
1083 rest = rest[len("linkonce_odr "):]
1084 } else if bytes.HasPrefix(rest, []byte("unnamed_addr ")) {
1085 rest = rest[len("unnamed_addr "):]
1086 } else {
1087 break
1088 }
1089 }
1090 parenClose := bytes.LastIndexByte(rest, ')')
1091 if parenClose > 0 {
1092 rest = rest[:parenClose+1]
1093 }
1094 var decl []byte
1095 decl = append(decl, "declare "...)
1096 decl = append(decl, rest...)
1097 return decl
1098 }
1099
1100 func extractMissingDeclarations(fullIR []byte, moduleIR []byte) []byte {
1101 existing := map[string]bool{}
1102 modLines := bytes.Split(moduleIR, []byte("\n"))
1103 for _, line := range modLines {
1104 trimmed := bytes.TrimSpace(line)
1105 if bytes.HasPrefix(trimmed, []byte("define ")) || bytes.HasPrefix(trimmed, []byte("declare ")) {
1106 name := iskra.ExtractAtName(trimmed)
1107 if name != "" {
1108 existing[iskra.NormalizeLLVMName(name)] = true
1109 }
1110 }
1111 }
1112
1113 refs := map[string]bool{}
1114 for _, line := range modLines {
1115 collectGlobalRefs(line, refs)
1116 }
1117
1118 allDecls := map[string][]byte{}
1119 fullLines := bytes.Split(fullIR, []byte("\n"))
1120 for _, line := range fullLines {
1121 trimmed := bytes.TrimSpace(line)
1122 if bytes.HasPrefix(trimmed, []byte("declare ")) {
1123 name := iskra.ExtractAtName(trimmed)
1124 if name != "" {
1125 norm := iskra.NormalizeLLVMName(name)
1126 if _, ok := allDecls[norm]; !ok {
1127 allDecls[norm] = trimmed
1128 }
1129 }
1130 }
1131 }
1132
1133 allDefineToDecl := map[string][]byte{}
1134 i := 0
1135 for i < len(fullLines) {
1136 trimmed := bytes.TrimSpace(fullLines[i])
1137 if bytes.HasPrefix(trimmed, []byte("define ")) {
1138 name := iskra.ExtractAtName(trimmed)
1139 if name != "" {
1140 norm := iskra.NormalizeLLVMName(name)
1141 if _, ok := allDecls[norm]; !ok {
1142 if _, ok2 := allDefineToDecl[norm]; !ok2 {
1143 decl := defineToDecl(trimmed)
1144 if len(decl) > 0 {
1145 allDefineToDecl[norm] = decl
1146 }
1147 }
1148 }
1149 }
1150 depth := 0
1151 for i < len(fullLines) {
1152 for _, ch := range string(fullLines[i]) {
1153 if ch == '{' {
1154 depth++
1155 } else if ch == '}' {
1156 depth--
1157 }
1158 }
1159 i++
1160 if depth == 0 {
1161 break
1162 }
1163 }
1164 continue
1165 }
1166 i++
1167 }
1168
1169 var out []byte
1170 added := 0
1171 for ref := range refs {
1172 if existing[ref] {
1173 continue
1174 }
1175 if dline, ok := allDecls[ref]; ok {
1176 cleaned := stripAttrGroupRefs(iskra.StripDebugMetadata(dline))
1177 out = append(out, cleaned...)
1178 out = append(out, '\n')
1179 existing[ref] = true
1180 added++
1181 } else if dline, ok := allDefineToDecl[ref]; ok {
1182 cleaned := stripAttrGroupRefs(iskra.StripDebugMetadata(dline))
1183 out = append(out, cleaned...)
1184 out = append(out, '\n')
1185 existing[ref] = true
1186 added++
1187 }
1188 }
1189
1190 if added > 0 {
1191 fmt.Fprintln(os.Stderr, "inject-deps: added " | fmt.Sprint(added) | " missing declarations")
1192 }
1193 return out
1194 }
1195
1196 func collectGlobalRefs(data []byte, refs map[string]bool) {
1197 i := 0
1198 for i < len(data) {
1199 atIdx := bytes.IndexByte(data[i:], '@')
1200 if atIdx < 0 {
1201 break
1202 }
1203 atIdx += i
1204 name := extractGlobalName(data[atIdx:])
1205 if name != "" && !bytes.HasPrefix([]byte(name), []byte("@llvm.")) {
1206 refs[iskra.NormalizeLLVMName(name)] = true
1207 }
1208 i = atIdx + 1
1209 }
1210 }
1211
1212 func extractGlobalName(line []byte) string {
1213 if len(line) == 0 || line[0] != '@' {
1214 return ""
1215 }
1216 if len(line) > 1 && line[1] == '"' {
1217 closeQuote := bytes.IndexByte(line[2:], '"')
1218 if closeQuote < 0 {
1219 return ""
1220 }
1221 return string(line[:closeQuote+3])
1222 }
1223 for i := 1; i < len(line); i++ {
1224 ch := line[i]
1225 if ch == ' ' || ch == '=' || ch == '(' || ch == ',' || ch == ')' || ch == '\n' || ch == '\r' {
1226 return string(line[:i])
1227 }
1228 }
1229 return string(line)
1230 }
1231
1232 func removeStubDeclarations(ir []byte, names map[string]bool) []byte {
1233 lines := bytes.Split(ir, []byte("\n"))
1234 var out []byte
1235 removed := 0
1236 for _, line := range lines {
1237 trimmed := bytes.TrimSpace(line)
1238 if bytes.HasPrefix(trimmed, []byte("declare ")) {
1239 name := iskra.ExtractAtName(trimmed)
1240 if name != "" && names[iskra.NormalizeLLVMName(name)] {
1241 removed++
1242 continue
1243 }
1244 }
1245 out = append(out, line...)
1246 out = append(out, '\n')
1247 }
1248 if removed > 0 {
1249 fmt.Fprintln(os.Stderr, "inject-deps: removed " | fmt.Sprint(removed) | " stub declarations")
1250 }
1251 return out
1252 }
1253
1254 func findIRBlock(blocks map[string][]byte, funcName string, pkg string) []byte {
1255 candidates := []string{
1256 "@" | pkg | "." | funcName,
1257 "@\"" | pkg | "." | funcName | "\"",
1258 }
1259 for _, c := range candidates {
1260 if b, ok := blocks[c]; ok {
1261 return b
1262 }
1263 }
1264 suffix := "." | funcName
1265 pkgDot := pkg | "."
1266 for name, b := range blocks {
1267 if bytes.HasSuffix([]byte(name), []byte(suffix)) && bytes.Contains([]byte(name), []byte(pkgDot)) {
1268 return b
1269 }
1270 }
1271 return nil
1272 }
1273
1274 func filterScaffoldForPkg(scaffold []byte, pkg string, modPath string) []byte {
1275 lines := bytes.Split(scaffold, []byte("\n"))
1276 var out []byte
1277 modPrefix := []byte(modPath | "/")
1278 pkgDot := []byte(pkg | ".")
1279 pkgDollar := []byte(pkg | "$")
1280 isAppPkg := func(name []byte) bool {
1281 return bytes.Contains(name, pkgDot) || bytes.Contains(name, pkgDollar) || bytes.Contains(name, modPrefix)
1282 }
1283 for _, line := range lines {
1284 trimmed := bytes.TrimSpace(line)
1285 if len(trimmed) == 0 {
1286 out = append(out, '\n')
1287 continue
1288 }
1289 if bytes.HasPrefix(trimmed, []byte("target ")) ||
1290 bytes.HasPrefix(trimmed, []byte("source_filename")) {
1291 out = append(out, line...)
1292 out = append(out, '\n')
1293 continue
1294 }
1295 if trimmed[0] == '%' {
1296 out = append(out, line...)
1297 out = append(out, '\n')
1298 continue
1299 }
1300 if trimmed[0] == '@' {
1301 gname := trimmed
1302 spIdx := bytes.IndexByte(gname, ' ')
1303 if spIdx > 0 {
1304 gname = gname[:spIdx]
1305 }
1306 if isAppPkg(gname) {
1307 out = append(out, line...)
1308 out = append(out, '\n')
1309 }
1310 continue
1311 }
1312 if bytes.HasPrefix(trimmed, []byte("declare")) {
1313 if isAppPkg(trimmed) || bytes.Contains(trimmed, []byte("wasm-import-module")) {
1314 out = append(out, line...)
1315 out = append(out, '\n')
1316 }
1317 continue
1318 }
1319 out = append(out, line...)
1320 out = append(out, '\n')
1321 }
1322 return out
1323 }
1324
1325 func stripAttrGroupRefsAll(ir []byte) []byte {
1326 lines := bytes.Split(ir, []byte("\n"))
1327 wasmAttrs := map[string]string{}
1328 for _, line := range lines {
1329 trimmed := bytes.TrimSpace(line)
1330 if !bytes.HasPrefix(trimmed, []byte("attributes #")) {
1331 continue
1332 }
1333 if !bytes.Contains(trimmed, []byte("wasm-import-module")) {
1334 continue
1335 }
1336 idx := bytes.Index(trimmed, []byte("#"))
1337 end := idx + 1
1338 for end < len(trimmed) && trimmed[end] >= '0' && trimmed[end] <= '9' {
1339 end++
1340 }
1341 groupID := string(trimmed[idx:end])
1342 var attrs []byte
1343 if m := extractQuotedAttr(trimmed, "wasm-import-module"); m != "" {
1344 attrs = append(attrs, " \"wasm-import-module\"=\""...)
1345 attrs = append(attrs, m...)
1346 attrs = append(attrs, '"')
1347 }
1348 if n := extractQuotedAttr(trimmed, "wasm-import-name"); n != "" {
1349 attrs = append(attrs, " \"wasm-import-name\"=\""...)
1350 attrs = append(attrs, n...)
1351 attrs = append(attrs, '"')
1352 }
1353 if len(attrs) > 0 {
1354 wasmAttrs[groupID] = string(attrs)
1355 }
1356 }
1357 var out []byte
1358 for _, line := range lines {
1359 trimmed := bytes.TrimSpace(line)
1360 if bytes.HasPrefix(trimmed, []byte("attributes #")) {
1361 continue
1362 }
1363 if len(wasmAttrs) > 0 && bytes.HasPrefix(trimmed, []byte("declare ")) {
1364 idx := bytes.LastIndex(line, []byte(" #"))
1365 if idx >= 0 {
1366 end := idx + 2
1367 if end < len(line) && line[end] >= '0' && line[end] <= '9' {
1368 for end < len(line) && line[end] >= '0' && line[end] <= '9' {
1369 end++
1370 }
1371 ref := string(line[idx+1 : end])
1372 if attrs, ok := wasmAttrs[ref]; ok {
1373 var newLine []byte
1374 newLine = append(newLine, line[:idx]...)
1375 newLine = append(newLine, attrs...)
1376 newLine = append(newLine, line[end:]...)
1377 line = newLine
1378 }
1379 }
1380 }
1381 }
1382 line = stripAttrGroupRefsLine(line)
1383 out = append(out, line...)
1384 out = append(out, '\n')
1385 }
1386 return out
1387 }
1388
1389 func extractQuotedAttr(line []byte, key string) string {
1390 needle := []byte("\"" | key | "\"=\"")
1391 idx := bytes.Index(line, needle)
1392 if idx < 0 {
1393 return ""
1394 }
1395 start := idx + len(needle)
1396 end := bytes.IndexByte(line[start:], '"')
1397 if end < 0 {
1398 return ""
1399 }
1400 return string(line[start : start+end])
1401 }
1402
1403 func stripAttrGroupRefsLine(line []byte) []byte {
1404 limit := len(line)
1405 if cIdx := bytes.Index(line, []byte(" c\"")); cIdx >= 0 {
1406 limit = cIdx
1407 }
1408 for {
1409 sub := line[:limit]
1410 idx := bytes.LastIndex(sub, []byte(" #"))
1411 if idx < 0 {
1412 break
1413 }
1414 end := idx + 2
1415 if end >= len(line) || line[end] < '0' || line[end] > '9' {
1416 break
1417 }
1418 for end < len(line) && line[end] >= '0' && line[end] <= '9' {
1419 end++
1420 }
1421 var newLine []byte
1422 newLine = append(newLine, line[:idx]...)
1423 newLine = append(newLine, line[end:]...)
1424 line = newLine
1425 limit = idx
1426 }
1427 return line
1428 }
1429
1430 func stripAttrGroupRefs(ir []byte) []byte {
1431 nl := bytes.IndexByte(ir, '\n')
1432 if nl < 0 {
1433 return ir
1434 }
1435 first := ir[:nl]
1436 rest := ir[nl:]
1437 for {
1438 idx := bytes.LastIndex(first, []byte(" #"))
1439 if idx < 0 {
1440 break
1441 }
1442 end := idx + 2
1443 if end >= len(first) || first[end] < '0' || first[end] > '9' {
1444 break
1445 }
1446 for end < len(first) && first[end] >= '0' && first[end] <= '9' {
1447 end++
1448 }
1449 var newFirst []byte
1450 newFirst = append(newFirst, first[:idx]...)
1451 newFirst = append(newFirst, first[end:]...)
1452 first = newFirst
1453 }
1454 var out []byte
1455 out = append(out, first...)
1456 out = append(out, rest...)
1457 return out
1458 }
1459
1460 func generateIntrinsics(ir []byte, target string) []byte {
1461 lines := bytes.Split(ir, []byte("\n"))
1462 var out []byte
1463
1464 dl, triple := targetDataLayout(target)
1465 out = append(out, []byte("target datalayout = \"" | dl | "\"\n")...)
1466 out = append(out, []byte("target triple = \"" | triple | "\"\n\n")...)
1467
1468 needTypeAssert := false
1469
1470 for _, line := range lines {
1471 trimmed := bytes.TrimSpace(line)
1472
1473 if bytes.HasPrefix(trimmed, []byte("@\"reflect/types.typeid:")) && bytes.Contains(trimmed, []byte("= external constant i8")) {
1474 nameEnd := bytes.Index(trimmed, []byte("\" ="))
1475 if nameEnd > 0 {
1476 name := trimmed[:nameEnd+1]
1477 out = append(out, name...)
1478 out = append(out, []byte(" = constant i8 0\n")...)
1479 }
1480 continue
1481 }
1482
1483 if bytes.HasPrefix(trimmed, []byte("declare")) && bytes.Contains(trimmed, []byte("@runtime.typeAssert")) {
1484 needTypeAssert = true
1485 continue
1486 }
1487
1488 if bytes.HasPrefix(trimmed, []byte("declare")) && bytes.Contains(trimmed, []byte("@\"interface:")) {
1489 name := iskra.ExtractAtName(trimmed)
1490 if name != "" {
1491 norm := iskra.NormalizeLLVMName(name)
1492 if bytes.HasSuffix([]byte(norm), []byte(".$typeassert")) {
1493 out = append(out, []byte("define i1 " | name | "(ptr %0, ptr %1) {\n ret i1 false\n}\n")...)
1494 } else if bytes.HasSuffix([]byte(norm), []byte("$invoke")) {
1495 out = append(out, []byte("define void " | name | "(ptr %0, ptr %1, ptr %2) {\n unreachable\n}\n")...)
1496 }
1497 }
1498 continue
1499 }
1500
1501 if target == "x86_64" && bytes.HasPrefix(trimmed, []byte("@\"internal/cpu.X86\"")) && bytes.Contains(trimmed, []byte("= external")) {
1502 out = append(out, []byte("@\"internal/cpu.X86\" = global [1 x i8] zeroinitializer\n")...)
1503 continue
1504 }
1505 }
1506
1507 if needTypeAssert {
1508 out = append(out, []byte("define i1 @runtime.typeAssert(ptr %actual, ptr %expected, ptr %context) {\n %cmp = icmp eq ptr %actual, %expected\n ret i1 %cmp\n}\n")...)
1509 }
1510
1511 return out
1512 }
1513
1514 func targetDataLayout(target string) (string, string) {
1515 if target == "wasm32" {
1516 return "e-m:e-p:32:32-p10:8:8-p20:8:8-i64:64-n32:64-S128-ni:1:10:20", "wasm32-unknown-js"
1517 }
1518 return "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", "x86_64-unknown-linux-musleabihf"
1519 }
1520
1521 func llcFlags(target string, optLevel string, input string, output string) []string {
1522 if target == "wasm32" {
1523 return []string{"llc", "-filetype=obj", "-mtriple=wasm32-unknown-js", "-O" | optLevel, input, "-o", output}
1524 }
1525 return []string{"llc", "-filetype=obj", "-relocation-model=pic", "-O" | optLevel, input, "-o", output}
1526 }
1527
1528 func intrinsicCompileFlags(target string, input string, output string) []string {
1529 if target == "wasm32" {
1530 return []string{"clang", "-c", "--target=wasm32-unknown-js", "-o", output, input}
1531 }
1532 return []string{"clang", "-c", "-o", output, input}
1533 }
1534
1535 func linkFlagsLTO(target string, objFile string, intrinsicsObj string, outPath string, home string, lto bool, skipStdlib bool) []string {
1536 if target == "wasm32" {
1537 args := []string{"wasm-ld", objFile, intrinsicsObj}
1538 runtimeObj := filepath.Join(home, ".local", "share", "moxie-iskra", "runtime.wasm32.o")
1539 if _, err := os.Stat(runtimeObj); err == nil {
1540 args = append(args, runtimeObj)
1541 fmt.Fprintln(os.Stderr, "linking with runtime: " | runtimeObj)
1542 }
1543 if !lto && !skipStdlib {
1544 stdlibArchive := resolveStdlibArchive(target)
1545 if stdlibArchive != "" {
1546 args = append(args, stdlibArchive)
1547 fmt.Fprintln(os.Stderr, "linking with stdlib: " | stdlibArchive)
1548 }
1549 }
1550 args = append(args, "--allow-undefined", "--gc-sections", "--no-entry", "--export=__alloc", "--export=_start", "--export=memory", "-o", outPath)
1551 return args
1552 }
1553 args := []string{"clang", objFile, intrinsicsObj}
1554 runtimeObj := filepath.Join(home, ".local", "share", "moxie-iskra", "runtime.o")
1555 if _, err := os.Stat(runtimeObj); err == nil {
1556 args = append(args, runtimeObj)
1557 fmt.Fprintln(os.Stderr, "linking with runtime: " | runtimeObj)
1558 }
1559 if lto {
1560 bdwgcObj := filepath.Join(home, ".local", "share", "moxie-iskra", "bdwgc.o")
1561 if _, err := os.Stat(bdwgcObj); err == nil {
1562 args = append(args, bdwgcObj)
1563 }
1564 asmStubsObj := filepath.Join(home, ".local", "share", "moxie-iskra", "asm_stubs.o")
1565 if _, err := os.Stat(asmStubsObj); err == nil {
1566 args = append(args, asmStubsObj)
1567 }
1568 } else {
1569 stdlibArchive := resolveStdlibArchive(target)
1570 if stdlibArchive != "" {
1571 args = append(args, stdlibArchive)
1572 fmt.Fprintln(os.Stderr, "linking with stdlib: " | stdlibArchive)
1573 }
1574 }
1575 args = append(args, "-no-pie", "-o", outPath, "-lm", "-Wl,--gc-sections")
1576 return args
1577 }
1578
1579 func resolveStdlibArchive(target string) string {
1580 home, _ := os.UserHomeDir()
1581 if target == "wasm32" {
1582 wasmCorpus := os.Getenv("ISKRA_CORPUS_WASM")
1583 if wasmCorpus == "" {
1584 wasmCorpus = "/tmp/iskra-corpus-wasm"
1585 }
1586 archive := filepath.Join(wasmCorpus, "libstdlib.a")
1587 if _, err := os.Stat(archive); err == nil {
1588 return archive
1589 }
1590 if home != "" {
1591 archive = filepath.Join(home, ".local", "share", "moxie-iskra", "libstdlib.wasm32.a")
1592 if _, err := os.Stat(archive); err == nil {
1593 return archive
1594 }
1595 }
1596 return ""
1597 }
1598 corpusDir := os.Getenv("ISKRA_CORPUS")
1599 if corpusDir == "" {
1600 corpusDir = "/tmp/iskra-corpus"
1601 }
1602 archive := filepath.Join(corpusDir, "libstdlib.a")
1603 if _, err := os.Stat(archive); err == nil {
1604 return archive
1605 }
1606 if home != "" {
1607 archive = filepath.Join(home, ".local", "share", "moxie-iskra", "libstdlib.a")
1608 if _, err := os.Stat(archive); err == nil {
1609 return archive
1610 }
1611 }
1612 return ""
1613 }
1614
1615 func resolveStdlibBitcode(target string) string {
1616 home, _ := os.UserHomeDir()
1617 if home == "" {
1618 return ""
1619 }
1620 bc := filepath.Join(home, ".local", "share", "moxie-iskra", "stdlib.bc")
1621 if target != "" && target != "x86_64" {
1622 bc = filepath.Join(home, ".local", "share", "moxie-iskra", "stdlib." | target | ".bc")
1623 }
1624 if _, err := os.Stat(bc); err == nil {
1625 return bc
1626 }
1627 return ""
1628 }
1629
1630 func fatal(msg string) {
1631 fmt.Fprintln(os.Stderr, "moxie-iskra: " | msg)
1632 os.Exit(1)
1633 }
1634
1635 // --- per-package compilation pipeline ---
1636
1637 type pkgSpec struct {
1638 importPath string
1639 srcDir string
1640 pkgName string
1641 }
1642
1643 func findMoxieRoot() string {
1644 if r := os.Getenv("MOXIEROOT"); r != "" {
1645 return r
1646 }
1647 p := which("moxie")
1648 if p == "" {
1649 return ""
1650 }
1651 // ask moxie env for MOXIEROOT
1652 tmpDir, err := os.MkdirTemp("", "moxie-iskra-env-")
1653 if err == nil {
1654 outFile := filepath.Join(tmpDir, "env.txt")
1655 shScript := filepath.Join(tmpDir, "run.sh")
1656 os.WriteFile(shScript, []byte("#!/bin/sh\n" | p | " env > " | outFile | "\n"), 0755)
1657 runTool(which("sh"), shScript)
1658 if data, err := os.ReadFile(outFile); err == nil {
1659 for _, line := range bytes.Split(data, []byte("\n")) {
1660 if bytes.HasPrefix(line, []byte("MOXIEROOT=")) {
1661 val := string(bytes.Trim(line[10:], "\""))
1662 if val != "" {
1663 return val
1664 }
1665 }
1666 }
1667 }
1668 }
1669 // fallback: look for src/ sibling to binary
1670 dir := filepath.Dir(p)
1671 if filepath.Base(dir) == "bin" {
1672 dir = filepath.Dir(dir)
1673 }
1674 if _, err := os.Stat(filepath.Join(dir, "src")); err == nil {
1675 return dir
1676 }
1677 return ""
1678 }
1679
1680 func pkgFileSafe(importPath string) string {
1681 b := []byte(importPath)
1682 for i := 0; i < len(b); i++ {
1683 if b[i] == '/' {
1684 b[i] = '_'
1685 }
1686 }
1687 return string(b)
1688 }
1689
1690 func extractImportStr(data []byte) string {
1691 start := bytes.IndexByte(data, '"')
1692 if start < 0 {
1693 return ""
1694 }
1695 end := bytes.IndexByte(data[start+1:], '"')
1696 if end < 0 {
1697 return ""
1698 }
1699 return string(data[start+1 : start+1+end])
1700 }
1701
1702 func parseFileImports(data []byte) []string {
1703 var out []string
1704 lines := bytes.Split(data, []byte("\n"))
1705 inBlock := false
1706 for _, line := range lines {
1707 t := bytes.TrimSpace(line)
1708 if bytes.Equal(t, []byte("import (")) {
1709 inBlock = true
1710 continue
1711 }
1712 if inBlock {
1713 if len(t) > 0 && t[0] == ')' {
1714 inBlock = false
1715 continue
1716 }
1717 if s := extractImportStr(t); s != "" {
1718 out = append(out, s)
1719 }
1720 continue
1721 }
1722 if bytes.HasPrefix(t, []byte("import \"")) {
1723 if s := extractImportStr(t[7:]); s != "" {
1724 out = append(out, s)
1725 }
1726 }
1727 }
1728 return out
1729 }
1730
1731 func isBuiltinPkg(imp string) bool {
1732 switch imp {
1733 case "moxie", "unsafe", "C":
1734 return true
1735 }
1736 return false
1737 }
1738
1739 func transitiveDeps(initial []string, moxieRoot string) []pkgSpec {
1740 seen := map[string]bool{}
1741 var result []pkgSpec
1742 var queue []string
1743 for _, imp := range initial {
1744 queue = append(queue, imp)
1745 }
1746 for len(queue) > 0 {
1747 imp := queue[0]
1748 queue = queue[1:]
1749 if seen[imp] || isBuiltinPkg(imp) {
1750 continue
1751 }
1752 seen[imp] = true
1753 srcDir := filepath.Join(moxieRoot, "src", imp)
1754 if _, err := os.Stat(srcDir); err != nil {
1755 continue
1756 }
1757 srcFiles, err := iskra.DiscoverMxFiles(srcDir)
1758 if err != nil || len(srcFiles) == 0 {
1759 continue
1760 }
1761 pkgName := detectPackage(srcFiles)
1762 result = append(result, pkgSpec{importPath: imp, srcDir: srcDir, pkgName: pkgName})
1763 for _, f := range srcFiles {
1764 data, _ := os.ReadFile(f)
1765 for _, sub := range parseFileImports(data) {
1766 if !seen[sub] {
1767 queue = append(queue, sub)
1768 }
1769 }
1770 }
1771 }
1772 return result
1773 }
1774
1775 // pathMtime returns the mtime (seconds) of a path, 0 on error.
1776 func pathMtime(path string) int64 {
1777 fi, err := os.Stat(path)
1778 if err != nil {
1779 return 0
1780 }
1781 return fi.ModTime().Unix()
1782 }
1783
1784 // srcDirMtime returns the max mtime of any .mx file in srcDir.
1785 func srcDirMtime(srcDir string) int64 {
1786 files, err := iskra.DiscoverMxFiles(srcDir)
1787 if err != nil {
1788 return 0
1789 }
1790 var maxMtime int64
1791 for _, f := range files {
1792 if mt := pathMtime(f); mt > maxMtime {
1793 maxMtime = mt
1794 }
1795 }
1796 return maxMtime
1797 }
1798
1799 // pkgCacheDir returns the cache directory for a package source dir.
1800 // Layout: $HOME/.local/share/moxie-iskra/cache/<abs-src-path-without-leading-slash>
1801 func pkgCacheDir(home string, srcDir string) string {
1802 rel := srcDir
1803 if len(rel) > 0 && rel[0] == '/' {
1804 rel = rel[1:]
1805 }
1806 return filepath.Join(home, ".local", "share", "moxie-iskra", "cache", rel)
1807 }
1808
1809 func pkgCacheObj(home string, srcDir string) string {
1810 return filepath.Join(pkgCacheDir(home, srcDir), "pkg.o")
1811 }
1812
1813 func pkgCacheIR(home string, srcDir string) string {
1814 return filepath.Join(pkgCacheDir(home, srcDir), "pkg.ir")
1815 }
1816
1817 // isCacheValid returns true if the cached .o exists and is newer than all source files.
1818 func isCacheValid(cacheObj string, srcDir string) bool {
1819 objMtime := pathMtime(cacheObj)
1820 if objMtime == 0 {
1821 return false
1822 }
1823 return objMtime >= srcDirMtime(srcDir)
1824 }
1825
1826 // fetchAppIR runs moxie -internal-printir on the app and returns the IR bytes.
1827 func fetchAppIR(appSrcDir string, target string) []byte {
1828 moxiePath := which("moxie")
1829 if moxiePath == "" {
1830 return nil
1831 }
1832 tmpDir, err := os.MkdirTemp("", "moxie-iskra-appir-")
1833 if err != nil {
1834 return nil
1835 }
1836 irFile := filepath.Join(tmpDir, "app.ll")
1837 shScript := filepath.Join(tmpDir, "run.sh")
1838 envPrefix := ""
1839 if target == "wasm32" {
1840 envPrefix = "GOOS=js GOARCH=wasm "
1841 }
1842 shContent := "#!/bin/sh\ncd " | appSrcDir | " && " | envPrefix | moxiePath | " build -internal-printir -o /dev/null . > " | irFile | " 2>/dev/null\n"
1843 os.WriteFile(shScript, []byte(shContent), 0755)
1844 shPath := which("sh")
1845 runTool(shPath, shScript)
1846 data, err := os.ReadFile(irFile)
1847 if err != nil || len(data) == 0 {
1848 return nil
1849 }
1850 return data
1851 }
1852
1853 // expandLatticeFromIR expands the lattice using pre-fetched app IR instead of re-running moxie.
1854 // Returns the number of new entries added.
1855 func expandLatticeFromIR(t *iskra.Tree, meshPath string, unmatched []iskra.UnmatchedFunc, moduleIR []byte, pkg string, modPath string, target string) int {
1856 if len(moduleIR) == 0 {
1857 return 0
1858 }
1859 irBlocks := splitIRFunctions(moduleIR)
1860 scaffold := iskra.ExtractModuleScaffold(moduleIR)
1861 scaffold = stripAttrGroupRefsAll(scaffold)
1862 if target != "wasm32" {
1863 scaffold = filterScaffoldForPkg(scaffold, pkg, modPath)
1864 }
1865 modMetaIdx := iskra.InsertSegment(t, iskra.StageIR, iskra.KindPkg, pkg | ".__module__", scaffold)
1866 srcByFile := map[string][]byte{}
1867 added := 0
1868 for _, u := range unmatched {
1869 src, ok := srcByFile[u.SrcFile]
1870 if !ok {
1871 var err error
1872 src, err = os.ReadFile(u.SrcFile)
1873 if err != nil {
1874 continue
1875 }
1876 srcByFile[u.SrcFile] = src
1877 }
1878 var decl []byte
1879 for _, d := range iskra.SplitDecls(src) {
1880 if iskra.DeclName(d) == u.Name {
1881 decl = d
1882 break
1883 }
1884 }
1885 if len(decl) == 0 {
1886 continue
1887 }
1888 astDump := iskra.GenAST(decl)
1889 if len(astDump) == 0 {
1890 continue
1891 }
1892 irBlock := findIRBlock(irBlocks, u.Name, pkg)
1893 if len(irBlock) == 0 {
1894 continue
1895 }
1896 irBlock = stripAttrGroupRefs(irBlock)
1897 irBlock = iskra.StripDebugMetadata(irBlock)
1898 funcName := pkg | "." | u.Name
1899 astIdx := iskra.InsertSegment(t, iskra.StageAST, iskra.KindFunc, funcName, astDump)
1900 irIdx := iskra.InsertSegment(t, iskra.StageIR, iskra.KindFunc, funcName, irBlock)
1901 t.AddAdj(astIdx, irIdx)
1902 t.AddAdj(irIdx, astIdx)
1903 t.AddAdj(irIdx, modMetaIdx)
1904 t.AddAdj(modMetaIdx, irIdx)
1905 fmt.Fprintln(os.Stderr, "EXPANDED: " | funcName)
1906 added++
1907 }
1908 if added > 0 {
1909 t.FinalizeAdj()
1910 if err := iskra.MeshSaveFile(meshPath, t, iskra.StageAST, iskra.StageIR); err != nil {
1911 fmt.Fprintln(os.Stderr, "expand: saving mesh: " | err.Error())
1912 } else {
1913 fmt.Fprintln(os.Stderr, "expand: added " | fmt.Sprint(added) | " entries, mesh saved")
1914 }
1915 }
1916 return added
1917 }
1918
1919 // compilePkgToObj compiles a single package to a native object file.
1920 // Returns (objPath, irBytes). Uses the cache when valid; populates cache on miss.
1921 func compilePkgToObj(t *iskra.Tree, pkg pkgSpec, appIR []byte, meshPath string, target string, optLevel string, tmpDir string, home string) (string, []byte) {
1922 cacheObj := pkgCacheObj(home, pkg.srcDir)
1923 cacheIRFile := pkgCacheIR(home, pkg.srcDir)
1924
1925 if isCacheValid(cacheObj, pkg.srcDir) {
1926 irBytes, err := os.ReadFile(cacheIRFile)
1927 if err == nil {
1928 fmt.Fprintln(os.Stderr, "pkg " | pkg.importPath | ": cached")
1929 return cacheObj, irBytes
1930 }
1931 }
1932
1933 srcFiles, err := iskra.DiscoverMxFiles(pkg.srcDir)
1934 if err != nil || len(srcFiles) == 0 {
1935 return "", nil
1936 }
1937 modPath := iskra.ReadModulePath(pkg.srcDir)
1938 result := iskra.CompileFiles(t, srcFiles, "", pkg.pkgName, true, modPath)
1939 fmt.Fprintln(os.Stderr, "pkg " | pkg.importPath | ": matched=" | fmt.Sprint(result.Matched) | " unmatched=" | fmt.Sprint(result.Unmatched))
1940
1941 if result.Unmatched > 0 && len(appIR) > 0 {
1942 expandLatticeFromIR(t, meshPath, result.UnmatchedList, appIR, pkg.pkgName, modPath, target)
1943 result = iskra.CompileFiles(t, srcFiles, "", pkg.pkgName, true, modPath)
1944 if result.Unmatched > 0 {
1945 fmt.Fprintln(os.Stderr, "pkg " | pkg.importPath | ": " | fmt.Sprint(result.Unmatched) | " unmatched after expansion (unreachable, skipped)")
1946 }
1947 }
1948
1949 if len(result.IR) == 0 {
1950 return "", nil
1951 }
1952
1953 base := pkgFileSafe(pkg.importPath)
1954 llFile := filepath.Join(tmpDir, base | ".ll")
1955 bcFile := filepath.Join(tmpDir, base | ".bc")
1956 objFile := filepath.Join(tmpDir, base | ".o")
1957
1958 if err := os.WriteFile(llFile, result.IR, 0644); err != nil {
1959 return "", nil
1960 }
1961 if rc := runTool("opt", "-O" | optLevel, llFile, "-o", bcFile); rc != 0 {
1962 fmt.Fprintln(os.Stderr, "pkg " | pkg.importPath | ": opt failed")
1963 return "", nil
1964 }
1965 llcArgs := llcFlags(target, optLevel, bcFile, objFile)
1966 if rc := runToolArgv(llcArgs); rc != 0 {
1967 fmt.Fprintln(os.Stderr, "pkg " | pkg.importPath | ": llc failed")
1968 return "", nil
1969 }
1970
1971 // populate cache
1972 cacheDir := pkgCacheDir(home, pkg.srcDir)
1973 if err := os.MkdirAll(cacheDir, 0755); err == nil {
1974 objData, err := os.ReadFile(objFile)
1975 if err == nil {
1976 os.WriteFile(cacheObj, objData, 0644)
1977 os.WriteFile(cacheIRFile, result.IR, 0644)
1978 }
1979 }
1980
1981 return objFile, result.IR
1982 }
1983
1984 // linkMultiObj builds the linker command for multiple per-package object files.
1985 func linkMultiObj(target string, objs []string, outPath string, home string) []string {
1986 if target == "wasm32" {
1987 args := []string{"wasm-ld"}
1988 args = append(args, objs...)
1989 runtimeObj := filepath.Join(home, ".local", "share", "moxie-iskra", "runtime.wasm32.o")
1990 if _, err := os.Stat(runtimeObj); err == nil {
1991 args = append(args, runtimeObj)
1992 fmt.Fprintln(os.Stderr, "linking with runtime: " | runtimeObj)
1993 }
1994 args = append(args, "--allow-undefined", "--gc-sections", "--no-entry",
1995 "--export=__alloc", "--export=_start", "--export=memory", "-o", outPath)
1996 return args
1997 }
1998 args := []string{"clang"}
1999 args = append(args, objs...)
2000 runtimeObj := filepath.Join(home, ".local", "share", "moxie-iskra", "runtime.o")
2001 if _, err := os.Stat(runtimeObj); err == nil {
2002 args = append(args, runtimeObj)
2003 fmt.Fprintln(os.Stderr, "linking with runtime: " | runtimeObj)
2004 }
2005 bdwgcObj := filepath.Join(home, ".local", "share", "moxie-iskra", "bdwgc.o")
2006 if _, err := os.Stat(bdwgcObj); err == nil {
2007 args = append(args, bdwgcObj)
2008 }
2009 asmStubsObj := filepath.Join(home, ".local", "share", "moxie-iskra", "asm_stubs.o")
2010 if _, err := os.Stat(asmStubsObj); err == nil {
2011 args = append(args, asmStubsObj)
2012 }
2013 args = append(args, "-no-pie", "-o", outPath, "-lm", "-Wl,--gc-sections")
2014 return args
2015 }
2016
2017 func cmdBuildPerPkg(t *iskra.Tree, meshPath string, outPath string, optLevel string, targetDir string, target string, moxieRoot string, srcFiles []string, pkgName string, modPath string) {
2018 fmt.Fprintln(os.Stderr, "per-package mode (moxie root: " | moxieRoot | ")")
2019
2020 tmpDir, err := os.MkdirTemp("", "moxie-iskra-")
2021 if err != nil {
2022 fatal("creating temp dir: " | err.Error())
2023 }
2024 home, _ := os.UserHomeDir()
2025
2026 // collect direct imports from app source
2027 seen := map[string]bool{}
2028 var directImports []string
2029 for _, f := range srcFiles {
2030 data, _ := os.ReadFile(f)
2031 for _, imp := range parseFileImports(data) {
2032 if !seen[imp] {
2033 seen[imp] = true
2034 directImports = append(directImports, imp)
2035 }
2036 }
2037 }
2038
2039 deps := transitiveDeps(directImports, moxieRoot)
2040 fmt.Fprintln(os.Stderr, "stdlib deps: " | fmt.Sprint(len(deps)) | " packages")
2041
2042 // fetch app IR once, lazily on first unmatched
2043 var appIR []byte
2044 appIRFetched := false
2045 getAppIR := func() []byte {
2046 if !appIRFetched {
2047 appIRFetched = true
2048 appIR = fetchAppIR(targetDir, target)
2049 if len(appIR) > 0 {
2050 fmt.Fprintln(os.Stderr, "app IR: " | fmt.Sprint(len(appIR)) | " bytes")
2051 }
2052 }
2053 return appIR
2054 }
2055
2056 var allObjFiles []string
2057 var allIR []byte
2058
2059 for _, dep := range deps {
2060 // trigger app IR fetch only if we'll need it (check first)
2061 srcFiles2, _ := iskra.DiscoverMxFiles(dep.srcDir)
2062 cacheObj := pkgCacheObj(home, dep.srcDir)
2063 if !isCacheValid(cacheObj, dep.srcDir) && len(srcFiles2) > 0 {
2064 getAppIR()
2065 }
2066 objFile, ir := compilePkgToObj(t, dep, appIR, meshPath, target, optLevel, tmpDir, home)
2067 if objFile != "" {
2068 allObjFiles = append(allObjFiles, objFile)
2069 allIR = append(allIR, ir...)
2070 }
2071 }
2072
2073 // compile main package
2074 fmt.Fprintln(os.Stderr, "compiling " | fmt.Sprint(len(srcFiles)) | " files from " | targetDir | " (package " | pkgName | ")")
2075 if modPath != "" {
2076 fmt.Fprintln(os.Stderr, "module: " | modPath)
2077 }
2078 result := iskra.CompileFiles(t, srcFiles, "", pkgName, true, modPath)
2079 fmt.Fprintln(os.Stderr, "matched: " | fmt.Sprint(result.Matched))
2080 fmt.Fprintln(os.Stderr, "unmatched: " | fmt.Sprint(result.Unmatched))
2081
2082 if result.Unmatched > 0 {
2083 for _, u := range result.UnmatchedList {
2084 fmt.Fprintln(os.Stderr, "UNMATCHED: " | u.Name | " from " | u.SrcFile)
2085 }
2086 ir := getAppIR()
2087 if len(ir) > 0 {
2088 expandLatticeFromIR(t, meshPath, result.UnmatchedList, ir, pkgName, modPath, target)
2089 } else {
2090 expanded := expandLattice(t, meshPath, result.UnmatchedList, targetDir, pkgName, modPath, target)
2091 if !expanded {
2092 fatal("lattice expansion failed - moxie not on PATH or -internal-printir failed")
2093 }
2094 }
2095 result = iskra.CompileFiles(t, srcFiles, "", pkgName, true, modPath)
2096 fmt.Fprintln(os.Stderr, "after expansion: matched=" | fmt.Sprint(result.Matched) | " unmatched=" | fmt.Sprint(result.Unmatched))
2097 if result.Unmatched > 0 {
2098 for _, u := range result.UnmatchedList {
2099 fmt.Fprintln(os.Stderr, "STILL UNMATCHED: " | u.Name)
2100 }
2101 fatal("lattice expansion did not resolve all functions - algorithm bug")
2102 }
2103 }
2104
2105 if len(result.IR) == 0 {
2106 fatal("no functions compiled")
2107 }
2108
2109 mainLL := filepath.Join(tmpDir, "main.ll")
2110 mainBC := filepath.Join(tmpDir, "main.bc")
2111 mainObj := filepath.Join(tmpDir, "main.o")
2112
2113 if err := os.WriteFile(mainLL, result.IR, 0644); err != nil {
2114 fatal("writing main IR: " | err.Error())
2115 }
2116 fmt.Fprintln(os.Stderr, "wrote main IR: " | mainLL | " (" | fmt.Sprint(len(result.IR)) | " bytes)")
2117
2118 if rc := runTool("opt", "-O" | optLevel, mainLL, "-o", mainBC); rc != 0 {
2119 fatal("opt failed for main")
2120 }
2121 llcArgs := llcFlags(target, optLevel, mainBC, mainObj)
2122 if rc := runToolArgv(llcArgs); rc != 0 {
2123 fatal("llc failed for main")
2124 }
2125 allObjFiles = append(allObjFiles, mainObj)
2126 allIR = append(allIR, result.IR...)
2127
2128 intrinsicsLL := generateIntrinsics(allIR, target)
2129 intrinsicsFile := filepath.Join(tmpDir, "intrinsics.ll")
2130 intrinsicsObj := filepath.Join(tmpDir, "intrinsics.o")
2131 if err := os.WriteFile(intrinsicsFile, intrinsicsLL, 0644); err != nil {
2132 fatal("writing intrinsics: " | err.Error())
2133 }
2134 intrinsicClangArgs := intrinsicCompileFlags(target, intrinsicsFile, intrinsicsObj)
2135 if rc := runToolArgv(intrinsicClangArgs); rc != 0 {
2136 fatal("compiling intrinsics failed")
2137 }
2138 allObjFiles = append(allObjFiles, intrinsicsObj)
2139
2140 linkArgs := linkMultiObj(target, allObjFiles, outPath, home)
2141 if rc := runToolArgv(linkArgs); rc != 0 {
2142 fatal("link failed")
2143 }
2144 fmt.Fprintln(os.Stderr, "built: " | outPath)
2145 }
2146