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