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) } } }