///|
priv enum TokenKind {
Word(String)
Number(Int)
Quoted(String)
KeywordSpec
KeywordFeature
KeywordGoal
KeywordIn
KeywordCost
KeywordLocked
KeywordAnd
KeywordOr
KeywordNot
KeywordPresent
KeywordTrue
KeywordFalse
Assign
EqualEqual
BangEqual
LessToken
LessEqualToken
GreaterToken
GreaterEqualToken
LeftBracket
RightBracket
LeftParen
RightParen
Comma
Newline
End
}
///|
priv struct Token {
kind : TokenKind
span : Span
}
///|
fn token_name(kind : TokenKind) -> String {
match kind {
Word(value) => "identifier '\{value}'"
Number(value) => "integer '\{value}'"
Quoted(_) => "text value"
KeywordSpec => "'spec'"
KeywordFeature => "'feature'"
KeywordGoal => "'goal'"
KeywordIn => "'in'"
KeywordCost => "'cost'"
KeywordLocked => "'locked'"
KeywordAnd => "'and'"
KeywordOr => "'or'"
KeywordNot => "'not'"
KeywordPresent => "'present'"
KeywordTrue => "'true'"
KeywordFalse => "'false'"
Assign => "'='"
EqualEqual => "'=='"
BangEqual => "'!='"
LessToken => "'<'"
LessEqualToken => "'<='"
GreaterToken => "'>'"
GreaterEqualToken => "'>='"
LeftBracket => "'['"
RightBracket => "']'"
LeftParen => "'('"
RightParen => "')'"
Comma => "','"
Newline => "end of line"
End => "end of input"
}
}
///|
fn lex(source : String) -> Result[Array[Token], Diagnostic] {
let chars = source.to_array()
let tokens : Array[Token] = []
let mut index = 0
let mut line = 1
let mut column = 1
while index < chars.length() {
let ch = chars[index]
if ch == ' ' || ch == '\t' || ch == '\r' {
index = index + 1
column = column + 1
continue
}
if ch == '\n' {
tokens.push({ kind: Newline, span: { offset: index, line, column } })
index = index + 1
line = line + 1
column = 1
continue
}
if ch == '#' {
while index < chars.length() && chars[index] != '\n' {
index = index + 1
column = column + 1
}
continue
}
let start = Span::{ offset: index, line, column }
if is_identifier_start(ch) {
let begin = index
while index < chars.length() && is_identifier_continue(chars[index]) {
index = index + 1
column = column + 1
}
let text = String::from_array(chars[begin:index])
tokens.push({ kind: keyword_or_word(text), span: start })
continue
}
if is_ascii_digit(ch) ||
(
ch == '-' &&
index + 1 < chars.length() &&
is_ascii_digit(chars[index + 1])
) {
let negative = ch == '-'
if negative {
index = index + 1
column = column + 1
}
let mut value = 0
while index < chars.length() && is_ascii_digit(chars[index]) {
let digit = chars[index].to_int() - '0'.to_int()
let final_digit_limit = if negative { 8 } else { 7 }
if value > 214748364 ||
(value == 214748364 && digit > final_digit_limit) {
return Err({
code: "E_INTEGER_RANGE",
message: "integer is outside the supported 32-bit range",
span: start,
})
}
value = value * 10 + digit
index = index + 1
column = column + 1
}
tokens.push({
kind: Number(if negative { -value } else { value }),
span: start,
})
continue
}
if ch == '"' {
index = index + 1
column = column + 1
let text = StringBuilder::new()
let mut closed = false
while index < chars.length() {
let current = chars[index]
if current == '"' {
index = index + 1
column = column + 1
closed = true
break
}
if current == '\n' {
return Err({
code: "E_STRING_NEWLINE",
message: "text values cannot span lines",
span: start,
})
}
if current == '\\' {
if index + 1 >= chars.length() {
return Err({
code: "E_STRING_ESCAPE",
message: "unfinished escape sequence",
span: start,
})
}
let escaped = chars[index + 1]
match escaped {
'n' => text.write_char('\n')
'r' => text.write_char('\r')
't' => text.write_char('\t')
'\\' => text.write_char('\\')
'"' => text.write_char('"')
_ =>
return Err({
code: "E_STRING_ESCAPE",
message: "unsupported escape '\\\{escaped}'",
span: { offset: index, line, column },
})
}
index = index + 2
column = column + 2
} else {
text.write_char(current)
index = index + 1
column = column + 1
}
}
if !closed {
return Err({
code: "E_STRING_UNCLOSED",
message: "unterminated text value",
span: start,
})
}
tokens.push({ kind: Quoted(text.to_string()), span: start })
continue
}
match ch {
'=' =>
if index + 1 < chars.length() && chars[index + 1] == '=' {
tokens.push({ kind: EqualEqual, span: start })
index = index + 2
column = column + 2
} else {
tokens.push({ kind: Assign, span: start })
index = index + 1
column = column + 1
}
'!' =>
if index + 1 < chars.length() && chars[index + 1] == '=' {
tokens.push({ kind: BangEqual, span: start })
index = index + 2
column = column + 2
} else {
return Err({
code: "E_UNEXPECTED_CHAR",
message: "expected '=' after '!'",
span: start,
})
}
'<' =>
if index + 1 < chars.length() && chars[index + 1] == '=' {
tokens.push({ kind: LessEqualToken, span: start })
index = index + 2
column = column + 2
} else {
tokens.push({ kind: LessToken, span: start })
index = index + 1
column = column + 1
}
'>' =>
if index + 1 < chars.length() && chars[index + 1] == '=' {
tokens.push({ kind: GreaterEqualToken, span: start })
index = index + 2
column = column + 2
} else {
tokens.push({ kind: GreaterToken, span: start })
index = index + 1
column = column + 1
}
'[' => {
tokens.push({ kind: LeftBracket, span: start })
index = index + 1
column = column + 1
}
']' => {
tokens.push({ kind: RightBracket, span: start })
index = index + 1
column = column + 1
}
'(' => {
tokens.push({ kind: LeftParen, span: start })
index = index + 1
column = column + 1
}
')' => {
tokens.push({ kind: RightParen, span: start })
index = index + 1
column = column + 1
}
',' => {
tokens.push({ kind: Comma, span: start })
index = index + 1
column = column + 1
}
_ =>
return Err({
code: "E_UNEXPECTED_CHAR",
message: "unexpected character '\{ch}'",
span: start,
})
}
}
tokens.push({ kind: End, span: { offset: chars.length(), line, column } })
Ok(tokens)
}
///|
fn is_ascii_digit(ch : Char) -> Bool {
ch >= '0' && ch <= '9'
}
///|
fn is_identifier_start(ch : Char) -> Bool {
(ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_'
}
///|
fn is_identifier_continue(ch : Char) -> Bool {
is_identifier_start(ch) || is_ascii_digit(ch) || ch == '-'
}
///|
fn keyword_or_word(text : String) -> TokenKind {
match text {
"spec" => KeywordSpec
"feature" => KeywordFeature
"goal" => KeywordGoal
"in" => KeywordIn
"cost" => KeywordCost
"locked" => KeywordLocked
"and" => KeywordAnd
"or" => KeywordOr
"not" => KeywordNot
"present" => KeywordPresent
"true" => KeywordTrue
"false" => KeywordFalse
_ => Word(text)
}
}