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