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