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