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