///|
fn parser_error(
code : String,
message : String,
span : @scanner.Span,
title~ : String,
report~ : Array[@scanner.Block],
) -> @scanner.Diagnostic {
{ code, severity: Error, message, span, title, report, }
}
///|
fn declaration_error_code(kind : DeclarationKind) -> (String, String) {
match kind {
FunctionKind => ("KR-PARSE-004", "Malformed function declaration")
TypeKind => ("KR-PARSE-003", "Malformed type declaration")
PortKind | InfixKind => ("KR-PARSE-008", "Syntax error")
}
}
///|
/// Skip to the next token on column 1 (the next top-level item). The item
/// started at `start`; the cursor moves past it at least one token, so a
/// failure on the item's last token does not skip the next item.
fn skip_to_top_level(c : Cursor, start : Int) -> Unit {
if !c.at_end() && c.pos <= start {
c.pos = start + 1
}
// A token in column 1 that cannot start a declaration (a badly indented
// `in`, `else`, `,` or type) belongs to the failed item: skip it too, so it
// is not reported a second time.
while c.peek() is Some(t) &&
(t.span.start.column != 1 || !starts_declaration(c)) {
c.pos += 1
}
}
///|
/// Whether the token at the cursor can start a top-level declaration: `type`,
/// `port`, `infix`, or a lower-case name with `=` or `:` later on its line.
fn starts_declaration(c : Cursor) -> Bool {
guard c.peek() is Some(t) else { return false }
match t.kind {
Keyword(Type | Port | Import | Module) => true
Identifier if t.lexeme == "infix" => true
Identifier if is_lower(t) => {
let mut i = 1
while c.peek_at(i) is Some(n) && n.span.start.line == t.span.start.line {
if n.kind is (Equals | Colon) {
return true
}
i += 1
}
false
}
_ => false
}
}
///|
/// The problem for a token `t` that is not at column 1 where a top-level
/// declaration must start.
fn not_at_module_level(c : Cursor, t : @scanner.Token) -> Problem {
ignore(c)
let highlight = point(t.span.start)
if t.kind is Equals {
make_problem(
"UNEXPECTED EQUALS",
[Plain("I was not expecting to see this equals sign:")],
highlight,
highlight,
[
plain(
"Maybe this is part of a definition that does not start in the first column? Top-level definitions must start at the very beginning of a line.",
),
],
)
} else {
make_problem(
"SYNTAX PROBLEM",
[Plain("I got stuck here:")],
highlight,
highlight,
[
plain(
"I was expecting a new declaration at the start of a line. Is something indented too little or too much above?",
),
],
)
}
}
///|
/// Span of the tokens from `start` up to the cursor.
fn span_since(c : Cursor, start : Int) -> @scanner.Span {
let first = c.tokens[start]
let last = if c.pos > start { c.tokens[c.pos - 1] } else { first }
{ start: first.span.start, end: last.span.end, }
}
///|
/// The last doc comment in `trivia`.
fn last_doc_comment(trivia : Array[@scanner.Trivia]) -> @scanner.Comment? {
let mut last : @scanner.Comment? = None
for item in trivia {
if item is Comment({ kind: Doc, .. } as comment) {
last = Some(comment)
}
}
last
}
///|
/// The first doc comment in `trivia`.
fn first_doc_comment(trivia : Array[@scanner.Trivia]) -> @scanner.Comment? {
for item in trivia {
if item is Comment({ kind: Doc, .. } as comment) {
return Some(comment)
}
}
None
}
///|
/// All comments for `File.comments` in source order: regular comments, plus
/// the doc comments in `listed` (module documentation, docs before ports).
/// Doc comments in `attached` belong to declarations. Any other doc comment
/// is stray: elm-syntax rejects the file.
fn collect_comments(
tokens : Array[@scanner.Token],
attached : Array[@scanner.Span],
listed : Array[@scanner.Span],
) -> (Array[@ast.Node[@ast.Comment]], Array[@scanner.Span]) {
let comments : Array[@ast.Node[@ast.Comment]] = []
let stray = []
for token in tokens {
for trivia in [..token.trivia_before, ..token.trivia_after] {
guard trivia is Comment(comment) else { continue }
let range : @ast.Range = {
start: location(comment.span.start),
end: location(comment.span.end),
}
match comment.kind {
Line | Block => comments.push(node(range, comment.text))
Doc =>
if listed.contains(comment.span) {
comments.push(node(range, comment.text))
} else if !attached.contains(comment.span) {
stray.push(comment.span)
}
}
}
}
(comments, stray)
}
///|
/// CST entry for a top-level declaration from its AST (ports and infix
/// declarations have no CST form).
fn declaration_cst(
decl : @ast.Node[@ast.Declaration],
tokens : Array[@scanner.Token],
span : @scanner.Span,
) -> @cst.DeclarationCst? {
match decl.value {
FunctionDeclaration(f) =>
Some(
Function({
name: f.declaration.value.name.value,
args: f.declaration.value.arguments.map(pattern_text),
tokens,
span,
}),
)
AliasDeclaration(a) =>
Some(
TypeAlias({
name: a.name.value,
type_params: a.generics.map(g => g.value),
tokens,
span,
}),
)
CustomTypeDeclaration(t) =>
Some(
UnionType({
name: t.name.value,
type_params: t.generics.map(g => g.value),
tokens,
span,
}),
)
_ => None
}
}
///|
/// Short text for a pattern in the CST.
fn pattern_text(p : @ast.Node[@ast.Pattern]) -> String {
match p.value {
VarPattern(name) => name
AllPattern => "_"
UnitPattern => "()"
NamedPattern(name_ref, _) => name_ref.name
_ => "_"
}
}
///|
fn header_module_name(m : @ast.Module) -> @ast.ModuleName {
match m {
NormalModule(d) | PortModule(d) => d.module_name.value
EffectModule(d) => d.module_name.value
}
}
///|
fn header_exposing(m : @ast.Module) -> @ast.Node[@ast.Exposing] {
match m {
NormalModule(d) | PortModule(d) => d.exposing_list
EffectModule(d) => d.exposing_list
}
}
///|
/// Exposed names as the CST records them (empty for `(..)`).
fn exposed_names(e : @ast.Node[@ast.Exposing]) -> Array[String] {
match e.value {
All(_) => []
Explicit(items) =>
items.map(i => {
match i.value {
InfixExpose(n) | FunctionExpose(n) | TypeOrAliasExpose(n) => n
TypeExpose(t) => t.name
}
})
}
}
///|
/// Parse a token stream into an elm-syntax AST, a CST and diagnostics,
/// following stil4m/elm-syntax 7.3.9.
pub fn parse_tokens(
tokens : @scanner.TokenStream,
dialect? : @dialect.Dialect = @dialect.Dialect::elm_0_19_1(),
) -> ParseResult {
let stream = tokens.tokens
let c = Cursor::new(stream, dialect)
let diagnostics = []
let attached = []
let listed = []
let mut module_definition : @ast.Node[@ast.Module]? = None
let mut cst_header : @cst.ModuleHeaderCst? = None
let imports : Array[@ast.Node[@ast.Import]] = []
let cst_imports = []
let attribute_groups : Array[AttributeGroup] = []
let declarations : Array[@ast.Node[@ast.Declaration]] = []
let cst_decls = []
let starts_header = match c.peek() {
Some({ kind: Keyword(Module | Port), .. }) => true
Some({ kind: Identifier, lexeme: "effect", .. }) => true
_ => false
}
if starts_header {
c.start_item()
let start = c.pos
try module_header(c) catch {
SyntaxError(message, span, problem) => {
diagnostics.push(
parser_error(
"KR-PARSE-001",
"Malformed module header: \{message}",
span,
title=problem.title,
report=problem.report,
),
)
skip_to_top_level(c, start)
}
} noraise {
header => {
module_definition = Some(header)
c.port_module = header.value is PortModule(_)
let header_tokens = stream[start:c.pos].to_owned()
cst_header = Some({
module_name: Some(header_module_name(header.value).join(".")),
exposing: exposed_names(header_exposing(header.value)),
tokens: header_tokens,
span: span_since(c, start),
})
if c.peek() is Some(next) &&
first_doc_comment(next.trivia_before) is Some(doc) {
listed.push(doc.span)
collect_attributes(c, doc, Module, attribute_groups, diagnostics)
}
}
}
} else {
diagnostics.push(
parser_error(
"KR-PARSE-006",
"Missing module header",
c.here(),
title="MODULE NAME MISSING",
report=[
Text([
Plain(
"I need the module name to be declared at the top of this file, like this:",
),
]),
Example("module Fixture exposing (..)"),
plain("Try adding that as the first line of your file!"),
],
),
)
}
let mut first_import = true
while c.is_keyword(Import) {
c.start_item()
let start = c.pos
let first = first_import
first_import = false
try import_declaration(c, first) catch {
SyntaxError(message, span, problem) => {
diagnostics.push(
parser_error(
"KR-PARSE-002",
"Malformed import declaration: \{message}",
span,
title=problem.title,
report=problem.report,
),
)
skip_to_top_level(c, start)
}
} noraise {
imp => {
imports.push(imp)
cst_imports.push(@cst.ImportCst::{
module_name: imp.value.module_name.value.join("."),
exposing: imp.value.exposing_list.map(exposed_names),
tokens: stream[start:c.pos].to_owned(),
span: span_since(c, start),
})
}
}
}
while c.peek() is Some(t) {
if t.span.start.column != 1 {
let start = c.pos
let problem = not_at_module_level(c, t)
skip_to_top_level(c, start)
diagnostics.push(
parser_error(
"KR-PARSE-007",
"Unexpected syntax: token not at module level",
span_since(c, start),
title=problem.title,
report=problem.report,
),
)
continue
}
let kind = declaration_kind(c)
let mut port_doc : @scanner.Comment? = None
let doc = match kind {
FunctionKind | TypeKind =>
match last_doc_comment(t.trivia_before) {
Some(d) if !listed.contains(d.span) => Some(d)
_ => None
}
PortKind =>
match last_doc_comment(t.trivia_before) {
Some(d) if !listed.contains(d.span) => {
listed.push(d.span)
port_doc = Some(d)
None
}
_ => None
}
InfixKind => None
}
let documentation = doc.map(d => {
node(
{ start: location(d.span.start), end: location(d.span.end), },
d.text,
)
})
let start = c.pos
c.start_item()
try declaration(c, documentation) catch {
SyntaxError(message, span, problem) => {
let (code, what) = declaration_error_code(kind)
diagnostics.push(
parser_error(
code,
"\{what}: \{message}",
span,
title=problem.title,
report=problem.report,
),
)
skip_to_top_level(c, start)
}
} noraise {
decl if declaration_depth(decl.value) > MAX_AST_DEPTH => {
let (code, what) = declaration_error_code(kind)
let at = point(stream[start].span.start)
diagnostics.push(
parser_error(
code,
"\{what}: nested too deeply",
stream[start].span,
title="TOO MUCH NESTING",
report=[
plain("This declaration is nested too deeply for me:"),
Excerpt(context=at, highlight=at),
plain(
"Its expressions, types or patterns go more than \{MAX_AST_DEPTH} levels deep. Try splitting it into smaller definitions.",
),
],
),
)
}
decl => {
if doc is Some(d) {
attached.push(d.span)
}
let target_doc = if doc is Some(_) { doc } else { port_doc }
if target_doc is Some(d) && declaration_name(decl) is Some(name) {
collect_attributes(
c,
d,
Declaration(name~, range=decl.range),
attribute_groups,
diagnostics,
)
}
declarations.push(decl)
let decl_tokens = stream[start:c.pos].to_owned()
if declaration_cst(decl, decl_tokens, span_since(c, start))
is Some(entry) {
cst_decls.push(entry)
}
}
}
}
diagnostics.append(c.warnings)
let (comments, stray) = collect_comments(stream, attached, listed)
for span in stray {
diagnostics.push(
parser_error(
"KR-PARSE-007",
"Unexpected syntax: doc comment not attached to a declaration",
span,
title="WEIRD DECLARATION",
report=[
plain("I was not expecting this documentation comment here:"),
Excerpt(context=point(span.start), highlight=point(span.start)),
Text([
Plain("A documentation comment ("),
@scanner.Chunk::code("{-| ... -}"),
Plain(
") must come right before the declaration it documents, or right after the module header.",
),
]),
],
),
)
}
ParseResult::{
ast: module_definition.map(m => {
module_definition: m,
imports,
declarations,
comments,
}),
cst: Some({
tokens: stream,
header: cst_header,
imports: cst_imports,
declarations: cst_decls,
span: match (stream.get(0), stream.last()) {
(Some(first), Some(last)) =>
Some({ start: first.span.start, end: last.span.end, })
_ => None
},
}),
diagnostics,
attributes: attribute_groups,
}
}
///|
/// The name of a declaration that can carry attributes.
fn declaration_name(decl : @ast.Node[@ast.Declaration]) -> String? {
match decl.value {
FunctionDeclaration(f) => Some(f.declaration.value.name.value)
AliasDeclaration(a) => Some(a.name.value)
CustomTypeDeclaration(t) => Some(t.name.value)
PortDeclaration(p) => Some(p.name.value)
_ => None
}
}
///|
/// Read the attributes of `comment` for `target`, when the dialect parses
/// them; malformed ones become warnings.
fn collect_attributes(
c : Cursor,
comment : @scanner.Comment,
target : AttributeTarget,
groups : Array[AttributeGroup],
diagnostics : Array[@scanner.Diagnostic],
) -> Unit {
guard c.dialect.attributes is DocComment else { return }
let (attributes, warnings) = doc_attributes(comment, c.dialect)
diagnostics.append(warnings)
if !attributes.is_empty() {
groups.push({ target, attributes, })
}
}
///|
/// Tokenize `source` with `scanner` and parse it in `dialect`. The caller
/// builds `scanner` for the same dialect.
pub fn[S : @scanner.Scanner] parse_module(
source : @scanner.SourceText,
scanner : S,
dialect? : @dialect.Dialect = @dialect.Dialect::elm_0_19_1(),
) -> ParseResult {
match scanner.tokenize(source) {
Ok(tokens) => parse_tokens(tokens, dialect~)
Err(errs) =>
{ ast: None, cst: None, diagnostics: errs.diagnostics, attributes: [], }
}
}