rewrite.go raw
1 // rewrite.go — Moxie literal syntax preprocessing for moxiejs.
2 //
3 // Rewrites moxie-specific syntax to valid Go before parser.ParseFile:
4 // []T{:len} → make([]T, len)
5 // []T{:len:cap} → make([]T, len, cap)
6 // chan T{} → make(chan T)
7 // chan T{expr} → make(chan T, expr)
8 //
9 // Ported from moxie/loader/mxrewrite.go for the JS backend.
10
11 package main
12
13 import (
14 "bytes"
15 "go/scanner"
16 "go/token"
17 "strings"
18 )
19
20 // rewriteMoxieLiterals applies all text-level rewrites to moxie source.
21 func rewriteMoxieLiterals(src []byte) []byte {
22 fset := token.NewFileSet()
23 src = rewritePipeConcat(src, fset)
24 src = rewriteChanLiterals(src, fset)
25 src = rewriteSliceLiterals(src, fset)
26 return src
27 }
28
29 // rewritePipeConcat converts `|` used as string concatenation to `+`.
30 // In Moxie, `|` is the text concat operator, but Go's type checker only
31 // accepts `+` for string concatenation. This rewrites `|` to `+` when
32 // the operands are string-typed (not bitwise integer OR).
33 //
34 // Heuristic: a `|` token is pipe-concat when it is NOT between two
35 // expressions that are clearly integer-typed. We detect integer context by
36 // checking if both adjacent tokens are integer literals, or if the `|` is
37 // inside a clearly bitwise context (e.g. `flags |= mask`).
38 //
39 // Conservative rule: rewrite `|` to `+` unless:
40 // - it's `|=` (compound assign — always bitwise)
41 // - both sides are integer literals
42 // - it's inside a const block (const expressions are numeric)
43 func rewritePipeConcat(src []byte, fset *token.FileSet) []byte {
44 toks := scanTokens(src, fset)
45 if len(toks) == 0 {
46 return src
47 }
48
49 // Identify const blocks to skip (| in const is always bitwise).
50 constRanges := findConstRanges(toks)
51
52 var result bytes.Buffer
53 lastEnd := 0
54
55 for i, t := range toks {
56 if t.tok != token.OR {
57 continue
58 }
59 // Skip |= (compound assign).
60 if i+1 < len(toks) && toks[i+1].tok == token.ASSIGN && toks[i+1].pos == t.end {
61 continue
62 }
63 // Skip if inside a const block.
64 if inConstRange(t.pos, constRanges) {
65 continue
66 }
67 // Skip if both neighbors are integer literals.
68 if i > 0 && i+1 < len(toks) &&
69 toks[i-1].tok == token.INT && toks[i+1].tok == token.INT {
70 continue
71 }
72 // Rewrite | → +
73 result.Write(src[lastEnd:t.pos])
74 result.WriteByte('+')
75 lastEnd = t.end
76 }
77
78 if lastEnd == 0 {
79 return src
80 }
81 result.Write(src[lastEnd:])
82 return result.Bytes()
83 }
84
85 // constRange marks byte offsets of const(...) blocks.
86 type constRange struct{ start, end int }
87
88 func findConstRanges(toks []tok) []constRange {
89 var ranges []constRange
90 for i := 0; i < len(toks); i++ {
91 if toks[i].tok != token.CONST {
92 continue
93 }
94 // const ( ... ) or const name = expr
95 if i+1 < len(toks) && toks[i+1].tok == token.LPAREN {
96 depth := 1
97 start := toks[i].pos
98 for j := i + 2; j < len(toks); j++ {
99 if toks[j].tok == token.LPAREN {
100 depth++
101 }
102 if toks[j].tok == token.RPAREN {
103 depth--
104 if depth == 0 {
105 ranges = append(ranges, constRange{start, toks[j].end})
106 i = j
107 break
108 }
109 }
110 }
111 }
112 }
113 return ranges
114 }
115
116 func inConstRange(pos int, ranges []constRange) bool {
117 for _, r := range ranges {
118 if pos >= r.start && pos < r.end {
119 return true
120 }
121 }
122 return false
123 }
124
125 // tok is a scanned token with byte offsets.
126 type tok struct {
127 pos int
128 end int
129 tok token.Token
130 lit string
131 }
132
133 // scanTokens tokenizes src into a slice of tok.
134 func scanTokens(src []byte, fset *token.FileSet) []tok {
135 file := fset.AddFile("", fset.Base(), len(src))
136 var s scanner.Scanner
137 s.Init(file, src, nil, scanner.ScanComments)
138
139 var toks []tok
140 for {
141 pos, t, lit := s.Scan()
142 if t == token.EOF {
143 break
144 }
145 offset := file.Offset(pos)
146 end := offset + len(lit)
147 if lit == "" {
148 end = offset + len(t.String())
149 }
150 toks = append(toks, tok{pos: offset, end: end, tok: t, lit: lit})
151 }
152 return toks
153 }
154
155 // rewriteChanLiterals rewrites chan T{} → make(chan T) and chan T{N} → make(chan T, N).
156 func rewriteChanLiterals(src []byte, fset *token.FileSet) []byte {
157 toks := scanTokens(src, fset)
158
159 var result bytes.Buffer
160 lastEnd := 0
161
162 for i := 0; i < len(toks); i++ {
163 if toks[i].tok != token.CHAN {
164 continue
165 }
166
167 chanIdx := i
168 braceIdx := -1
169
170 depth := 0
171 for j := i + 1; j < len(toks); j++ {
172 switch toks[j].tok {
173 case token.LBRACE:
174 if depth == 0 {
175 braceIdx = j
176 }
177 depth++
178 case token.RBRACE:
179 depth--
180 case token.LPAREN:
181 depth++
182 case token.RPAREN:
183 depth--
184 }
185 if braceIdx >= 0 {
186 break
187 }
188 if toks[j].tok == token.SEMICOLON || toks[j].tok == token.ASSIGN ||
189 toks[j].tok == token.DEFINE || toks[j].tok == token.COMMA ||
190 toks[j].tok == token.RPAREN {
191 break
192 }
193 }
194
195 if braceIdx < 0 || braceIdx <= chanIdx+1 {
196 continue
197 }
198
199 // Check expression context.
200 inExprContext := false
201 if chanIdx > 0 {
202 prev := toks[chanIdx-1].tok
203 switch prev {
204 case token.ASSIGN, token.DEFINE,
205 token.COLON, token.COMMA,
206 token.LPAREN, token.LBRACK,
207 token.LBRACE, token.RETURN,
208 token.SEMICOLON:
209 inExprContext = true
210 }
211 } else {
212 inExprContext = true
213 }
214
215 if !inExprContext {
216 continue
217 }
218
219 // Find closing brace.
220 closeIdx := -1
221 depth = 1
222 for j := braceIdx + 1; j < len(toks); j++ {
223 switch toks[j].tok {
224 case token.LBRACE:
225 depth++
226 case token.RBRACE:
227 depth--
228 if depth == 0 {
229 closeIdx = j
230 }
231 }
232 if closeIdx >= 0 {
233 break
234 }
235 }
236
237 if closeIdx < 0 {
238 continue
239 }
240
241 // Extract type text.
242 typeStart := toks[chanIdx+1].pos
243 typeEnd := toks[braceIdx].pos
244 typeText := strings.TrimSpace(string(src[typeStart:typeEnd]))
245
246 if typeText == "" {
247 continue
248 }
249
250 // Handle chan struct{}{} and chan interface{}{}.
251 if typeText == "struct" || typeText == "interface" {
252 if closeIdx+1 >= len(toks) || toks[closeIdx+1].tok != token.LBRACE {
253 continue
254 }
255 typeText = typeText + "{}"
256 braceIdx = closeIdx + 1
257 closeIdx = -1
258 depth = 1
259 for j := braceIdx + 1; j < len(toks); j++ {
260 switch toks[j].tok {
261 case token.LBRACE:
262 depth++
263 case token.RBRACE:
264 depth--
265 if depth == 0 {
266 closeIdx = j
267 }
268 }
269 if closeIdx >= 0 {
270 break
271 }
272 }
273 if closeIdx < 0 {
274 continue
275 }
276 }
277
278 // Extract buffer size expression.
279 var bufExpr string
280 if closeIdx > braceIdx+1 {
281 bufStart := toks[braceIdx+1].pos
282 bufEnd := toks[closeIdx].pos
283 bufExpr = strings.TrimSpace(string(src[bufStart:bufEnd]))
284 }
285
286 result.Write(src[lastEnd:toks[chanIdx].pos])
287 result.WriteString("make(chan ")
288 result.WriteString(typeText)
289 if bufExpr != "" {
290 result.WriteString(", ")
291 result.WriteString(bufExpr)
292 }
293 result.WriteString(")")
294
295 lastEnd = toks[closeIdx].end
296 i = closeIdx
297 }
298
299 if lastEnd == 0 {
300 return src
301 }
302 result.Write(src[lastEnd:])
303 return result.Bytes()
304 }
305
306 // rewriteSliceLiterals rewrites []T{:len} → make([]T, len) and []T{:len:cap} → make([]T, len, cap).
307 func rewriteSliceLiterals(src []byte, fset *token.FileSet) []byte {
308 toks := scanTokens(src, fset)
309
310 var result bytes.Buffer
311 lastEnd := 0
312
313 for i := 0; i < len(toks); i++ {
314 if toks[i].tok != token.LBRACK {
315 continue
316 }
317 if i+1 >= len(toks) || toks[i+1].tok != token.RBRACK {
318 continue
319 }
320
321 lbrackIdx := i
322
323 // Scan forward past element type to find LBRACE.
324 braceIdx := -1
325 depth := 0
326 for j := i + 2; j < len(toks); j++ {
327 switch toks[j].tok {
328 case token.LBRACK:
329 depth++
330 case token.RBRACK:
331 depth--
332 case token.LPAREN:
333 depth++
334 case token.RPAREN:
335 depth--
336 case token.LBRACE:
337 if depth == 0 {
338 braceIdx = j
339 }
340 }
341 if braceIdx >= 0 {
342 break
343 }
344 if depth == 0 && (toks[j].tok == token.SEMICOLON ||
345 toks[j].tok == token.ASSIGN ||
346 toks[j].tok == token.DEFINE ||
347 toks[j].tok == token.COMMA) {
348 break
349 }
350 }
351
352 if braceIdx < 0 || braceIdx <= lbrackIdx+2 {
353 continue
354 }
355
356 // Discriminator: token after { must be COLON.
357 if braceIdx+1 >= len(toks) || toks[braceIdx+1].tok != token.COLON {
358 continue
359 }
360
361 // Find closing brace, collecting colon positions.
362 closeIdx := -1
363 colonPositions := []int{braceIdx + 1}
364 depth = 1
365 bracketDepth := 0
366 parenDepth := 0
367 for j := braceIdx + 2; j < len(toks); j++ {
368 switch toks[j].tok {
369 case token.LBRACE:
370 depth++
371 case token.RBRACE:
372 depth--
373 if depth == 0 {
374 closeIdx = j
375 }
376 case token.LBRACK:
377 bracketDepth++
378 case token.RBRACK:
379 bracketDepth--
380 case token.LPAREN:
381 parenDepth++
382 case token.RPAREN:
383 parenDepth--
384 case token.COLON:
385 if depth == 1 && bracketDepth == 0 && parenDepth == 0 {
386 colonPositions = append(colonPositions, j)
387 }
388 }
389 if closeIdx >= 0 {
390 break
391 }
392 }
393
394 if closeIdx < 0 {
395 continue
396 }
397
398 // Extract type text (from [ through to {, not including {).
399 typeText := strings.TrimSpace(string(src[toks[lbrackIdx].pos:toks[braceIdx].pos]))
400
401 if len(colonPositions) == 1 {
402 // []T{:len} → make([]T, len)
403 lenStart := toks[colonPositions[0]+1].pos
404 lenEnd := toks[closeIdx].pos
405 lenExpr := strings.TrimSpace(string(src[lenStart:lenEnd]))
406 if lenExpr == "" {
407 continue
408 }
409
410 result.Write(src[lastEnd:toks[lbrackIdx].pos])
411 result.WriteString("make(")
412 result.WriteString(typeText)
413 result.WriteString(", ")
414 result.WriteString(lenExpr)
415 result.WriteString(")")
416 } else if len(colonPositions) == 2 {
417 // []T{:len:cap} → make([]T, len, cap)
418 lenStart := toks[colonPositions[0]+1].pos
419 lenEnd := toks[colonPositions[1]].pos
420 lenExpr := strings.TrimSpace(string(src[lenStart:lenEnd]))
421
422 capStart := toks[colonPositions[1]+1].pos
423 capEnd := toks[closeIdx].pos
424 capExpr := strings.TrimSpace(string(src[capStart:capEnd]))
425
426 if lenExpr == "" || capExpr == "" {
427 continue
428 }
429
430 result.Write(src[lastEnd:toks[lbrackIdx].pos])
431 result.WriteString("make(")
432 result.WriteString(typeText)
433 result.WriteString(", ")
434 result.WriteString(lenExpr)
435 result.WriteString(", ")
436 result.WriteString(capExpr)
437 result.WriteString(")")
438 } else {
439 continue
440 }
441
442 lastEnd = toks[closeIdx].end
443 i = closeIdx
444 }
445
446 if lastEnd == 0 {
447 return src
448 }
449 result.Write(src[lastEnd:])
450 return result.Bytes()
451 }
452