///|
/// A rejection rule: one class of syntax that a dialect rejects. A rule only
/// runs when the dialect enables it.
pub(all) enum Rule {
LeadingZero
EmptyHex
BadUnicodeEscape
CharLength
IndentedContinuation
LetDeclarationColumn
ModuleLevelColumn
SpacedOperatorName
UppercaseHexPrefix
ExponentWithoutDigits
ImportColumn
NonAsciiDigitInName
TitlecaseNameStart
EffectModule
InfixDeclaration
DuplicateEffectKey
PortInNormalModule
} derive(Eq, Hash, Debug)
///|
/// Kebab-case name, shown in diagnostics as `[rule: ]`.
pub fn Rule::name(self : Rule) -> String {
match self {
LeadingZero => "leading-zero"
EmptyHex => "empty-hex"
BadUnicodeEscape => "bad-unicode-escape"
CharLength => "char-length"
IndentedContinuation => "indented-continuation"
LetDeclarationColumn => "let-declaration-column"
ModuleLevelColumn => "module-level-column"
SpacedOperatorName => "spaced-operator-name"
UppercaseHexPrefix => "uppercase-hex-prefix"
ExponentWithoutDigits => "exponent-without-digits"
ImportColumn => "import-column"
NonAsciiDigitInName => "non-ascii-digit-in-name"
TitlecaseNameStart => "titlecase-name-start"
EffectModule => "effect-module"
InfixDeclaration => "infix-declaration"
DuplicateEffectKey => "duplicate-effect-key"
PortInNormalModule => "port-in-normal-module"
}
}
///|
/// An infix operator: its symbol, precedence (1 to 9) and direction.
pub(all) struct OperatorDef {
symbol : String
precedence : Int
direction : @ast.InfixDirection
} derive(Eq, Debug)
///|
/// Whether attributes in doc comments are parsed.
pub(all) enum AttributeSyntax {
Off
DocComment
} derive(Eq, Debug)
///|
/// A variant of Elm: which rejection rules run, and the extension data (operator
/// table, extra reserved words, extra operator symbols) the scanner and the
/// parser read. The AST shape is elm-syntax 7.3.9 in every dialect.
///
/// - An extra operator symbol must start with a character the lexer already
/// treats as a symbol (`+ - / * = . < > : & | ^ ?`) and must not start with
/// `--` (a line comment).
/// - An extra reserved word must not be `alias`, `infix` or `effect`: the
/// parser reads those by name.
/// - `{ ..d, field: value }` shares `rules` and `operators` with `d`; copy them
/// before changing either.
pub(all) struct Dialect {
name : String
rules : @hashset.HashSet[Rule]
operators : Array[OperatorDef]
reserved_words : Array[String]
operator_symbols : Array[String]
attributes : AttributeSyntax
/// Whether the file belongs to an `elm/*` or `elm-explorations/*` package.
/// `elm make` lets only
/// those declare infix operators and effect modules.
core_package : Bool
}
///|
/// The infix operators of Elm 0.19.1 as elm-syntax 7.3.9 knows them, including
/// `>` and `>` from elm/url.
pub fn standard_operators() -> Array[OperatorDef] {
let table : Array[(String, Int, @ast.InfixDirection)] = [
("|>", 1, Left),
("<|", 1, Right),
("||", 2, Right),
("&&", 3, Right),
("==", 4, Non),
("/=", 4, Non),
("<=", 4, Non),
(">=", 4, Non),
(">", 4, Non),
("<", 4, Non),
("++", 5, Right),
("::", 5, Right),
("|=", 5, Left),
("+", 6, Left),
("-", 6, Left),
("|.", 6, Left),
("//", 7, Left),
("*", 7, Left),
("/", 7, Left),
(">", 7, Right),
(">", 8, Left),
("^", 8, Right),
(">>", 9, Right),
("<<", 9, Left),
]
table.map(entry => {
let (symbol, precedence, direction) = entry
{ symbol, precedence, direction, }
})
}
///|
/// Rules both built-in dialects enable: `elm make` and elm-syntax reject
/// these.
fn shared_rules() -> Array[Rule] {
[
IndentedContinuation,
LetDeclarationColumn,
ModuleLevelColumn,
CharLength,
EmptyHex,
SpacedOperatorName,
]
}
///|
fn standard(name : String, rules : Array[Rule]) -> Dialect {
{
name,
rules: @hashset.HashSet(rules),
operators: standard_operators(),
reserved_words: [],
operator_symbols: [],
attributes: DocComment,
core_package: false,
}
}
///|
/// The default dialect: rejects what `elm make` 0.19.1 rejects as syntax.
pub fn Dialect::elm_0_19_1() -> Dialect {
standard(
"elm-0.19.1",
[
..shared_rules(),
LeadingZero,
UppercaseHexPrefix,
ExponentWithoutDigits,
BadUnicodeEscape,
ImportColumn,
NonAsciiDigitInName,
EffectModule,
InfixDeclaration,
DuplicateEffectKey,
PortInNormalModule,
],
)
}
///|
/// Rejects exactly what elm-syntax 7.3.9 rejects.
pub fn Dialect::elm_syntax_7_3_9() -> Dialect {
standard("elm-syntax-7.3.9", [..shared_rules(), TitlecaseNameStart])
}
///|
/// Whether the dialect enables `rule`.
pub fn Dialect::has(self : Dialect, rule : Rule) -> Bool {
self.rules.contains(rule)
}
///|
/// The operator table entry for `symbol`, if the dialect has one.
pub fn Dialect::operator(self : Dialect, symbol : String) -> OperatorDef? {
for op in self.operators {
if op.symbol == symbol {
return Some(op)
}
}
None
}
///|
/// Problems with the dialect's extension data, as messages (empty when it is
/// valid). The scanner and parser do not check these; call this when a
/// dialect comes from outside (a config file, a CLI flag).
pub fn Dialect::validate(self : Dialect) -> Array[String] {
let problems = []
let seen : Array[String] = []
for op in self.operators {
if op.symbol == "" {
problems.push("an operator has an empty symbol")
continue
}
if seen.contains(op.symbol) {
problems.push("operator `\{op.symbol}` is defined twice")
}
seen.push(op.symbol)
if op.precedence < 1 || op.precedence > 9 {
problems.push(
"operator `\{op.symbol}` has precedence \{op.precedence}; precedences are 1 to 9",
)
}
}
for symbol in self.operator_symbols {
match symbol.get_char(0) {
None => problems.push("operator symbol `` is empty")
Some(c) =>
if !"+-/*=.<>:&|^?".contains_char(c) {
problems.push(
"operator symbol `\{symbol}` does not start with a symbol character",
)
} else if symbol.has_prefix("--") {
problems.push("operator symbol `\{symbol}` starts a line comment")
}
}
}
for word in self.reserved_words {
if ["alias", "infix", "effect"].contains(word) {
problems.push(
"reserved word `\{word}` is a name the parser reads by its text",
)
}
}
problems
}