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 · 9h ago
reference_test.go · 208 lines · 7.5 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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
package moonbitlang_test

import (
	"os"
	"strings"
	"testing"

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

	"rickub.com/turbo-editors/turbo-moonbit/internal/moonbitlang"
)

// TestTheLanguagesReferenceIsTrue holds docs/*/reference/languages.md to the
// scanner. Every row of its MoonBit table that no other test here covers is
// checked, so a reference claim and the code cannot drift apart quietly.
//
// The last rows document *limitations* rather than features — an enum
// constructor read as a type, a leading dot that is not a number, a reserved
// word left as an identifier. The reference says each of those in so many
// words, and these rows are what stops somebody "fixing" one without also
// fixing the sentence.
func TestTheLanguagesReferenceIsTrue(t *testing.T) {
	tests := []struct {
		src  string
		word string
		want syntax.Class
	}{
		// Keywords the reference lists that no other test exercises.
		{"suberror MyError String", "suberror", syntax.ClassKeyword},
		{"defer cleanup()", "defer", syntax.ClassKeyword},
		{"letrec f = fn(x) { x }", "letrec", syntax.ClassKeyword},
		{"extern \"c\" fn f() -> Unit", "extern", syntax.ClassKeyword},
		{"nobreak { x }", "nobreak", syntax.ClassKeyword},
		{"using @json", "using", syntax.ClassKeyword},
		{"where T : Show", "where", syntax.ClassKeyword},
		{"package \"example/demo\"", "package", syntax.ClassKeyword},
		{"test \"it works\" { }", "test", syntax.ClassKeyword},
		{"trait Shape { }", "trait", syntax.ClassKeyword},

		// Constants and the prelude.
		{"let x = Err(\"no\")", "Err", syntax.ClassConstant},
		{"let x = Ok(1)", "Ok", syntax.ClassConstant},
		{"abort(\"stop\")", "abort", syntax.ClassBuiltin},
		{"panic()", "panic", syntax.ClassBuiltin},
		{"inspect(value)", "inspect", syntax.ClassBuiltin},
		{"debug_assert(fn() { true })", "debug_assert", syntax.ClassBuiltin},
		{"physical_equal(a, b)", "physical_equal", syntax.ClassBuiltin},
		{"json_inspect(value)", "json_inspect", syntax.ClassBuiltin},
		{"let x = null", "null", syntax.ClassBuiltin},

		// Types, by the language's own case rule.
		{"let b : StringBuilder = StringBuilder::new()", "StringBuilder", syntax.ClassType},
		{"let x : FixedArray[Int] = []", "FixedArray", syntax.ClassType},

		// Literals.
		{"let s = b\"\\xFF\"", "b\"\\xFF\"", syntax.ClassString},
		{"let r = re\"[a-z]+\"", "re\"[a-z]+\"", syntax.ClassString},
		{"let c = 'x'", "'x'", syntax.ClassChar},
		{"let c = b'x'", "b'x'", syntax.ClassChar},
		{"let c = '\\n'", "'\\n'", syntax.ClassChar},

		// Numbers, one per row of the reference's list.
		{"let n = 0o17", "0o17", syntax.ClassNumber},
		{"let n = 0b1010", "0b1010", syntax.ClassNumber},
		{"let n = 0xFF_FF", "0xFF_FF", syntax.ClassNumber},
		{"let n = 1_000", "1_000", syntax.ClassNumber},
		{"let n = 1.", "1.", syntax.ClassNumber},
		{"let n = 1.5e-3", "1.5e-3", syntax.ClassNumber},
		{"let n = 0x1.8p3F", "0x1.8p3F", syntax.ClassNumber},
		{"let n = 42U", "42U", syntax.ClassNumber},
		{"let n = 42L", "42L", syntax.ClassNumber},
		{"let n = 42UL", "42UL", syntax.ClassNumber},
		{"let n = 42N", "42N", syntax.ClassNumber},
		{"let n = 1.0F", "1.0F", syntax.ClassNumber},

		// Comments, attributes, labels, packages.
		{"/// Adds two numbers.", "/// Adds two numbers.", syntax.ClassComment},
		{"///|", "///|", syntax.ClassComment},
		{"#external", "#external", syntax.ClassAttribute},
		{"#custom.attribute(key=\"v\")", "#custom.attribute(key=\"v\")", syntax.ClassAttribute},
		{"fn greet(name~ : String) -> Unit", "name~", syntax.ClassAttribute},
		{"@moonbitlang/core/builtin.foo()", "@moonbitlang/core/builtin", syntax.ClassType},
		{"@my-pkg.foo()", "@my-pkg", syntax.ClassType},

		// Operators and punctuation.
		{"for i in 1..<10 { }", "..", syntax.ClassOperator},
		{"let x = a |> f", "|>", syntax.ClassOperator},
		{"let x = pair.0", ".", syntax.ClassPunctuation},

		// The documented limitations.
		{"Circle(1.0)", "Circle", syntax.ClassType},
		{"let n = .5", ".", syntax.ClassPunctuation},
		{"let n = 42u", "42", syntax.ClassNumber},
		{"let ref = 1", "ref", syntax.ClassIdentifier},
		{"let move = 1", "move", syntax.ClassIdentifier},
		{"config.if", "if", syntax.ClassIdentifier},
		{"let s = \"\\{count + 1}\"", "\"\\{count + 1}\"", syntax.ClassString},
	}

	for _, test := range tests {
		t.Run(test.word, func(t *testing.T) {
			index := strings.Index(test.src, test.word)
			if index < 0 {
				t.Fatalf("%q not in %q", test.word, test.src)
			}
			got, ok := classAt(moonbitlang.Highlight(test.src), 0, index)
			if !ok || got != test.want {
				t.Errorf("%q in %q is %v (covered %v), want %v", test.word, test.src, got, ok, test.want)
			}
		})
	}
}

