///|
/// `Elm.Parser.Base.moduleName`: `Name` or `Name.Name...` with touching dots.
fn module_name(c : Cursor) -> @ast.Node[@ast.ModuleName] raise SyntaxError {
let first = c.upper("a module name")
let parts = [first.lexeme]
let mut last = first
while c.dotted_name(is_upper) is Some(_) {
ignore(c.advance())
last = c.advance()
parts.push(last.lexeme)
}
node(range_from(first, last), parts[:])
}
///|
/// One exposed item: `value`, `Type`, `Type(..)` or `(op)`.
fn exposable(c : Cursor) -> @ast.Node[@ast.TopLevelExpose] raise SyntaxError {
guard c.peek() is Some(t) else { raise c.fail("an exposed name") }
if is_lower(t) {
ignore(c.advance())
return token_node(t, FunctionExpose(t.lexeme))
}
if is_upper(t) {
ignore(c.advance())
if c.at_kind(LParen) && c.peek_at(1) is Some({ kind: DotDot, .. }) {
let open = c.advance()
ignore(c.advance())
let close = c.expect(RParen, "`)`")
let open_range = range_from(open, close)
return node(
{ start: location(t.span.start), end: open_range.end, },
TypeExpose({ name: t.lexeme, open: Some(open_range), }),
)
}
return token_node(t, TypeOrAliasExpose(t.lexeme))
}
if t.kind == LParen {
let open = c.advance()
guard c.peek() is Some({ kind: Operator(op), .. } as op_token) &&
(!c.dialect.has(SpacedOperatorName) || touching(open, op_token)) else {
raise c.fail("an operator like `(+)`")
}
ignore(c.advance())
if c.dialect.has(SpacedOperatorName) &&
c.peek() is Some(close) &&
!touching(op_token, close) {
raise c.fail("`)` right after the operator")
}
let close = c.expect(RParen, "`)`")
return node(range_from(open, close), InfixExpose(op))
}
raise c.fail("an exposed name")
}
///|
/// `exposing (..)` or `exposing (a, B, C(..), (+))`; the range runs from
/// `exposing` to `)`.
fn exposing_list(c : Cursor) -> @ast.Node[@ast.Exposing] raise SyntaxError {
c.within(Exposing, () => exposing_list_body(c))
}
///|
fn exposing_list_body(c : Cursor) -> @ast.Node[@ast.Exposing] raise SyntaxError {
let keyword = c.expect(Keyword(Exposing), "`exposing`")
ignore(c.expect(LParen, "`(`"))
if c.at_kind(DotDot) {
let dots = c.advance()
let close = c.expect(RParen, "`)`")
return node(
range_from(keyword, close),
All({ start: location(dots.span.start), end: location(close.span.start), }),
)
}
let items = [exposable(c)]
while c.at_kind(Comma) {
ignore(c.advance())
items.push(exposable(c))
}
let close = c.expect(RParen, "`,` or `)`")
node(range_from(keyword, close), Explicit(items))
}
///|
/// `module`, `port module` or `effect module ... where { ... }` header.
fn module_header(c : Cursor) -> @ast.Node[@ast.Module] raise SyntaxError {
c.within(ModuleHeader, () => module_header_body(c))
}
///|
fn module_header_body(c : Cursor) -> @ast.Node[@ast.Module] raise SyntaxError {
module_level_column(c, "module")
guard c.peek() is Some(first) else { raise c.fail("a module header") }
if first.kind == Keyword(Port) {
ignore(c.advance())
ignore(c.expect(Keyword(Module), "`module`"))
let name = module_name(c)
let exposing = exposing_list(c)
return node(
{ start: location(first.span.start), end: exposing.range.end, },
PortModule({ module_name: name, exposing_list: exposing, }),
)
}
if first.kind == Identifier && first.lexeme == "effect" {
ignore(c.advance())
ignore(c.expect(Keyword(Module), "`module`"))
let name = module_name(c)
ignore(c.expect(Keyword(Where), "`where`"))
ignore(c.expect(LBrace, "`{`"))
let mut command : @ast.Node[String]? = None
let mut subscription : @ast.Node[String]? = None
while true {
let key = c.lower("`command` or `subscription`")
let repeated = match key.lexeme {
"command" => command is Some(_)
"subscription" => subscription is Some(_)
_ => false
}
if repeated && c.dialect.has(DuplicateEffectKey) {
raise c.error_at(
"repeated effect key `\{key.lexeme}` [rule: duplicate-effect-key]",
key.span,
"BAD MODULE DECLARATION",
"I was parsing the `where` part of an effect module and got stuck on this repeated key:",
[
Text([
Plain("An effect module names its "),
@scanner.Chunk::code("command"),
Plain(" and "),
@scanner.Chunk::code("subscription"),
Plain(" types at most once each."),
]),
],
)
}
ignore(c.expect(Equals, "`=`"))
let value = c.upper("a type name")
// elm-syntax keeps the first value of a repeated key.
match key.lexeme {
"command" if command is None =>
command = Some(token_node(value, value.lexeme))
"subscription" if subscription is None =>
subscription = Some(token_node(value, value.lexeme))
_ => ()
}
if c.at_kind(Comma) {
ignore(c.advance())
} else {
break
}
}
ignore(c.expect(RBrace, "`}`"))
let exposing = exposing_list(c)
if c.dialect.has(EffectModule) && !c.dialect.core_package {
raise c.error_at(
"effect modules are only allowed in elm packages [rule: effect-module]",
first.span,
"INVALID EFFECT MODULE",
"This module is declared as an effect module:",
[
Text([
Plain("Effect modules are only allowed in "),
@scanner.Chunk::code("elm/*"),
Plain(" and "),
@scanner.Chunk::code("elm-explorations/*"),
Plain(" packages. Use "),
@scanner.Chunk::code("module"),
Plain(" or "),
@scanner.Chunk::code("port module"),
Plain(" instead."),
]),
],
)
}
return node(
{ start: location(first.span.start), end: exposing.range.end, },
EffectModule({
module_name: name,
exposing_list: exposing,
command,
subscription,
}),
)
}
ignore(c.expect(Keyword(Module), "`module`"))
let name = module_name(c)
let exposing = exposing_list(c)
node(
{ start: location(first.span.start), end: exposing.range.end, },
NormalModule({ module_name: name, exposing_list: exposing, }),
)
}
///|
/// `import A.B as C exposing (...)`; the range ends at the last part.
fn import_declaration(
c : Cursor,
first : Bool,
) -> @ast.Node[@ast.Import] raise SyntaxError {
c.within(Import, () => import_declaration_body(c, first))
}
///|
fn import_declaration_body(
c : Cursor,
first : Bool,
) -> @ast.Node[@ast.Import] raise SyntaxError {
// elm-syntax checks the column of the first import only; elm make reads an
// indented import as part of the one before.
if first {
module_level_column(c, "import")
} else if c.dialect.has(ImportColumn) &&
c.peek() is Some(t) &&
t.span.start.column != 1 {
raise c.error_at(
"an import must start in column 1 [rule: import-column]",
t.span,
"UNFINISHED IMPORT",
"I am partway through parsing an import, but I got stuck here:",
[
plain(
"Each import starts at the beginning of its own line. Delete the spaces before `import`.",
),
],
)
}
let keyword = c.expect(Keyword(Import), "`import`")
let name = module_name(c)
let mut end = name.range.end
let mut module_alias : @ast.Node[@ast.ModuleName]? = None
if c.is_keyword(As) {
ignore(c.advance())
if c.at_lower() {
raise c.fail_titled(
"expected a module alias",
"EXPECTING IMPORT ALIAS",
[Plain("I was parsing an import, but I got stuck here:")],
[
Text([
Plain("I was expecting an alias like "),
@scanner.Chunk::code("D"),
Plain(" in "),
@scanner.Chunk::code("import Dict as D"),
Plain(". Aliases must start with a capital letter."),
]),
],
)
}
let alias_token = c.upper("a module alias")
let alias_node : @ast.Node[@ast.ModuleName] = token_node(
alias_token,
[alias_token.lexeme][:],
)
end = alias_node.range.end
module_alias = Some(alias_node)
}
let mut exposing : @ast.Node[@ast.Exposing]? = None
if c.is_keyword(Exposing) {
let list = exposing_list(c)
end = list.range.end
exposing = Some(list)
}
node({ start: location(keyword.span.start), end, }, {
module_name: name,
module_alias,
exposing_list: exposing,
})
}
///|
/// A module header or import must start in column 1.
fn module_level_column(c : Cursor, keyword : String) -> Unit raise SyntaxError {
guard c.dialect.has(ModuleLevelColumn) &&
c.peek() is Some(t) &&
t.span.start.column != 1 else {
return
}
raise SyntaxError(
"`\{keyword}` must start in column 1 [rule: \{@dialect.Rule::ModuleLevelColumn.name()}]",
t.span,
span_problem(
"TOO MUCH INDENTATION",
"This `\{keyword}` should not have any spaces before it:",
point(t.span.start),
[
plain(
"Delete the spaces before `\{keyword}` until there are none left!",
),
],
),
)
}