turbo-editors/turbo-moonbitpublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-moonbit.git
git clone ssh://git@rickub.com/turbo-editors/turbo-moonbit.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

📦 Turbo MoonBit cc1f595 · on main · k33g · 10h ago
literals.go · 116 lines · 3.8 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package moonbitlang

// The literals of MoonBit: five quoted forms, and one rule that governs all of
// them — none may reach the next line.

import "rickub.com/turbo-editors/turbo-core/syntax"

// literalPrefixes are the letters that may come before a quote, and what
// opening quote each of them expects.
//
// b takes either quote — b"bytes" and b'x' are both byte literals — while re
// takes only the double one. A prefix is only a prefix when the quote touches
// it, which is what keeps the variable b in `b + 1` from starting a literal.
var literalPrefixes = []struct {
	prefix string
	quotes string
}{
	{"b", `"'`},
	{"re", `"`},
}

// isLiteralStart reports whether a quoted literal opens at the scanner's
// position, prefix included.
func isLiteralStart(s *syntax.LineScanner) bool {
	_, opens := literalPrefixLength(s)
	return opens
}

// literalPrefixLength returns how many prefix runes come before the quote, and
// whether a literal opens here at all.
func literalPrefixLength(s *syntax.LineScanner) (int, bool) {
	if isQuote(s.Peek(0)) {
		return 0, true
	}
	for _, candidate := range literalPrefixes {
		length := len(candidate.prefix)
		if s.HasPrefix(0, candidate.prefix) && containsRune(candidate.quotes, s.Peek(length)) {
			return length, true
		}
	}
	return 0, false
}

// isQuote reports whether a rune opens a literal on its own.
func isQuote(r rune) bool { return r == '"' || r == '\'' }

// containsRune reports whether a set of runes, written as a string, holds one.
func containsRune(set string, r rune) bool {
	for _, member := range set {
		if member == r {
			return true
		}
	}
	return false
}

// takeLiteral colours a quoted literal from its prefix to its closing quote, or
// to the end of the line when it has none.
//
// An unterminated literal is coloured to the end of the line and left there. In
// most languages that is a tolerance; in MoonBit it is the rule, because "a
// newline before the closing quote reports an unterminated string literal" — so
// a line ending inside a literal is broken source, and carrying the colour onto
// the next line would paint the rest of the file for one stray quote.
//
// An interpolated \{expression} is *not* scanned as code. The grammar matches
// it to "the matching }", with braces inside nested literals not counting, so
// finding where one ends needs the parser rather than the scanner; and a brace
// counter that got it wrong would end the string early, which is the loudest
// way a highlighter can be broken. One flat run is the honest answer, and it is
// the one Turbo Python gives an f-string for the same reason.
func takeLiteral(s *syntax.LineScanner) {
	start := s.Pos()

	prefix, _ := literalPrefixLength(s)
	s.Advance(prefix)

	quote := s.Peek(0)
	s.Advance(1)
	consumeLiteral(s, quote)

	s.Emit(start, s.Pos(), classOfQuote(quote))
}

// classOfQuote says which class a literal gets from the quote that opened it.
//
// The apostrophe forms — 'c' and b'x' — are characters; the double-quoted
// forms — "s", b"s" and re"s" — are strings. A regex literal is a string
// rather than a class of its own: the closed Class set has no regex, and a
// regex is a string with a second reader.
func classOfQuote(quote rune) syntax.Class {
	if quote == '\'' {
		return syntax.ClassChar
	}
	return syntax.ClassString
}

// consumeLiteral runs to the closing quote, or to the end of the line.
//
// A backslash takes the rune after it out of consideration. That one rule
// covers every escape the language has — \n, \u{1F600}, \xFF and the \{ that
// opens an interpolation — because all any of them need from this scanner is
// that the rune after the backslash cannot close the literal.
func consumeLiteral(s *syntax.LineScanner, quote rune) {
	for !s.AtEnd() {
		switch s.Peek(0) {
		case '\\':
			s.Advance(2)
		case quote:
			s.Advance(1)
			return
		default:
			s.Advance(1)
		}
	}
}