// classAt returns the class covering one rune column of one line.
func classAt(spans [][]syntax.Span, line, col int) (syntax.Class, bool) {
	if line < 0 || line >= len(spans) {
		return 0, false
	}
	for _, s := range spans[line] {
		if col >= s.Start && col < s.End {
			return s.Class, true
		}
	}
	return 0, false
}

// The reference's Classes table says MoonBit produces no heading, tag,
// emphasis or link. That is a claim about every span the scanner can ever
// emit, so it is checked over a file that uses every construct the MoonBit
// table names.
func TestTheScannerNeverProducesAMarkupClass(t *testing.T) {
	const src = "///| A doc comment.\n" +
		"// An ordinary one.\n" +
		"#deprecated(\"use area\")\n" +
		"pub fn area(shape~ : Shape, scale~ : Double) -> Double raise {\n" +
		"  let table : Map[String, Int] = { \"a\": 0xFF }\n" +
		"  let text = \"got \\{shape}\"\n" +
		"  let raw =\n" +
		"    #|literal\n" +
		"    $|and \\{interpolated}\n" +
		"  guard scale > 0.0 else { fail(\"scale\") }\n" +
		"  match shape { Circle(r) => 3.14 * r ; _ => 1.0 }\n" +
		"  let range = 1..=2\n" +
		"  let pair = (1, 2).0\n" +
		"  let bytes = b\"\\xFF\"\n" +
		"  let ch = 'x'\n" +
		"  let pattern = re\"[a-z]+\"\n" +
		"  ignore(@json.parse(text))\n" +
		"}\n"

	forbidden := map[syntax.Class]bool{
		syntax.ClassHeading:  true,
		syntax.ClassTag:      true,
		syntax.ClassEmphasis: true,
		syntax.ClassLink:     true,
	}
	for line, spans := range moonbitlang.Highlight(src) {
		for _, span := range spans {
			if forbidden[span.Class] {
				t.Errorf("line %d holds a %s span at %d; the reference says MoonBit produces none", line+1, span.Class, span.Start)
			}
		}
	}
}

// Every keyword the reference's table lists is one the scanner really treats as
// a keyword. The table is read out of the page rather than repeated here, so a
// word added to one and not the other is what fails.
func TestEveryKeywordTheReferenceListsIsAKeyword(t *testing.T) {
	const page = "../../docs/en/reference/languages.md"

	raw, err := os.ReadFile(page)
	if err != nil {
		t.Fatalf("reading %s: %v", page, err)
	}

	words := keywordRowOf(t, string(raw))
	if len(words) < 40 {
		t.Fatalf("only %d keywords found in %s; the table's shape has changed", len(words), page)
	}
	for _, word := range words {
		src := word + " x"
		got, ok := classAt(moonbitlang.Highlight(src), 0, 0)
		if !ok || got != syntax.ClassKeyword {
			t.Errorf("%s says %q is a keyword; the scanner colours it %v", page, word, got)
		}
	}
}

// keywordRowOf pulls the back-quoted words out of the reference's keyword row.
func keywordRowOf(t *testing.T, page string) []string {
	t.Helper()

	for _, line := range strings.Split(page, "\n") {
		if !strings.HasSuffix(line, "| keyword |") || !strings.Contains(line, "`fn`") {
			continue
		}
		var words []string
		for i, part := range strings.Split(line, "`") {
			if i%2 == 1 {
				words = append(words, part)
			}
		}
		return words
	}
	t.Fatal("no keyword row found in the reference")
	return nil
}