///|
/// Multi-word keywords, longest first so the first match is the longest.
let phrases : ReadOnlyArray[ReadOnlyArray[String]] = [
["insert", "or", "replace"],
["insert", "or", "ignore"],
["left", "outer", "join"],
["right", "outer", "join"],
["full", "outer", "join"],
["if", "not", "exists"],
["create", "unique", "index"],
["create", "virtual", "table"],
["create", "table"],
["create", "view"],
["order", "by"],
["group", "by"],
["partition", "by"],
["inner", "join"],
["left", "join"],
["cross", "join"],
["primary", "key"],
["foreign", "key"],
["on", "conflict"],
["union", "all"],
["create", "index"],
["without", "rowid"],
]
///|
/// Single-word keywords, given lowercased. Window-frame words (`rows`,
/// `range`, `preceding`) are deliberately not keywords; they lex as
/// identifiers, which is harmless because window functions pass through.
fn is_keyword(lw : String) -> Bool {
match lw {
"select"
| "from"
| "where"
| "and"
| "or"
| "on"
| "as"
| "set"
| "update"
| "delete"
| "insert"
| "into"
| "values"
| "returning"
| "with"
| "limit"
| "having"
| "union"
| "like"
| "glob"
| "not"
| "null"
| "is"
| "in"
| "between"
| "exists"
| "case"
| "when"
| "then"
| "else"
| "end"
| "join"
| "unique"
| "default"
| "references"
| "filter"
| "over"
| "window"
| "distinct"
| "recursive"
| "desc"
| "asc"
| "collate"
| "all"
| "intersect"
| "except"
| "using" => true
_ => false
}
}
///|
fn match_phrase(words : ArrayView[String?]) -> ReadOnlyArray[String]? {
for phrase in phrases {
guard phrase.length() <= words.length() else { continue }
let matches = for k, word in phrase {
guard words[k] is Some(w) && w == word else { break false }
} nobreak {
true
}
if matches {
return Some(phrase)
}
}
None
}
///|
fn assemble(raw : Array[Spanned]) -> Array[Spanned] {
let words = raw.map(sp => {
if sp.token is Ident(w) {
Some(@text.ascii_lowercase(w))
} else {
None
}
})
let out = []
for i = 0; i < raw.length(); {
let { token, start, end, } = raw[i]
guard words[i] is Some(lw) else {
out.push(raw[i])
continue i + 1
}
match match_phrase(words[i:]) {
Some(phrase) => {
let last = i + phrase.length() - 1
out.push({
token: Keyword(phrase.iter().join(" ")),
start,
end: raw[last].end,
})
continue last + 1
}
None => {
let token = if is_keyword(lw) { Keyword(lw) } else { token }
out.push({ token, start, end, })
continue i + 1
}
}
}
out
}