rewrite.go raw

   1  package mxtext
   2  
   3  // Moxie source rewrites.
   4  //
   5  // These transforms run before or during the parse/typecheck pipeline to
   6  // bridge Moxie syntax to what Go's parser and type checker accept.
   7  //
   8  // 1. rewriteChanLiterals: text-level rewrite before parsing.
   9  //    chan T{}  → make(chan T)
  10  //    chan T{N} → make(chan T, N)
  11  //
  12  // 1b. rewriteSliceLiterals: text-level rewrite before parsing.
  13  //    []T{:len}     → make([]T, len)
  14  //    []T{:len:cap} → make([]T, len, cap)
  15  //
  16  // 2. rewriteStringLiterals: AST-level rewrite after parsing, before typecheck.
  17  //    "hello" → []byte("hello")
  18  //    "a" + "b" → []byte("a" + "b")
  19  //
  20  // 3. rewritePipeConcat: AST-level rewrite after first typecheck pass.
  21  //    a | b (where both are []byte) → __moxie_concat(a, b)
  22  
  23  import (
  24  	"bytes"
  25  	"fmt"
  26  	"go/ast"
  27  	"go/scanner"
  28  	"go/token"
  29  	"go/types"
  30  	"strings"
  31  )
  32  
  33  // RewriteResult holds rewritten source and metadata about generated tokens.
  34  type RewriteResult struct {
  35  	Src            []byte
  36  	MakeOffsets    []int // byte offsets of loader-generated 'make' tokens
  37  	// PriorOffsets are make offsets from an earlier rewrite pass, adjusted
  38  	// to account for byte shifts introduced by this rewrite. Only populated
  39  	// when the rewriter receives prior offsets to remap.
  40  	PriorOffsets   []int
  41  }
  42  
  43  // isMoxieStringTarget returns true if a package should get string→[]byte
  44  // rewrites (string literal wrapping, | concat, comparison rewrites).
  45  //
  46  // Permanently exempt packages implement low-level primitives or
  47  // syscall interfaces that require native Go string/uintptr types.
  48  func IsMoxieStringTarget(importPath string) bool {
  49  	// All packages use the Moxie string/[]byte unified model.
  50  	// The entire goroot src/ tree is Moxie source.
  51  	if importPath == "unsafe" {
  52  		return false // built-in, no files
  53  	}
  54  	return true
  55  }
  56  
  57  // ---------------------------------------------------------------------------
  58  // 1. Channel literal rewrite (text-level, before parsing)
  59  // ---------------------------------------------------------------------------
  60  
  61  // rewriteChanLiterals scans source bytes for channel literal syntax and
  62  // rewrites to make(chan T) calls that Go's parser accepts.
  63  //
  64  // Patterns:
  65  //   chan T{}   → make(chan T)
  66  //   chan T{N}  → make(chan T, N)
  67  //
  68  // The rewrite is token-aware: it uses go/scanner to avoid matching inside
  69  // strings or comments. It only rewrites when 'chan' is followed by a type
  70  // expression and then '{' in expression context.
  71  func RewriteChanLiterals(src []byte, fset *token.FileSet) RewriteResult {
  72  	type tok struct {
  73  		pos    int
  74  		end    int
  75  		tok    token.Token
  76  		lit    string
  77  		offset int // byte offset in src
  78  	}
  79  
  80  	localFset := token.NewFileSet()
  81  	file := localFset.AddFile("", localFset.Base(), len(src))
  82  	var s scanner.Scanner
  83  	s.Init(file, src, nil, scanner.ScanComments)
  84  
  85  	var toks []tok
  86  	for {
  87  		pos, t, lit := s.Scan()
  88  		if t == token.EOF {
  89  			break
  90  		}
  91  		offset := file.Offset(pos)
  92  		end := offset + len(lit)
  93  		if lit == "" {
  94  			end = offset + len(t.String())
  95  		}
  96  		toks = append(toks, tok{pos: offset, end: end, tok: t, lit: lit})
  97  	}
  98  
  99  	// Scan for pattern: CHAN typeTokens... LBRACE [expr] RBRACE
 100  	// where typeTokens form a valid channel element type.
 101  	var result bytes.Buffer
 102  	var offsets []int
 103  	lastEnd := 0
 104  
 105  	for i := 0; i < len(toks); i++ {
 106  		if toks[i].tok != token.CHAN {
 107  			continue
 108  		}
 109  
 110  		// Found 'chan'. Now find the type expression and the '{'.
 111  		// Type expression is everything between 'chan' and '{'.
 112  		// It could be: int32, *Foo, []byte, <-chan int, etc.
 113  
 114  		chanIdx := i
 115  		braceIdx := -1
 116  
 117  		// Find the opening brace. Track nesting to handle complex types
 118  		// like chan []byte (which contains no braces in the type).
 119  		// Skip tokens that are part of the type expression.
 120  		depth := 0
 121  		for j := i + 1; j < len(toks); j++ {
 122  			switch toks[j].tok {
 123  			case token.LBRACE:
 124  				if depth == 0 {
 125  					braceIdx = j
 126  				}
 127  				depth++
 128  			case token.RBRACE:
 129  				depth--
 130  			case token.LPAREN:
 131  				depth++
 132  			case token.RPAREN:
 133  				depth--
 134  			}
 135  			if braceIdx >= 0 {
 136  				break
 137  			}
 138  			// Stop if we hit something that can't be part of a type expression.
 139  			if toks[j].tok == token.SEMICOLON || toks[j].tok == token.ASSIGN ||
 140  				toks[j].tok == token.DEFINE || toks[j].tok == token.COMMA ||
 141  				toks[j].tok == token.RPAREN {
 142  				break
 143  			}
 144  		}
 145  
 146  		if braceIdx < 0 || braceIdx <= chanIdx+1 {
 147  			continue // no brace found, or nothing between chan and {
 148  		}
 149  
 150  		// Check this is in expression context by whitelisting tokens that
 151  		// can precede a channel literal. In type contexts (var/func/field
 152  		// declarations), the { is a block/body opener, not a literal.
 153  		inExprContext := false
 154  		if chanIdx > 0 {
 155  			prev := toks[chanIdx-1].tok
 156  			switch prev {
 157  			case token.ASSIGN, token.DEFINE, // x = chan T{}, x := chan T{}
 158  				token.COLON,                  // field: chan T{}
 159  				token.COMMA,                  // f(a, chan T{})
 160  				token.LPAREN,                 // f(chan T{})
 161  				token.LBRACK,                 // []chan T{}
 162  				token.LBRACE,                 // {chan T{}}
 163  				token.RETURN,                 // return chan T{}
 164  				token.SEMICOLON:              // ; chan T{}
 165  				inExprContext = true
 166  			}
 167  		} else {
 168  			inExprContext = true // first token
 169  		}
 170  
 171  		if !inExprContext {
 172  			continue
 173  		}
 174  
 175  		// Find the matching closing brace.
 176  		closeIdx := -1
 177  		depth = 1
 178  		for j := braceIdx + 1; j < len(toks); j++ {
 179  			switch toks[j].tok {
 180  			case token.LBRACE:
 181  				depth++
 182  			case token.RBRACE:
 183  				depth--
 184  				if depth == 0 {
 185  					closeIdx = j
 186  				}
 187  			}
 188  			if closeIdx >= 0 {
 189  				break
 190  			}
 191  		}
 192  
 193  		if closeIdx < 0 {
 194  			continue
 195  		}
 196  
 197  		// Extract the type expression text (between chan and {).
 198  		typeStart := toks[chanIdx+1].pos
 199  		typeEnd := toks[braceIdx].pos
 200  		typeText := strings.TrimSpace(string(src[typeStart:typeEnd]))
 201  
 202  		if typeText == "" {
 203  			continue
 204  		}
 205  
 206  		// Handle chan struct{}{} and chan interface{}{}: the first {} is
 207  		// part of the type, the second {} is the channel literal body.
 208  		if typeText == "struct" || typeText == "interface" {
 209  			// closeIdx points to the } that closes struct{}/interface{}.
 210  			// Look for another {…} pair after it — that's the literal body.
 211  			if closeIdx+1 >= len(toks) || toks[closeIdx+1].tok != token.LBRACE {
 212  				continue // just "chan struct{}" in type context, no literal
 213  			}
 214  			// Include the struct{}/interface{} braces in the type text.
 215  			typeText = typeText + "{}"
 216  			braceIdx = closeIdx + 1
 217  			// Find the matching close for the literal body.
 218  			closeIdx = -1
 219  			depth = 1
 220  			for j := braceIdx + 1; j < len(toks); j++ {
 221  				switch toks[j].tok {
 222  				case token.LBRACE:
 223  					depth++
 224  				case token.RBRACE:
 225  					depth--
 226  					if depth == 0 {
 227  						closeIdx = j
 228  					}
 229  				}
 230  				if closeIdx >= 0 {
 231  					break
 232  				}
 233  			}
 234  			if closeIdx < 0 {
 235  				continue
 236  			}
 237  		}
 238  
 239  		// Extract the buffer size expression (between { and }).
 240  		var bufExpr string
 241  		if closeIdx > braceIdx+1 {
 242  			bufStart := toks[braceIdx+1].pos
 243  			bufEnd := toks[closeIdx].pos
 244  			bufExpr = strings.TrimSpace(string(src[bufStart:bufEnd]))
 245  		}
 246  
 247  		// Write everything before this channel literal.
 248  		result.Write(src[lastEnd:toks[chanIdx].pos])
 249  
 250  		makeOffset := result.Len()
 251  		result.WriteString("make(chan ")
 252  		result.WriteString(typeText)
 253  		if bufExpr != "" {
 254  			result.WriteString(", ")
 255  			result.WriteString(bufExpr)
 256  		}
 257  		result.WriteString(")")
 258  		offsets = append(offsets, makeOffset)
 259  
 260  		lastEnd = toks[closeIdx].end
 261  		i = closeIdx // skip past the closing brace
 262  	}
 263  
 264  	if lastEnd == 0 {
 265  		return RewriteResult{Src: src}
 266  	}
 267  	result.Write(src[lastEnd:])
 268  	return RewriteResult{Src: result.Bytes(), MakeOffsets: offsets}
 269  }
 270  
 271  // ---------------------------------------------------------------------------
 272  // 1b. Slice size literal rewrite (text-level, before parsing)
 273  // ---------------------------------------------------------------------------
 274  
 275  // rewriteSliceLiterals scans source bytes for slice size literal syntax and
 276  // rewrites to make() calls that Go's parser accepts.
 277  //
 278  // Patterns:
 279  //   []T{:len}      → make([]T, len)
 280  //   []T{:len:cap}  → make([]T, len, cap)
 281  //
 282  // The leading colon after { distinguishes this from regular composite literals
 283  // ([]int{1, 2, 3} has no colon). The syntax mirrors Go's three-index slice
 284  // expression a[low:high:max].
 285  func RewriteSliceLiterals(src []byte, fset *token.FileSet, priorOffsets ...[]int) RewriteResult {
 286  	type tok struct {
 287  		pos    int
 288  		end    int
 289  		tok    token.Token
 290  		lit    string
 291  	}
 292  
 293  	localFset := token.NewFileSet()
 294  	file := localFset.AddFile("", localFset.Base(), len(src))
 295  	var s scanner.Scanner
 296  	s.Init(file, src, nil, scanner.ScanComments)
 297  
 298  	var toks []tok
 299  	for {
 300  		pos, t, lit := s.Scan()
 301  		if t == token.EOF {
 302  			break
 303  		}
 304  		offset := file.Offset(pos)
 305  		end := offset + len(lit)
 306  		if lit == "" {
 307  			end = offset + len(t.String())
 308  		}
 309  		toks = append(toks, tok{pos: offset, end: end, tok: t, lit: lit})
 310  	}
 311  
 312  	var result bytes.Buffer
 313  	var offsets []int
 314  	lastEnd := 0
 315  
 316  	// Track input-to-output byte delta for remapping prior offsets.
 317  	// Each entry: replacement consumed src[replStart:replEnd] and wrote
 318  	// outputLen bytes. Prior offsets after replStart shift by the
 319  	// cumulative (outputLen - inputLen) of all preceding replacements.
 320  	type deltaEntry struct {
 321  		inputEnd int // end of replaced input region
 322  		cumDelta int // cumulative delta after this replacement
 323  	}
 324  	var deltas []deltaEntry
 325  	cumDelta := 0
 326  
 327  	for i := 0; i < len(toks); i++ {
 328  		// Look for LBRACK RBRACK ... LBRACE COLON pattern.
 329  		if toks[i].tok != token.LBRACK {
 330  			continue
 331  		}
 332  		if i+1 >= len(toks) || toks[i+1].tok != token.RBRACK {
 333  			continue
 334  		}
 335  
 336  		lbrackIdx := i
 337  
 338  		// Scan forward past the element type to find LBRACE.
 339  		braceIdx := -1
 340  		depth := 0
 341  		for j := i + 2; j < len(toks); j++ {
 342  			switch toks[j].tok {
 343  			case token.LBRACK:
 344  				depth++
 345  			case token.RBRACK:
 346  				depth--
 347  			case token.LPAREN:
 348  				depth++
 349  			case token.RPAREN:
 350  				depth--
 351  			case token.LBRACE:
 352  				if depth == 0 {
 353  					braceIdx = j
 354  				}
 355  			}
 356  			if braceIdx >= 0 {
 357  				break
 358  			}
 359  			// Stop at tokens that can't be part of a type expression.
 360  			if depth == 0 && (toks[j].tok == token.SEMICOLON ||
 361  				toks[j].tok == token.ASSIGN ||
 362  				toks[j].tok == token.DEFINE ||
 363  				toks[j].tok == token.COMMA) {
 364  				break
 365  			}
 366  		}
 367  
 368  		if braceIdx < 0 || braceIdx <= lbrackIdx+2 {
 369  			continue // no brace, or nothing between [] and {
 370  		}
 371  
 372  		// Check that the token after { is COLON — this is the discriminator.
 373  		if braceIdx+1 >= len(toks) || toks[braceIdx+1].tok != token.COLON {
 374  			continue // regular composite literal, not slice size
 375  		}
 376  
 377  		// Find the closing brace, collecting colon positions for len:cap.
 378  		// Track all bracket types so colons inside subscripts (e.g. buf[:2])
 379  		// aren't mistaken for the len:cap separator.
 380  		closeIdx := -1
 381  		colonPositions := []int{braceIdx + 1} // first colon already found
 382  		depth = 1
 383  		bracketDepth := 0
 384  		parenDepth := 0
 385  		for j := braceIdx + 2; j < len(toks); j++ {
 386  			switch toks[j].tok {
 387  			case token.LBRACE:
 388  				depth++
 389  			case token.RBRACE:
 390  				depth--
 391  				if depth == 0 {
 392  					closeIdx = j
 393  				}
 394  			case token.LBRACK:
 395  				bracketDepth++
 396  			case token.RBRACK:
 397  				bracketDepth--
 398  			case token.LPAREN:
 399  				parenDepth++
 400  			case token.RPAREN:
 401  				parenDepth--
 402  			case token.COLON:
 403  				if depth == 1 && bracketDepth == 0 && parenDepth == 0 {
 404  					colonPositions = append(colonPositions, j)
 405  				}
 406  			}
 407  			if closeIdx >= 0 {
 408  				break
 409  			}
 410  		}
 411  
 412  		if closeIdx < 0 {
 413  			continue
 414  		}
 415  
 416  		// Extract the type text (between [ and {, inclusive of []).
 417  		typeText := string(src[toks[lbrackIdx].pos:toks[braceIdx].pos])
 418  		typeText = strings.TrimSpace(typeText)
 419  
 420  		// Detect the secure-allocator marker: a trailing `, secure` IDENT
 421  		// just before the closing brace. This only applies to the
 422  		// `[]T{:len}` form (no len:cap variant — secure allocations are
 423  		// page-aligned and have an implicit cap).
 424  		secureMarker := false
 425  		secureExprEnd := closeIdx
 426  		if len(colonPositions) == 1 && closeIdx-2 > colonPositions[0] {
 427  			lastTok := toks[closeIdx-1]
 428  			prevTok := toks[closeIdx-2]
 429  			if lastTok.tok == token.IDENT && lastTok.lit == "secure" &&
 430  				prevTok.tok == token.COMMA {
 431  				secureMarker = true
 432  				secureExprEnd = closeIdx - 2 // index of the COMMA
 433  			}
 434  		}
 435  
 436  		replInputStart := toks[lbrackIdx].pos
 437  
 438  		if secureMarker {
 439  			if typeText != "[]byte" {
 440  				continue
 441  			}
 442  			lenStart := toks[colonPositions[0]+1].pos
 443  			lenEnd := toks[secureExprEnd].pos
 444  			lenExpr := strings.TrimSpace(string(src[lenStart:lenEnd]))
 445  			if lenExpr == "" {
 446  				continue
 447  			}
 448  			result.Write(src[lastEnd:replInputStart])
 449  			result.WriteString("__moxie_secalloc(")
 450  			result.WriteString(lenExpr)
 451  			result.WriteString(")")
 452  		} else if len(colonPositions) == 1 {
 453  			lenStart := toks[colonPositions[0]+1].pos
 454  			lenEnd := toks[closeIdx].pos
 455  			lenExpr := strings.TrimSpace(string(src[lenStart:lenEnd]))
 456  			if lenExpr == "" {
 457  				continue
 458  			}
 459  			result.Write(src[lastEnd:replInputStart])
 460  			makeOffset := result.Len()
 461  			result.WriteString("make(")
 462  			result.WriteString(typeText)
 463  			result.WriteString(", ")
 464  			result.WriteString(lenExpr)
 465  			result.WriteString(")")
 466  			offsets = append(offsets, makeOffset)
 467  		} else if len(colonPositions) == 2 {
 468  			lenStart := toks[colonPositions[0]+1].pos
 469  			lenEnd := toks[colonPositions[1]].pos
 470  			lenExpr := strings.TrimSpace(string(src[lenStart:lenEnd]))
 471  			capStart := toks[colonPositions[1]+1].pos
 472  			capEnd := toks[closeIdx].pos
 473  			capExpr := strings.TrimSpace(string(src[capStart:capEnd]))
 474  			if lenExpr == "" || capExpr == "" {
 475  				continue
 476  			}
 477  			result.Write(src[lastEnd:replInputStart])
 478  			makeOffset := result.Len()
 479  			result.WriteString("make(")
 480  			result.WriteString(typeText)
 481  			result.WriteString(", ")
 482  			result.WriteString(lenExpr)
 483  			result.WriteString(", ")
 484  			result.WriteString(capExpr)
 485  			result.WriteString(")")
 486  			offsets = append(offsets, makeOffset)
 487  		} else {
 488  			continue
 489  		}
 490  
 491  		replInputEnd := toks[closeIdx].end
 492  		cumDelta = result.Len() - replInputEnd
 493  		deltas = append(deltas, deltaEntry{inputEnd: replInputEnd, cumDelta: cumDelta})
 494  
 495  		lastEnd = toks[closeIdx].end
 496  		i = closeIdx
 497  	}
 498  
 499  	if lastEnd == 0 {
 500  		r := RewriteResult{Src: src}
 501  		if len(priorOffsets) > 0 {
 502  			r.PriorOffsets = priorOffsets[0]
 503  		}
 504  		return r
 505  	}
 506  	result.Write(src[lastEnd:])
 507  
 508  	// Remap prior offsets from the earlier rewrite pass.
 509  	var remapped []int
 510  	if len(priorOffsets) > 0 && len(priorOffsets[0]) > 0 {
 511  		remapped = make([]int, len(priorOffsets[0]))
 512  		for i, off := range priorOffsets[0] {
 513  			delta := 0
 514  			for _, d := range deltas {
 515  				if off >= d.inputEnd {
 516  					delta = d.cumDelta
 517  				} else {
 518  					break
 519  				}
 520  			}
 521  			remapped[i] = off + delta
 522  		}
 523  	}
 524  
 525  	return RewriteResult{Src: result.Bytes(), MakeOffsets: offsets, PriorOffsets: remapped}
 526  }
 527  
 528  // ---------------------------------------------------------------------------
 529  // 1c. String type annotation rewrite (AST-level, before typecheck)
 530  // ---------------------------------------------------------------------------
 531  
 532  // RewriteStringTypes converts `string` type identifiers to `[]byte` in all
 533  // type positions: function params, returns, struct fields, var declarations,
 534  // type specs, map keys/values, slice elements, and interface method signatures.
 535  // This is the AST-level equivalent of what mxpurify does to source files,
 536  // needed because the standard Go type checker treats string and []byte as
 537  // distinct types.
 538  func RewriteStringTypes(file *ast.File) {
 539  	ast.Inspect(file, func(n ast.Node) bool {
 540  		switch node := n.(type) {
 541  		case *ast.FuncDecl:
 542  			// Interface-mandated methods (Error() string, String() string) must
 543  			// keep their `string` return type — they're bound to language-level
 544  			// interfaces (error, fmt.Stringer) we can't rewrite. Their return
 545  			// wrapping is deferred to WrapInterfaceMandatedReturns so it runs
 546  			// AFTER RewriteStringConversions (which would otherwise undo the
 547  			// string(...) wrap by converting it to []byte(...)).
 548  			if isInterfaceMandatedMethod(node) {
 549  				return false
 550  			}
 551  			rewriteFieldListTypes(node.Type.Params)
 552  			rewriteFieldListTypes(node.Type.Results)
 553  		case *ast.FuncLit:
 554  			rewriteFieldListTypes(node.Type.Params)
 555  			rewriteFieldListTypes(node.Type.Results)
 556  		case *ast.FuncType:
 557  			rewriteFieldListTypes(node.Params)
 558  			rewriteFieldListTypes(node.Results)
 559  		case *ast.Field:
 560  			node.Type = rewriteStringTypeExpr(node.Type)
 561  		case *ast.ValueSpec:
 562  			if node.Type != nil {
 563  				node.Type = rewriteStringTypeExpr(node.Type)
 564  			}
 565  		case *ast.TypeSpec:
 566  			// `type X string` must stay as defined string type so
 567  			// `const c X = "..."` remains legal. Rewriting to []byte
 568  			// makes the const invalid (slice types can't be const).
 569  			if id, ok := node.Type.(*ast.Ident); ok && id.Name == "string" {
 570  				return false
 571  			}
 572  			node.Type = rewriteStringTypeExpr(node.Type)
 573  		case *ast.ArrayType:
 574  			node.Elt = rewriteStringTypeExpr(node.Elt)
 575  		case *ast.MapType:
 576  			// Do NOT rewrite map keys — []byte is not comparable in Go's type
 577  			// system, so map[[]byte]V would be rejected. Leave key as string
 578  			// (valid map key). The string/[]byte mismatch filter handles any
 579  			// residual type errors from key lookups.
 580  			node.Value = rewriteStringTypeExpr(node.Value)
 581  		}
 582  		return true
 583  	})
 584  }
 585  
 586  func rewriteFieldListTypes(fl *ast.FieldList) {
 587  	if fl == nil {
 588  		return
 589  	}
 590  	for _, field := range fl.List {
 591  		field.Type = rewriteStringTypeExpr(field.Type)
 592  	}
 593  }
 594  
 595  func rewriteStringTypeExpr(expr ast.Expr) ast.Expr {
 596  	ident, ok := expr.(*ast.Ident)
 597  	if !ok || ident.Name != "string" {
 598  		return expr
 599  	}
 600  	// Replace `string` with `[]byte`.
 601  	return &ast.ArrayType{
 602  		Elt: ast.NewIdent("byte"),
 603  	}
 604  }
 605  
 606  // WrapInterfaceMandatedReturns wraps return statements inside interface-
 607  // mandated methods (Error, String) with string(...) conversions. Must run
 608  // AFTER RewriteStringConversions (which rewrites string→[]byte everywhere)
 609  // so the wraps this function introduces are preserved.
 610  func WrapInterfaceMandatedReturns(file *ast.File) {
 611  	ast.Inspect(file, func(n ast.Node) bool {
 612  		fd, ok := n.(*ast.FuncDecl)
 613  		if !ok {
 614  			return true
 615  		}
 616  		if !isInterfaceMandatedMethod(fd) {
 617  			return true
 618  		}
 619  		wrapReturnsInStringConv(fd.Body)
 620  		return false
 621  	})
 622  }
 623  
 624  // wrapReturnsInStringConv wraps every return-statement value in an explicit
 625  // string(...) conversion. Used for interface-mandated methods that keep their
 626  // string return type while the receiver fields have been rewritten to []byte.
 627  func wrapReturnsInStringConv(body *ast.BlockStmt) {
 628  	if body == nil {
 629  		return
 630  	}
 631  	ast.Inspect(body, func(n ast.Node) bool {
 632  		ret, ok := n.(*ast.ReturnStmt)
 633  		if !ok {
 634  			return true
 635  		}
 636  		for i, r := range ret.Results {
 637  			// Skip already-wrapped string(...) calls.
 638  			if call, ok := r.(*ast.CallExpr); ok {
 639  				if id, ok := call.Fun.(*ast.Ident); ok && id.Name == "string" {
 640  					continue
 641  				}
 642  			}
 643  			ret.Results[i] = &ast.CallExpr{
 644  				Fun:  ast.NewIdent("string"),
 645  				Args: []ast.Expr{r},
 646  			}
 647  		}
 648  		return true
 649  	})
 650  }
 651  
 652  // isInterfaceMandatedMethod returns true if the function is a method whose
 653  // signature is mandated by a language built-in interface we can't rewrite:
 654  //   - Error() string (error interface)
 655  //   - String() string (fmt.Stringer, used by print formatting)
 656  //
 657  // These methods must keep their `string` return type; their bodies are also
 658  // skipped by RewriteStringLiterals via funcReturnsString.
 659  func isInterfaceMandatedMethod(fd *ast.FuncDecl) bool {
 660  	if fd.Recv == nil || len(fd.Recv.List) != 1 {
 661  		return false
 662  	}
 663  	if fd.Name == nil {
 664  		return false
 665  	}
 666  	name := fd.Name.Name
 667  	if name != "Error" && name != "String" {
 668  		return false
 669  	}
 670  	// Must take no params and return exactly one string value.
 671  	if fd.Type.Params != nil && len(fd.Type.Params.List) != 0 {
 672  		return false
 673  	}
 674  	if fd.Type.Results == nil || len(fd.Type.Results.List) != 1 {
 675  		return false
 676  	}
 677  	field := fd.Type.Results.List[0]
 678  	if len(field.Names) != 0 {
 679  		// Named return — still check the type.
 680  	}
 681  	ident, ok := field.Type.(*ast.Ident)
 682  	return ok && ident.Name == "string"
 683  }
 684  
 685  // RewriteStringConversions rewrites `string(expr)` → `[]byte(expr)` in the
 686  // AST. Since Moxie unifies string and []byte, these conversions are identity
 687  // but the Go type checker rejects returning a string where []byte is expected.
 688  // Must run after RewriteStringTypes so return types are already []byte.
 689  func RewriteStringConversions(file *ast.File) {
 690  	// No-op: string(x) conversions are left as-is. In Moxie, string and
 691  	// []byte are the same underlying type, so string(x) is valid regardless
 692  	// of whether x is a rune, []byte, or other type. RewriteStringTypes
 693  	// handles the type declarations; explicit conversions don't need rewriting.
 694  }
 695  
 696  // RewriteUnsafeString rewrites `unsafe.String(ptr, len)` → `unsafe.Slice(ptr,
 697  // len)` and `unsafe.StringData(s)` → `unsafe.SliceData(s)` in the AST. In
 698  // stock Go the two function pairs differ in argument/return type (`string` vs
 699  // `[]T`), but in Moxie string and []byte are the same type so the swaps are
 700  // identity. Must run before typecheck so the type mismatch errors never get
 701  // produced.
 702  func RewriteUnsafeString(file *ast.File) {
 703  	ast.Inspect(file, func(n ast.Node) bool {
 704  		call, ok := n.(*ast.CallExpr)
 705  		if !ok {
 706  			return true
 707  		}
 708  		sel, ok := call.Fun.(*ast.SelectorExpr)
 709  		if !ok {
 710  			return true
 711  		}
 712  		pkg, ok := sel.X.(*ast.Ident)
 713  		if !ok || pkg.Name != "unsafe" {
 714  			return true
 715  		}
 716  		switch sel.Sel.Name {
 717  		case "String":
 718  			sel.Sel = ast.NewIdent("Slice")
 719  		case "StringData":
 720  			sel.Sel = ast.NewIdent("SliceData")
 721  		}
 722  		return true
 723  	})
 724  }
 725  
 726  // ---------------------------------------------------------------------------
 727  // 2. String literal rewrite (AST-level, after parsing, before typecheck)
 728  // ---------------------------------------------------------------------------
 729  
 730  // rewriteStringLiterals wraps string literals and string binary expressions
 731  // in []byte() conversions throughout the AST of a user package.
 732  //
 733  // "hello"       → []byte("hello")
 734  // "a" + "b"     → []byte("a" + "b")
 735  //
 736  // This makes Go's type checker see []byte instead of string for all text
 737  // values in user code.
 738  func RewriteStringLiterals(file *ast.File) {
 739  	// Rewrite "X == \"\"" and "X != \"\"" to "len(X) == 0" and "len(X) != 0"
 740  	// BEFORE wrapping literals. After RewriteStringTypes turns the LHS into
 741  	// []byte, a slice == []byte("") would be rejected (slices only compare to
 742  	// nil). len-based check is the correct semantic replacement.
 743  	rewriteEmptyStringComparisons(file)
 744  	// Split mixed-type const blocks so non-string specs stay untyped.
 745  	splitConstBlocks(file)
 746  	// Walk the AST and replace string expressions with []byte() wrapped versions.
 747  	// We need to walk parent nodes to replace children in-place.
 748  	rewriteStringExprs(file)
 749  }
 750  
 751  // rewriteEmptyStringComparisons converts `X == ""` → `len(X) == 0` and
 752  // `X != ""` → `len(X) != 0` in the AST. Must run before RewriteStringLiterals'
 753  // literal wrapping, since wrapping turns "" into []byte("") which then makes
 754  // the comparison illegal (slice vs slice).
 755  func rewriteEmptyStringComparisons(file *ast.File) {
 756  	replace := func(expr *ast.Expr) {
 757  		be, ok := (*expr).(*ast.BinaryExpr)
 758  		if !ok {
 759  			return
 760  		}
 761  		if be.Op != token.EQL && be.Op != token.NEQ {
 762  			return
 763  		}
 764  		// Detect `X == ""` or `"" == X`.
 765  		var other ast.Expr
 766  		if isEmptyStringLit(be.Y) {
 767  			other = be.X
 768  		} else if isEmptyStringLit(be.X) {
 769  			other = be.Y
 770  		} else {
 771  			return
 772  		}
 773  		// Replace with len(other) OP 0
 774  		lenCall := &ast.CallExpr{
 775  			Fun:  ast.NewIdent("len"),
 776  			Args: []ast.Expr{other},
 777  		}
 778  		*expr = &ast.BinaryExpr{
 779  			X:  lenCall,
 780  			Op: be.Op,
 781  			Y:  &ast.BasicLit{Kind: token.INT, Value: "0"},
 782  		}
 783  	}
 784  	ast.Inspect(file, func(n ast.Node) bool {
 785  		switch node := n.(type) {
 786  		case *ast.IfStmt:
 787  			replace(&node.Cond)
 788  		case *ast.ForStmt:
 789  			if node.Cond != nil {
 790  				replace(&node.Cond)
 791  			}
 792  		case *ast.BinaryExpr:
 793  			replace(&node.X)
 794  			replace(&node.Y)
 795  		case *ast.AssignStmt:
 796  			for i := range node.Rhs {
 797  				replace(&node.Rhs[i])
 798  			}
 799  		case *ast.ReturnStmt:
 800  			for i := range node.Results {
 801  				replace(&node.Results[i])
 802  			}
 803  		case *ast.CallExpr:
 804  			for i := range node.Args {
 805  				replace(&node.Args[i])
 806  			}
 807  		case *ast.UnaryExpr:
 808  			replace(&node.X)
 809  		case *ast.ParenExpr:
 810  			replace(&node.X)
 811  		case *ast.SwitchStmt:
 812  			if node.Tag != nil {
 813  				replace(&node.Tag)
 814  			}
 815  		case *ast.KeyValueExpr:
 816  			replace(&node.Value)
 817  		case *ast.ValueSpec:
 818  			for i := range node.Values {
 819  				replace(&node.Values[i])
 820  			}
 821  		}
 822  		return true
 823  	})
 824  }
 825  
 826  func isEmptyStringLit(expr ast.Expr) bool {
 827  	bl, ok := expr.(*ast.BasicLit)
 828  	if !ok {
 829  		return false
 830  	}
 831  	return bl.Kind == token.STRING && (bl.Value == `""` || bl.Value == "``")
 832  }
 833  
 834  // rewriteStringExprs walks the AST and wraps string-typed expressions in []byte().
 835  func rewriteStringExprs(node ast.Node) {
 836  	ast.Inspect(node, func(n ast.Node) bool {
 837  		// Don't descend into []byte() wrappers we created — prevents
 838  		// infinite recursion (walker would visit the inner string literal
 839  		// and try to wrap it again).
 840  		if expr, ok := n.(ast.Expr); ok && isSliceByteConversion(expr) {
 841  			return false
 842  		}
 843  		// Const blocks are handled at file-scope by splitConstBlocks so
 844  		// mixed string/non-string blocks can be split into a preserved
 845  		// const (for untyped integer constants) and a companion var.
 846  		if gd, ok := n.(*ast.GenDecl); ok && gd.Tok == token.CONST {
 847  			return false
 848  		}
 849  		// Interface-mandated methods (Error/String) keep their string
 850  		// return type, but RewriteStringTypes has already wrapped every
 851  		// return value in `string(...)`. So we CAN wrap inner string
 852  		// literals here — e.g. `return "strconv." | e.Func` becomes
 853  		// `return string([]byte("strconv.") + e.Func + ...)`, which
 854  		// RewriteTextConcat then lowers to __moxie_concat. The outer
 855  		// string() conversion takes the []byte result back to string.
 856  		switch parent := n.(type) {
 857  		case *ast.AssignStmt:
 858  			for i, rhs := range parent.Rhs {
 859  				if wrapped := wrapStringExpr(rhs); wrapped != nil {
 860  					parent.Rhs[i] = wrapped
 861  				}
 862  			}
 863  		case *ast.ValueSpec:
 864  			for i, val := range parent.Values {
 865  				if wrapped := wrapStringExpr(val); wrapped != nil {
 866  					parent.Values[i] = wrapped
 867  				}
 868  			}
 869  		case *ast.ReturnStmt:
 870  			// Don't wrap return values - the function's return type
 871  			// is still `string` in the Go type checker.
 872  		case *ast.CallExpr:
 873  			// Don't wrap CallExpr args - the callee signature types
 874  			// are resolved by the Go type checker from imported packages
 875  			// and may still be `string`. Wrapping to []byte would create
 876  			// a type mismatch. The Moxie codegen handles string/[]byte
 877  			// equivalence at the LLVM level.
 878  		case *ast.SendStmt:
 879  			if wrapped := wrapStringExpr(parent.Value); wrapped != nil {
 880  				parent.Value = wrapped
 881  			}
 882  		case *ast.KeyValueExpr:
 883  			if wrapped := wrapStringExpr(parent.Value); wrapped != nil {
 884  				parent.Value = wrapped
 885  			}
 886  		case *ast.BinaryExpr:
 887  			// Wrap string literals on either side of comparison operators.
 888  			if wrapped := wrapStringExpr(parent.X); wrapped != nil {
 889  				parent.X = wrapped
 890  			}
 891  			if wrapped := wrapStringExpr(parent.Y); wrapped != nil {
 892  				parent.Y = wrapped
 893  			}
 894  		case *ast.CaseClause:
 895  			// Wrap string literals in switch case values.
 896  			for i, val := range parent.List {
 897  				if wrapped := wrapStringExpr(val); wrapped != nil {
 898  					parent.List[i] = wrapped
 899  				}
 900  			}
 901  		case *ast.CompositeLit:
 902  			for i, elt := range parent.Elts {
 903  				// Skip KeyValueExpr — handled above for values.
 904  				if _, isKV := elt.(*ast.KeyValueExpr); isKV {
 905  					continue
 906  				}
 907  				if wrapped := wrapStringExpr(elt); wrapped != nil {
 908  					parent.Elts[i] = wrapped
 909  				}
 910  			}
 911  		case *ast.IndexExpr:
 912  			if wrapped := wrapStringExpr(parent.Index); wrapped != nil {
 913  				parent.Index = wrapped
 914  			}
 915  		case *ast.IfStmt:
 916  			// Wrap in if-init statements (e.g. if x := "val"; ...).
 917  			// Cond is a BinaryExpr, handled above.
 918  		case *ast.SwitchStmt:
 919  			// Wrap switch tag if it's a string literal.
 920  			if parent.Tag != nil {
 921  				if wrapped := wrapStringExpr(parent.Tag); wrapped != nil {
 922  					parent.Tag = wrapped
 923  				}
 924  			}
 925  		}
 926  		return true
 927  	})
 928  }
 929  
 930  // wrapStringExpr returns a []byte(expr) wrapping if expr is a string-producing
 931  // expression (string literal or binary + of string expressions). Returns nil
 932  // if no wrapping is needed.
 933  func wrapStringExpr(expr ast.Expr) ast.Expr {
 934  	if !isStringExpr(expr) {
 935  		return nil
 936  	}
 937  	// Already wrapped in []byte() — don't double-wrap.
 938  	if isSliceByteConversion(expr) {
 939  		return nil
 940  	}
 941  	// Normalize | to + so the wrapped []byte(...) expression type-checks
 942  	// (the patched type checker accepts | only on slice types, not untyped
 943  	// string constants).
 944  	normalizePipeToAdd(expr)
 945  	return makeSliceByteCall(expr)
 946  }
 947  
 948  // normalizePipeToAdd rewrites | to + in a string-literal subtree, in place.
 949  func normalizePipeToAdd(e ast.Expr) {
 950  	switch n := e.(type) {
 951  	case *ast.BinaryExpr:
 952  		if n.Op == token.OR {
 953  			n.Op = token.ADD
 954  		}
 955  		normalizePipeToAdd(n.X)
 956  		normalizePipeToAdd(n.Y)
 957  	case *ast.ParenExpr:
 958  		normalizePipeToAdd(n.X)
 959  	}
 960  }
 961  
 962  // isSyntacticText returns true if expr is syntactically recognizable as text:
 963  // containsSyntacticText returns true if the expression tree contains any
 964  // syntactic text node (string literal or []byte conversion) at the top
 965  // level — in binary expressions and parens, but NOT inside function call
 966  // arguments (len("x"), strconv.Itoa(...), etc. return integers, not text).
 967  func containsSyntacticText(expr ast.Expr) bool {
 968  	if isSyntacticText(expr) {
 969  		return true
 970  	}
 971  	switch e := expr.(type) {
 972  	case *ast.BinaryExpr:
 973  		return containsSyntacticText(e.X) || containsSyntacticText(e.Y)
 974  	case *ast.ParenExpr:
 975  		return containsSyntacticText(e.X)
 976  	}
 977  	return false
 978  }
 979  
 980  // a string literal, a []byte(...) conversion, or a BinaryExpr whose operands
 981  // are themselves syntactically text. Used to rewrite + to | pre-typecheck
 982  // when info.TypeOf is not yet available.
 983  func isSyntacticText(expr ast.Expr) bool {
 984  	switch e := expr.(type) {
 985  	case *ast.BasicLit:
 986  		return e.Kind == token.STRING
 987  	case *ast.CallExpr:
 988  		return isSliceByteConversion(e)
 989  	case *ast.BinaryExpr:
 990  		if e.Op == token.ADD || e.Op == token.OR {
 991  			return isSyntacticText(e.X) || isSyntacticText(e.Y)
 992  		}
 993  	case *ast.ParenExpr:
 994  		return isSyntacticText(e.X)
 995  	}
 996  	return false
 997  }
 998  
 999  // RewriteAddToPipe walks the file's AST (after RewriteStringLiterals has
