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.

words.go · 355 lines · 13.2 KBGo Blame HistoryRaw
📦 Turbo MoonBit cc1f595 k33g 10h ago1package moonbitlang
2
3// Numbers and words: what a run of digits or letters turns out to be.
4
5import (
6 "strings"
7
8 "rickub.com/turbo-editors/turbo-core/syntax"
9)
10
11// --- numbers ----------------------------------------------------------------
12
13// numberSuffixes are the literal suffixes MoonBit recognises, longest first so
14// that UL is matched before U.
15//
16// They are upper case and nothing else: the grammar spells out "Uppercase
17// suffixes select UInt (U), Int64 (L), UInt64 (UL), BigInt (N), or Float (F)",
18// so 123u is the number 123 followed by the identifier u, and colouring it
19// otherwise would be inventing a literal the compiler will reject.
20var numberSuffixes = []string{"UL", "U", "L", "N", "F"}
21
22// takeNumber colours a numeric literal: 1_000, 0xFF, 0o17, 0b1010, 1.5e-3,
23// 0x1.8p3F, 42UL.
24//
25// It follows the grammar rather than being generous, because one case needs it
26// to. "Before .., an integer ends first, so 1..=2 begins with 1 and ..=" — a
27// scanner that swallowed any dot would read 1. as a double and leave .=2
28// behind, and a range would be miscoloured everywhere it appeared.
29//
30// The whole literal is one span, so every step here advances the scanner
31// without colouring and the single Emit at the end covers what they consumed.
32func takeNumber(s *syntax.LineScanner) {
33 start := s.Pos()
34 digit, hexadecimal := takeIntegerPart(s)
35
36 // A floating-point literal always has a point, and the point is only part
37 // of the number when a second one does not follow it.
38 if s.Peek(0) == '.' && s.Peek(1) != '.' {
39 s.Advance(1)
40 advanceWhile(s, digit)
41 }
42
43 takeExponent(s, hexadecimal)
44 takeNumberSuffix(s)
45 s.Emit(start, s.Pos(), syntax.ClassNumber)
46}
47
48// takeIntegerPart consumes the digits before any point. It returns the
49// predicate saying which runes count as digits for the rest of the literal, and
50// whether the literal is hexadecimal.
51//
52// The base prefix decides both. After 0x every hexadecimal digit is a digit,
53// which is what makes the F of 0x1.F part of the number rather than a Float
54// suffix — and an exponent is introduced by p rather than by e, because e is
55// itself a hexadecimal digit.
56func takeIntegerPart(s *syntax.LineScanner) (digit func(rune) bool, hexadecimal bool) {
57 if s.Peek(0) == '0' {
58 switch s.Peek(1) {
59 case 'x', 'X':
60 return takeBase(s, isHexDigit), true
61 case 'o', 'O':
62 return takeBase(s, isOctalDigit), false
63 case 'b', 'B':
64 return takeBase(s, isBinaryDigit), false
65 }
66 }
67
68 advanceWhile(s, isDecimalDigit)
69 return isDecimalDigit, false
70}
71
72// takeBase consumes a base prefix and the digits after it, and hands back the
73// predicate that recognised them.
74func takeBase(s *syntax.LineScanner, digit func(rune) bool) func(rune) bool {
75 s.Advance(2) // the 0 and its base letter
76 advanceWhile(s, digit)
77 return digit
78}
79
80// takeExponent consumes an exponent when one is there: e or E for a decimal
81// literal, p or P for a hexadecimal one, each with an optional sign.
82//
83// The sign is only taken when a digit follows it, so 1e-x stops at the e and
84// leaves the - and the x to be coloured as an operator and a name.
85func takeExponent(s *syntax.LineScanner, hexadecimal bool) {
86 if !isExponentLetter(s.Peek(0), hexadecimal) {
87 return
88 }
89
90 offset := 1
91 if s.Peek(offset) == '+' || s.Peek(offset) == '-' {
92 offset++
93 }
94 if !syntax.IsDigit(s.Peek(offset)) {
95 return
96 }
97
98 s.Advance(offset)
99 advanceWhile(s, isDecimalDigit)
100}
101
102// takeNumberSuffix consumes UL, U, L, N or F when the literal ends in one.
103//
104// A suffix must not be followed by another word rune: 1Length is not the Int64
105// 1 followed by ength, it is a number and then a name the compiler will
106// complain about, and stopping short of it is the reading that says so.
107func takeNumberSuffix(s *syntax.LineScanner) {
108 for _, suffix := range numberSuffixes {
109 if s.HasPrefix(0, suffix) && !syntax.IsWordRune(s.Peek(len(suffix))) {
110 s.Advance(len(suffix))
111 return
112 }
113 }
114}
115
116// advanceWhile steps over runes that match, without colouring any of them. It
117// is what a construct emitted as a single span uses in place of TakeWhile,
118// which would colour each run it consumed and leave the Emit overlapping it.
119func advanceWhile(s *syntax.LineScanner, matches func(rune) bool) {
120 for !s.AtEnd() && matches(s.Peek(0)) {
121 s.Advance(1)
122 }
123}
124
125// isExponentLetter reports whether a rune introduces an exponent, which depends
126// on the base: a hexadecimal literal uses p, because e is one of its digits.
127func isExponentLetter(r rune, hexadecimal bool) bool {
128 if hexadecimal {
129 return r == 'p' || r == 'P'
130 }
131 return r == 'e' || r == 'E'
132}
133
134// isDecimalDigit reports whether a rune may appear in a decimal literal after
135// its first digit. An underscore may, and "underscores may repeat or trail".
136func isDecimalDigit(r rune) bool { return syntax.IsDigit(r) || r == '_' }
137
138// isHexDigit reports whether a rune may appear in a hexadecimal literal.
139func isHexDigit(r rune) bool {
140 return isDecimalDigit(r) ||
141 r >= 'a' && r <= 'f' ||
142 r >= 'A' && r <= 'F'
143}
144
145// isOctalDigit reports whether a rune may appear in an octal literal.
146func isOctalDigit(r rune) bool { return r >= '0' && r <= '7' || r == '_' }
147
148// isBinaryDigit reports whether a rune may appear in a binary literal.
149func isBinaryDigit(r rune) bool { return r == '0' || r == '1' || r == '_' }
150
151// --- words ------------------------------------------------------------------
152
153// bangKeywords are the two keywords that end in an exclamation mark.
154//
155// The grammar lists try! and guard! among the keywords, and ! is an operator
156// rune — so without this the mark would be coloured as an operator hanging off
157// the end of a keyword, which is not what the language sees there.
158var bangKeywords = map[string]bool{"try": true, "guard": true}
159
160// takeWord colours an identifier, deciding what kind of thing it is from the
161// word itself and from the rune that follows it.
162func takeWord(s *syntax.LineScanner) {
163 start := s.Pos()
164 advanceWhile(s, syntax.IsWordRune)
165 word := wordAt(s, start)
166
167 if bangKeywords[word] && s.Peek(0) == '!' {
168 s.Advance(1)
169 s.Emit(start, s.Pos(), syntax.ClassKeyword)
170 return
171 }
172 if isLabel(s, word) {
173 s.Advance(1) // the ~
174 s.Emit(start, s.Pos(), syntax.ClassAttribute)
175 return
176 }
177
178 s.Emit(start, s.Pos(), classOfWord(word, s.Peek(0)))
179}
180
181// takeMember colours the name after a dot: a field, or a method.
182//
183// It is takeWord without the keyword table, because MoonBit's dot-identifiers
184// "use the identifier case rules without consulting the keyword table, so .if
185// is valid". A record with a field called `type` is ordinary MoonBit, and
186// colouring that field as a keyword would be a claim about the language that
187// the language contradicts.
188func takeMember(s *syntax.LineScanner) {
189 start := s.Pos()
190 advanceWhile(s, syntax.IsWordRune)
191 s.Emit(start, s.Pos(), classOfMember(wordAt(s, start), s.Peek(0)))
192}
193
194// wordAt returns the word running from start to the scanner's position.
195func wordAt(s *syntax.LineScanner, start int) string {
196 var b strings.Builder
197 for at := start; at < s.Pos(); at++ {
198 b.WriteRune(s.Peek(at - s.Pos()))
199 }
200 return b.String()
201}
202
203// isLabel reports whether the word just consumed is a labelled argument's name,
204// which is to say whether a tilde touches it.
205//
206// The tilde is MoonBit's alone: it appears in no operator the language has, so
207// a tilde against the end of a name can only be a label. The two exclusions are
208// the grammar's own — "ASCII-uppercase identifiers and keywords cannot form
209// labels" — and they matter, because without the first, Foo~ in a piece of
210// half-typed code would colour a type as a label.
211func isLabel(s *syntax.LineScanner, word string) bool {
212 if s.Peek(0) != '~' || word == "" {
213 return false
214 }
215 if startsUpperCase(word) {
216 return false
217 }
218 _, reserved := knownWords[word]
219 return !reserved
220}
221
222// classOfWord decides what a word is, given the rune that follows it.
223//
224// The order is the design, and MoonBit lets it be shorter than any other
225// scanner in this family. A word the language names is what the language says
226// it is. After that comes the case rule — and in MoonBit that is a *lexical*
227// rule rather than a convention: a uident "begins with an ASCII uppercase
228// letter", and only a type, a trait or an enum constructor may be spelt that
229// way. So there is no table of built-in types here, and there does not need to
230// be: Int, StringBuilder and a type somebody wrote this morning are all
231// capitalised, and all coloured by the same line.
232//
233// What that costs is that a constructor of your own enum is coloured as a type.
234// Nothing in the syntax separates Circle(1.0) from a type applied to arguments,
235// and inventing a separation would mean being wrong in both directions instead
236// of one.
237func classOfWord(word string, next rune) syntax.Class {
238 if class, known := knownWords[word]; known {
239 return class
240 }
241 if startsUpperCase(word) {
242 return syntax.ClassType
243 }
244 if next == '(' {
245 return syntax.ClassFunction
246 }
247 return syntax.ClassIdentifier
248}
249
250// classOfMember decides what the name after a dot is. It is classOfWord with
251// the keyword table left out; see takeMember.
252func classOfMember(word string, next rune) syntax.Class {
253 if startsUpperCase(word) {
254 return syntax.ClassType
255 }
256 if next == '(' {
257 return syntax.ClassFunction
258 }
259 return syntax.ClassIdentifier
260}
261
262// startsUpperCase reports whether a word begins with an ASCII capital, which is
263// what makes it a uident.
264func startsUpperCase(word string) bool {
265 return word != "" && word[0] >= 'A' && word[0] <= 'Z'
266}
267
268// knownWords is every word the language itself names, and what each one is.
269//
270// It is one table rather than three because it answers one question. The three
271// groups below are kept apart only so that each can carry the reasoning that
272// belongs to it.
273var knownWords = merge(
274 classify(syntax.ClassKeyword, keywords),
275 classify(syntax.ClassConstant, constants),
276 classify(syntax.ClassBuiltin, builtinValues),
277)
278
279// keywords are the words MoonBit reserves, taken from the grammar's keyword
280// production.
281//
282// true and false are in it there and are not here: they are keywords to the
283// lexer and values to the reader, and every editor in this family colours them
284// as constants. try! and guard! are not here either — the mark is glued on by
285// takeWord, because a table cannot hold a word whose last rune is an operator.
286//
287// package is here although in a .mbt file it is only a *reserved* word, which
288// the lexer treats as an identifier and warns about. It is a real keyword in
289// the .mbti interface files this editor also colours, and in a .mbt file
290// colouring it says exactly what the compiler is about to: this word is not
291// yours to use.
292//
293// The rest of the reserved list — move, ref, static, unsafe, await, and the
294// forty others — is deliberately absent. Those are identifiers that earn a
295// warning, and a scanner that coloured them as keywords would be telling a
296// reader they cannot write `let ref = 1` when they can.
297var keywords = words(
298 "and", "as", "async", "break", "catch", "const", "continue", "declare",
299 "defer", "derive", "else", "enum", "enumview", "extend", "extenum",
300 "extern", "fn", "for", "guard", "if", "impl", "import", "in", "is", "let",
301 "letrec", "lexscan", "loop", "match", "mut", "nobreak", "nocancel",
302 "noraise", "package", "priv", "proof_assert", "proof_let", "pub", "raise",
303 "readonly", "return", "struct", "suberror", "test", "throw", "trait",
304 "try", "type", "using", "where", "while", "with",
305)
306
307// constants are the values a reader meets as the language's own.
308//
309// None, Some, Ok and Err belong to Option and Result rather than to the
310// language, but a reader meets them everywhere and reads them as built in, the
311// same argument Turbo Rust records for the same four names.
312var constants = words("true", "false", "None", "Some", "Ok", "Err")
313
314// builtinValues are the lower-case names the prelude puts in scope without an
315// import, read out of moonbitlang/core/prelude rather than remembered.
316//
317// The prelude's deprecated names — dump, not, tap, then, to_repr — are left
318// out on purpose: colouring them as builtins would present as the language's
319// own four things it is trying to retire. There is no print here for the same
320// kind of reason, and it is the one worth stating: MoonBit has println and has
321// never had print, so a table written from habit would have coloured a name
322// that does not exist.
323var builtinValues = words(
324 "abort", "assert_eq", "assert_false", "assert_not_eq", "assert_true",
325 "compare", "debug", "debug_assert", "debug_inspect", "fail", "hash",
326 "ignore", "inspect", "json_inspect", "null", "panic", "physical_equal",
327 "println", "repr",
328)
329
330// words gathers a group of them, which reads better at the call sites above
331// than a slice literal does.
332func words(list ...string) []string { return list }
333
334// classify pairs every word in a group with the class it belongs to.
335func classify(class syntax.Class, list []string) map[string]syntax.Class {
336 out := make(map[string]syntax.Class, len(list))
337 for _, word := range list {
338 out[word] = class
339 }
340 return out
341}
342
343// merge folds the groups into one table. An earlier group wins a word a later
344// one repeats, which is what keeps a keyword a keyword.
345func merge(groups ...map[string]syntax.Class) map[string]syntax.Class {
346 out := map[string]syntax.Class{}
347 for _, group := range groups {
348 for word, class := range group {
349 if _, taken := out[word]; !taken {
350 out[word] = class
351 }
352 }
353 }
354 return out
355}