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.

reference_test.go · 208 lines · 7.5 KBGo Blame HistoryRaw
📦 Turbo MoonBit cc1f595 k33g 11h ago1package moonbitlang_test
2
3import (
4 "os"
5 "strings"
6 "testing"
7
8 "rickub.com/turbo-editors/turbo-core/syntax"
9
10 "rickub.com/turbo-editors/turbo-moonbit/internal/moonbitlang"
11)
12
13// TestTheLanguagesReferenceIsTrue holds docs/*/reference/languages.md to the
14// scanner. Every row of its MoonBit table that no other test here covers is
15// checked, so a reference claim and the code cannot drift apart quietly.
16//
17// The last rows document *limitations* rather than features — an enum
18// constructor read as a type, a leading dot that is not a number, a reserved
19// word left as an identifier. The reference says each of those in so many
20// words, and these rows are what stops somebody "fixing" one without also
21// fixing the sentence.
22func TestTheLanguagesReferenceIsTrue(t *testing.T) {
23 tests := []struct {
24 src string
25 word string
26 want syntax.Class
27 }{
28 // Keywords the reference lists that no other test exercises.
29 {"suberror MyError String", "suberror", syntax.ClassKeyword},
30 {"defer cleanup()", "defer", syntax.ClassKeyword},
31 {"letrec f = fn(x) { x }", "letrec", syntax.ClassKeyword},
32 {"extern \"c\" fn f() -> Unit", "extern", syntax.ClassKeyword},
33 {"nobreak { x }", "nobreak", syntax.ClassKeyword},
34 {"using @json", "using", syntax.ClassKeyword},
35 {"where T : Show", "where", syntax.ClassKeyword},
36 {"package \"example/demo\"", "package", syntax.ClassKeyword},
37 {"test \"it works\" { }", "test", syntax.ClassKeyword},
38 {"trait Shape { }", "trait", syntax.ClassKeyword},
39
40 // Constants and the prelude.
41 {"let x = Err(\"no\")", "Err", syntax.ClassConstant},
42 {"let x = Ok(1)", "Ok", syntax.ClassConstant},
43 {"abort(\"stop\")", "abort", syntax.ClassBuiltin},
44 {"panic()", "panic", syntax.ClassBuiltin},
45 {"inspect(value)", "inspect", syntax.ClassBuiltin},
46 {"debug_assert(fn() { true })", "debug_assert", syntax.ClassBuiltin},
47 {"physical_equal(a, b)", "physical_equal", syntax.ClassBuiltin},
48 {"json_inspect(value)", "json_inspect", syntax.ClassBuiltin},
49 {"let x = null", "null", syntax.ClassBuiltin},
50
51 // Types, by the language's own case rule.
52 {"let b : StringBuilder = StringBuilder::new()", "StringBuilder", syntax.ClassType},
53 {"let x : FixedArray[Int] = []", "FixedArray", syntax.ClassType},
54
55 // Literals.
56 {"let s = b\"\\xFF\"", "b\"\\xFF\"", syntax.ClassString},
57 {"let r = re\"[a-z]+\"", "re\"[a-z]+\"", syntax.ClassString},
58 {"let c = 'x'", "'x'", syntax.ClassChar},
59 {"let c = b'x'", "b'x'", syntax.ClassChar},
60 {"let c = '\\n'", "'\\n'", syntax.ClassChar},
61
62 // Numbers, one per row of the reference's list.
63 {"let n = 0o17", "0o17", syntax.ClassNumber},
64 {"let n = 0b1010", "0b1010", syntax.ClassNumber},
65 {"let n = 0xFF_FF", "0xFF_FF", syntax.ClassNumber},
66 {"let n = 1_000", "1_000", syntax.ClassNumber},
67 {"let n = 1.", "1.", syntax.ClassNumber},
68 {"let n = 1.5e-3", "1.5e-3", syntax.ClassNumber},
69 {"let n = 0x1.8p3F", "0x1.8p3F", syntax.ClassNumber},
70 {"let n = 42U", "42U", syntax.ClassNumber},
71 {"let n = 42L", "42L", syntax.ClassNumber},
72 {"let n = 42UL", "42UL", syntax.ClassNumber},
73 {"let n = 42N", "42N", syntax.ClassNumber},
74 {"let n = 1.0F", "1.0F", syntax.ClassNumber},
75
76 // Comments, attributes, labels, packages.
77 {"/// Adds two numbers.", "/// Adds two numbers.", syntax.ClassComment},
78 {"///|", "///|", syntax.ClassComment},
79 {"#external", "#external", syntax.ClassAttribute},
80 {"#custom.attribute(key=\"v\")", "#custom.attribute(key=\"v\")", syntax.ClassAttribute},
81 {"fn greet(name~ : String) -> Unit", "name~", syntax.ClassAttribute},
82 {"@moonbitlang/core/builtin.foo()", "@moonbitlang/core/builtin", syntax.ClassType},
83 {"@my-pkg.foo()", "@my-pkg", syntax.ClassType},
84
85 // Operators and punctuation.
86 {"for i in 1..<10 { }", "..", syntax.ClassOperator},
87 {"let x = a |> f", "|>", syntax.ClassOperator},
88 {"let x = pair.0", ".", syntax.ClassPunctuation},
89
90 // The documented limitations.
91 {"Circle(1.0)", "Circle", syntax.ClassType},
92 {"let n = .5", ".", syntax.ClassPunctuation},
93 {"let n = 42u", "42", syntax.ClassNumber},
94 {"let ref = 1", "ref", syntax.ClassIdentifier},
95 {"let move = 1", "move", syntax.ClassIdentifier},
96 {"config.if", "if", syntax.ClassIdentifier},
97 {"let s = \"\\{count + 1}\"", "\"\\{count + 1}\"", syntax.ClassString},
98 }
99
100 for _, test := range tests {
101 t.Run(test.word, func(t *testing.T) {
102 index := strings.Index(test.src, test.word)
103 if index < 0 {
104 t.Fatalf("%q not in %q", test.word, test.src)
105 }
106 got, ok := classAt(moonbitlang.Highlight(test.src), 0, index)
107 if !ok || got != test.want {
108 t.Errorf("%q in %q is %v (covered %v), want %v", test.word, test.src, got, ok, test.want)
109 }
110 })
111 }
112}
113
114// classAt returns the class covering one rune column of one line.
115func classAt(spans [][]syntax.Span, line, col int) (syntax.Class, bool) {
116 if line < 0 || line >= len(spans) {
117 return 0, false
118 }
119 for _, s := range spans[line] {
120 if col >= s.Start && col < s.End {
121 return s.Class, true
122 }
123 }
124 return 0, false
125}
126
127// The reference's Classes table says MoonBit produces no heading, tag,
128// emphasis or link. That is a claim about every span the scanner can ever
129// emit, so it is checked over a file that uses every construct the MoonBit
130// table names.
131func TestTheScannerNeverProducesAMarkupClass(t *testing.T) {
132 const src = "///| A doc comment.\n" +
133 "// An ordinary one.\n" +
134 "#deprecated(\"use area\")\n" +
135 "pub fn area(shape~ : Shape, scale~ : Double) -> Double raise {\n" +
136 " let table : Map[String, Int] = { \"a\": 0xFF }\n" +
137 " let text = \"got \\{shape}\"\n" +
138 " let raw =\n" +
139 " #|literal\n" +
140 " $|and \\{interpolated}\n" +
141 " guard scale > 0.0 else { fail(\"scale\") }\n" +
142 " match shape { Circle(r) => 3.14 * r ; _ => 1.0 }\n" +
143 " let range = 1..=2\n" +
144 " let pair = (1, 2).0\n" +
145 " let bytes = b\"\\xFF\"\n" +
146 " let ch = 'x'\n" +
147 " let pattern = re\"[a-z]+\"\n" +
148 " ignore(@json.parse(text))\n" +
149 "}\n"
150
151 forbidden := map[syntax.Class]bool{
152 syntax.ClassHeading: true,
153 syntax.ClassTag: true,
154 syntax.ClassEmphasis: true,
155 syntax.ClassLink: true,
156 }
157 for line, spans := range moonbitlang.Highlight(src) {
158 for _, span := range spans {
159 if forbidden[span.Class] {
160 t.Errorf("line %d holds a %s span at %d; the reference says MoonBit produces none", line+1, span.Class, span.Start)
161 }
162 }
163 }
164}
165
166// Every keyword the reference's table lists is one the scanner really treats as
167// a keyword. The table is read out of the page rather than repeated here, so a
168// word added to one and not the other is what fails.
169func TestEveryKeywordTheReferenceListsIsAKeyword(t *testing.T) {
170 const page = "../../docs/en/reference/languages.md"
171
172 raw, err := os.ReadFile(page)
173 if err != nil {
174 t.Fatalf("reading %s: %v", page, err)
175 }
176
177 words := keywordRowOf(t, string(raw))
178 if len(words) < 40 {
179 t.Fatalf("only %d keywords found in %s; the table's shape has changed", len(words), page)
180 }
181 for _, word := range words {
182 src := word + " x"
183 got, ok := classAt(moonbitlang.Highlight(src), 0, 0)
184 if !ok || got != syntax.ClassKeyword {
185 t.Errorf("%s says %q is a keyword; the scanner colours it %v", page, word, got)
186 }
187 }
188}
189
190// keywordRowOf pulls the back-quoted words out of the reference's keyword row.
191func keywordRowOf(t *testing.T, page string) []string {
192 t.Helper()
193
194 for _, line := range strings.Split(page, "\n") {
195 if !strings.HasSuffix(line, "| keyword |") || !strings.Contains(line, "`fn`") {
196 continue
197 }
198 var words []string
199 for i, part := range strings.Split(line, "`") {
200 if i%2 == 1 {
201 words = append(words, part)
202 }
203 }
204 return words
205 }
206 t.Fatal("no keyword row found in the reference")
207 return nil
208}