1000  // wrapped string literals in []byte) and changes BinaryExpr + to | whenever
1001  // the expression is syntactically text. This permits the patched go/types —
1002  // which accepts | but not + on []byte — to succeed on the first typecheck
1003  // pass for vendored/stdlib packages that still use + for text concatenation.
1004  // Also converts += to |= for compound assignments whose RHS contains
1005  // syntactic text. Intentionally NOT called on main-module packages —
1006  // user code with + on text is a compile error (CheckPlusOnText).
1007  func RewriteAddToPipe(file *ast.File) bool {
1008  	modified := false
1009  	ast.Inspect(file, func(n ast.Node) bool {
1010  		// Don't descend into const decls or []byte wraps — their + is
1011  		// needed for compile-time constant folding.
1012  		if gd, ok := n.(*ast.GenDecl); ok && gd.Tok == token.CONST {
1013  			return false
1014  		}
1015  		if expr, ok := n.(ast.Expr); ok && isSliceByteConversion(expr) {
1016  			return false
1017  		}
1018  		if bin, ok := n.(*ast.BinaryExpr); ok && bin.Op == token.ADD {
1019  			if isSyntacticText(bin.X) || isSyntacticText(bin.Y) {
1020  				bin.Op = token.OR
1021  			}
1022  		}
1023  		if assign, ok := n.(*ast.AssignStmt); ok && assign.Tok == token.ADD_ASSIGN && len(assign.Rhs) == 1 {
1024  			if containsSyntacticText(assign.Rhs[0]) {
1025  				lhs := assign.Lhs[0]
1026  				rhs := assign.Rhs[0]
1027  				assign.Tok = token.ASSIGN
1028  				assign.Rhs[0] = &ast.CallExpr{
1029  					Fun:  &ast.Ident{Name: "__moxie_concat"},
1030  					Args: []ast.Expr{lhs, wrapForMoxieConcat(rhs)},
1031  				}
1032  				modified = true
1033  			}
1034  		}
1035  		return true
1036  	})
1037  	return modified
1038  }
1039  
1040  // isStringExpr returns true if the expression is syntactically a string literal
1041  // or a binary +/| chain of string expressions (constant string concatenation).
1042  func isStringExpr(expr ast.Expr) bool {
1043  	switch e := expr.(type) {
1044  	case *ast.BasicLit:
1045  		return e.Kind == token.STRING
1046  	case *ast.BinaryExpr:
1047  		if e.Op == token.ADD || e.Op == token.OR {
1048  			return isStringExpr(e.X) && isStringExpr(e.Y)
1049  		}
1050  	case *ast.ParenExpr:
1051  		return isStringExpr(e.X)
1052  	}
1053  	return false
1054  }
1055  
1056  // isSliceByteConversion returns true if expr is []byte(...).
1057  func isSliceByteConversion(expr ast.Expr) bool {
1058  	call, ok := expr.(*ast.CallExpr)
1059  	if !ok || len(call.Args) != 1 {
1060  		return false
1061  	}
1062  	arr, ok := call.Fun.(*ast.ArrayType)
1063  	if !ok || arr.Len != nil {
1064  		return false
1065  	}
1066  	ident, ok := arr.Elt.(*ast.Ident)
1067  	return ok && ident.Name == "byte"
1068  }
1069  
1070  // convertStringConstsToVars converts pure string constants to var declarations
1071  // with []byte values. Only converts specs where ALL values are string literals
1072  // and there's no explicit type or iota. Leaves numeric/mixed consts untouched.
1073  // splitConstBlocks walks the file's top-level declarations AND function
1074  // bodies, splitting each const block that mixes string and non-string
1075  // specs into a const block (non-string) and a var block (string, with
1076  // literals wrapped in []byte). Keeping the non-string specs as const
1077  // preserves their untyped-ness so comparisons like `rune >= runeSelf`
1078  // still typecheck.
1079  func splitConstBlocks(file *ast.File) {
1080  	splitBlockStmtConsts(file)
1081  	var newDecls []ast.Decl
1082  	for _, decl := range file.Decls {
1083  		gd, ok := decl.(*ast.GenDecl)
1084  		if !ok || gd.Tok != token.CONST {
1085  			newDecls = append(newDecls, decl)
1086  			continue
1087  		}
1088  		hasString := false
1089  		for _, spec := range gd.Specs {
1090  			vs, ok := spec.(*ast.ValueSpec)
1091  			if !ok {
1092  				continue
1093  			}
1094  			if vs.Type != nil {
1095  				continue
1096  			}
1097  			for _, val := range vs.Values {
1098  				if isStringExpr(val) {
1099  					hasString = true
1100  					break
1101  				}
1102  			}
1103  			if hasString {
1104  				break
1105  			}
1106  		}
1107  		if !hasString {
1108  			newDecls = append(newDecls, decl)
1109  			continue
1110  		}
1111  		var varSpecs []ast.Spec
1112  		var constSpecs []ast.Spec
1113  		for _, spec := range gd.Specs {
1114  			vs, ok := spec.(*ast.ValueSpec)
1115  			if !ok {
1116  				constSpecs = append(constSpecs, spec)
1117  				continue
1118  			}
1119  			allString := vs.Type == nil && len(vs.Values) > 0
1120  			if allString {
1121  				for _, val := range vs.Values {
1122  					if !isStringExpr(val) {
1123  						allString = false
1124  						break
1125  					}
1126  				}
1127  			}
1128  			if !allString {
1129  				constSpecs = append(constSpecs, vs)
1130  				continue
1131  			}
1132  			for i, val := range vs.Values {
1133  				if wrapped := wrapStringExpr(val); wrapped != nil {
1134  					vs.Values[i] = wrapped
1135  				}
1136  			}
1137  			for _, name := range vs.Names {
1138  				if name.Obj != nil {
1139  					name.Obj.Kind = ast.Var
1140  				}
1141  			}
1142  			varSpecs = append(varSpecs, vs)
1143  		}
1144  		if len(varSpecs) == 0 {
1145  			newDecls = append(newDecls, decl)
1146  			continue
1147  		}
1148  		varSpecs, constSpecs = cascadeDemotion(varSpecs, constSpecs)
1149  		if len(constSpecs) == 0 {
1150  			gd.Tok = token.VAR
1151  			gd.Specs = varSpecs
1152  			newDecls = append(newDecls, gd)
1153  			continue
1154  		}
1155  		// Mixed: emit a const block with non-string specs, then a var
1156  		// block with string specs.
1157  		gd.Specs = constSpecs
1158  		newDecls = append(newDecls, gd)
1159  		newDecls = append(newDecls, &ast.GenDecl{
1160  			Tok:   token.VAR,
1161  			Specs: varSpecs,
1162  		})
1163  	}
1164  	file.Decls = newDecls
1165  }
1166  
1167  // cascadeDemotion moves any remaining const spec whose value references a
1168  // name already demoted to var into the var specs. Because demoting a string
1169  // const to a []byte var makes len() on it non-constant, any const spec that
1170  // depends on such a name can no longer typecheck as const and must also
1171  // become var. Iterates to transitive closure.
1172  func cascadeDemotion(varSpecs, constSpecs []ast.Spec) ([]ast.Spec, []ast.Spec) {
1173  	demoted := map[string]bool{}
1174  	for _, spec := range varSpecs {
1175  		vs, ok := spec.(*ast.ValueSpec)
1176  		if !ok {
1177  			continue
1178  		}
1179  		for _, name := range vs.Names {
1180  			demoted[name.Name] = true
1181  		}
1182  	}
1183  	for {
1184  		var keep []ast.Spec
1185  		changed := false
1186  		for _, spec := range constSpecs {
1187  			vs, ok := spec.(*ast.ValueSpec)
1188  			if !ok {
1189  				keep = append(keep, spec)
1190  				continue
1191  			}
1192  			dep := false
1193  			for _, val := range vs.Values {
1194  				if referencesIdent(val, demoted) {
1195  					dep = true
1196  					break
1197  				}
1198  			}
1199  			if !dep {
1200  				keep = append(keep, spec)
1201  				continue
1202  			}
1203  			for _, name := range vs.Names {
1204  				if name.Obj != nil {
1205  					name.Obj.Kind = ast.Var
1206  				}
1207  				demoted[name.Name] = true
1208  			}
1209  			varSpecs = append(varSpecs, vs)
1210  			changed = true
1211  		}
1212  		constSpecs = keep
1213  		if !changed {
1214  			break
1215  		}
1216  	}
1217  	return varSpecs, constSpecs
1218  }
1219  
1220  // referencesIdent returns true if expr references any identifier in names.
1221  func referencesIdent(expr ast.Expr, names map[string]bool) bool {
1222  	if len(names) == 0 {
1223  		return false
1224  	}
1225  	found := false
1226  	ast.Inspect(expr, func(n ast.Node) bool {
1227  		if found {
1228  			return false
1229  		}
1230  		if id, ok := n.(*ast.Ident); ok && names[id.Name] {
1231  			found = true
1232  			return false
1233  		}
1234  		return true
1235  	})
1236  	return found
1237  }
1238  
1239  // splitBlockStmtConsts walks function bodies and splits function-scoped
1240  // const blocks the same way splitConstBlocks splits top-level const blocks.
1241  // Function-scoped consts appear as DeclStmt{Decl:&GenDecl{Tok:CONST}}.
1242  func splitBlockStmtConsts(file *ast.File) {
1243  	ast.Inspect(file, func(n ast.Node) bool {
1244  		block, ok := n.(*ast.BlockStmt)
1245  		if !ok {
1246  			return true
1247  		}
1248  		var newStmts []ast.Stmt
1249  		for _, stmt := range block.List {
1250  			ds, ok := stmt.(*ast.DeclStmt)
1251  			if !ok {
1252  				newStmts = append(newStmts, stmt)
1253  				continue
1254  			}
1255  			gd, ok := ds.Decl.(*ast.GenDecl)
1256  			if !ok || gd.Tok != token.CONST {
1257  				newStmts = append(newStmts, stmt)
1258  				continue
1259  			}
1260  			hasString := false
1261  			for _, spec := range gd.Specs {
1262  				vs, ok := spec.(*ast.ValueSpec)
1263  				if !ok {
1264  					continue
1265  				}
1266  				if vs.Type != nil {
1267  					continue
1268  				}
1269  				for _, val := range vs.Values {
1270  					if isStringExpr(val) {
1271  						hasString = true
1272  						break
1273  					}
1274  				}
1275  				if hasString {
1276  					break
1277  				}
1278  			}
1279  			if !hasString {
1280  				newStmts = append(newStmts, stmt)
1281  				continue
1282  			}
1283  			var varSpecs []ast.Spec
1284  			var constSpecs []ast.Spec
1285  			for _, spec := range gd.Specs {
1286  				vs, ok := spec.(*ast.ValueSpec)
1287  				if !ok {
1288  					constSpecs = append(constSpecs, spec)
1289  					continue
1290  				}
1291  				allString := vs.Type == nil && len(vs.Values) > 0
1292  				if allString {
1293  					for _, val := range vs.Values {
1294  						if !isStringExpr(val) {
1295  							allString = false
1296  							break
1297  						}
1298  					}
1299  				}
1300  				if !allString {
1301  					constSpecs = append(constSpecs, vs)
1302  					continue
1303  				}
1304  				for i, val := range vs.Values {
1305  					if wrapped := wrapStringExpr(val); wrapped != nil {
1306  						vs.Values[i] = wrapped
1307  					}
1308  				}
1309  				for _, name := range vs.Names {
1310  					if name.Obj != nil {
1311  						name.Obj.Kind = ast.Var
1312  					}
1313  				}
1314  				varSpecs = append(varSpecs, vs)
1315  			}
1316  			if len(varSpecs) == 0 {
1317  				newStmts = append(newStmts, stmt)
1318  				continue
1319  			}
1320  			varSpecs, constSpecs = cascadeDemotion(varSpecs, constSpecs)
1321  			if len(constSpecs) == 0 {
1322  				gd.Tok = token.VAR
1323  				gd.Specs = varSpecs
1324  				newStmts = append(newStmts, stmt)
1325  				continue
1326  			}
1327  			gd.Specs = constSpecs
1328  			newStmts = append(newStmts, stmt)
1329  			newStmts = append(newStmts, &ast.DeclStmt{
1330  				Decl: &ast.GenDecl{
1331  					Tok:   token.VAR,
1332  					Specs: varSpecs,
1333  				},
1334  			})
1335  		}
1336  		block.List = newStmts
1337  		return true
1338  	})
1339  }
1340  
1341  // funcReturnsString returns true if a FuncDecl has string in its return types.
1342  // isExemptPackageCall returns true if a call expression targets a function
1343  // from a package exempt from string rewrites (e.g. os.Open, errors.New before
1344  // conversion). These calls expect string parameters, not []byte.
1345  func isExemptPackageCall(call *ast.CallExpr) bool {
1346  	sel, ok := call.Fun.(*ast.SelectorExpr)
1347  	if !ok {
1348  		return false
1349  	}
1350  	ident, ok := sel.X.(*ast.Ident)
1351  	if !ok {
1352  		return false
1353  	}
1354  	return !IsMoxieStringTarget(ident.Name)
1355  }
1356  
1357  func funcReturnsString(fd *ast.FuncDecl) bool {
1358  	return funcTypeReturnsString(fd.Type)
1359  }
1360  
1361  // funcTypeReturnsString returns true if a FuncType has string in its return types.
1362  func funcTypeReturnsString(ft *ast.FuncType) bool {
1363  	if ft.Results == nil {
1364  		return false
1365  	}
1366  	for _, field := range ft.Results.List {
1367  		if ident, ok := field.Type.(*ast.Ident); ok && ident.Name == "string" {
1368  			return true
1369  		}
1370  	}
1371  	return false
1372  }
1373  
1374  // makeSliceByteCall creates an AST node for []byte(expr).
1375  func makeSliceByteCall(expr ast.Expr) *ast.CallExpr {
1376  	return &ast.CallExpr{
1377  		Fun: &ast.ArrayType{
1378  			Elt: &ast.Ident{Name: "byte"},
1379  		},
1380  		Args: []ast.Expr{expr},
1381  	}
1382  }
1383  
1384  // FindExemptCrossBoundaryMismatches walks the AST of an exempt package and
1385  // returns a list of argument expressions that need to be wrapped in
1386  // []byte(...) to match the callee's rewritten []byte signature. Requires
1387  // type info from a prior typecheck pass to inspect callee param types and
1388  // caller arg types.
1389  //
1390  // Pattern: pkg.Func(x) where pkg is non-exempt, the Func's param is []byte,
1391  // and x is of type string.
1392  func FindExemptCrossBoundaryMismatches(files []*ast.File, info *types.Info) []ast.Expr {
1393  	var result []ast.Expr
1394  	for _, file := range files {
1395  		imports := map[string]bool{}
1396  		for _, imp := range file.Imports {
1397  			path := strings.Trim(imp.Path.Value, "\"")
1398  			if imp.Name != nil {
1399  				imports[imp.Name.Name] = true
1400  				continue
1401  			}
1402  			name := path
1403  			if i := strings.LastIndex(path, "/"); i >= 0 {
1404  				name = path[i+1:]
1405  			}
1406  			imports[name] = true
1407  		}
1408  		ast.Inspect(file, func(n ast.Node) bool {
1409  			call, ok := n.(*ast.CallExpr)
1410  			if !ok {
1411  				return true
1412  			}
1413  			sel, ok := call.Fun.(*ast.SelectorExpr)
1414  			if !ok {
1415  				return true
1416  			}
1417  			pkgIdent, ok := sel.X.(*ast.Ident)
1418  			if !ok {
1419  				return true
1420  			}
1421  			if !imports[pkgIdent.Name] {
1422  				return true
1423  			}
1424  			if !IsMoxieStringTarget(pkgIdent.Name) {
1425  				return true
1426  			}
1427  			// Get callee signature.
1428  			tv, ok := info.Types[sel]
1429  			if !ok {
1430  				return true
1431  			}
1432  			sig, ok := tv.Type.(*types.Signature)
1433  			if !ok {
1434  				return true
1435  			}
1436  			params := sig.Params()
1437  			for i, arg := range call.Args {
1438  				if i >= params.Len() {
1439  					break // variadic overflow
1440  				}
1441  				paramType := params.At(i).Type()
1442  				// Only interested when param is []byte.
1443  				slice, ok := paramType.(*types.Slice)
1444  				if !ok {
1445  					continue
1446  				}
1447  				basic, ok := slice.Elem().(*types.Basic)
1448  				if !ok || basic.Kind() != types.Byte {
1449  					continue
1450  				}
1451  				// Check arg's type: string means mismatch.
1452  				argTV, ok := info.Types[arg]
1453  				if !ok {
1454  					continue
1455  				}
1456  				argBasic, ok := argTV.Type.(*types.Basic)
1457  				if !ok {
1458  					continue
1459  				}
1460  				if argBasic.Kind() == types.String || argBasic.Kind() == types.UntypedString {
1461  					// Already wrapped in []byte(...)?
1462  					if isSliceByteConversion(arg) {
1463  						continue
1464  					}
1465  					result = append(result, arg)
1466  				}
1467  			}
1468  			return true
1469  		})
1470  	}
1471  	return result
1472  }
1473  
1474  // ApplyExemptCrossBoundaryMismatches wraps each identified arg expression
1475  // in []byte(...). Identifies args by pointer equality — must be called
1476  // with the exact nodes returned by FindExemptCrossBoundaryMismatches.
1477  func ApplyExemptCrossBoundaryMismatches(files []*ast.File, exprs []ast.Expr) {
1478  	if len(exprs) == 0 {
1479  		return
1480  	}
1481  	targets := map[ast.Expr]bool{}
1482  	for _, e := range exprs {
1483  		targets[e] = true
1484  	}
1485  	// Remove from targets once wrapped to prevent infinite revisits.
1486  	for _, file := range files {
1487  		ast.Inspect(file, func(n ast.Node) bool {
1488  			call, ok := n.(*ast.CallExpr)
1489  			if !ok {
1490  				return true
1491  			}
1492  			for i, arg := range call.Args {
1493  				if targets[arg] {
1494  					call.Args[i] = makeSliceByteCall(arg)
1495  					delete(targets, arg)
1496  				}
1497  			}
1498  			return true
1499  		})
1500  	}
1501  }
1502  
1503  // makeStringCall wraps an expression in `string(...)`.
1504  func makeStringCall(expr ast.Expr) *ast.CallExpr {
1505  	return &ast.CallExpr{
1506  		Fun:  &ast.Ident{Name: "string"},
1507  		Args: []ast.Expr{expr},
1508  	}
1509  }
1510  
1511  // FindExemptStructLiteralMismatches walks the AST of an exempt package and
1512  // returns value expressions in struct literals whose field type is []byte
1513  // but the supplied value is string-typed (typically a literal). These need
1514  // wrapping in []byte(...) so stock go/types accepts them.
1515  //
1516  // Pattern: &PathError{Op: "readdir unimplemented"} inside os where PathError
1517  // comes from io/fs (non-exempt) and its Op field was rewritten to []byte.
1518  func FindExemptStructLiteralMismatches(files []*ast.File, info *types.Info) []ast.Expr {
1519  	var result []ast.Expr
1520  	for _, file := range files {
1521  		ast.Inspect(file, func(n ast.Node) bool {
1522  			cl, ok := n.(*ast.CompositeLit)
1523  			if !ok {
1524  				return true
1525  			}
1526  			tv, ok := info.Types[cl]
1527  			if !ok {
1528  				return true
1529  			}
1530  			t := tv.Type
1531  			for {
1532  				p, ok := t.(*types.Pointer)
1533  				if !ok {
1534  					break
1535  				}
1536  				t = p.Elem()
1537  			}
1538  			if t == nil {
1539  				return true
1540  			}
1541  			st, ok := t.Underlying().(*types.Struct)
1542  			if !ok {
1543  				return true
1544  			}
1545  			for i, elt := range cl.Elts {
1546  				var fieldType types.Type
1547  				var value ast.Expr
1548  				if kv, ok := elt.(*ast.KeyValueExpr); ok {
1549  					keyIdent, ok := kv.Key.(*ast.Ident)
1550  					if !ok {
1551  						continue
1552  					}
1553  					for j := 0; j < st.NumFields(); j++ {
1554  						if st.Field(j).Name() == keyIdent.Name {
1555  							fieldType = st.Field(j).Type()
1556  							break
1557  						}
1558  					}
1559  					value = kv.Value
1560  				} else {
1561  					if i >= st.NumFields() {
1562  						continue
1563  					}
1564  					fieldType = st.Field(i).Type()
1565  					value = elt
1566  				}
1567  				if fieldType == nil {
1568  					continue
1569  				}
1570  				slice, ok := fieldType.(*types.Slice)
1571  				if !ok {
1572  					continue
1573  				}
1574  				basic, ok := slice.Elem().(*types.Basic)
1575  				if !ok || basic.Kind() != types.Byte {
1576  					continue
1577  				}
1578  				vTV, ok := info.Types[value]
1579  				if !ok {
1580  					continue
1581  				}
1582  				vBasic, ok := vTV.Type.(*types.Basic)
1583  				if !ok {
1584  					continue
1585  				}
1586  				if vBasic.Kind() == types.String || vBasic.Kind() == types.UntypedString {
1587  					if isSliceByteConversion(value) {
1588  						continue
1589  					}
1590  					result = append(result, value)
1591  				}
1592  			}
1593  			return true
1594  		})
1595  	}
1596  	return result
1597  }
1598  
1599  // FindNonExemptReturnMismatches scans non-exempt package files for return
1600  // statements where the enclosing function's result type is []byte but the
1601  // returned expression is string-typed (typically from an interface-mandated
1602  // String() method that kept its string return). These need wrapping in
1603  // []byte(...).
1604  func FindNonExemptReturnMismatches(files []*ast.File, info *types.Info) []ast.Expr {
1605  	var result []ast.Expr
1606  	walk := func(sig *types.Signature, body *ast.BlockStmt) {
1607  		if sig == nil || body == nil {
1608  			return
1609  		}
1610  		results := sig.Results()
1611  		if results.Len() == 0 {
1612  			return
1613  		}
1614  		ast.Inspect(body, func(n ast.Node) bool {
1615  			// Don't descend into nested FuncLit — their returns bind to
1616  			// their own signature, handled separately.
1617  			if _, ok := n.(*ast.FuncLit); ok {
1618  				return false
1619  			}
1620  			ret, ok := n.(*ast.ReturnStmt)
1621  			if !ok {
1622  				return true
1623  			}
1624  			for i, expr := range ret.Results {
1625  				if i >= results.Len() {
1626  					break
1627  				}
1628  				rt := results.At(i).Type()
1629  				slice, ok := rt.(*types.Slice)
1630  				if !ok {
1631  					continue
1632  				}
1633  				basic, ok := slice.Elem().(*types.Basic)
1634  				if !ok || basic.Kind() != types.Byte {
1635  					continue
1636  				}
1637  				eTV, ok := info.Types[expr]
1638  				if !ok {
1639  					continue
1640  				}
1641  				eBasic, ok := eTV.Type.(*types.Basic)
1642  				if !ok {
1643  					continue
1644  				}
1645  				if eBasic.Kind() == types.String || eBasic.Kind() == types.UntypedString {
1646  					if isSliceByteConversion(expr) {
1647  						continue
1648  					}
1649  					result = append(result, expr)
1650  				}
1651  			}
1652  			return true
1653  		})
1654  	}
1655  	for _, file := range files {
1656  		ast.Inspect(file, func(n ast.Node) bool {
1657  			switch fn := n.(type) {
1658  			case *ast.FuncDecl:
1659  				if fn.Body == nil {
1660  					return true
1661  				}
1662  				obj := info.Defs[fn.Name]
1663  				if obj == nil {
1664  					return true
1665  				}
1666  				sig, _ := obj.Type().(*types.Signature)
1667  				walk(sig, fn.Body)
1668  			case *ast.FuncLit:
1669  				tv, ok := info.Types[fn]
1670  				if !ok {
1671  					return true
1672  				}
1673  				sig, _ := tv.Type.(*types.Signature)
1674  				walk(sig, fn.Body)
1675  			}
1676  			return true
1677  		})
1678  	}
1679  	return result
1680  }
1681  
1682  // ApplyNonExemptReturnMismatches wraps each identified return-expr in []byte(...).
1683  func ApplyNonExemptReturnMismatches(files []*ast.File, exprs []ast.Expr) {
1684  	if len(exprs) == 0 {
1685  		return
1686  	}
1687  	targets := map[ast.Expr]bool{}
1688  	for _, e := range exprs {
1689  		targets[e] = true
1690  	}
1691  	for _, file := range files {
1692  		ast.Inspect(file, func(n ast.Node) bool {
1693  			ret, ok := n.(*ast.ReturnStmt)
1694  			if !ok {
1695  				return true
1696  			}
1697  			for i, expr := range ret.Results {
1698  				if targets[expr] {
1699  					ret.Results[i] = makeSliceByteCall(expr)
1700  					delete(targets, expr)
1701  				}
1702  			}
1703  			return true
1704  		})
1705  	}
1706  }
1707  
1708  // AssignMismatch carries an assignment-RHS expression plus the direction of
1709  // the wrap needed: "toBytes" wraps in []byte(...), "toString" wraps in string(...).
1710  type AssignMismatch struct {
1711  	Expr ast.Expr
1712  	Kind string
1713  }
1714  
1715  // FindNonExemptAssignMismatches scans for `a = b` or `a, b = c, d` assigns
1716  // where the LHS and RHS straddle the string/[]byte boundary. Returns a list
1717  // of fixes keyed by RHS expression pointer.
1718  func FindNonExemptAssignMismatches(files []*ast.File, info *types.Info) []AssignMismatch {
1719  	var result []AssignMismatch
1720  	for _, file := range files {
1721  		ast.Inspect(file, func(n ast.Node) bool {
1722  			assign, ok := n.(*ast.AssignStmt)
1723  			if !ok {
1724  				return true
1725  			}
1726  			if assign.Tok != token.ASSIGN {
1727  				return true
1728  			}
1729  			if len(assign.Lhs) != len(assign.Rhs) {
1730  				return true
1731  			}
1732  			for i, lhs := range assign.Lhs {
1733  				lTV, ok := info.Types[lhs]
1734  				if !ok {
1735  					continue
1736  				}
1737  				rhs := assign.Rhs[i]
1738  				rTV, ok := info.Types[rhs]
1739  				if !ok {
1740  					continue
1741  				}
1742  				// LHS []byte, RHS string → wrap in []byte(...).
1743  				if lSlice, ok := lTV.Type.(*types.Slice); ok {
1744  					if lb, ok := lSlice.Elem().(*types.Basic); ok && lb.Kind() == types.Byte {
1745  						if rBasic, ok := rTV.Type.(*types.Basic); ok && (rBasic.Kind() == types.String || rBasic.Kind() == types.UntypedString) {
1746  							if !isSliceByteConversion(rhs) {
1747  								result = append(result, AssignMismatch{Expr: rhs, Kind: "toBytes"})
1748  							}
1749  							continue
1750  						}
1751  					}
1752  				}
1753  				// LHS string, RHS []byte → wrap in string(...).
1754  				if lBasic, ok := lTV.Type.(*types.Basic); ok && lBasic.Kind() == types.String {
1755  					if rSlice, ok := rTV.Type.(*types.Slice); ok {
1756  						if rb, ok := rSlice.Elem().(*types.Basic); ok && rb.Kind() == types.Byte {
1757  							if !isStringConversion(rhs) {
1758  								result = append(result, AssignMismatch{Expr: rhs, Kind: "toString"})
1759  							}
1760  						}
1761  					}
1762  				}
1763  			}
1764  			return true
1765  		})
1766  	}
1767  	return result
1768  }
1769  
1770  // ApplyNonExemptAssignMismatches wraps each identified RHS per its Kind.
1771  func ApplyNonExemptAssignMismatches(files []*ast.File, fixes []AssignMismatch) {
1772  	if len(fixes) == 0 {
1773  		return
1774  	}
1775  	targets := map[ast.Expr]string{}
1776  	for _, f := range fixes {
1777  		targets[f.Expr] = f.Kind
1778  	}
1779  	for _, file := range files {
1780  		ast.Inspect(file, func(n ast.Node) bool {
1781  			assign, ok := n.(*ast.AssignStmt)
1782  			if !ok {
1783  				return true
1784  			}
1785  			for i, rhs := range assign.Rhs {
1786  				if kind, ok := targets[rhs]; ok {
1787  					switch kind {
1788  					case "toBytes":
1789  						assign.Rhs[i] = makeSliceByteCall(rhs)
1790  					case "toString":
1791  						assign.Rhs[i] = makeStringCall(rhs)
1792  					}
1793  					delete(targets, rhs)
1794  				}
1795  			}
1796  			return true
1797  		})
1798  	}
1799  }
1800  
1801  // isStringConversion reports whether expr is already a `string(x)` call.
1802  func isStringConversion(expr ast.Expr) bool {
1803  	call, ok := expr.(*ast.CallExpr)
1804  	if !ok || len(call.Args) != 1 {
1805  		return false
1806  	}
1807  	id, ok := call.Fun.(*ast.Ident)
1808  	return ok && id.Name == "string"
1809  }
1810  
1811  // ByteConvFix describes a rewrite to apply to a `[]byte(x)` CallExpr that
1812  // was produced by RewriteStringConversions from a non-slice arg.
1813  //
1814  //	Kind "compLit": []byte(x) → []byte{x} (for byte/untypedInt args).
1815  //	Kind "revert":  []byte(x) → string(x) (for rune/int32/int args; phase 2
1816  //	                wrapping will re-wrap when flowing into []byte contexts).
1817  type ByteConvFix struct {
1818  	Call *ast.CallExpr
1819  	Kind string
1820  }
1821  
1822  // FindByteToSliceConversions scans for `[]byte(x)` calls where `x` is not a
1823  // slice or string. These arise from the aggressive `string(x)` → `[]byte(x)`
1824  // rewrite in RewriteStringConversions, which is correct for slice args but
1825  // breaks for single-byte/rune args.
1826  func FindByteToSliceConversions(files []*ast.File, info *types.Info) []ByteConvFix {
1827  	var result []ByteConvFix
1828  	for _, file := range files {
1829  		ast.Inspect(file, func(n ast.Node) bool {
1830  			call, ok := n.(*ast.CallExpr)
1831  			if !ok || len(call.Args) != 1 {
1832  				return true
1833  			}
1834  			at, ok := call.Fun.(*ast.ArrayType)
1835  			if !ok || at.Len != nil {
1836  				return true
1837  			}
1838  			elt, ok := at.Elt.(*ast.Ident)
1839  			if !ok || elt.Name != "byte" {
1840  				return true
1841  			}
1842  			argTV, ok := info.Types[call.Args[0]]
1843  			if !ok {
1844  				return true
1845  			}
1846  			basic, ok := argTV.Type.(*types.Basic)
1847  			if !ok {
1848  				return true
1849  			}
1850  			switch basic.Kind() {
1851  			case types.Byte, types.UntypedInt:
1852  				// byte (uint8): []byte{x} is exact single-byte slice.
1853  				result = append(result, ByteConvFix{Call: call, Kind: "compLit"})
1854  			case types.Int32, types.Int, types.UntypedRune:
1855  				// rune: revert to string(x) so UTF-8 semantics kick in.
1856  				// Phase 2 wrapping handles the []byte context.
1857  				result = append(result, ByteConvFix{Call: call, Kind: "revert"})
1858  			}
1859  			return true
1860  		})
1861  	}
1862  	return result
1863  }
1864  
1865  // ApplyByteToSliceConversions walks files and applies each ByteConvFix.
1866  func ApplyByteToSliceConversions(files []*ast.File, fixes []ByteConvFix) {
1867  	if len(fixes) == 0 {
1868  		return
1869  	}
1870  	targets := map[*ast.CallExpr]string{}
1871  	for _, f := range fixes {
1872  		targets[f.Call] = f.Kind
1873  	}
1874  	replace := func(e ast.Expr) ast.Expr {
1875  		ce, ok := e.(*ast.CallExpr)
1876  		if !ok {
1877  			return e
1878  		}
1879  		kind, ok := targets[ce]
1880  		if !ok {
1881  			return e
1882  		}
1883  		switch kind {
1884  		case "compLit":
1885  			return &ast.CompositeLit{
1886  				Type: &ast.ArrayType{Elt: ast.NewIdent("byte")},
1887  				Elts: []ast.Expr{ce.Args[0]},
1888  			}
1889  		case "revert":
1890  			// rune → UTF-8 bytes: []byte(string(rune)).
1891  			// Stock go/types accepts: string(rune) → string (UTF-8),
1892  			// []byte(string) → []byte.
1893  			return &ast.CallExpr{
1894  				Fun: &ast.ArrayType{Elt: ast.NewIdent("byte")},
1895  				Args: []ast.Expr{
1896  					&ast.CallExpr{
1897  						Fun:  ast.NewIdent("string"),
1898  						Args: []ast.Expr{ce.Args[0]},
1899  					},
1900  				},
1901  			}
1902  		}
1903  		return e
1904  	}
1905  	for _, file := range files {
1906  		ast.Inspect(file, func(n ast.Node) bool {
1907  			switch v := n.(type) {
1908  			case *ast.AssignStmt:
1909  				for i, r := range v.Rhs {
1910  					v.Rhs[i] = replace(r)
1911  				}
1912  			case *ast.ValueSpec:
1913  				for i, r := range v.Values {
1914  					v.Values[i] = replace(r)
1915  				}
1916  			case *ast.ReturnStmt:
1917  				for i, r := range v.Results {
1918  					v.Results[i] = replace(r)
1919  				}
1920  			case *ast.CallExpr:
1921  				for i, a := range v.Args {
1922  					v.Args[i] = replace(a)
1923  				}
1924  			case *ast.KeyValueExpr:
1925  				v.Value = replace(v.Value)
1926  			case *ast.BinaryExpr:
1927  				v.X = replace(v.X)
1928  				v.Y = replace(v.Y)
1929  			case *ast.UnaryExpr:
1930  				v.X = replace(v.X)
1931  			case *ast.ParenExpr:
1932  				v.X = replace(v.X)
1933  			case *ast.IndexExpr:
1934  				v.X = replace(v.X)
1935  				v.Index = replace(v.Index)
1936  			case *ast.SliceExpr:
1937  				v.X = replace(v.X)
1938  				if v.Low != nil {
1939  					v.Low = replace(v.Low)
1940  				}
1941  				if v.High != nil {
1942  					v.High = replace(v.High)
1943  				}
1944  				if v.Max != nil {
1945  					v.Max = replace(v.Max)
1946  				}
1947  			case *ast.CompositeLit:
1948  				for i, e := range v.Elts {
1949  					v.Elts[i] = replace(e)
1950  				}
1951  			case *ast.SelectorExpr:
1952  				v.X = replace(v.X)
1953  			case *ast.StarExpr:
1954  				v.X = replace(v.X)
1955  			case *ast.TypeAssertExpr:
1956  				v.X = replace(v.X)
1957  			case *ast.IncDecStmt:
1958  				v.X = replace(v.X)
1959  			case *ast.SendStmt:
1960  				v.Chan = replace(v.Chan)
1961  				v.Value = replace(v.Value)
1962  			case *ast.ExprStmt:
1963  				v.X = replace(v.X)
1964  			case *ast.ForStmt:
1965  				if v.Cond != nil {
1966  					v.Cond = replace(v.Cond)
1967  				}
1968  			case *ast.IfStmt:
1969  				if v.Cond != nil {
1970  					v.Cond = replace(v.Cond)
1971  				}
1972  			case *ast.SwitchStmt:
1973  				if v.Tag != nil {
1974  					v.Tag = replace(v.Tag)
1975  				}
1976  			case *ast.CaseClause:
1977  				for i, e := range v.List {
1978  					v.List[i] = replace(e)
1979  				}
1980  			case *ast.RangeStmt:
1981  				v.X = replace(v.X)
1982  			}
1983  			return true
1984  		})
1985  	}
1986  }
1987  
1988  // ApplyExemptStructLiteralMismatches wraps each identified struct-literal
1989  // value in []byte(...). Identifies by pointer equality — must be called
1990  // with the exact nodes returned by FindExemptStructLiteralMismatches.
1991  func ApplyExemptStructLiteralMismatches(files []*ast.File, exprs []ast.Expr) {
1992  	if len(exprs) == 0 {
1993  		return
1994  	}
1995  	targets := map[ast.Expr]bool{}
1996  	for _, e := range exprs {
1997  		targets[e] = true
1998  	}
1999  	for _, file := range files {
2000  		ast.Inspect(file, func(n ast.Node) bool {
2001  			cl, ok := n.(*ast.CompositeLit)
2002  			if !ok {
2003  				return true
2004  			}
2005  			for i, elt := range cl.Elts {
2006  				if kv, ok := elt.(*ast.KeyValueExpr); ok {
2007  					if targets[kv.Value] {
2008  						orig := kv.Value
2009  						kv.Value = makeSliceByteCall(kv.Value)
2010  						delete(targets, orig)
2011  					}
2012  				} else {
2013  					if targets[elt] {
2014  						cl.Elts[i] = makeSliceByteCall(elt)
2015  						delete(targets, elt)
2016  					}
2017  				}
2018  			}
2019  			return true
2020  		})
2021  	}
2022  }
2023  
2024  // NonExemptBoundaryFix describes an arg that needs wrapping; the Kind
2025  // field picks the wrap form.
2026  type NonExemptBoundaryFix struct {
2027  	Arg  ast.Expr
2028  	Kind string // "toString" or "toBytes"
2029  }
2030  
2031  // FindNonExemptCrossBoundaryMismatches walks the AST of a Moxie-target
2032  // (non-exempt) package and returns a list of call arguments that need a
2033  // type-bridge wrap to reconcile stock go/types with the Moxie string==[]byte
2034  // identity.
2035  //
2036  // Two directions are handled:
2037  //  1. []byte arg → string param: happens when calling into an exempt package
2038  //     whose signature still uses native Go string (e.g. syscall.Open). Wrap
2039  //     in `string(...)`.
2040  //  2. string arg → []byte param: happens when the arg came from an exempt
2041  //     package's return (e.g. runtime.GOROOT()) but is being passed to a
2042  //     non-exempt callee whose signature was rewritten to []byte. Wrap in
2043  //     `[]byte(...)`.
2044  func FindNonExemptCrossBoundaryMismatches(files []*ast.File, info *types.Info) []NonExemptBoundaryFix {
2045  	var result []NonExemptBoundaryFix
2046  	for _, file := range files {
2047  		ast.Inspect(file, func(n ast.Node) bool {
2048  			call, ok := n.(*ast.CallExpr)
2049  			if !ok {
2050  				return true
2051  			}
2052  			// Special-case builtin append: additional args after the slice
2053  			// must match the element type. If the slice is [][]byte and a
2054  			// subsequent arg is a string, wrap in []byte(...).
2055  			if id, ok := call.Fun.(*ast.Ident); ok && id.Name == "append" && len(call.Args) >= 2 {
2056  				if tv, ok := info.Types[call.Args[0]]; ok {
2057  					if slice, ok := tv.Type.(*types.Slice); ok {
2058  						if eb, ok := slice.Elem().(*types.Slice); ok {
2059  							if bb, ok := eb.Elem().(*types.Basic); ok && bb.Kind() == types.Byte {
2060  								// Element type is []byte; wrap string args.
2061  								for i := 1; i < len(call.Args); i++ {
2062  									arg := call.Args[i]
2063  									argTV, ok := info.Types[arg]
2064  									if !ok {
2065  										continue
2066  									}
2067  									if ab, ok := argTV.Type.(*types.Basic); ok && (ab.Kind() == types.String || ab.Kind() == types.UntypedString) {
2068  										if isSliceByteConversion(arg) {
2069  											continue
2070  										}
2071  										result = append(result, NonExemptBoundaryFix{Arg: arg, Kind: "toBytes"})
2072  									}
2073  								}
2074  							}
2075  						}
2076  					}
2077  				}
2078  				return true
2079  			}
2080  			// Special-case builtin delete(m, k): map keys stay as string
2081  			// after the []byte rewrite, so if k is []byte, wrap in string().
2082  			if id, ok := call.Fun.(*ast.Ident); ok && id.Name == "delete" && len(call.Args) == 2 {
2083  				if tv, ok := info.Types[call.Args[0]]; ok {
2084  					if m, ok := tv.Type.Underlying().(*types.Map); ok {
2085  						if kb, ok := m.Key().(*types.Basic); ok && kb.Kind() == types.String {
2086  							arg := call.Args[1]
2087  							if argTV, ok := info.Types[arg]; ok {
2088  								if slice, ok := argTV.Type.(*types.Slice); ok {
2089  									if bb, ok := slice.Elem().(*types.Basic); ok && bb.Kind() == types.Byte {
2090  										result = append(result, NonExemptBoundaryFix{Arg: arg, Kind: "toString"})
2091  									}
2092  								}
2093  							}
2094  						}
2095  					}
2096  				}
2097  				return true
2098  			}
2099  			var sig *types.Signature
2100  			switch fn := call.Fun.(type) {
2101  			case *ast.SelectorExpr:
2102  				if tv, ok := info.Types[fn]; ok {
2103  					sig, _ = tv.Type.(*types.Signature)
2104  				}
2105  			case *ast.Ident:
2106  				if tv, ok := info.Types[fn]; ok {
2107  					sig, _ = tv.Type.(*types.Signature)
2108  				}
2109  			}
2110  			if sig == nil {
2111  				return true
2112  			}
2113  			params := sig.Params()
2114  			for i, arg := range call.Args {
2115  				if i >= params.Len() {
2116  					break
2117  				}
2118  				paramType := params.At(i).Type()
2119  				argTV, ok := info.Types[arg]
2120  				if !ok {
2121  					continue
2122  				}
2123  				// paramString := paramType is *types.Basic with Kind string
2124  				if pb, ok := paramType.(*types.Basic); ok && pb.Kind() == types.String {
2125  					// Arg should be []byte; if so, wrap in string.
2126  					if slice, ok := argTV.Type.(*types.Slice); ok {
2127  						if bb, ok := slice.Elem().(*types.Basic); ok && bb.Kind() == types.Byte {
2128  							// Already string(...)?
2129  							if c, ok := arg.(*ast.CallExpr); ok {
2130  								if id, ok := c.Fun.(*ast.Ident); ok && id.Name == "string" && len(c.Args) == 1 {
2131  									continue
2132  								}
2133  							}
2134  							result = append(result, NonExemptBoundaryFix{Arg: arg, Kind: "toString"})
2135  						}
2136  					}
2137  					continue
2138  				}
2139  				// paramType is []byte?
2140  				if slice, ok := paramType.(*types.Slice); ok {
2141  					if bb, ok := slice.Elem().(*types.Basic); ok && bb.Kind() == types.Byte {
2142  						// Arg should be string; if so, wrap in []byte.
2143  						if ab, ok := argTV.Type.(*types.Basic); ok && (ab.Kind() == types.String || ab.Kind() == types.UntypedString) {
2144  							if isSliceByteConversion(arg) {
2145  								continue
2146  							}
2147  							result = append(result, NonExemptBoundaryFix{Arg: arg, Kind: "toBytes"})
2148  						}
2149  					}
2150  				}
2151  			}
2152  			return true
2153  		})
2154  	}
2155  	return result
2156  }
2157  
2158  // ApplyNonExemptCrossBoundaryMismatches wraps each identified arg expression
2159  // per its Kind: `string(...)` for toString, `[]byte(...)` for toBytes.
2160  // Identifies args by pointer equality — must be called with the exact nodes
2161  // returned by FindNonExemptCrossBoundaryMismatches.
2162  func ApplyNonExemptCrossBoundaryMismatches(files []*ast.File, fixes []NonExemptBoundaryFix) {
2163  	if len(fixes) == 0 {
2164  		return
2165  	}
2166  	targets := map[ast.Expr]string{}
2167  	for _, f := range fixes {
2168  		targets[f.Arg] = f.Kind
2169  	}
2170  	for _, file := range files {
2171  		ast.Inspect(file, func(n ast.Node) bool {
2172  			call, ok := n.(*ast.CallExpr)
2173  			if !ok {
2174  				return true
2175  			}
2176  			for i, arg := range call.Args {
2177  				if kind, ok := targets[arg]; ok {
2178  					switch kind {
2179  					case "toString":
2180  						call.Args[i] = makeStringCall(arg)
2181  					case "toBytes":
2182  						call.Args[i] = makeSliceByteCall(arg)
2183  					}
2184  					delete(targets, arg)
2185  				}
2186  			}
2187  			return true
2188  		})
2189  	}
2190  }
2191  
2192  // RewriteExemptCrossBoundaryCalls wraps string literals passed as arguments
2193  // to calls that cross from an exempt package (e.g. syscall, os, runtime)
2194  // into a non-exempt package whose signatures have already been rewritten to
2195  // []byte. The exempt package keeps native Go string types internally, but
2196  // its calls into errors.New/fmt.Errorf/etc must match the rewritten []byte
2197  // signatures seen by stock go/types.
2198  func RewriteExemptCrossBoundaryCalls(file *ast.File) {
2199  	// Collect import names that are in scope (both explicit names and the
2200  	// trailing path component of each import). Local identifiers that don't
2201  	// match an import must not be treated as package references.
2202  	imports := map[string]bool{}
2203  	for _, imp := range file.Imports {
2204  		path := strings.Trim(imp.Path.Value, "\"")
2205  		if imp.Name != nil {
2206  			imports[imp.Name.Name] = true
2207  			continue
2208  		}
2209  		// Default package ident is the last path segment.
2210  		name := path
2211  		if i := strings.LastIndex(path, "/"); i >= 0 {
2212  			name = path[i+1:]
2213  		}
2214  		imports[name] = true
2215  	}
2216  	ast.Inspect(file, func(n ast.Node) bool {
2217  		call, ok := n.(*ast.CallExpr)
2218  		if !ok {
2219  			return true
2220  		}
2221  		sel, ok := call.Fun.(*ast.SelectorExpr)
2222  		if !ok {
2223  			return true
2224  		}
2225  		pkgIdent, ok := sel.X.(*ast.Ident)
2226  		if !ok {
2227  			return true
2228  		}
2229  		// Only wrap when pkgIdent is actually an imported package name.
2230  		if !imports[pkgIdent.Name] {
2231  			return true
2232  		}
2233  		// Only wrap when calling into a NON-exempt package.
2234  		if !IsMoxieStringTarget(pkgIdent.Name) {
2235  			return true
2236  		}
2237  		// Wrap only string literals. Wrapping arbitrary identifiers would
2238  		// mis-type args like uintptr or int. Variable-typed string args
2239  		// are handled in a type-info-driven second pass.
2240  		for i, arg := range call.Args {
2241  			if lit, ok := arg.(*ast.BasicLit); ok && lit.Kind == token.STRING {
2242  				call.Args[i] = makeSliceByteCall(lit)
2243  			}
2244  		}
2245  		return true
2246  	})
2247  }
2248  
2249  // RewriteTextConcat converts syntactically-detectable text concatenations
2250  // (ADD or OR between text operands) to __moxie_concat(X, Y) calls and
2251  // syntactic text comparisons (EQL/NEQ/LSS/LEQ/GTR/GEQ) to __moxie_eq /
2252  // __moxie_lt calls, BEFORE typecheck. Runs before the patched go/types
2253  // that accepts []byte ops.
2254  //
2255  // Runs after RewriteStringLiterals so stringlit operands are already wrapped
2256  // in []byte(...). An operand is considered text when it is:
2257  //   - []byte(...) CallExpr (including the wraps from RewriteStringLiterals)
2258  //   - a ParenExpr whose inner expr is text
2259  //   - an existing __moxie_concat(...) call
2260  func RewriteTextConcat(file *ast.File) {
2261  	replace := func(expr ast.Expr) ast.Expr {
2262  		return rewriteTextConcatExpr(expr)
2263  	}
2264  	ast.Inspect(file, func(n ast.Node) bool {
2265  		// Skip const declarations — __moxie_concat/__moxie_eq/__moxie_lt
2266  		// are runtime calls and cannot appear in const expressions.
2267  		// `const X = GOARCH == "amd64" || ...` must stay Go-native.
2268  		if gd, ok := n.(*ast.GenDecl); ok && gd.Tok == token.CONST {
2269  			return false
2270  		}
2271  		// Skip `[]byte(...)` casts whose body is purely string literals
2272  		// so stock Go can still constant-fold `[]byte("a"+"b")`. If the
2273  		// body references a variable (as in `[]byte("prefix" | x)` where
2274  		// x was rewritten to []byte), rewrite the inner concat so stock
2275  		// go/types accepts it.
2276  		if call, ok := n.(*ast.CallExpr); ok && isSliceByteConversion(call) {
2277  			if allStringLitBinaryArg(call) {
2278  				return false
2279  			}
2280  			// Unwrap: `[]byte(X + Y)` → replace with the rewritten X ⊕ Y
2281  			// (which will be a __moxie_concat(...) call returning []byte,
2282  			// so the outer []byte(...) wrap is redundant). We transform in
2283  			// place via the parent-node replacements below.
2284  		}
2285  		switch parent := n.(type) {
2286  		case *ast.AssignStmt:
2287  			for i := range parent.Rhs {
2288  				parent.Rhs[i] = replace(parent.Rhs[i])
2289  			}
2290  		case *ast.ValueSpec:
2291  			for i := range parent.Values {
2292  				parent.Values[i] = replace(parent.Values[i])
2293  			}
2294  		case *ast.ReturnStmt:
2295  			for i := range parent.Results {
2296  				parent.Results[i] = replace(parent.Results[i])
2297  			}
2298  		case *ast.CallExpr:
2299  			for i := range parent.Args {
2300  				parent.Args[i] = replace(parent.Args[i])
2301  			}
2302  		case *ast.SendStmt:
2303  			parent.Value = replace(parent.Value)
2304  		case *ast.BinaryExpr:
2305  			parent.X = replace(parent.X)
2306  			parent.Y = replace(parent.Y)
2307  		case *ast.ParenExpr:
2308  			parent.X = replace(parent.X)
2309  		case *ast.IndexExpr:
2310  			parent.Index = replace(parent.Index)
2311  		case *ast.KeyValueExpr:
2312  			parent.Value = replace(parent.Value)
2313  		case *ast.CompositeLit:
2314  			for i := range parent.Elts {
2315  				parent.Elts[i] = replace(parent.Elts[i])
2316  			}
2317  		case *ast.IfStmt:
2318  			if parent.Cond != nil {
2319  				parent.Cond = replace(parent.Cond)
2320  			}
2321  		case *ast.ForStmt:
2322  			if parent.Cond != nil {
2323  				parent.Cond = replace(parent.Cond)
2324  			}
2325  		case *ast.SwitchStmt:
2326  			if parent.Tag != nil {
2327  				parent.Tag = replace(parent.Tag)
2328  			}
2329  		case *ast.CaseClause:
2330  			for i := range parent.List {
2331  				parent.List[i] = replace(parent.List[i])
2332  			}
2333  		case *ast.ExprStmt:
2334  			parent.X = replace(parent.X)
2335  		case *ast.IncDecStmt:
2336  			parent.X = replace(parent.X)
2337  		case *ast.UnaryExpr:
2338  			parent.X = replace(parent.X)
2339  		case *ast.StarExpr:
2340  			parent.X = replace(parent.X)
2341  		case *ast.SliceExpr:
2342  			if parent.Low != nil {
2343  				parent.Low = replace(parent.Low)
2344  			}
2345  			if parent.High != nil {
2346  				parent.High = replace(parent.High)
2347  			}
2348  			if parent.Max != nil {
2349  				parent.Max = replace(parent.Max)
2350  			}
2351  		case *ast.TypeAssertExpr:
2352  			parent.X = replace(parent.X)
2353  		}
2354  		return true
2355  	})
2356  }
2357  
2358  // rewriteTextConcatExpr recursively transforms BinaryExpr ADD/OR nodes whose
2359  // operands are syntactically text into __moxie_concat calls, and comparison
2360  // BinaryExprs (EQL/NEQ/LSS/LEQ/GTR/GEQ) whose operands are syntactically
2361  // text into __moxie_eq / __moxie_lt calls. Returns the rewritten expression
2362  // (or the original when no rewrite applies).
2363  func rewriteTextConcatExpr(expr ast.Expr) ast.Expr {
2364  	if expr == nil {
2365  		return expr
2366  	}
2367  	switch e := expr.(type) {
2368  	case *ast.BinaryExpr:
2369  		e.X = rewriteTextConcatExpr(e.X)
2370  		e.Y = rewriteTextConcatExpr(e.Y)
2371  		switch e.Op {
2372  		case token.ADD, token.OR:
2373  			xText := isSyntacticTextExpr(e.X)
2374  			yText := isSyntacticTextExpr(e.Y)
2375  			// Only rewrite when at least one side is syntactically
2376  			// text. This keeps bitwise `|` on user-defined int
2377  			// types intact (e.g. `Int16(b[0]) | Int16(b[1])<<8`)
2378  			// while correctly catching text concat (since
2379  			// RewriteStringLiterals has already wrapped every
2380  			// stringlit in `[]byte(...)`).
2381  			if !xText && !yText {
2382  				return e
2383  			}
2384  			// Once we commit to rewriting (because at least one side
2385  			// is text), any nested `+`/`|` on the other side must also
2386  			// be part of a text concat chain. Force-rewrite them so
2387  			// `out + short + []byte(":")` doesn't leave an inner
2388  			// `out + short` BinaryExpr under a `[]byte(...)` wrap,
2389  			// which fails stock go/types as `[]byte + string`.
2390  			e.X = forceTextConcat(e.X)
2391  			e.Y = forceTextConcat(e.Y)
2392  			// Wrap non-text operands in `[]byte(...)` so the
2393  			// __moxie_concat([]byte, []byte) signature is satisfied
2394  			// regardless of whether the operand is string-typed (method
2395  			// calls like e.Err.Error()) or []byte-typed (field accesses
2396  			// post-RewriteStringTypes). Identity conversion for []byte,
2397  			// explicit conversion for string — both accepted by
2398  			// stock go/types.
2399  			return &ast.CallExpr{
2400  				Fun:  &ast.Ident{Name: "__moxie_concat"},
2401  				Args: []ast.Expr{wrapForMoxieConcat(e.X), wrapForMoxieConcat(e.Y)},
2402  			}
2403  		case token.EQL, token.NEQ, token.LSS, token.LEQ, token.GTR, token.GEQ:
2404  			// Slice comparison rewrite: same syntactic rule. If
2405  			// either operand is syntactically text, rewrite so the
2406  			// standard typechecker doesn't reject slice-to-slice
2407  			// comparison.
2408  			if !isSyntacticTextExpr(e.X) && !isSyntacticTextExpr(e.Y) {
2409  				return e
2410  			}
2411  			return rewriteTextCompare(e)
2412  		}
2413  		return e
2414  	case *ast.ParenExpr:
2415  		e.X = rewriteTextConcatExpr(e.X)
2416  		return e
2417  	case *ast.CallExpr:
2418  		// Don't descend into `[]byte(...)` casts — see the walker comment.
2419  		if isSliceByteConversion(e) {
2420  			return e
2421  		}
2422  		for i := range e.Args {
2423  			e.Args[i] = rewriteTextConcatExpr(e.Args[i])
2424  		}
2425  		return e
2426  	case *ast.UnaryExpr:
2427  		e.X = rewriteTextConcatExpr(e.X)
2428  		return e
2429  	}
2430  	return expr
2431  }
2432  
2433  // wrapForMoxieConcat wraps an expression in `[]byte(...)` unless it's
2434  // already a syntactic text expression. Used when constructing arguments
2435  // to __moxie_concat so that operands like `e.Err.Error()` (string-returning
2436  // method calls) or `e.Func` (named []byte field accesses) end up as []byte
2437  // per stock go/types — Moxie's identity-conversion rule lets []byte→[]byte
2438  // pass too.
2439  func wrapForMoxieConcat(e ast.Expr) ast.Expr {
2440  	if isSyntacticTextExpr(e) {
2441  		return e
2442  	}
2443  	return &ast.CallExpr{
2444  		Fun:  &ast.ArrayType{Elt: ast.NewIdent("byte")},
2445  		Args: []ast.Expr{e},
2446  	}
2447  }
2448  
2449  // allStringLitBinaryArg reports whether `call` is a `[]byte(X)` cast whose
2450  // body X is a tree of ADD/OR binary expressions over string literals only.
2451  // In that case stock Go can constant-fold (`[]byte("a"+"b")`), so the
2452  // walker must NOT descend and rewrite the inner `+` to __moxie_concat.
2453  // Anything else — an Ident, SelectorExpr, CallExpr, etc. — means the body
2454  // references a variable and must be lowered.
2455  func allStringLitBinaryArg(call *ast.CallExpr) bool {
2456  	if len(call.Args) != 1 {
2457  		return false
2458  	}
2459  	var check func(e ast.Expr) bool
2460  	check = func(e ast.Expr) bool {
2461  		switch x := e.(type) {
2462  		case *ast.BasicLit:
2463  			return x.Kind == token.STRING
2464  		case *ast.BinaryExpr:
2465  			if x.Op != token.ADD && x.Op != token.OR {
2466  				return false
2467  			}
2468  			return check(x.X) && check(x.Y)
2469  		case *ast.ParenExpr:
2470  			return check(x.X)
2471  		}
2472  		return false
2473  	}
2474  	return check(call.Args[0])
2475  }
2476  
2477  // forceTextConcat rewrites any ADD/OR BinaryExpr inside expr as a
2478  // __moxie_concat call, even when neither operand is syntactically text.
2479  // Used by rewriteTextConcatExpr when the enclosing expression has already
2480  // committed to a text-concat rewrite, so nested `+`/`|` chains between
2481  // variables (whose types we don't know yet) must be lowered too.
2482  func forceTextConcat(expr ast.Expr) ast.Expr {
2483  	if expr == nil {
2484  		return expr
2485  	}
2486  	switch e := expr.(type) {
2487  	case *ast.BinaryExpr:
2488  		if e.Op == token.ADD || e.Op == token.OR {
2489  			e.X = forceTextConcat(e.X)
2490  			e.Y = forceTextConcat(e.Y)
2491  			return &ast.CallExpr{
2492  				Fun:  &ast.Ident{Name: "__moxie_concat"},
2493  				Args: []ast.Expr{wrapForMoxieConcat(e.X), wrapForMoxieConcat(e.Y)},
2494  			}
2495  		}
2496  		return e
2497  	case *ast.ParenExpr:
2498  		e.X = forceTextConcat(e.X)
2499  		return e
2500  	}
2501  	return expr
2502  }
2503  
2504  // rewriteTextCompare lowers a BinaryExpr comparison between syntactically
2505  // text operands to the appropriate __moxie_eq / __moxie_lt form.
2506  func rewriteTextCompare(e *ast.BinaryExpr) ast.Expr {
2507  	// Wrap non-text operands in []byte(...) so __moxie_eq / __moxie_lt's
2508  	// []byte parameters see consistent types. Mirrors wrapForMoxieConcat.
2509  	// Handles the common case of cross-package selectors like runtime.GOOS
2510  	// (untyped string constant from exempt runtime) being compared to a
2511  	// string literal that's already been wrapped as []byte(...).
2512  	x := wrapForMoxieConcat(e.X)
2513  	y := wrapForMoxieConcat(e.Y)
2514  	eq := func(x, y ast.Expr) *ast.CallExpr {
2515  		return &ast.CallExpr{Fun: &ast.Ident{Name: "__moxie_eq"}, Args: []ast.Expr{x, y}}
2516  	}
2517  	lt := func(x, y ast.Expr) *ast.CallExpr {
2518  		return &ast.CallExpr{Fun: &ast.Ident{Name: "__moxie_lt"}, Args: []ast.Expr{x, y}}
2519  	}
2520  	not := func(x ast.Expr) *ast.UnaryExpr { return &ast.UnaryExpr{Op: token.NOT, X: x} }
2521  	switch e.Op {
2522  	case token.EQL:
2523  		return eq(x, y)
2524  	case token.NEQ:
2525  		return not(eq(x, y))
2526  	case token.LSS:
2527  		return lt(x, y)
2528  	case token.LEQ:
2529  		return not(lt(y, x))
2530  	case token.GTR:
2531  		return lt(y, x)
2532  	case token.GEQ:
2533  		return not(lt(x, y))
2534  	}
2535  	return e
2536  }
2537  
2538  // isSyntacticTextExpr returns true when the expression is recognisable as a
2539  // text value before typecheck: a []byte(...) conversion, a string literal,
2540  // a __moxie_concat call, a slice expression (which in Moxie-target packages
2541  // almost always yields a []byte sub-slice), or a parenthesised text expression.
2542  func isSyntacticTextExpr(expr ast.Expr) bool {
2543  	switch e := expr.(type) {
2544  	case *ast.BasicLit:
2545  		return e.Kind == token.STRING
2546  	case *ast.CallExpr:
2547  		if isSliceByteConversion(e) {
2548  			return true
2549  		}
2550  		if isMoxieConcatCall(e) {
2551  			return true
2552  		}
2553  	case *ast.ParenExpr:
2554  		return isSyntacticTextExpr(e.X)
2555  	case *ast.SliceExpr:
2556  		// foo[i:j] — in Moxie-target packages, slicing produces a
2557  		// []byte sub-slice for string/[]byte variables. Treating it
2558  		// as text lets comparisons like `line[i:i+n] == prefix`
2559  		// rewrite to __moxie_eq without post-typecheck type info.
2560  		return true
2561  	}
2562  	return false
2563  }
2564  
2565  // ---------------------------------------------------------------------------
2566  // 2b. Builtin int→int32 wrapping (AST-level, after parsing, before typecheck)
2567  // ---------------------------------------------------------------------------
2568  
2569  // rewriteBuiltinIntReturns wraps len(), cap(), and copy() calls in int32()
2570  // conversions. These builtins return int (from Go's universe), but Moxie
2571  // uses int32 as the standard sized integer. Without this wrapping, mixing
2572  // len() results with int32 values causes type checker errors.
2573  //
2574  //	len(x)       → int32(len(x))
2575  //	cap(x)       → int32(cap(x))
2576  //	copy(dst,src)→ int32(copy(dst,src))
2577  func rewriteBuiltinIntReturns(file *ast.File) {
2578  	// Builtins whose return type is int.
2579  	intBuiltins := map[string]bool{"len": true, "cap": true, "copy": true}
2580  
2581  	ast.Inspect(file, func(n ast.Node) bool {
2582  		// Skip const declarations — const expressions must stay untyped.
2583  		if gd, ok := n.(*ast.GenDecl); ok && gd.Tok == token.CONST {
2584  			return false
2585  		}
2586  		// Don't descend into int32() wrappers we just created — prevents
2587  		// infinite recursion (walker would find len() inside and re-wrap).
2588  		if call, ok := n.(*ast.CallExpr); ok {
2589  			if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "int32" {
2590  				return false
2591  			}
2592  		}
2593  		switch parent := n.(type) {
2594  		case *ast.AssignStmt:
2595  			for i, rhs := range parent.Rhs {
2596  				if wrapped := wrapIntBuiltin(rhs, intBuiltins); wrapped != nil {
2597  					parent.Rhs[i] = wrapped
2598  				}
2599  			}
2600  		case *ast.ValueSpec:
2601  			for i, val := range parent.Values {
2602  				if wrapped := wrapIntBuiltin(val, intBuiltins); wrapped != nil {
2603  					parent.Values[i] = wrapped
2604  				}
2605  			}
2606  		case *ast.ReturnStmt:
2607  			for i, result := range parent.Results {
2608  				if wrapped := wrapIntBuiltin(result, intBuiltins); wrapped != nil {
2609  					parent.Results[i] = wrapped
2610  				}
2611  			}
2612  		case *ast.CallExpr:
2613  			for i, arg := range parent.Args {
2614  				if wrapped := wrapIntBuiltin(arg, intBuiltins); wrapped != nil {
2615  					parent.Args[i] = wrapped
2616  				}
2617  			}
2618  		case *ast.BinaryExpr:
2619  			if wrapped := wrapIntBuiltin(parent.X, intBuiltins); wrapped != nil {
2620  				parent.X = wrapped
2621  			}
2622  			if wrapped := wrapIntBuiltin(parent.Y, intBuiltins); wrapped != nil {
2623  				parent.Y = wrapped
2624  			}
2625  		case *ast.IndexExpr:
2626  			if wrapped := wrapIntBuiltin(parent.Index, intBuiltins); wrapped != nil {
2627  				parent.Index = wrapped
2628  			}
2629  		case *ast.SendStmt:
2630  			if wrapped := wrapIntBuiltin(parent.Value, intBuiltins); wrapped != nil {
2631  				parent.Value = wrapped
2632  			}
2633  		case *ast.KeyValueExpr:
2634  			if wrapped := wrapIntBuiltin(parent.Value, intBuiltins); wrapped != nil {
2635  				parent.Value = wrapped
2636  			}
2637  		}
2638  		return true
2639  	})
2640  }
2641  
2642  // wrapIntBuiltin checks if expr is a call to a builtin that returns int,
2643  // and if so, wraps it in int32(). Returns nil if no wrapping needed.
2644  func wrapIntBuiltin(expr ast.Expr, builtins map[string]bool) ast.Expr {
2645  	call, ok := expr.(*ast.CallExpr)
2646  	if !ok {
2647  		return nil
2648  	}
2649  	ident, ok := call.Fun.(*ast.Ident)
2650  	if !ok || !builtins[ident.Name] {
2651  		return nil
2652  	}
2653  	// Already wrapped in int32() — don't double-wrap.
2654  	// (Check grandparent, but simpler: check if Fun is already int32.)
2655  	return &ast.CallExpr{
2656  		Fun:  &ast.Ident{Name: "int32"},
2657  		Args: []ast.Expr{call},
2658  	}
2659  }
2660  
2661  // ---------------------------------------------------------------------------
2662  // 3. Pipe concatenation rewrite (AST-level, after first typecheck pass)
2663  // ---------------------------------------------------------------------------
2664  
2665  // RewriteConstPipes changes | to + in all expressions where operands are
2666  // string literals. Go's type checker panics on | applied to untyped strings
2667  // (it's bitwise OR), so this must run before typecheck. The constPipeToAdd
2668  // helper only rewrites when all leaves are literals, so mixed chains
2669  // (containing vars/calls) are left for the post-typecheck FindPipeConcat pass.
2670  func RewriteConstPipes(files []*ast.File) {
2671  	for _, file := range files {
2672  		ast.Inspect(file, func(n ast.Node) bool {
2673  			if bin, ok := n.(*ast.BinaryExpr); ok {
2674  				constPipeToAdd(bin)
2675  			}
2676  			return true
2677  		})
2678  	}
2679  }
2680  
2681  // constPipeToAdd recursively rewrites | to + in binary expressions
2682  // where all leaves are string literals.
2683  func constPipeToAdd(e ast.Expr) bool {
2684  	switch n := e.(type) {
2685  	case *ast.BasicLit:
2686  		return n.Kind == token.STRING
2687  	case *ast.BinaryExpr:
2688  		xLit := constPipeToAdd(n.X)
2689  		yLit := constPipeToAdd(n.Y)
2690  		if xLit && yLit && n.Op == token.OR {
2691  			n.Op = token.ADD
2692  		}
2693  		return xLit && yLit
2694  	case *ast.ParenExpr:
2695  		return constPipeToAdd(n.X)
2696  	}
2697  	return false
2698  }
2699  
2700  // PipeRewrite records a | expression that should become __moxie_concat.
2701  type PipeRewrite struct {
2702  	parent ast.Node
2703  	expr   *ast.BinaryExpr
2704  }
2705  
2706  // findPipeConcat walks the AST and finds | and + expressions where operands
2707  // are []byte, using type information from a completed typecheck pass.
2708  // Catches both explicit | (pipe concat) and + (string concat that wasn't
2709  // converted by mxpurify).
2710  func FindPipeConcat(files []*ast.File, info *types.Info) []PipeRewrite {
2711  	var rewrites []PipeRewrite
2712  	for _, file := range files {
2713  		ast.Inspect(file, func(n ast.Node) bool {
2714  			// Don't descend into const declarations — __moxie_concat is a
2715  			// runtime call and cannot appear in const expressions.
2716  			if gd, ok := n.(*ast.GenDecl); ok && gd.Tok == token.CONST {
2717  				return false
2718  			}
2719  			// Skip `[]byte(...)` casts ONLY when the inner expression
2720  			// is fully literal — Go's constant folder will merge
2721  			// `"a" + "b"` at compile time. When the cast wraps a
2722  			// mixed chain (containing vars, calls, etc. — e.g.
2723  			// `[]byte(k | ": " | v)`), descend so we can convert
2724  			// `|`/`+` to `__moxie_concat` at runtime.
2725  			if call, ok := n.(*ast.CallExpr); ok && isSliceByteConversion(call) {
2726  				if len(call.Args) == 1 && isFullyLiteralChain(call.Args[0]) {
2727  					return false
2728  				}
2729  			}
2730  			bin, ok := n.(*ast.BinaryExpr)
2731  			if !ok || (bin.Op != token.OR && bin.Op != token.ADD) {
2732  				return true
2733  			}
2734  			xType := info.TypeOf(bin.X)
2735  			yType := info.TypeOf(bin.Y)
2736  			xOk := (xType != nil && isTextType(xType)) || isSliceByteConversion(bin.X) || isMoxieConcatCall(bin.X) || isStringLit(bin.X)
2737  			yOk := (yType != nil && isTextType(yType)) || isSliceByteConversion(bin.Y) || isMoxieConcatCall(bin.Y) || isStringLit(bin.Y)
2738  			if bin.Op == token.OR || bin.Op == token.ADD {
2739  				if !xOk && yOk {
2740  					if inner, ok := bin.X.(*ast.BinaryExpr); ok && (inner.Op == token.OR || inner.Op == token.ADD) {
2741  						xOk = true
2742  					}
2743  					if _, ok := bin.X.(*ast.Ident); ok {
2744  						xOk = true
2745  					}
2746  				}
2747  				if !yOk && xOk {
2748  					if inner, ok := bin.Y.(*ast.BinaryExpr); ok && (inner.Op == token.OR || inner.Op == token.ADD) {
2749  						yOk = true
2750  					}
2751  					if _, ok := bin.Y.(*ast.Ident); ok {
2752  						yOk = true
2753  					}
2754  				}
2755  			}
2756  			if xOk && yOk {
2757  				rewrites = append(rewrites, PipeRewrite{expr: bin})
2758  			}
2759  			return true
2760  		})
2761  	}
2762  	return rewrites
2763  }
2764  
2765  // isStringLit returns true if e is a string literal (token.STRING).
2766  func isStringLit(e ast.Expr) bool {
2767  	lit, ok := e.(*ast.BasicLit)
2768  	return ok && lit.Kind == token.STRING
2769  }
2770  
2771  // isFullyLiteralChain returns true if e is entirely string literals joined
2772  // by + or | — i.e. a chain Go's constant folder can collapse. Used to decide
2773  // whether to preserve `[]byte(...)` wraps untouched (fold) or descend into
2774  // them for `__moxie_concat` rewriting (runtime-only ops like var operands).
2775  func isFullyLiteralChain(e ast.Expr) bool {
2776  	switch n := e.(type) {
2777  	case *ast.BasicLit:
2778  		return n.Kind == token.STRING
2779  	case *ast.BinaryExpr:
2780  		if n.Op != token.ADD && n.Op != token.OR {
2781  			return false
2782  		}
2783  		return isFullyLiteralChain(n.X) && isFullyLiteralChain(n.Y)
2784  	case *ast.ParenExpr:
2785  		return isFullyLiteralChain(n.X)
2786  	}
2787  	return false
2788  }
2789  
2790  // CheckPlusOnText walks the AST and returns errors for any + or += used on
2791  // text types. Call this for user packages only — stdlib/vendor may still use +.
2792  func CheckPlusOnText(files []*ast.File, info *types.Info, fset *token.FileSet) []error {
2793  	var errs []error
2794  	for _, file := range files {
2795  		ast.Inspect(file, func(n ast.Node) bool {
2796  			// Skip the inside of []byte(...) wraps — the literal-only
2797  			// subchains rewriter-synthesized by rewriteStringExprs use
2798  			// `+` internally (normalizePipeToAdd) so Go's constant folder
2799  			// can merge them. These are not user-written + operators.
2800  			if expr, ok := n.(ast.Expr); ok && isSliceByteConversion(expr) {
2801  				return false
2802  			}
2803  			switch node := n.(type) {
2804  			case *ast.BinaryExpr:
2805  				if node.Op != token.ADD {
2806  					return true
2807  				}
2808  				xType := info.TypeOf(node.X)
2809  				yType := info.TypeOf(node.Y)
2810  				xText := (xType != nil && isTextType(xType)) || isSliceByteConversion(node.X) || isStringLit(node.X)
2811  				yText := (yType != nil && isTextType(yType)) || isSliceByteConversion(node.Y) || isStringLit(node.Y)
2812  				if xText || yText {
2813  					pos := fset.Position(node.Pos())
2814  					errs = append(errs, fmt.Errorf("%s: moxie: '+' is not allowed for text concatenation, use | operator", pos))
2815  				}
2816  			case *ast.AssignStmt:
2817  				if node.Tok != token.ADD_ASSIGN || len(node.Lhs) != 1 {
2818  					return true
2819  				}
2820  				lhsType := info.TypeOf(node.Lhs[0])
2821  				if lhsType != nil && isTextType(lhsType) {
2822  					pos := fset.Position(node.Pos())
2823  					errs = append(errs, fmt.Errorf("%s: moxie: '+=' is not allowed for text concatenation, use |= operator", pos))
2824  				}
2825  			}
2826  			return true
2827  		})
2828  	}
2829  	return errs
2830  }
2831  
2832  // isByteSlice returns true if t is []byte (or []uint8).
2833  func isByteSlice(t types.Type) bool {
2834  	sl, ok := t.Underlying().(*types.Slice)
2835  	if !ok {
2836  		return false
2837  	}
2838  	basic, ok := sl.Elem().(*types.Basic)
2839  	return ok && basic.Kind() == types.Byte
2840  }
2841  
2842  // isMoxieConcatCall returns true if the expression is a __moxie_concat call.
2843  // Needed for chained + detection after earlier rewrites replaced inner + nodes.
2844  func isMoxieConcatCall(e ast.Expr) bool {
2845  	call, ok := e.(*ast.CallExpr)
2846  	if !ok {
2847  		return false
2848  	}
2849  	ident, ok := call.Fun.(*ast.Ident)
2850  	return ok && ident.Name == "__moxie_concat"
2851  }
2852  
2853  // isTextType returns true if t is []byte or string (equivalent under Moxie's
2854  // string=[]byte unification).
2855  func isTextType(t types.Type) bool {
2856  	if isByteSlice(t) {
2857  		return true
2858  	}
2859  	basic, ok := t.Underlying().(*types.Basic)
2860  	return ok && basic.Info()&types.IsString != 0
2861  }
2862  
2863  func isTextLike(t types.Type) bool {
2864  	if t == nil {
2865  		return false
2866  	}
2867  	if isByteSlice(t) {
2868  		return true
2869  	}
2870  	basic, ok := t.Underlying().(*types.Basic)
2871  	return ok && basic.Info()&types.IsString != 0
2872  }
2873  
2874  // RewriteAddAssign converts `s += expr` and `s |= expr` to
2875  // `s = __moxie_concat_move(s, expr)` for text types. Move semantics:
2876  // old s buffer is owned and can be freed on growth. Both forms compile,
2877  // but CheckPlusOnText rejects += for user packages.
2878  func RewriteAddAssign(files []*ast.File, info *types.Info) int {
2879  	count := 0
2880  	for _, file := range files {
2881  		ast.Inspect(file, func(n ast.Node) bool {
2882  			assign, ok := n.(*ast.AssignStmt)
2883  			if !ok || len(assign.Lhs) != 1 {
2884  				return true
2885  			}
2886  			if assign.Tok != token.ADD_ASSIGN && assign.Tok != token.OR_ASSIGN {
2887  				return true
2888  			}
2889  			lhsType := info.TypeOf(assign.Lhs[0])
2890  			if lhsType == nil || !isTextType(lhsType) {
2891  				return true
2892  			}
2893  			assign.Tok = token.ASSIGN
2894  			assign.Rhs[0] = &ast.CallExpr{
2895  				Fun:  &ast.Ident{Name: "__moxie_concat_move"},
2896  				Args: []ast.Expr{
2897  					wrapForMoxieConcat(assign.Lhs[0]),
2898  					wrapForMoxieConcat(assign.Rhs[0]),
2899  				},
2900  			}
2901  			count++
2902  			return true
2903  		})
2904  	}
2905  	return count
2906  }
2907  
2908  // applyPipeRewrites replaces | binary expressions with __moxie_concat calls.
2909  // It walks the AST and replaces matching BinaryExpr nodes in-place.
2910  func ApplyPipeRewrites(files []*ast.File, rewrites []PipeRewrite) {
2911  	// Build a set of expressions to rewrite.
2912  	rewriteSet := make(map[*ast.BinaryExpr]bool)
2913  	for _, r := range rewrites {
2914  		rewriteSet[r.expr] = true
2915  	}
2916  	if len(rewriteSet) == 0 {
2917  		return
2918  	}
2919  
2920  	// Walk AST and replace in parent nodes.
2921  	for _, file := range files {
2922  		replaceInNode(file, rewriteSet)
2923  	}
2924  }
2925  
2926  // replaceInNode walks a node and replaces any child expressions that are
2927  // in the rewrite set with __moxie_concat(left, right) calls.
2928  func replaceInNode(node ast.Node, set map[*ast.BinaryExpr]bool) {
2929  	// Collect parameter names for the enclosing function so we can
2930  	// distinguish owned locals (move OK) from borrowed params (move unsafe).
2931  	var paramNames map[string]bool
2932  	ast.Inspect(node, func(n ast.Node) bool {
2933  		switch parent := n.(type) {
2934  		case *ast.FuncDecl:
2935  			paramNames = collectParamNames(parent.Type)
2936  		case *ast.FuncLit:
2937  			paramNames = collectParamNames(parent.Type)
2938  		case *ast.AssignStmt:
2939  			for i, rhs := range parent.Rhs {
2940  				if parent.Tok == token.ASSIGN && i < len(parent.Lhs) {
2941  					parent.Rhs[i] = maybeReplacePipeMove(rhs, set, parent.Lhs[i], paramNames)
2942  				} else {
2943  					parent.Rhs[i] = maybeReplacePipe(rhs, set)
2944  				}
2945  			}
2946  		case *ast.ValueSpec:
2947  			for i, val := range parent.Values {
2948  				parent.Values[i] = maybeReplacePipe(val, set)
2949  			}
2950  		case *ast.ReturnStmt:
2951  			for i, result := range parent.Results {
2952  				parent.Results[i] = maybeReplacePipe(result, set)
2953  			}
2954  		case *ast.CallExpr:
2955  			for i, arg := range parent.Args {
2956  				parent.Args[i] = maybeReplacePipe(arg, set)
2957  			}
2958  		case *ast.SendStmt:
2959  			parent.Value = maybeReplacePipe(parent.Value, set)
2960  		case *ast.BinaryExpr:
2961  			parent.X = maybeReplacePipe(parent.X, set)
2962  			parent.Y = maybeReplacePipe(parent.Y, set)
2963  		case *ast.ParenExpr:
2964  			parent.X = maybeReplacePipe(parent.X, set)
2965  		case *ast.IndexExpr:
2966  			parent.Index = maybeReplacePipe(parent.Index, set)
2967  		case *ast.KeyValueExpr:
2968  			parent.Value = maybeReplacePipe(parent.Value, set)
2969  		case *ast.CompositeLit:
2970  			for i, elt := range parent.Elts {
2971  				parent.Elts[i] = maybeReplacePipe(elt, set)
2972  			}
2973  		}
2974  		return true
2975  	})
2976  }
2977  
2978  func maybeReplacePipe(expr ast.Expr, set map[*ast.BinaryExpr]bool) ast.Expr {
2979  	bin, ok := expr.(*ast.BinaryExpr)
2980  	if !ok || !set[bin] {
2981  		return expr
2982  	}
2983  	// Check if inner X is also a pipe (chain). If so, the inner result
2984  	// is a fresh temp - subsequent pipes can use move semantics.
2985  	if inner, ok := bin.X.(*ast.BinaryExpr); ok && set[inner] {
2986  		// Chain: inner pipes produce fresh temps. First pipe is borrow,
2987  		// rest are moves extending the fresh intermediate in place.
2988  		lhs := replacePipeChainBorrow(inner, set)
2989  		return &ast.CallExpr{
2990  			Fun:  &ast.Ident{Name: "__moxie_concat_move"},
2991  			Args: []ast.Expr{lhs, wrapForMoxieConcat(bin.Y)},
2992  		}
2993  	}
2994  	return &ast.CallExpr{
2995  		Fun:  &ast.Ident{Name: "__moxie_concat"},
2996  		Args: []ast.Expr{wrapForMoxieConcat(bin.X), wrapForMoxieConcat(bin.Y)},
2997  	}
2998  }
2999  
3000  // replacePipeChainBorrow: first pipe in a borrow chain is borrow (fresh alloc),
3001  // subsequent pipes are moves (extend the fresh intermediate).
3002  func replacePipeChainBorrow(bin *ast.BinaryExpr, set map[*ast.BinaryExpr]bool) ast.Expr {
3003  	if inner, ok := bin.X.(*ast.BinaryExpr); ok && set[inner] {
3004  		lhs := replacePipeChainBorrow(inner, set)
3005  		return &ast.CallExpr{
3006  			Fun:  &ast.Ident{Name: "__moxie_concat_move"},
3007  			Args: []ast.Expr{lhs, wrapForMoxieConcat(bin.Y)},
3008  		}
3009  	}
3010  	// Leftmost pair: borrow (fresh allocation, original untouched)
3011  	return &ast.CallExpr{
3012  		Fun:  &ast.Ident{Name: "__moxie_concat"},
3013  		Args: []ast.Expr{wrapForMoxieConcat(bin.X), wrapForMoxieConcat(bin.Y)},
3014  	}
3015  }
3016  
3017  // maybeReplacePipeMove: for `x = x | a | b | c` (= assignment, not :=).
3018  // If the leftmost leaf of the pipe chain matches the assignment target
3019  // AND the target is not a function parameter (borrowed from caller),
3020  // ALL pipes in the chain become __moxie_concat_move.
3021  func maybeReplacePipeMove(expr ast.Expr, set map[*ast.BinaryExpr]bool, assignTarget ast.Expr, params map[string]bool) ast.Expr {
3022  	bin, ok := expr.(*ast.BinaryExpr)
3023  	if !ok || !set[bin] {
3024  		return expr
3025  	}
3026  	leftmost := pipeChainLeftmost(bin, set)
3027  	// Check: leftmost matches assign target AND target is not a parameter.
3028  	// Parameters are borrowed from the caller - freeing their buffer on
3029  	// growth is use-after-free. Only locals and struct fields are owned.
3030  	if sameExprIdent(leftmost, assignTarget) && !isParamIdent(assignTarget, params) {
3031  		return replacePipeChainMove(bin, set)
3032  	}
3033  	// Fall back to borrow (with chain optimization for intermediates).
3034  	return maybeReplacePipe(expr, set)
3035  }
3036  
3037  // collectParamNames extracts parameter names from a function type.
3038  func collectParamNames(ft *ast.FuncType) map[string]bool {
3039  	if ft == nil || ft.Params == nil {
3040  		return nil
3041  	}
3042  	names := map[string]bool{}
3043  	for _, field := range ft.Params.List {
3044  		for _, name := range field.Names {
3045  			names[name.Name] = true
3046  		}
3047  	}
3048  	// Also collect receiver names from results (named returns are owned).
3049  	return names
3050  }
3051  
3052  // isParamIdent returns true if the expression is a simple identifier
3053  // that names a function parameter.
3054  func isParamIdent(expr ast.Expr, params map[string]bool) bool {
3055  	if params == nil {
3056  		return false
3057  	}
3058  	ident, ok := expr.(*ast.Ident)
3059  	if !ok {
3060  		return false
3061  	}
3062  	return params[ident.Name]
3063  }
3064  
3065  // pipeChainLeftmost returns the leftmost non-pipe operand of a chain.
3066  // For a | b | c (parsed as (a | b) | c), returns a.
3067  func pipeChainLeftmost(bin *ast.BinaryExpr, set map[*ast.BinaryExpr]bool) ast.Expr {
3068  	if inner, ok := bin.X.(*ast.BinaryExpr); ok && set[inner] {
3069  		return pipeChainLeftmost(inner, set)
3070  	}
3071  	return bin.X
3072  }
3073  
3074  // replacePipeChainMove recursively rewrites all pipes in a chain to move.
3075  func replacePipeChainMove(bin *ast.BinaryExpr, set map[*ast.BinaryExpr]bool) ast.Expr {
3076  	var lhs ast.Expr
3077  	if inner, ok := bin.X.(*ast.BinaryExpr); ok && set[inner] {
3078  		lhs = replacePipeChainMove(inner, set)
3079  	} else {
3080  		lhs = wrapForMoxieConcat(bin.X)
3081  	}
3082  	return &ast.CallExpr{
3083  		Fun:  &ast.Ident{Name: "__moxie_concat_move"},
3084  		Args: []ast.Expr{lhs, wrapForMoxieConcat(bin.Y)},
3085  	}
3086  }
3087  
3088  // sameExprIdent returns true if two AST expressions refer to the same
3089  // identifier or selector (e.g., x and x, or e.buf and e.buf).
3090  func sameExprIdent(a, b ast.Expr) bool {
3091  	switch av := a.(type) {
3092  	case *ast.Ident:
3093  		bv, ok := b.(*ast.Ident)
3094  		return ok && av.Name == bv.Name
3095  	case *ast.SelectorExpr:
3096  		bv, ok := b.(*ast.SelectorExpr)
3097  		return ok && av.Sel.Name == bv.Sel.Name && sameExprIdent(av.X, bv.X)
3098  	case *ast.IndexExpr:
3099  		bv, ok := b.(*ast.IndexExpr)
3100  		return ok && sameExprIdent(av.X, bv.X) && sameExprIdent(av.Index, bv.Index)
3101  	}
3102  	return false
3103  }
3104  
3105  // FilterPipeErrors removes type errors about | and + on text types from the
3106  // error list. Both are rewritten to __moxie_concat — the user-facing rejection
3107  // of + happens separately via CheckPlusOnText.
3108  func FilterPipeErrors(errs []error) []error {
3109  	var filtered []error
3110  	for _, err := range errs {
3111  		msg := err.Error()
3112  		if strings.Contains(msg, "operator |") && strings.Contains(msg, "[]") {
3113  			continue
3114  		}
3115  		if strings.Contains(msg, "operator |") && strings.Contains(msg, "string") {
3116  			continue
3117  		}
3118  		if strings.Contains(msg, "operator +") && strings.Contains(msg, "[]byte") {
3119  			continue
3120  		}
3121  		if strings.Contains(msg, "mismatched types") && strings.Contains(msg, "operator +") {
3122  			continue
3123  		}
3124  		if strings.Contains(msg, "operator |=") {
3125  			continue
3126  		}
3127  		if strings.Contains(msg, "operator +=") && strings.Contains(msg, "[]byte") {
3128  			continue
3129  		}
3130  		filtered = append(filtered, err)
3131  	}
3132  	return filtered
3133  }
3134  
3135  // ---------------------------------------------------------------------------
3136  // 4. Byte slice comparison rewrite (AST-level, after first typecheck pass)
3137  // ---------------------------------------------------------------------------
3138  //
3139  // Moxie uses []byte as its text type. Go's type checker doesn't allow
3140  // ==, !=, <, <=, >, >= on slices. This rewrite converts []byte comparisons
3141  // to __moxie_eq / __moxie_lt calls, and converts switch statements on []byte
3142  // to tag-less switches with __moxie_eq calls.
3143  
3144  // findByteComparisons finds binary expressions comparing two []byte values.
3145  // If one side is []byte and the other is string (e.g. map-key string compared
3146  // against a []byte var), wraps the string side in []byte(...) so the emitted
3147  // __moxie_eq/__moxie_lt receives matching types.
3148  func FindByteComparisons(files []*ast.File, info *types.Info) []*ast.BinaryExpr {
3149  	var result []*ast.BinaryExpr
3150  	for _, file := range files {
3151  		ast.Inspect(file, func(n ast.Node) bool {
3152  			bin, ok := n.(*ast.BinaryExpr)
3153  			if !ok {
3154  				return true
3155  			}
3156  			switch bin.Op {
3157  			case token.EQL, token.NEQ, token.LSS, token.LEQ, token.GTR, token.GEQ:
3158  			default:
3159  				return true
3160  			}
3161  			xType := info.TypeOf(bin.X)
3162  			yType := info.TypeOf(bin.Y)
3163  			xBytes := (xType != nil && isByteSlice(xType)) || isMoxieConcatCall(bin.X) || isSliceByteConversion(bin.X)
3164  			yBytes := (yType != nil && isByteSlice(yType)) || isMoxieConcatCall(bin.Y) || isSliceByteConversion(bin.Y)
3165  			if !xBytes && !yBytes {
3166  				return true
3167  			}
3168  			// Bridge string↔[]byte mismatches by wrapping string side.
3169  			if xBytes && !yBytes && yType != nil && isStringKind(yType) {
3170  				bin.Y = wrapInByteSlice(bin.Y)
3171  			}
3172  			if yBytes && !xBytes && xType != nil && isStringKind(xType) {
3173  				bin.X = wrapInByteSlice(bin.X)
3174  			}
3175  			result = append(result, bin)
3176  			return true
3177  		})
3178  	}
3179  	return result
3180  }
3181  
3182  // wrapInByteSlice wraps expr in []byte(expr).
3183  func wrapInByteSlice(expr ast.Expr) ast.Expr {
3184  	return &ast.CallExpr{
3185  		Fun:  &ast.ArrayType{Elt: ast.NewIdent("byte")},
3186  		Args: []ast.Expr{expr},
3187  	}
3188  }
3189  
3190  // applyByteComparisonRewrites replaces []byte comparison expressions with
3191  // __moxie_eq / __moxie_lt function calls.
3192  func ApplyByteComparisonRewrites(files []*ast.File, exprs []*ast.BinaryExpr) {
3193  	set := make(map[*ast.BinaryExpr]bool)
3194  	for _, e := range exprs {
3195  		set[e] = true
3196  	}
3197  	if len(set) == 0 {
3198  		return
3199  	}
3200  	for _, file := range files {
3201  		replaceComparisons(file, set)
3202  	}
3203  }
3204  
3205  func replaceComparisons(node ast.Node, set map[*ast.BinaryExpr]bool) {
3206  	ast.Inspect(node, func(n ast.Node) bool {
3207  		switch parent := n.(type) {
3208  		case *ast.AssignStmt:
3209  			for i, rhs := range parent.Rhs {
3210  				parent.Rhs[i] = maybeReplaceCmp(rhs, set)
3211  			}
3212  		case *ast.ValueSpec:
3213  			for i, val := range parent.Values {
3214  				parent.Values[i] = maybeReplaceCmp(val, set)
3215  			}
3216  		case *ast.ReturnStmt:
3217  			for i, result := range parent.Results {
3218  				parent.Results[i] = maybeReplaceCmp(result, set)
3219  			}
3220  		case *ast.CallExpr:
3221  			for i, arg := range parent.Args {
3222  				parent.Args[i] = maybeReplaceCmp(arg, set)
3223  			}
3224  		case *ast.IfStmt:
3225  			parent.Cond = maybeReplaceCmp(parent.Cond, set)
3226  		case *ast.ForStmt:
3227  			if parent.Cond != nil {
3228  				parent.Cond = maybeReplaceCmp(parent.Cond, set)
3229  			}
3230  		case *ast.BinaryExpr:
3231  			// Handle nested: (a == b) && (c == d)
3232  			parent.X = maybeReplaceCmp(parent.X, set)
3233  			parent.Y = maybeReplaceCmp(parent.Y, set)
3234  		case *ast.UnaryExpr:
3235  			parent.X = maybeReplaceCmp(parent.X, set)
3236  		case *ast.ParenExpr:
3237  			parent.X = maybeReplaceCmp(parent.X, set)
3238  		case *ast.CaseClause:
3239  			for i, val := range parent.List {
3240  				parent.List[i] = maybeReplaceCmp(val, set)
3241  			}
3242  		case *ast.SendStmt:
3243  			parent.Value = maybeReplaceCmp(parent.Value, set)
3244  		case *ast.CompositeLit:
3245  			for i, elt := range parent.Elts {
3246  				parent.Elts[i] = maybeReplaceCmp(elt, set)
3247  			}
3248  		}
3249  		return true
3250  	})
3251  }
3252  
3253  func maybeReplaceCmp(expr ast.Expr, set map[*ast.BinaryExpr]bool) ast.Expr {
3254  	bin, ok := expr.(*ast.BinaryExpr)
3255  	if !ok || !set[bin] {
3256  		return expr
3257  	}
3258  	switch bin.Op {
3259  	case token.EQL:
3260  		// a == b → __moxie_eq(a, b)
3261  		return &ast.CallExpr{
3262  			Fun:  &ast.Ident{Name: "__moxie_eq"},
3263  			Args: []ast.Expr{bin.X, bin.Y},
3264  		}
3265  	case token.NEQ:
3266  		// a != b → !__moxie_eq(a, b)
3267  		return &ast.UnaryExpr{
3268  			Op: token.NOT,
3269  			X: &ast.CallExpr{
3270  				Fun:  &ast.Ident{Name: "__moxie_eq"},
3271  				Args: []ast.Expr{bin.X, bin.Y},
3272  			},
3273  		}
3274  	case token.LSS:
3275  		// a < b → __moxie_lt(a, b)
3276  		return &ast.CallExpr{
3277  			Fun:  &ast.Ident{Name: "__moxie_lt"},
3278  			Args: []ast.Expr{bin.X, bin.Y},
3279  		}
3280  	case token.LEQ:
3281  		// a <= b → !__moxie_lt(b, a)
3282  		return &ast.UnaryExpr{
3283  			Op: token.NOT,
3284  			X: &ast.CallExpr{
3285  				Fun:  &ast.Ident{Name: "__moxie_lt"},
3286  				Args: []ast.Expr{bin.Y, bin.X},
3287  			},
3288  		}
3289  	case token.GTR:
3290  		// a > b → __moxie_lt(b, a)
3291  		return &ast.CallExpr{
3292  			Fun:  &ast.Ident{Name: "__moxie_lt"},
3293  			Args: []ast.Expr{bin.Y, bin.X},
3294  		}
3295  	case token.GEQ:
3296  		// a >= b → !__moxie_lt(a, b)
3297  		return &ast.UnaryExpr{
3298  			Op: token.NOT,
3299  			X: &ast.CallExpr{
3300  				Fun:  &ast.Ident{Name: "__moxie_lt"},
3301  				Args: []ast.Expr{bin.X, bin.Y},
3302  			},
3303  		}
3304  	}
3305  	return expr
3306  }
3307  
3308  // findByteSwitches finds switch statements that switch on a []byte expression.
3309  func FindByteSwitches(files []*ast.File, info *types.Info) []*ast.SwitchStmt {
3310  	var result []*ast.SwitchStmt
3311  	for _, file := range files {
3312  		ast.Inspect(file, func(n ast.Node) bool {
3313  			sw, ok := n.(*ast.SwitchStmt)
3314  			if !ok || sw.Tag == nil {
3315  				return true
3316  			}
3317  			tagType := info.TypeOf(sw.Tag)
3318  			if tagType != nil && isByteSlice(tagType) {
3319  				result = append(result, sw)
3320  			}
3321  			return true
3322  		})
3323  	}
3324  	return result
3325  }
3326  
3327  // applyByteSwitchRewrites converts switch statements on []byte to tag-less
3328  // switches with __moxie_eq calls.
3329  //
3330  //	switch x { case "a": ... }  →  switch { case __moxie_eq(x, []byte("a")): ... }
3331  func ApplyByteSwitchRewrites(switches []*ast.SwitchStmt) {
3332  	for _, sw := range switches {
3333  		tag := sw.Tag
3334  		sw.Tag = nil // make it a tag-less switch
3335  		for _, stmt := range sw.Body.List {
3336  			cc, ok := stmt.(*ast.CaseClause)
3337  			if !ok || cc.List == nil {
3338  				continue // default clause
3339  			}
3340  			for i, val := range cc.List {
3341  				cc.List[i] = &ast.CallExpr{
3342  					Fun:  &ast.Ident{Name: "__moxie_eq"},
3343  					Args: []ast.Expr{tag, val},
3344  				}
3345  			}
3346  		}
3347  	}
3348  }
3349  
3350  // FindByteMapKeys returns IndexExpr nodes where the container is a
3351  // map[string]V and the index expression has type []byte. Map keys stay
3352  // as `string` ([]byte is not comparable under stock go/types) but Moxie
3353  // source often indexes such maps with []byte values.
3354  // Each matched IndexExpr has its Index wrapped in a string(...) conversion
3355  // by ApplyByteMapKeyRewrites so the second typecheck accepts it.
3356  //
3357  // Same pattern for assignments `m[k] = v`: if the map value type is string
3358  // and v is []byte, the RHS is wrapped in string(v). Collected via
3359  // FindByteMapValues.
3360  func FindByteMapKeys(files []*ast.File, info *types.Info) []*ast.IndexExpr {
3361  	var result []*ast.IndexExpr
3362  	for _, file := range files {
3363  		ast.Inspect(file, func(n ast.Node) bool {
3364  			idx, ok := n.(*ast.IndexExpr)
3365  			if !ok {
3366  				return true
3367  			}
3368  			containerType := info.TypeOf(idx.X)
3369  			if containerType == nil {
3370  				return true
3371  			}
3372  			mapType, ok := containerType.Underlying().(*types.Map)
3373  			if !ok {
3374  				return true
3375  			}
3376  			if !isStringKind(mapType.Key()) {
3377  				return true
3378  			}
3379  			indexType := info.TypeOf(idx.Index)
3380  			if indexType == nil {
3381  				return true
3382  			}
3383  			if isByteSlice(indexType) {
3384  				result = append(result, idx)
3385  			}
3386  			return true
3387  		})
3388  	}
3389  	return result
3390  }
3391  
3392  // ApplyByteMapKeyRewrites wraps each matched IndexExpr's Index in a
3393  // string(...) conversion call.
3394  func ApplyByteMapKeyRewrites(exprs []*ast.IndexExpr) {
3395  	for _, idx := range exprs {
3396  		idx.Index = &ast.CallExpr{
3397  			Fun:  ast.NewIdent("string"),
3398  			Args: []ast.Expr{idx.Index},
3399  		}
3400  	}
3401  }
3402  
3403  // isStringKind reports whether t is the built-in `string` (not a named
3404  // alias or composite). Named types whose underlying is string also count.
3405  func isStringKind(t types.Type) bool {
3406  	basic, ok := t.Underlying().(*types.Basic)
3407  	return ok && basic.Kind() == types.String
3408  }
3409  
3410  // filterByteCompareErrors removes type errors about []byte comparison.
3411  func FilterByteCompareErrors(errs []error) []error {
3412  	var filtered []error
3413  	for _, err := range errs {
3414  		msg := err.Error()
3415  		if strings.Contains(msg, "slice can only be compared to nil") {
3416  			continue
3417  		}
3418  		if strings.Contains(msg, "mismatched types []byte and untyped string") {
3419  			continue
3420  		}
3421  		if strings.Contains(msg, "cannot convert") && strings.Contains(msg, "untyped string") && strings.Contains(msg, "[]byte") {
3422  			continue
3423  		}
3424  		// "invalid case" errors from switch on []byte
3425  		if strings.Contains(msg, "invalid case") && strings.Contains(msg, "[]byte") {
3426  			continue
3427  		}
3428  		filtered = append(filtered, err)
3429  	}
3430  	return filtered
3431  }
3432  
3433  // filterStringByteMismatch removes type errors about string/[]byte mismatches.
3434  // In moxie, string and []byte are the same type, so these errors are spurious.
3435  // FilterStringByteMismatch drops type errors caused by the string/[]byte
3436  // unification gap - the standard Go type checker sees them as different types
3437  // but Moxie treats them as identical.
3438  func FilterStringByteMismatch(errs []error) []error {
3439  	var filtered []error
3440  	for _, err := range errs {
3441  		msg := err.Error()
3442  
3443  		// Pattern 1: errors mentioning both []byte and string
3444  		hasByte := strings.Contains(msg, "[]byte")
3445  		hasString := strings.Contains(msg, "string")
3446  		if hasByte && hasString {
3447  			continue // any error involving both types is a unification gap
3448  		}
3449  
3450  		// Pattern 2: "slice can only be compared to nil" - []byte == comparison
3451  		if strings.Contains(msg, "slice can only be compared to nil") {
3452  			continue
3453  		}
3454  
3455  		// Pattern 3: "operator | not defined on" string variables (pipe operator)
3456  		if strings.Contains(msg, "operator | not defined") {
3457  			continue
3458  		}
3459  
3460  		// Pattern 4: "invalid case" with []byte (switch on string/byte)
3461  		if strings.Contains(msg, "invalid case") && hasByte {
3462  			continue
3463  		}
3464  
3465  		// Pattern 5: untyped string constants used as []byte
3466  		if strings.Contains(msg, "untyped string constant") && hasByte {
3467  			continue
3468  		}
3469  
3470  		// Pattern 6: cannot use string as []byte or vice versa in struct literal
3471  		if (hasByte || hasString) &&
3472  			(strings.Contains(msg, "cannot use") || strings.Contains(msg, "cannot convert")) {
3473  			continue
3474  		}
3475  
3476  		filtered = append(filtered, err)
3477  	}
3478  	return filtered
3479  }
3480