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 }