Parser and parsing utilities for Elm and Elm-like dialects (e.g. Morphir) in MoonBit
Dependencies
moon add moonrockz/kruegerimport {
"moonrockz/krueger",
}test "parse a module" {
let source = @krueger.SourceText::new(
"module Main exposing (main)\n\nmain =\n greet \"world\"\n",
)
let result = @krueger.parse_module(source)
guard result.diagnostics.is_empty() else { fail("syntax errors") }
guard result.ast is Some(file) else { fail("no AST") }
for declaration in file.declarations {
match declaration.value {
FunctionDeclaration(f) => println(f.declaration.value.name.value)
_ => ()
}
}
}test "report an error" {
let source = @krueger.SourceText::new("module Main exposing (..)\n\nx = (1, \n")
let result = @krueger.parse_module(source)
for diagnostic in result.diagnostics {
println(@krueger.render_plain(diagnostic, source, "src/Main.elm"))
}
}test "count expressions" {
let result = @krueger.parse_module(
@krueger.SourceText::new("module Main exposing (..)\n\nx = f 1 2\n"),
)
guard @krueger.NodeRef::of_result(result) is Some(root) else { return }
let count = @krueger.fold(root, 0, (n, node) => {
(if node.category() == "expression" { n + 1 } else { n }, Continue)
})
println(count)
}let result = @krueger.parse_module(@krueger.SourceText::new(source))
guard result.ast is Some(file) else { return }
let text = @krueger.print_file(file) // elm-format layout, width 120mise run hooks:installstruct Names {
functions : Array[String]
mut patterns : Int
}
impl @syntax.Visitor for Names with fn visit_function(self, n) {
match n {
Declaration({ value: FunctionDeclaration(f), .. }, _) =>
self.functions.push(f.declaration.value.name.value)
_ => ()
}
Continue
}
impl @syntax.Visitor for Names with fn visit_pattern(self, _) {
self.patterns 1
Continue
}
test {
let src = "module Main exposing (..)\n\nadd x y =\n x + y\n\nz = 0\n"
let result = @parser.parse_module(
@scanner.SourceText::new(src),
@scanner.DefaultScanner::new(),
)
let root = @syntax.NodeRef::of_result(result).unwrap()
let names = { functions: [], patterns: 0, }
@syntax.accept(root, names)
debug_inspect(names.functions, content="[\"add\", \"z\"]")
inspect(names.patterns, content="2")
}fn diagnostic_description(code : String) -> String?test {
inspect(
@krueger.diagnostic_description("KR-SCAN-004").unwrap_or("?"),
content="Unterminated string, char or GLSL literal",
)
debug_inspect(@krueger.diagnostic_description("KR-NONE"), content="None")
}test {
let text =
#|module Main exposing (x)
#|
#|{-| Answers.
#|
#|@since 2
#|-}
#|x = 42
#|
let source = @scanner.SourceText::new(text)
let result = @parser.parse_module(source, @scanner.DefaultScanner::new())
// The first doc comment documents the module.
let json = @parser.encode_attributes(result.attributes)
inspect(
json.stringify(),
content=(
#|[{"target":"module","attributes":[{"name":{"range":[5,2,5,7],"value":"since"},"arguments":[{"range":[5,8,5,9],"value":{"type":"integer","integer":2}}],"range":[5,1,5,9]}]}]
),
)
}test {
let text =
#|module Main exposing (x)
#|
#|{-| Ids.
#|
#|@id 9007199254740993
#|-}
#|x = 1
#|
let source = @scanner.SourceText::new(text)
let groups = @parser.parse_module(source, @scanner.DefaultScanner::new()).attributes
let exact = @parser.encode_attributes_with(groups, exact_ints=true).stringify()
inspect(exact.contains("9007199254740993"), content="true")
let nearest = @parser.encode_attributes(groups).stringify()
inspect(nearest.contains("9007199254740993"), content="false")
}test {
let source = @scanner.SourceText::new(
"module Main exposing (..)\n\nmain =\n (1 + 2\n",
)
let result = @parser.parse_module(source, @scanner.DefaultScanner::new())
let json = @report.encode_diagnostics(result.diagnostics)
inspect(
json.stringify(),
content=(
#|[{"code":"KR-PARSE-004","severity":"error","title":"UNFINISHED PARENTHESES","message":"Malformed function declaration: expected `,` or `)`, found end of input","span":{"start":{"line":4,"column":11},"end":{"line":4,"column":11}}}]
),
)
}test {
let src = "module Main exposing (..)\n\nadd x y =\n x + y\n"
let result = @parser.parse_module(
@scanner.SourceText::new(src),
@scanner.DefaultScanner::new(),
)
let root = @syntax.NodeRef::of_result(result).unwrap()
// Count the expressions and find the deepest nesting.
let (count, _, deepest) = @syntax.fold(
root,
(0, 0, 0),
(acc, n) => {
let (count, depth, deepest) = acc
let count = count + (if n.category() == "expression" { 1 } else { 0 })
let depth = depth + 1
let deepest = if depth > deepest { depth } else { deepest }
((count, depth, deepest), Continue)
},
leave=(acc, _) => (acc.0, acc.1 - 1, acc.2),
)
inspect(count, content="3")
inspect(deepest, content="5")
}fn is_lower_start(c : Char) -> Booltest {
inspect(@scanner.is_lower_start('a'), content="true")
inspect(@scanner.is_lower_start('é'), content="true")
inspect(@scanner.is_lower_start('A'), content="false")
inspect(@scanner.is_lower_start('_'), content="false")
}fn is_name_part(c : Char) -> Booltest {
inspect(@scanner.is_name_part('x'), content="true")
inspect(@scanner.is_name_part('_'), content="true")
inspect(@scanner.is_name_part('7'), content="true")
inspect(@scanner.is_name_part('ß'), content="true")
inspect(@scanner.is_name_part('-'), content="false")
inspect(@scanner.is_name_part('\''), content="false")
}fn is_upper_start(c : Char) -> Booltest {
inspect(@scanner.is_upper_start('M'), content="true")
inspect(@scanner.is_upper_start('Ä'), content="true")
inspect(@scanner.is_upper_start('Dž'), content="true")
inspect(@scanner.is_upper_start('m'), content="false")
}test {
let rows = @syntax.kind_table()
let row = rows.filter(r => r.0 == "expression" && r.1 == "ifBlock")[0]
debug_inspect(row.2, content="[\"clause\", \"then\", \"else\"]")
// `list` is a kind of both expressions and patterns.
let lists = rows.filter(r => r.1 == "list").map(r => r.0)
debug_inspect(lists, content="[\"expression\", \"pattern\"]")
}test {
let source = @krueger.SourceText::new(
"module Main exposing (main)\n\nmain =\n 1 + 2\n",
)
let result = @krueger.parse_module(source)
inspect(result.diagnostics.length(), content="0")
guard result.ast is Some(file) else { fail("no AST") }
guard file.declarations[0].value is FunctionDeclaration(f) else {
fail("not a function")
}
inspect(f.declaration.value.name.value, content="main")
debug_inspect(
f.declaration.value.name.range,
content="{ start: { row: 3, column: 1 }, end: { row: 3, column: 5 } }",
)
}test {
let source = @krueger.SourceText::new(
"module Main exposing (..)\n\nmain =\n (1 + 2\n\nother = 3\n",
)
let result = @krueger.parse_module(source)
let d = result.diagnostics[0]
inspect(d.code, content="KR-PARSE-004")
inspect(d.title, content="UNFINISHED PARENTHESES")
debug_inspect(d.severity, content="Error")
// The declaration after the error still parses.
guard result.ast is Some(file) else { fail("no AST") }
inspect(file.declarations.length(), content="1")
}test {
let source = @krueger.SourceText::new(
"module Main exposing (..)\n\nx =\n 007\n",
)
let strict = @krueger.parse_module(source)
inspect(
strict.diagnostics[0].message,
content="Malformed function declaration: numbers cannot start with zeros [rule: leading-zero]",
)
let lenient = @krueger.parse_module(
source,
dialect=@krueger.Dialect::elm_syntax_7_3_9(),
)
inspect(lenient.diagnostics.length(), content="0")
}test {
let dialect = @krueger.Dialect::elm_0_19_1()
let source = @krueger.SourceText::new("module Main exposing (..)\n\nx = 1\n")
guard @krueger.tokenize(source, dialect~) is Ok(tokens) else {
fail("scan error")
}
let result = @krueger.parse_tokens(tokens, dialect~)
inspect(result.diagnostics.length(), content="0")
inspect(result == @krueger.parse_module(source, dialect~), content="true")
}fn print_declaration(d : Node[Declaration], width? : Int, dialect? : Dialect) -> String raise PrintErrortest {
let r : @ast.Range = {
start: { row: 0, column: 0, },
end: { row: 0, column: 0, },
}
let int : @ast.Node[@ast.TypeAnnotation] = {
range: r,
value: Typed({ range: r, value: ([][:], "Int"), }, [][:]),
}
let decl : @ast.Node[@ast.Declaration] = {
range: r,
value: AliasDeclaration({
documentation: None,
name: { range: r, value: "Age", },
generics: [][:],
type_annotation: int,
}),
}
inspect(
@printer.print_declaration(decl),
content=(
#|type alias Age =
#| Int
),
)
}fn print_expression(e : Node[Expression], width? : Int, dialect? : Dialect) -> String raise PrintErrortest {
let r : @ast.Range = {
start: { row: 0, column: 0, },
end: { row: 0, column: 0, },
}
fn v(name : String) -> @ast.Node[@ast.Expression] {
{ range: r, value: FunctionOrValue([][:], name), }
}
let sum : @ast.Node[@ast.Expression] = {
range: r,
value: OperatorApplication("+", Left, v("a"), v("b")),
}
let product : @ast.Node[@ast.Expression] = {
range: r,
value: OperatorApplication("*", Left, sum, v("c")),
}
inspect(@printer.print_expression(product), content="(a + b) * c")
}test {
let text = "module Main exposing (main)\n\n\nmain =\n 1 + 2\n"
let result = @parser.parse_module(
@scanner.SourceText::new(text),
@scanner.DefaultScanner::new(),
)
inspect(@printer.print_file(result.ast.unwrap()) == text, content="true")
}test {
let r : @ast.Range = {
start: { row: 0, column: 0, },
end: { row: 0, column: 0, },
}
fn pvar(name : String) -> @ast.Node[@ast.Pattern] {
{ range: r, value: VarPattern(name), }
}
let inner : @ast.Node[@ast.Pattern] = {
range: r,
value: UnConsPattern(pvar("a"), pvar("b")),
}
let outer : @ast.Node[@ast.Pattern] = {
range: r,
value: UnConsPattern(inner, pvar("rest")),
}
// `::` is right-associative, so an `::` on its left gets parentheses.
inspect(@printer.print_pattern(outer), content="(a :: b) :: rest")
}fn print_type_annotation(t : Node[TypeAnnotation], width? : Int, dialect? : Dialect) -> String raise PrintErrortest {
let r : @ast.Range = {
start: { row: 0, column: 0, },
end: { row: 0, column: 0, },
}
let a : @ast.Node[@ast.TypeAnnotation] = {
range: r,
value: GenericType("a"),
}
let maybe : @ast.Node[@ast.TypeAnnotation] = {
range: r,
value: Typed({ range: r, value: ([][:], "Maybe"), }, [a][:]),
}
let list : @ast.Node[@ast.TypeAnnotation] = {
range: r,
value: Typed({ range: r, value: ([][:], "List"), }, [maybe][:]),
}
inspect(@printer.print_type_annotation(list), content="List (Maybe a)")
}struct Paths {
found : Array[String]
}
impl @syntax.Handler for Paths with fn on_enter(self, e) {
if e.category == "pattern" {
self.found.push(e.path.to_string())
}
Continue
}
test {
let src = "module Main exposing (..)\n\nadd x y =\n x + y\n"
let result = @parser.parse_module(
@scanner.SourceText::new(src),
@scanner.DefaultScanner::new(),
)
let root = @syntax.NodeRef::of_result(result).unwrap()
let paths = { found: [], }
@syntax.push_events(@syntax.EventReader::new(root), paths)
inspect(paths.found.length(), content="2")
inspect(paths.found[1], content="declarations[0].declaration[0].arguments[1]")
}test {
let source = @scanner.SourceText::new(
"module Main exposing (..)\n\nmain =\n (1 + 2\n",
module_name="Main",
)
let result = @parser.parse_module(source, @scanner.DefaultScanner::new())
let json = @report.render_elm_json(result.diagnostics, source, "src/Main.elm")
guard json is { "errors": [{ "problems": [problem], .. }], .. } else {
fail("unexpected shape")
}
guard problem is { "title": String(title), "region": region, .. } else {
fail("unexpected problem")
}
inspect(title, content="UNFINISHED PARENTHESES")
inspect(
region.stringify(),
content=(
#|{"start":{"line":4,"column":11},"end":{"line":4,"column":11}}
),
)
}test {
let source = @scanner.SourceText::new(
"module Main exposing (..)\n\nmain =\n (1 + 2\n",
)
let result = @parser.parse_module(source, @scanner.DefaultScanner::new())
let text = @report.render_plain(result.diagnostics[0], source, "src/Main.elm")
inspect(
text,
content=(
#|-- UNFINISHED PARENTHESES ----------------------------------------- src/Main.elm
#|
#|I was partway through parsing an expression in parentheses, but I got stuck
#|here:
#|
#|4| (1 + 2
#| ^
#|I was expecting to see `,` or `)` next.
#|
),
)
}test {
let source = @scanner.SourceText::new(
"module Main exposing (..)\n\nmain =\n (1 + 2\n",
)
let result = @parser.parse_module(source, @scanner.DefaultScanner::new())
let text = @report.render_terminal(
result.diagnostics[0],
source,
"src/Main.elm",
)
// The header is cyan: ESC [36m.
inspect(
text.has_prefix("\u{1b}[36m-- UNFINISHED PARENTHESES"),
content="true",
)
}test {
let source = @krueger.SourceText::new("x = 1 -- one\n")
guard @krueger.tokenize(source) is Ok(stream) else { fail("scan error") }
debug_inspect(
stream.tokens.map(t => t.lexeme),
content=(
#|["x", "=", "1"]
),
)
// The comment is trivia after the last token.
inspect(stream.tokens[2].trivia_after.length() > 0, content="true")
let bad = @krueger.SourceText::new("s = \"abc\n")
guard @krueger.tokenize(bad) is Err(errors) else { fail("no error") }
inspect(errors.diagnostics[0].code, content="KR-SCAN-004")
}fn version() -> Stringtest {
inspect(@krueger.version().split(".").count(), content="3")
}test {
let src = "module Main exposing (..)\n\nf x = x\n"
let result = @parser.parse_module(
@scanner.SourceText::new(src),
@scanner.DefaultScanner::new(),
)
let root = @syntax.NodeRef::of_result(result).unwrap()
// Log enter and leave; skip the module header.
let log = []
@syntax.walk(
root,
n => {
log.push("+" + n.kind())
if n.category() == "module" {
SkipChildren
} else {
Continue
}
},
leave=n => log.push("-" + n.kind()),
)
inspect(
log.join(" "),
content="+file +normal -normal +function +implementation +name -name +var -var +functionOrValue -functionOrValue -implementation -function -file",
)
// Stop at the first pattern: the walk ends there, with no more `leave`.
let seen = []
@syntax.walk(
root,
n => {
seen.push("+" + n.kind())
if n.category() == "pattern" {
Stop
} else {
Continue
}
},
leave=n => seen.push("-" + n.kind()),
)
inspect(
seen.join(" "),
content="+file +normal +module_name -module_name +all -all -normal +function +implementation +name -name +var",
)
}Install
Download zipParser and parsing utilities for Elm and Elm-like dialects (e.g. Morphir) in MoonBit
Dependencies