repair.go raw

   1  package emit
   2  
   3  import (
   4  	"context"
   5  	"fmt"
   6  	"os"
   7  	"os/exec"
   8  	"path/filepath"
   9  	"regexp"
  10  	"sort"
  11  	"strings"
  12  	"sync"
  13  	"time"
  14  	"unicode"
  15  )
  16  
  17  // compileMu serializes CompileAndRepair calls. Each go build invocation
  18  // uses ~200MB+ per compiler process; running multiple concurrently from
  19  // colony instances blows past 16GB RAM. One at a time.
  20  var compileMu sync.Mutex
  21  
  22  // CompileAndRepair attempts to compile source, and if compilation fails,
  23  // iteratively fixes errors by adding type/var stubs and removing unused
  24  // imports. Returns the repaired source or error if it cannot be fixed
  25  // within maxPasses attempts.
  26  func CompileAndRepair(source, goRoot string, maxPasses int) (string, error) {
  27  	compileMu.Lock()
  28  	defer compileMu.Unlock()
  29  
  30  	// Hard deadline for the entire repair process.
  31  	deadline := time.Now().Add(2 * time.Minute)
  32  	originalSize := len(source)
  33  
  34  	if maxPasses < 8 {
  35  		maxPasses = 8
  36  	}
  37  
  38  	// Create a persistent workspace for all compile attempts in this repair
  39  	// session. Reusing the same directory avoids re-running go mod tidy and
  40  	// lets the Go build cache warm up across iterations.
  41  	workDir, err := os.MkdirTemp("", "repair-*")
  42  	if err != nil {
  43  		return source, err
  44  	}
  45  	defer os.RemoveAll(workDir)
  46  
  47  	// Write go.mod once — it doesn't change between iterations.
  48  	modContent := buildRepairGoMod(source)
  49  	os.WriteFile(filepath.Join(workDir, "go.mod"), []byte(modContent), 0o644)
  50  	if strings.Contains(source, selfModule) {
  51  		modRoot := findRepairModuleRoot()
  52  		if modRoot != "" {
  53  			if data, err := os.ReadFile(filepath.Join(modRoot, "go.sum")); err == nil {
  54  				os.WriteFile(filepath.Join(workDir, "go.sum"), data, 0o644)
  55  			}
  56  		}
  57  	}
  58  
  59  	goBin := filepath.Join(goRoot, "bin", "go")
  60  	env := repairCleanEnv(goRoot)
  61  
  62  	// Write initial source and run go mod tidy once. Go 1.24+ requires
  63  	// the go.sum to be current before building. We write the source first
  64  	// so tidy can see which imports are actually used.
  65  	os.WriteFile(filepath.Join(workDir, "main.go"), []byte(source), 0o644)
  66  	tidyCtx, tidyCancel := context.WithTimeout(context.Background(), 30*time.Second)
  67  	tidy := exec.CommandContext(tidyCtx, goBin, "mod", "tidy")
  68  	tidy.Dir = workDir
  69  	tidy.Env = env
  70  	tidy.CombinedOutput()
  71  	tidyCancel()
  72  
  73  	// tryCompileLocal is the fast path: only rewrites main.go and builds.
  74  	// Uses -p 1 to limit build parallelism (each compiler process uses
  75  	// ~200MB; with 16 cores that's 3.2GB just for the compiler).
  76  	// Each compile gets a 60-second timeout to prevent hanging.
  77  	tryCompileLocal := func(src string) (string, error) {
  78  		os.WriteFile(filepath.Join(workDir, "main.go"), []byte(src), 0o644)
  79  		ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
  80  		defer cancel()
  81  		cmd := exec.CommandContext(ctx, goBin, "build", "-p", "1", "-gcflags=-e", "-o", filepath.Join(workDir, "offspring"), ".")
  82  		cmd.Dir = workDir
  83  		cmd.Env = env
  84  		out, err := cmd.CombinedOutput()
  85  		return string(out), err
  86  	}
  87  
  88  	// Pre-scan: find names used as method receivers so we stub them as
  89  	// struct types instead of vars.
  90  	receiverTypes := findReceiverTypes(source)
  91  
  92  	// Phase A: Iterative stub + import/redecl fixes.
  93  	// Run until no more stubbable errors or maxPasses reached.
  94  	for pass := range maxPasses * 2 {
  95  		if time.Now().After(deadline) || len(source) > originalSize*3 {
  96  			break
  97  		}
  98  		compileOutput, compileErr := tryCompileLocal(source)
  99  		if compileErr == nil {
 100  			return source, nil
 101  		}
 102  
 103  		fixes := parseCompileErrors(compileOutput)
 104  		promoteReceiverFixes(fixes, receiverTypes)
 105  
 106  		if len(fixes) == 0 {
 107  			break // no more stubbable errors — move to removal
 108  		}
 109  
 110  		prev := source
 111  		source = applyFixes(source, fixes)
 112  		if source == prev {
 113  			break // no progress from stubs
 114  		}
 115  		_ = pass
 116  	}
 117  
 118  	// Phase B: Incremental repair — neutralize before removing.
 119  	// Strategy: (1) try to neutralize broken lines by replacing RHS with
 120  	// zero values, preserving variable declarations; (2) if neutralization
 121  	// makes no progress, remove error lines; (3) as last resort, replace
 122  	// entire broken functions with signature-preserving stubs.
 123  	for pass := range maxPasses * 4 {
 124  		if time.Now().After(deadline) || len(source) > originalSize*3 {
 125  			break
 126  		}
 127  		_ = pass
 128  		compileOutput, compileErr := tryCompileLocal(source)
 129  		if compileErr == nil {
 130  			return source, nil
 131  		}
 132  
 133  		prev := source
 134  
 135  		// Step 1: Try to neutralize error lines — replace broken RHS
 136  		// with zero values while keeping the variable declaration.
 137  		source = neutralizeErrorLines(source, compileOutput)
 138  
 139  		// Step 2: If neutralization didn't help, remove error lines.
 140  		if source == prev {
 141  			source = removeErrorLines(source, compileOutput)
 142  		}
 143  
 144  		// Step 3: If line removal didn't help, replace broken functions
 145  		// with signature-preserving stubs.
 146  		if source == prev {
 147  			source = removeErrorFunctions(source, compileOutput)
 148  		}
 149  
 150  		if source == prev {
 151  			break // no progress
 152  		}
 153  
 154  		// After each change, run stub/import fixes until stable.
 155  		// Cap at 3 rounds and abort if source grows beyond 3x original.
 156  		for innerPass := range 3 {
 157  			if len(source) > originalSize*3 {
 158  				break
 159  			}
 160  			compileOutput, compileErr = tryCompileLocal(source)
 161  			if compileErr == nil {
 162  				return source, nil
 163  			}
 164  			fixes := parseCompileErrors(compileOutput)
 165  			promoteReceiverFixes(fixes, receiverTypes)
 166  			if len(fixes) == 0 {
 167  				break
 168  			}
 169  			prevInner := source
 170  			source = applyFixes(source, fixes)
 171  			if source == prevInner {
 172  				break
 173  			}
 174  			_ = innerPass
 175  		}
 176  	}
 177  
 178  	// Ensure main() exists — it may have been emptied or removed.
 179  	source = ensureMain(source)
 180  
 181  	// Remove broken top-level declarations (unclosed braces, etc.)
 182  	source = removeIncompleteDecls(source)
 183  
 184  	// Final cleanup: remove any remaining orphaned blocks.
 185  	source = cleanOrphanedBlocks(source)
 186  
 187  	// One last compile attempt.
 188  	_, finalErr := tryCompileLocal(source)
 189  	if finalErr == nil {
 190  		return source, nil
 191  	}
 192  	return source, fmt.Errorf("still has errors after %d passes", maxPasses)
 193  }
 194  
 195  
 196  const selfModule = "git.mleku.dev/mleku/dendrite"
 197  
 198  // buildRepairGoMod generates a minimal go.mod for repair compilation.
 199  func buildRepairGoMod(source string) string {
 200  	var b strings.Builder
 201  	b.WriteString("module offspring\n\ngo 1.24\n")
 202  
 203  	if !strings.Contains(source, selfModule) {
 204  		return b.String()
 205  	}
 206  
 207  	modRoot := findRepairModuleRoot()
 208  	if modRoot == "" {
 209  		return b.String()
 210  	}
 211  
 212  	// Read parent go.mod for require blocks.
 213  	parentMod, err := os.ReadFile(filepath.Join(modRoot, "go.mod"))
 214  	if err != nil {
 215  		return b.String()
 216  	}
 217  
 218  	// Extract require blocks.
 219  	lines := strings.Split(string(parentMod), "\n")
 220  	inBlock := false
 221  	for _, line := range lines {
 222  		trimmed := strings.TrimSpace(line)
 223  		if trimmed == "require (" {
 224  			inBlock = true
 225  			b.WriteString(line + "\n")
 226  			continue
 227  		}
 228  		if inBlock {
 229  			b.WriteString(line + "\n")
 230  			if trimmed == ")" {
 231  				inBlock = false
 232  			}
 233  			continue
 234  		}
 235  		if strings.HasPrefix(trimmed, "require ") && !strings.HasPrefix(trimmed, "require (") {
 236  			b.WriteString(line + "\n")
 237  		}
 238  	}
 239  
 240  	b.WriteString("\nrequire " + selfModule + " v0.0.0\n")
 241  	b.WriteString("\nreplace " + selfModule + " => " + modRoot + "\n")
 242  
 243  	return b.String()
 244  }
 245  
 246  // findRepairModuleRoot walks up from cwd looking for go.mod declaring selfModule.
 247  func findRepairModuleRoot() string {
 248  	dir, err := os.Getwd()
 249  	if err != nil {
 250  		return ""
 251  	}
 252  	for {
 253  		data, err := os.ReadFile(filepath.Join(dir, "go.mod"))
 254  		if err == nil {
 255  			for _, line := range strings.Split(string(data), "\n") {
 256  				line = strings.TrimSpace(line)
 257  				if strings.HasPrefix(line, "module ") {
 258  					mod := strings.TrimSpace(strings.TrimPrefix(line, "module"))
 259  					if mod == selfModule {
 260  						return dir
 261  					}
 262  				}
 263  			}
 264  		}
 265  		parent := filepath.Dir(dir)
 266  		if parent == dir {
 267  			break
 268  		}
 269  		dir = parent
 270  	}
 271  	return ""
 272  }
 273  
 274  // repairCleanEnv returns a clean Go environment.
 275  func repairCleanEnv(root string) []string {
 276  	env := os.Environ()
 277  	clean := make([]string, 0, len(env)+3)
 278  	for _, e := range env {
 279  		if strings.HasPrefix(e, "GOROOT=") ||
 280  			strings.HasPrefix(e, "GOTOOLCHAIN=") ||
 281  			strings.HasPrefix(e, "PATH=") {
 282  			continue
 283  		}
 284  		clean = append(clean, e)
 285  	}
 286  	clean = append(clean,
 287  		"GOROOT="+root,
 288  		"GOTOOLCHAIN=local",
 289  		"GOMAXPROCS=1",
 290  		"PATH="+filepath.Join(root, "bin")+":"+os.Getenv("PATH"),
 291  	)
 292  	return clean
 293  }
 294  
 295  // reReceiverType matches method declarations to extract receiver type names.
 296  // Matches: func (x TypeName), func (x *TypeName)
 297  var reReceiverType = regexp.MustCompile(`func\s+\(\s*\w+\s+\*?(\w+)\)`)
 298  
 299  // findReceiverTypes scans source for method declarations and returns a set
 300  // of type names used as receivers.
 301  func findReceiverTypes(source string) map[string]bool {
 302  	types := make(map[string]bool)
 303  	for _, line := range strings.Split(source, "\n") {
 304  		if m := reReceiverType.FindStringSubmatch(line); m != nil {
 305  			types[m[1]] = true
 306  		}
 307  	}
 308  	return types
 309  }
 310  
 311  // promoteReceiverFixes upgrades "undefined_var" fixes to "undefined_type"
 312  // if the name is used as a method receiver.
 313  func promoteReceiverFixes(fixes []fix, receiverTypes map[string]bool) {
 314  	for i := range fixes {
 315  		if fixes[i].kind == "undefined_var" && receiverTypes[fixes[i].name] {
 316  			fixes[i].kind = "undefined_type"
 317  		}
 318  	}
 319  }
 320  
 321  // ensureMain adds an empty func main() if one is not present.
 322  func ensureMain(source string) string {
 323  	for _, line := range strings.Split(source, "\n") {
 324  		trimmed := strings.TrimSpace(line)
 325  		if strings.HasPrefix(trimmed, "func main()") {
 326  			return source
 327  		}
 328  	}
 329  	return source + "\nfunc main() {}\n"
 330  }
 331  
 332  // fix represents a single repair action.
 333  type fix struct {
 334  	kind string // "undefined_type", "undefined_var", "unused_import", "redeclared", "missing_return"
 335  	name string // the identifier, import path, or line number (for missing_return)
 336  }
 337  
 338  // Patterns for parsing Go compiler errors.
 339  var (
 340  	reUndefined      = regexp.MustCompile(`undefined:\s+([\w.]+)`)
 341  	reUnusedImport   = regexp.MustCompile(`"([^"]+)" imported and not used`)
 342  	reRedeclared     = regexp.MustCompile(`(\w+) redeclared in this block`)
 343  	reMissingReturn  = regexp.MustCompile(`^(.+):(\d+):\d+: missing return`)
 344  	reNotImplement   = regexp.MustCompile(`does not implement (\w+)`)
 345  	reErrorAtLine    = regexp.MustCompile(`\./main\.go:(\d+):\d+:`)
 346  	reAssignMismatch = regexp.MustCompile(`assignment mismatch: (\d+) variables but .* returns (\d+) values?`)
 347  	reMultiValue     = regexp.MustCompile(`multiple-value (\w+)\(`)
 348  	reDeclNotUsed    = regexp.MustCompile(`declared and not used: (\w+)`)
 349  )
 350  
 351  // parseCompileErrors extracts fixable errors from compiler output.
 352  func parseCompileErrors(output string) []fix {
 353  	seen := make(map[string]bool)
 354  	var fixes []fix
 355  
 356  	for _, line := range strings.Split(output, "\n") {
 357  		if m := reUndefined.FindStringSubmatch(line); m != nil {
 358  			name := m[1]
 359  			key := "undef:" + name
 360  			if !seen[key] {
 361  				seen[key] = true
 362  				if strings.Contains(name, ".") {
 363  					// Qualified reference (e.g., hash.Len) — can't stub,
 364  					// will be handled by removeErrorFunctions.
 365  				} else if isTypeName(name) {
 366  					fixes = append(fixes, fix{kind: "undefined_type", name: name})
 367  				} else {
 368  					fixes = append(fixes, fix{kind: "undefined_var", name: name})
 369  				}
 370  			}
 371  		}
 372  		if m := reUnusedImport.FindStringSubmatch(line); m != nil {
 373  			path := m[1]
 374  			key := "unused:" + path
 375  			if !seen[key] {
 376  				seen[key] = true
 377  				fixes = append(fixes, fix{kind: "unused_import", name: path})
 378  			}
 379  		}
 380  		if m := reRedeclared.FindStringSubmatch(line); m != nil {
 381  			name := m[1]
 382  			key := "redecl:" + name
 383  			if !seen[key] {
 384  				seen[key] = true
 385  				fixes = append(fixes, fix{kind: "redeclared", name: name})
 386  			}
 387  		}
 388  		// "missing return" at a line — remove the enclosing function.
 389  		if reMissingReturn.MatchString(line) {
 390  			// Extract line number and mark for function removal.
 391  			if m := reMissingReturn.FindStringSubmatch(line); m != nil {
 392  				key := "misret:" + m[2]
 393  				if !seen[key] {
 394  					seen[key] = true
 395  					fixes = append(fixes, fix{kind: "missing_return", name: m[2]})
 396  				}
 397  			}
 398  		}
 399  		// "does not implement X" — the struct is used as an interface
 400  		// it doesn't satisfy. Remove the offending statement.
 401  		if reNotImplement.MatchString(line) {
 402  			// Not easily fixable — skip for now, will be handled by
 403  			// removal of the enclosing function if it also has other errors.
 404  		}
 405  
 406  		// "X declared and not used" — suppress by adding "_ = X".
 407  		if m := reDeclNotUsed.FindStringSubmatch(line); m != nil {
 408  			name := m[1]
 409  			key := "declnotused:" + name
 410  			if !seen[key] {
 411  				seen[key] = true
 412  				fixes = append(fixes, fix{kind: "decl_not_used", name: name})
 413  			}
 414  		}
 415  
 416  		// "assignment mismatch: N variables but F returns M values" or
 417  		// "multiple-value X() used in single-value context" — the func
 418  		// stub has wrong return arity. Fix by regenerating the stub.
 419  		if m := reMultiValue.FindStringSubmatch(line); m != nil {
 420  			name := m[1]
 421  			key := "multiret:" + name
 422  			if !seen[key] {
 423  				seen[key] = true
 424  				fixes = append(fixes, fix{kind: "fix_return_arity", name: name})
 425  			}
 426  		}
 427  		if m := reAssignMismatch.FindStringSubmatch(line); m != nil {
 428  			// The error is on this line — we need to find the function name
 429  			// being called. Extract from the error line reference.
 430  			if lineM := reErrorAtLine.FindStringSubmatch(line); lineM != nil {
 431  				key := "assignmis:" + lineM[1]
 432  				if !seen[key] {
 433  					seen[key] = true
 434  					fixes = append(fixes, fix{kind: "fix_assign_mismatch", name: lineM[1]})
 435  				}
 436  			}
 437  		}
 438  	}
 439  
 440  	return fixes
 441  }
 442  
 443  // isTypeName heuristically determines if an identifier is likely a type name
 444  // (starts with uppercase letter).
 445  func isTypeName(name string) bool {
 446  	if len(name) == 0 {
 447  		return false
 448  	}
 449  	return unicode.IsUpper(rune(name[0]))
 450  }
 451  
 452  // applyFixes modifies the source to fix the given errors.
 453  func applyFixes(source string, fixes []fix) string {
 454  	for _, f := range fixes {
 455  		switch f.kind {
 456  		case "undefined_type":
 457  			source = addTypeStub(source, f.name)
 458  		case "undefined_var":
 459  			source = addVarStub(source, f.name)
 460  		case "unused_import":
 461  			source = removeImport(source, f.name)
 462  		case "redeclared":
 463  			source = removeRedeclaration(source, f.name)
 464  		case "missing_return":
 465  			source = removeFuncAtLine(source, f.name)
 466  		case "fix_return_arity":
 467  			source = fixReturnArity(source, f.name)
 468  		case "fix_assign_mismatch":
 469  			source = fixAssignMismatchAtLine(source, f.name)
 470  		case "decl_not_used":
 471  			source = suppressUnusedVar(source, f.name)
 472  		}
 473  	}
 474  	return source
 475  }
 476  
 477  // fixReturnArity fixes a func stub whose return arity doesn't match call sites.
 478  // Removes the existing stub and re-adds it with correct arity detection.
 479  func fixReturnArity(source, name string) string {
 480  	// Remove the existing func stub declaration.
 481  	lines := strings.Split(source, "\n")
 482  	var cleaned []string
 483  	for _, line := range lines {
 484  		trimmed := strings.TrimSpace(line)
 485  		if strings.HasPrefix(trimmed, "func "+name+"(args ...interface{})") {
 486  			continue // remove old stub
 487  		}
 488  		cleaned = append(cleaned, line)
 489  	}
 490  	source = strings.Join(cleaned, "\n")
 491  
 492  	// Re-add with correct arity.
 493  	return addVarStub(source, name)
 494  }
 495  
 496  // fixAssignMismatchAtLine handles "assignment mismatch" errors by finding
 497  // the function call on the error line and adjusting its stub.
 498  func fixAssignMismatchAtLine(source, lineNumStr string) string {
 499  	lineNum := 0
 500  	for _, ch := range lineNumStr {
 501  		if ch >= '0' && ch <= '9' {
 502  			lineNum = lineNum*10 + int(ch-'0')
 503  		}
 504  	}
 505  	if lineNum == 0 {
 506  		return source
 507  	}
 508  
 509  	lines := strings.Split(source, "\n")
 510  	if lineNum > len(lines) {
 511  		return source
 512  	}
 513  
 514  	// Extract the line and find the function name being called.
 515  	line := lines[lineNum-1]
 516  	trimmed := strings.TrimSpace(line)
 517  
 518  	// Look for patterns: "a, b := funcName(" or "a, b = funcName("
 519  	assignOps := []string{":=", "="}
 520  	for _, op := range assignOps {
 521  		idx := strings.Index(trimmed, op)
 522  		if idx < 0 {
 523  			continue
 524  		}
 525  		rhs := strings.TrimSpace(trimmed[idx+len(op):])
 526  		// rhs should start with funcName(
 527  		parenIdx := strings.IndexByte(rhs, '(')
 528  		if parenIdx <= 0 {
 529  			continue
 530  		}
 531  		funcName := strings.TrimSpace(rhs[:parenIdx])
 532  		// Check if this function has a stub we can fix.
 533  		if strings.Contains(source, "func "+funcName+"(args ...interface{})") {
 534  			return fixReturnArity(source, funcName)
 535  		}
 536  	}
 537  
 538  	return source
 539  }
 540  
 541  // addTypeStub adds a type stub after the import block.
 542  // Uses struct{} so methods can be defined on the type.
 543  func addTypeStub(source, name string) string {
 544  	stub := fmt.Sprintf("type %s struct{}\n", name)
 545  
 546  	// Check if already declared (any declaration form).
 547  	if nameIsDeclared(source, name) {
 548  		return source
 549  	}
 550  
 551  	return insertAfterImports(source, stub)
 552  }
 553  
 554  // addVarStub adds a var or func stub after the import block.
 555  // If the name is used as a function call in the source, it emits a
 556  // func stub. When another function in the source calls this name and
 557  // assigns the result, the stub uses types inferred from the assignment
 558  // context (e.g., "s, err := name()" → (string, error) if the vars
 559  // are used with string/error operations). Falls back to interface{}.
 560  func addVarStub(source, name string) string {
 561  	// Check if already declared (any declaration form).
 562  	if nameIsDeclared(source, name) {
 563  		return source
 564  	}
 565  
 566  	// If this name is called as a function, emit a func stub.
 567  	// Use word-boundary matching: the char before name( must not be
 568  	// an identifier char (to avoid matching "runGeneration(" for "gen(").
 569  	if isCalledAsFunc(source, name) {
 570  		retSig := inferReturnSignature(source, name)
 571  		if retSig != "" {
 572  			zr := zeroReturn(retSig)
 573  			if zr == "" {
 574  				zr = "return"
 575  			}
 576  			stub := fmt.Sprintf("func %s(args ...interface{}) %s { %s }\n", name, retSig, zr)
 577  			return insertAfterImports(source, stub)
 578  		}
 579  		// Fallback: infer arity from assignment LHS count.
 580  		n := inferReturnArity(source, name)
 581  		if n <= 1 {
 582  			stub := fmt.Sprintf("func %s(args ...interface{}) interface{} { return nil }\n", name)
 583  			return insertAfterImports(source, stub)
 584  		}
 585  		retTypes := make([]string, n)
 586  		retVals := make([]string, n)
 587  		for i := range n {
 588  			retTypes[i] = "interface{}"
 589  			retVals[i] = "nil"
 590  		}
 591  		stub := fmt.Sprintf("func %s(args ...interface{}) (%s) { return %s }\n",
 592  			name, strings.Join(retTypes, ", "), strings.Join(retVals, ", "))
 593  		return insertAfterImports(source, stub)
 594  	}
 595  
 596  	stub := fmt.Sprintf("var %s interface{}\n", name)
 597  	return insertAfterImports(source, stub)
 598  }
 599  
 600  // findWordBoundaryCall finds "name(" in text where name starts at a word
 601  // boundary (not preceded by an identifier character). Returns the index of
 602  // name in text, or -1 if not found.
 603  func findWordBoundaryCall(text, name string) int {
 604  	callPat := name + "("
 605  	idx := 0
 606  	for {
 607  		pos := strings.Index(text[idx:], callPat)
 608  		if pos < 0 {
 609  			return -1
 610  		}
 611  		absPos := idx + pos
 612  		if absPos > 0 {
 613  			prev := text[absPos-1]
 614  			if (prev >= 'a' && prev <= 'z') || (prev >= 'A' && prev <= 'Z') ||
 615  				(prev >= '0' && prev <= '9') || prev == '_' {
 616  				idx = absPos + len(callPat)
 617  				continue
 618  			}
 619  		}
 620  		return absPos
 621  	}
 622  }
 623  
 624  // isCalledAsFunc checks if name appears as a function call "name(" in the
 625  // source, using word-boundary matching to avoid false positives like matching
 626  // "runGeneration(" when looking for "gen(".
 627  func isCalledAsFunc(source, name string) bool {
 628  	callPat := name + "("
 629  	idx := 0
 630  	for {
 631  		pos := strings.Index(source[idx:], callPat)
 632  		if pos < 0 {
 633  			return false
 634  		}
 635  		absPos := idx + pos
 636  		// Check character before the match — must be a non-identifier char.
 637  		if absPos > 0 {
 638  			prev := source[absPos-1]
 639  			if (prev >= 'a' && prev <= 'z') || (prev >= 'A' && prev <= 'Z') ||
 640  				(prev >= '0' && prev <= '9') || prev == '_' {
 641  				idx = absPos + len(callPat)
 642  				continue // false positive: part of a longer identifier
 643  			}
 644  		}
 645  		return true
 646  	}
 647  }
 648  
 649  // inferReturnSignature tries to determine the return type signature for a
 650  // function call by examining assignment context. Looks for patterns like:
 651  //
 652  //	s, err := name(...)   → checks if "err" is used with error comparisons
 653  //	bin, err := name(...) → (string, error) if bin is used in string context
 654  //	ok := name(...)       → bool if "ok" is used as a bool
 655  //
 656  // Returns a Go return type string like "string", "error", "(string, error)"
 657  // or "" if inference fails.
 658  func inferReturnSignature(source, name string) string {
 659  	// Scan for assignment call sites with word-boundary matching.
 660  	for _, line := range strings.Split(source, "\n") {
 661  		trimmed := strings.TrimSpace(line)
 662  		callIdx := findWordBoundaryCall(trimmed, name)
 663  		if callIdx < 0 {
 664  			continue
 665  		}
 666  		prefix := strings.TrimSpace(trimmed[:callIdx])
 667  		isAssign := false
 668  		if strings.HasSuffix(prefix, ":=") {
 669  			prefix = strings.TrimSuffix(prefix, ":=")
 670  			isAssign = true
 671  		} else if strings.HasSuffix(prefix, "=") && !strings.HasSuffix(prefix, "!=") && !strings.HasSuffix(prefix, "==") {
 672  			prefix = strings.TrimSuffix(prefix, "=")
 673  			isAssign = true
 674  		}
 675  		if !isAssign {
 676  			continue
 677  		}
 678  		prefix = strings.TrimSpace(prefix)
 679  		if prefix == "" {
 680  			continue
 681  		}
 682  		parts := strings.Split(prefix, ",")
 683  		var varNames []string
 684  		for _, p := range parts {
 685  			p = strings.TrimSpace(p)
 686  			if p != "" {
 687  				varNames = append(varNames, p)
 688  			}
 689  		}
 690  		if len(varNames) == 0 {
 691  			continue
 692  		}
 693  		// Infer types from variable names and usage context.
 694  		types := make([]string, len(varNames))
 695  		for i, v := range varNames {
 696  			types[i] = inferVarType(source, v)
 697  		}
 698  		if len(types) == 1 {
 699  			return types[0]
 700  		}
 701  		return "(" + strings.Join(types, ", ") + ")"
 702  	}
 703  	return ""
 704  }
 705  
 706  // inferVarType guesses the type of a variable from its name and how it's
 707  // used in the source. Common patterns:
 708  //
 709  //	err       → error
 710  //	ok        → bool
 711  //	_         → interface{}
 712  //	n, count  → int
 713  //	s, str    → string
 714  //	b, buf    → []byte
 715  func inferVarType(source, name string) string {
 716  	if name == "_" {
 717  		return "interface{}"
 718  	}
 719  	if name == "err" {
 720  		return "error"
 721  	}
 722  	if name == "ok" {
 723  		return "bool"
 724  	}
 725  	// Check usage patterns in source.
 726  	if strings.Contains(source, name+" != nil") || strings.Contains(source, name+" == nil") {
 727  		// Nil-comparable — could be error, pointer, slice, map, interface.
 728  		if strings.Contains(source, "return "+name) {
 729  			// If returned alone or last, likely error.
 730  			return "error"
 731  		}
 732  		return "interface{}"
 733  	}
 734  	if strings.Contains(source, "string("+name+")") || strings.Contains(source, name+` + "`) || strings.Contains(source, `" + `+name) {
 735  		return "string"
 736  	}
 737  	if strings.Contains(source, "len("+name+")") {
 738  		return "string" // could be []byte too, string is safer
 739  	}
 740  	// Default to interface{} — most permissive.
 741  	return "interface{}"
 742  }
 743  
 744  // inferReturnArity scans source for assignment patterns that call name()
 745  // and returns the number of LHS variables. Handles:
 746  //   - a, b := name(...)         → 2
 747  //   - a, b, c = name(...)       → 3
 748  //   - a := name(...)            → 1
 749  //   - name(...)  (no assignment) → 1
 750  func inferReturnArity(source, name string) int {
 751  	maxArity := 1
 752  	for _, line := range strings.Split(source, "\n") {
 753  		trimmed := strings.TrimSpace(line)
 754  
 755  		// Find call sites with word-boundary matching.
 756  		callIdx := findWordBoundaryCall(trimmed, name)
 757  		if callIdx < 0 {
 758  			continue
 759  		}
 760  
 761  		// Check if this is part of an assignment (not just a standalone call).
 762  		// Look for := or = before the call.
 763  		prefix := trimmed[:callIdx]
 764  		prefix = strings.TrimSpace(prefix)
 765  
 766  		// Strip trailing := or =
 767  		isAssign := false
 768  		if strings.HasSuffix(prefix, ":=") {
 769  			prefix = strings.TrimSuffix(prefix, ":=")
 770  			isAssign = true
 771  		} else if strings.HasSuffix(prefix, "=") && !strings.HasSuffix(prefix, "!=") && !strings.HasSuffix(prefix, "==") {
 772  			prefix = strings.TrimSuffix(prefix, "=")
 773  			isAssign = true
 774  		}
 775  
 776  		if !isAssign {
 777  			continue
 778  		}
 779  
 780  		// Count comma-separated LHS variables.
 781  		prefix = strings.TrimSpace(prefix)
 782  		if prefix == "" {
 783  			continue
 784  		}
 785  		parts := strings.Split(prefix, ",")
 786  		arity := 0
 787  		for _, p := range parts {
 788  			p = strings.TrimSpace(p)
 789  			if p != "" && p != "_" || p == "_" {
 790  				arity++
 791  			}
 792  		}
 793  		if arity > maxArity {
 794  			maxArity = arity
 795  		}
 796  	}
 797  	return maxArity
 798  }
 799  
 800  // nameIsDeclared checks if a name is already declared in the source
 801  // as a type, var, const, or function.
 802  func nameIsDeclared(source, name string) bool {
 803  	for _, line := range strings.Split(source, "\n") {
 804  		trimmed := strings.TrimSpace(line)
 805  		if strings.HasPrefix(trimmed, "type "+name+" ") ||
 806  			strings.HasPrefix(trimmed, "type "+name+"=") ||
 807  			strings.HasPrefix(trimmed, "var "+name+" ") ||
 808  			strings.HasPrefix(trimmed, "const "+name+" ") ||
 809  			strings.HasPrefix(trimmed, "func "+name+"(") ||
 810  			strings.HasPrefix(trimmed, name+" :=") {
 811  			return true
 812  		}
 813  	}
 814  	return false
 815  }
 816  
 817  // insertAfterImports inserts text after the import block (or after package line).
 818  func insertAfterImports(source, text string) string {
 819  	lines := strings.Split(source, "\n")
 820  	insertIdx := -1
 821  
 822  	// Find end of import block.
 823  	inImport := false
 824  	for i, line := range lines {
 825  		trimmed := strings.TrimSpace(line)
 826  		if trimmed == "import (" {
 827  			inImport = true
 828  		}
 829  		if inImport && trimmed == ")" {
 830  			insertIdx = i + 1
 831  			break
 832  		}
 833  		// Single-line import.
 834  		if strings.HasPrefix(trimmed, "import ") && !strings.Contains(trimmed, "(") {
 835  			insertIdx = i + 1
 836  		}
 837  	}
 838  
 839  	// Fallback: after package line.
 840  	if insertIdx < 0 {
 841  		for i, line := range lines {
 842  			if strings.HasPrefix(strings.TrimSpace(line), "package ") {
 843  				insertIdx = i + 1
 844  				break
 845  			}
 846  		}
 847  	}
 848  	if insertIdx < 0 {
 849  		insertIdx = 0
 850  	}
 851  
 852  	// Insert.
 853  	result := make([]string, 0, len(lines)+2)
 854  	result = append(result, lines[:insertIdx]...)
 855  	result = append(result, text)
 856  	result = append(result, lines[insertIdx:]...)
 857  	return strings.Join(result, "\n")
 858  }
 859  
 860  // removeImport removes an import path from the source.
 861  func removeImport(source, importPath string) string {
 862  	lines := strings.Split(source, "\n")
 863  	result := make([]string, 0, len(lines))
 864  
 865  	for _, line := range lines {
 866  		trimmed := strings.TrimSpace(line)
 867  		// Match: "path" or alias "path"
 868  		if strings.Contains(trimmed, `"`+importPath+`"`) {
 869  			// Skip single-line import or import block entry.
 870  			if strings.HasPrefix(trimmed, "import ") || !strings.HasPrefix(trimmed, "import") {
 871  				continue
 872  			}
 873  		}
 874  		result = append(result, line)
 875  	}
 876  
 877  	return strings.Join(result, "\n")
 878  }
 879  
 880  // removeRedeclaration removes the SECOND occurrence of a declaration.
 881  func removeRedeclaration(source, name string) string {
 882  	lines := strings.Split(source, "\n")
 883  	seen := false
 884  	result := make([]string, 0, len(lines))
 885  
 886  	patterns := []string{
 887  		"type " + name + " ",
 888  		"type " + name + "=",
 889  		"var " + name + " ",
 890  		"func " + name + "(",
 891  	}
 892  
 893  	for i := 0; i < len(lines); i++ {
 894  		trimmed := strings.TrimSpace(lines[i])
 895  		isDecl := false
 896  		for _, pat := range patterns {
 897  			if strings.HasPrefix(trimmed, pat) {
 898  				isDecl = true
 899  				break
 900  			}
 901  		}
 902  
 903  		if isDecl {
 904  			if seen {
 905  				// Skip this declaration (and its body if it has braces).
 906  				depth := strings.Count(lines[i], "{") - strings.Count(lines[i], "}")
 907  				for depth > 0 && i+1 < len(lines) {
 908  					i++
 909  					depth += strings.Count(lines[i], "{") - strings.Count(lines[i], "}")
 910  				}
 911  				continue
 912  			}
 913  			seen = true
 914  		}
 915  
 916  		result = append(result, lines[i])
 917  	}
 918  
 919  	return strings.Join(result, "\n")
 920  }
 921  
 922  // removeFuncAtLine removes the function whose closing brace is at the
 923  // given line number (as a string). Walks backward to find "func " and
 924  // forward to find the matching "}".
 925  func removeFuncAtLine(source, lineNumStr string) string {
 926  	lineNum := 0
 927  	for _, ch := range lineNumStr {
 928  		if ch >= '0' && ch <= '9' {
 929  			lineNum = lineNum*10 + int(ch-'0')
 930  		}
 931  	}
 932  	if lineNum == 0 {
 933  		return source
 934  	}
 935  
 936  	lines := strings.Split(source, "\n")
 937  	if lineNum > len(lines) {
 938  		return source
 939  	}
 940  
 941  	// Walk backward from the error line to find the function declaration.
 942  	funcStart := -1
 943  	for i := lineNum - 1; i >= 0; i-- {
 944  		trimmed := strings.TrimSpace(lines[i])
 945  		if strings.HasPrefix(trimmed, "func ") {
 946  			funcStart = i
 947  			break
 948  		}
 949  	}
 950  	if funcStart < 0 {
 951  		return source
 952  	}
 953  
 954  	// Walk forward from funcStart to find the matching closing brace.
 955  	depth := 0
 956  	funcEnd := -1
 957  	for i := funcStart; i < len(lines); i++ {
 958  		depth += strings.Count(lines[i], "{") - strings.Count(lines[i], "}")
 959  		if depth <= 0 && strings.Count(lines[i], "{") > 0 || (depth == 0 && i > funcStart) {
 960  			funcEnd = i
 961  			break
 962  		}
 963  	}
 964  	if funcEnd < 0 {
 965  		funcEnd = len(lines) - 1
 966  	}
 967  
 968  	// Remove the function.
 969  	result := make([]string, 0, len(lines)-(funcEnd-funcStart+1))
 970  	result = append(result, lines[:funcStart]...)
 971  	result = append(result, lines[funcEnd+1:]...)
 972  	return strings.Join(result, "\n")
 973  }
 974  
 975  // removeErrorFunctions parses compiler output to find which line numbers
 976  // have errors, then removes the enclosing functions for those lines.
 977  func removeErrorFunctions(source, compileOutput string) string {
 978  	// Extract error line numbers from compiler output.
 979  	errorLines := make(map[int]bool)
 980  	for _, line := range strings.Split(compileOutput, "\n") {
 981  		if m := reErrorAtLine.FindStringSubmatch(line); m != nil {
 982  			lineNum := 0
 983  			for _, ch := range m[1] {
 984  				lineNum = lineNum*10 + int(ch-'0')
 985  			}
 986  			errorLines[lineNum] = true
 987  		}
 988  	}
 989  
 990  	if len(errorLines) == 0 {
 991  		return source
 992  	}
 993  
 994  	// Find which functions contain errors and remove them.
 995  	lines := strings.Split(source, "\n")
 996  	type funcRange struct {
 997  		start, end int
 998  	}
 999  
1000  	// Build a list of all function ranges.
1001  	var funcs []funcRange
1002  	for i := 0; i < len(lines); i++ {
1003  		trimmed := strings.TrimSpace(lines[i])
1004  		if !strings.HasPrefix(trimmed, "func ") {
1005  			continue
1006  		}
1007  		start := i
1008  		depth := 0
1009  		foundBody := false
1010  		for j := i; j < len(lines); j++ {
1011  			depth += strings.Count(lines[j], "{") - strings.Count(lines[j], "}")
1012  			if strings.Contains(lines[j], "{") {
1013  				foundBody = true
1014  			}
1015  			if foundBody && depth <= 0 {
1016  				funcs = append(funcs, funcRange{start, j})
1017  				i = j // skip past this function
1018  				break
1019  			}
1020  		}
1021  	}
1022  
1023  	// Find functions that contain error lines.
1024  	removeFuncs := make(map[int]bool)
1025  	for _, fr := range funcs {
1026  		for lineNum := range errorLines {
1027  			// Line numbers are 1-based; our slice is 0-based.
1028  			if lineNum-1 >= fr.start && lineNum-1 <= fr.end {
1029  				removeFuncs[fr.start] = true
1030  				break
1031  			}
1032  		}
1033  	}
1034  
1035  	if len(removeFuncs) == 0 {
1036  		return source
1037  	}
1038  
1039  	// Rebuild source, replacing error functions with signature-preserving stubs.
1040  	// This keeps the function callable (correct name, params, return types)
1041  	// while removing the broken body.
1042  	result := make([]string, 0, len(lines))
1043  	for i := 0; i < len(lines); i++ {
1044  		// Check if this is a function we're replacing.
1045  		skip := false
1046  		for _, fr := range funcs {
1047  			if fr.start == i && removeFuncs[fr.start] {
1048  				trimmed := strings.TrimSpace(lines[i])
1049  				if trimmed == "func main() {" || strings.HasPrefix(trimmed, "func main()") {
1050  					// Replace main with empty body.
1051  					result = append(result, "func main() {}")
1052  					i = fr.end
1053  					skip = true
1054  					break
1055  				}
1056  				// Extract the full func signature line and generate a stub
1057  				// with the correct return types.
1058  				stub := stubFromSignature(trimmed)
1059  				if stub != "" {
1060  					result = append(result, stub)
1061  				}
1062  				i = fr.end // skip to end of function
1063  				skip = true
1064  				break
1065  			}
1066  		}
1067  		if !skip {
1068  			result = append(result, lines[i])
1069  		}
1070  	}
1071  
1072  	return strings.Join(result, "\n")
1073  }
1074  
1075  // neutralizeErrorLines attempts to preserve variable declarations from error
1076  // lines by replacing broken RHS expressions with zero values. For example:
1077  //
1078  //	x, err := brokenCall()  →  var x interface{}; var err error
1079  //	s := undefined + "foo"  →  var s string
1080  //
1081  // This prevents cascading "undefined" errors from variables that were declared
1082  // on the removed line. Returns unchanged source if no neutralization is possible.
1083  func neutralizeErrorLines(source, compileOutput string) string {
1084  	errorLines := make(map[int]bool)
1085  	for _, line := range strings.Split(compileOutput, "\n") {
1086  		if m := reErrorAtLine.FindStringSubmatch(line); m != nil {
1087  			lineNum := 0
1088  			for _, ch := range m[1] {
1089  				lineNum = lineNum*10 + int(ch-'0')
1090  			}
1091  			if lineNum > 0 {
1092  				errorLines[lineNum] = true
1093  			}
1094  		}
1095  	}
1096  	if len(errorLines) == 0 {
1097  		return source
1098  	}
1099  
1100  	lines := strings.Split(source, "\n")
1101  	changed := false
1102  	for lineNum := range errorLines {
1103  		idx := lineNum - 1
1104  		if idx < 0 || idx >= len(lines) {
1105  			continue
1106  		}
1107  		line := lines[idx]
1108  		trimmed := strings.TrimSpace(line)
1109  
1110  		// Only neutralize short-assign statements: "a, b := expr"
1111  		assignIdx := strings.Index(trimmed, ":=")
1112  		if assignIdx <= 0 {
1113  			continue
1114  		}
1115  
1116  		// Don't neutralize structural lines.
1117  		if strings.HasPrefix(trimmed, "func ") || strings.HasPrefix(trimmed, "if ") ||
1118  			strings.HasPrefix(trimmed, "for ") || strings.HasPrefix(trimmed, "switch ") {
1119  			continue
1120  		}
1121  
1122  		lhs := strings.TrimSpace(trimmed[:assignIdx])
1123  		parts := strings.Split(lhs, ",")
1124  		var decls []string
1125  		for _, p := range parts {
1126  			p = strings.TrimSpace(p)
1127  			if p == "" || p == "_" {
1128  				continue
1129  			}
1130  			typ := inferVarType(source, p)
1131  			decls = append(decls, fmt.Sprintf("var %s %s", p, typ))
1132  		}
1133  		if len(decls) > 0 {
1134  			// Preserve indentation.
1135  			indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
1136  			lines[idx] = indent + strings.Join(decls, "; ")
1137  			changed = true
1138  		}
1139  	}
1140  	if !changed {
1141  		return source
1142  	}
1143  	return strings.Join(lines, "\n")
1144  }
1145  
1146  // removeErrorLines removes specific lines that the compiler reports errors on,
1147  // then cleans up orphaned syntax (empty var/const/type blocks, etc.).
1148  func removeErrorLines(source, compileOutput string) string {
1149  	// Extract error line numbers.
1150  	errorLines := make(map[int]bool)
1151  	for _, line := range strings.Split(compileOutput, "\n") {
1152  		if m := reErrorAtLine.FindStringSubmatch(line); m != nil {
1153  			lineNum := 0
1154  			for _, ch := range m[1] {
1155  				lineNum = lineNum*10 + int(ch-'0')
1156  			}
1157  			if lineNum > 0 {
1158  				errorLines[lineNum] = true
1159  			}
1160  		}
1161  	}
1162  
1163  	if len(errorLines) == 0 {
1164  		return source
1165  	}
1166  
1167  	lines := strings.Split(source, "\n")
1168  
1169  	// First, find which statements the error lines belong to.
1170  	// A "statement" spans from the line with a top-level expression to its
1171  	// closing paren/brace. If an error is inside a multi-line call like
1172  	// runColony(arg1,\n arg2,\n arg3), remove the entire call.
1173  	removeStmts := findStatementsToRemove(lines, errorLines)
1174  
1175  	result := make([]string, 0, len(lines))
1176  	for i, line := range lines {
1177  		lineNum := i + 1 // 1-based
1178  		if removeStmts[lineNum] || errorLines[lineNum] {
1179  			// Don't remove structural lines (func, closing braces, package, import).
1180  			trimmed := strings.TrimSpace(line)
1181  			if trimmed == "}" || trimmed == "{" ||
1182  				strings.HasPrefix(trimmed, "func ") ||
1183  				strings.HasPrefix(trimmed, "package ") ||
1184  				strings.HasPrefix(trimmed, "import") {
1185  				result = append(result, line)
1186  				continue
1187  			}
1188  			// Skip this error line or its containing statement.
1189  			continue
1190  		}
1191  		result = append(result, line)
1192  	}
1193  
1194  	source = strings.Join(result, "\n")
1195  
1196  	// Clean up orphaned group blocks: var\n), const\n), type\n)
1197  	// Also clean up stray "var" or "const" or "type" keywords on their own line.
1198  	source = cleanOrphanedBlocks(source)
1199  
1200  	return source
1201  }
1202  
1203  // cleanOrphanedBlocks removes broken group declarations like:
1204  //
1205  //	var
1206  //	}
1207  //
1208  // or empty groups like:
1209  //
1210  //	var (
1211  //	)
1212  func cleanOrphanedBlocks(source string) string {
1213  	lines := strings.Split(source, "\n")
1214  	result := make([]string, 0, len(lines))
1215  
1216  	for i := 0; i < len(lines); i++ {
1217  		trimmed := strings.TrimSpace(lines[i])
1218  
1219  		// Check for a lone keyword followed by } or ) on the next non-empty line.
1220  		if trimmed == "var" || trimmed == "const" || trimmed == "type" {
1221  			// Look ahead for closing brace or paren.
1222  			j := i + 1
1223  			for j < len(lines) && strings.TrimSpace(lines[j]) == "" {
1224  				j++
1225  			}
1226  			if j < len(lines) {
1227  				next := strings.TrimSpace(lines[j])
1228  				if next == "}" || next == ")" {
1229  					i = j // skip both
1230  					continue
1231  				}
1232  			}
1233  		}
1234  
1235  		// Check for empty group: "var (" followed (after blanks) by ")".
1236  		if (trimmed == "var (" || trimmed == "const (" || trimmed == "type (") {
1237  			j := i + 1
1238  			for j < len(lines) && strings.TrimSpace(lines[j]) == "" {
1239  				j++
1240  			}
1241  			if j < len(lines) && strings.TrimSpace(lines[j]) == ")" {
1242  				i = j // skip the empty group
1243  				continue
1244  			}
1245  		}
1246  
1247  		// Check for a "var // comment" pattern (broken var decl).
1248  		if strings.HasPrefix(trimmed, "var //") || strings.HasPrefix(trimmed, "const //") || strings.HasPrefix(trimmed, "type //") {
1249  			continue // remove broken declaration
1250  		}
1251  
1252  		result = append(result, lines[i])
1253  	}
1254  
1255  	return strings.Join(result, "\n")
1256  }
1257  
1258  // removeIncompleteDecls scans top-level declarations and removes those
1259  // with unbalanced braces (e.g., "var X = func() int {" with stripped body).
1260  func removeIncompleteDecls(source string) string {
1261  	lines := strings.Split(source, "\n")
1262  	result := make([]string, 0, len(lines))
1263  
1264  	for i := 0; i < len(lines); i++ {
1265  		trimmed := strings.TrimSpace(lines[i])
1266  
1267  		// Detect top-level declaration lines with opening brace.
1268  		isDecl := strings.HasPrefix(trimmed, "var ") ||
1269  			strings.HasPrefix(trimmed, "const ") ||
1270  			strings.HasPrefix(trimmed, "type ") ||
1271  			strings.HasPrefix(trimmed, "func ")
1272  
1273  		if isDecl && strings.Contains(trimmed, "{") {
1274  			// Count braces for this declaration.
1275  			depth := 0
1276  			j := i
1277  			for j < len(lines) {
1278  				depth += strings.Count(lines[j], "{") - strings.Count(lines[j], "}")
1279  				if depth <= 0 {
1280  					break
1281  				}
1282  				j++
1283  			}
1284  			if j >= len(lines) && depth > 0 {
1285  				// Unclosed declaration — remove from i to end of file.
1286  				// But don't discard everything; stop at the next top-level decl.
1287  				for k := i + 1; k < len(lines); k++ {
1288  					next := strings.TrimSpace(lines[k])
1289  					if next == "" {
1290  						continue
1291  					}
1292  					nextIsDecl := strings.HasPrefix(next, "var ") ||
1293  						strings.HasPrefix(next, "const ") ||
1294  						strings.HasPrefix(next, "type ") ||
1295  						strings.HasPrefix(next, "func ")
1296  					if nextIsDecl {
1297  						// Skip lines i through k-1 (the broken decl).
1298  						i = k - 1
1299  						break
1300  					}
1301  				}
1302  				continue // skip this broken declaration line
1303  			}
1304  		}
1305  
1306  		result = append(result, lines[i])
1307  	}
1308  
1309  	return strings.Join(result, "\n")
1310  }
1311  
1312  // findStatementsToRemove identifies multi-line statements that contain error
1313  // lines. When an error occurs inside a multi-line function call like
1314  // runColony(arg1,\n arg2,\n arg3), all lines of that call should be removed
1315  // together rather than one at a time (which leaves broken syntax).
1316  func findStatementsToRemove(lines []string, errorLines map[int]bool) map[int]bool {
1317  	removeLines := make(map[int]bool)
1318  
1319  	// For each error line, check if it's inside a multi-line statement
1320  	// (unclosed parentheses from above). If so, find the statement boundaries
1321  	// and mark all lines for removal.
1322  	for errLine := range errorLines {
1323  		idx := errLine - 1 // 0-based
1324  		if idx < 0 || idx >= len(lines) {
1325  			continue
1326  		}
1327  
1328  		// Walk backward to find where the unclosed paren starts.
1329  		parenDepth := 0
1330  		for i := idx; i >= 0; i-- {
1331  			line := lines[i]
1332  			for j := len(line) - 1; j >= 0; j-- {
1333  				if line[j] == ')' {
1334  					parenDepth++
1335  				} else if line[j] == '(' {
1336  					parenDepth--
1337  				}
1338  			}
1339  			if parenDepth < 0 {
1340  				// Found the start of the multi-line expression.
1341  				// This line has an unmatched '(' — find the matching ')'.
1342  				stmtStart := i + 1 // line AFTER the opening paren
1343  				stmtEnd := idx
1344  
1345  				// Walk forward from the error line to find the closing ')'.
1346  				depth := 0
1347  				for j := i; j < len(lines); j++ {
1348  					for _, c := range lines[j] {
1349  						if c == '(' {
1350  							depth++
1351  						} else if c == ')' {
1352  							depth--
1353  						}
1354  					}
1355  					if depth <= 0 {
1356  						stmtEnd = j
1357  						break
1358  					}
1359  				}
1360  
1361  				// Mark ALL lines of this statement for removal.
1362  				// Include the statement start line too (the call itself).
1363  				for k := stmtStart; k <= stmtEnd; k++ {
1364  					removeLines[k+1] = true // 1-based
1365  				}
1366  				// Also mark the line with the function call.
1367  				removeLines[i+1] = true
1368  				break
1369  			}
1370  		}
1371  	}
1372  
1373  	return removeLines
1374  }
1375  
1376  // suppressUnusedVar adds "_ = name" after the declaration line of an unused
1377  // variable to suppress the "declared and not used" error without removing it.
1378  func suppressUnusedVar(source, name string) string {
1379  	lines := strings.Split(source, "\n")
1380  	for i, line := range lines {
1381  		trimmed := strings.TrimSpace(line)
1382  		// Match ":=" assignments: "name := ..." or "name, other := ..."
1383  		if strings.Contains(trimmed, name) && strings.Contains(trimmed, ":=") {
1384  			// Check if this line declares the variable.
1385  			lhs := trimmed
1386  			if eqIdx := strings.Index(lhs, ":="); eqIdx > 0 {
1387  				lhs = strings.TrimSpace(lhs[:eqIdx])
1388  			}
1389  			// Check all LHS parts.
1390  			parts := strings.Split(lhs, ",")
1391  			for _, p := range parts {
1392  				if strings.TrimSpace(p) == name {
1393  					// Insert "_ = name" after this line.
1394  					suppression := "\t_ = " + name
1395  					result := make([]string, 0, len(lines)+1)
1396  					result = append(result, lines[:i+1]...)
1397  					result = append(result, suppression)
1398  					result = append(result, lines[i+1:]...)
1399  					return strings.Join(result, "\n")
1400  				}
1401  			}
1402  		}
1403  		// Also check "var name ..." declarations.
1404  		if strings.HasPrefix(trimmed, "var "+name+" ") || strings.HasPrefix(trimmed, "var "+name+"=") {
1405  			suppression := "\t_ = " + name
1406  			result := make([]string, 0, len(lines)+1)
1407  			result = append(result, lines[:i+1]...)
1408  			result = append(result, suppression)
1409  			result = append(result, lines[i+1:]...)
1410  			return strings.Join(result, "\n")
1411  		}
1412  	}
1413  	return source
1414  }
1415  
1416  // stubFromSignature takes a function declaration line (e.g.,
1417  // "func buildSelf() (string, error) {") and returns a stub with an
1418  // empty body and zero-value return. This preserves the function's
1419  // call signature so callers don't break.
1420  func stubFromSignature(funcLine string) string {
1421  	// Strip trailing "{" and whitespace.
1422  	sig := strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(funcLine), "{"))
1423  	if !strings.HasPrefix(sig, "func ") {
1424  		return ""
1425  	}
1426  
1427  	// Extract the return signature.
1428  	retSig := extractReturnSig(sig)
1429  	zr := zeroReturn(retSig)
1430  
1431  	if zr != "" {
1432  		return sig + " { " + zr + " }"
1433  	}
1434  	return sig + " {}"
1435  }
1436  
1437  // truncate shortens a string to maxLen characters.
1438  func truncate(s string, maxLen int) string {
1439  	if len(s) <= maxLen {
1440  		return s
1441  	}
1442  	return s[:maxLen] + "..."
1443  }
1444  
1445  // StubNames returns the names of all type, var, and func stubs added by CompileAndRepair.
1446  // Detects: "type X struct{}", "var x interface{}", and "func x(args ...interface{}) ..."
1447  func StubNames(source string) []string {
1448  	var names []string
1449  	for _, line := range strings.Split(source, "\n") {
1450  		trimmed := strings.TrimSpace(line)
1451  		if strings.HasPrefix(trimmed, "type ") && strings.HasSuffix(trimmed, "struct{}") {
1452  			fields := strings.Fields(trimmed)
1453  			if len(fields) >= 2 {
1454  				names = append(names, fields[1])
1455  			}
1456  		}
1457  		if strings.HasPrefix(trimmed, "var ") && strings.HasSuffix(trimmed, "interface{}") {
1458  			fields := strings.Fields(trimmed)
1459  			if len(fields) >= 2 {
1460  				names = append(names, fields[1])
1461  			}
1462  		}
1463  		// Detect func stubs: "func name(args ...interface{})"
1464  		if strings.HasPrefix(trimmed, "func ") && strings.Contains(trimmed, "(args ...interface{})") {
1465  			// Extract name from "func name(args ..."
1466  			rest := strings.TrimPrefix(trimmed, "func ")
1467  			if paren := strings.IndexByte(rest, '('); paren > 0 {
1468  				name := rest[:paren]
1469  				if name != "" && !strings.Contains(name, " ") {
1470  					names = append(names, name)
1471  				}
1472  			}
1473  		}
1474  	}
1475  	sort.Strings(names)
1476  	return names
1477  }
1478