// lexer.mbt — Tokenizer for matcher expressions.
//
// The tokenizer scans the text as an array of characters, so offsets are
// character offsets. Identifiers are `[A-Za-z_][A-Za-z0-9_]*`, numbers
// are integers or doubles, strings use single or double quotes with
// backslash escapes (`\\` and the quote character), and the operator set
// is the one in expr.mbt. Every token records the offset of its first
// character so syntax errors can point at the offending position.
///|
priv enum TokenKind {
Ident(String)
IntValue(Int)
DoubleValue(Double)
Str(String)
True
False
In
Or
And
Equal
NotEqual
Less
LessEqual
Greater
GreaterEqual
Plus
Minus
Star
Slash
Percent
Bang
LParen
RParen
LBracket
RBracket
Comma
Dot
} derive(Eq)
///|
priv struct Token {
kind : TokenKind
offset : Int
}
///|
/// Splits `text` into tokens, raising `MatcherSyntax` on the first
/// character or literal the grammar cannot tokenize.
fn tokenize(text : String) -> Array[Token] raise CasbinError {
let chars : Array[Char] = text.iter().collect()
let length = chars.length()
let tokens : Array[Token] = []
let mut index = 0
while index < length {
let ch = chars[index]
if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' {
index += 1
continue
}
if is_identifier_start(ch) {
let start = index
while index < length && is_identifier_char(chars[index]) {
index += 1
}
let word = slice_to_string(chars, start, index)
let kind = match word {
"true" => TokenKind::True
"false" => TokenKind::False
"in" => TokenKind::In
_ => TokenKind::Ident(word)
}
tokens.push({ kind, offset: start, })
continue
}
if ch.is_ascii_digit() {
let start = index
let mut mantissa = 0
let mut fraction_digits = 0
let mut is_double = false
while index < length && chars[index].is_ascii_digit() {
mantissa = mantissa * 10 + (chars[index].to_int() - '0'.to_int())
index += 1
}
if index < length &&
chars[index] == '.' &&
index + 1 < length &&
chars[index + 1].is_ascii_digit() {
is_double = true
index += 1
while index < length && chars[index].is_ascii_digit() {
mantissa = mantissa * 10 + (chars[index].to_int() - '0'.to_int())
fraction_digits += 1
index += 1
}
}
if is_double {
let value = mantissa.to_double() / power_of_ten(fraction_digits)
tokens.push({ kind: DoubleValue(value), offset: start, })
} else {
tokens.push({ kind: IntValue(mantissa), offset: start, })
}
continue
}
if ch == '\'' || ch == '"' {
let start = index
let (value, next) = read_string(chars, start)
tokens.push({ kind: Str(value), offset: start, })
index = next
continue
}
if index + 1 < length {
let two_character = match (ch, chars[index + 1]) {
('|', '|') => Some(TokenKind::Or)
('&', '&') => Some(TokenKind::And)
('=', '=') => Some(TokenKind::Equal)
('!', '=') => Some(TokenKind::NotEqual)
('<', '=') => Some(TokenKind::LessEqual)
('>', '=') => Some(TokenKind::GreaterEqual)
_ => None
}
match two_character {
Some(kind) => {
tokens.push({ kind, offset: index, })
index += 2
continue
}
None => ()
}
}
match ch {
'(' => {
tokens.push({ kind: LParen, offset: index, })
index += 1
}
')' => {
tokens.push({ kind: RParen, offset: index, })
index += 1
}
'[' => {
tokens.push({ kind: LBracket, offset: index, })
index += 1
}
']' => {
tokens.push({ kind: RBracket, offset: index, })
index += 1
}
',' => {
tokens.push({ kind: Comma, offset: index, })
index += 1
}
'.' => {
tokens.push({ kind: Dot, offset: index, })
index += 1
}
'<' => {
tokens.push({ kind: Less, offset: index, })
index += 1
}
'>' => {
tokens.push({ kind: Greater, offset: index, })
index += 1
}
'+' => {
tokens.push({ kind: Plus, offset: index, })
index += 1
}
'-' => {
tokens.push({ kind: Minus, offset: index, })
index += 1
}
'*' => {
tokens.push({ kind: Star, offset: index, })
index += 1
}
'/' => {
tokens.push({ kind: Slash, offset: index, })
index += 1
}
'%' => {
tokens.push({ kind: Percent, offset: index, })
index += 1
}
'!' => {
tokens.push({ kind: Bang, offset: index, })
index += 1
}
'=' =>
raise casbin_error_at_offset(
MatcherSyntax,
index,
"unsupported operator '='; use '==' for equality",
)
'&' =>
raise casbin_error_at_offset(
MatcherSyntax,
index,
"unsupported operator '&'; use '&&'",
)
'|' =>
raise casbin_error_at_offset(
MatcherSyntax,
index,
"unsupported operator '|'; use '||'",
)
'?' =>
raise casbin_error_at_offset(
MatcherSyntax,
index,
"unsupported operator '?'; ternary expressions are not supported",
)
':' =>
raise casbin_error_at_offset(
MatcherSyntax,
index,
"unsupported operator ':'; ternary expressions are not supported",
)
_ =>
raise casbin_error_at_offset(
MatcherSyntax,
index,
"unexpected character '" + ch.to_string() + "'",
)
}
}
tokens
}
///|
/// Reads a quoted string starting at `start` and returns the decoded
/// value plus the index just past the closing quote. Inside the literal,
/// `\` escapes the next character; an unterminated literal raises.
fn read_string(
chars : Array[Char],
start : Int,
) -> (String, Int) raise CasbinError {
let quote = chars[start]
let length = chars.length()
let value : Array[Char] = []
let mut index = start + 1
while index < length {
let ch = chars[index]
if ch == '\\' {
if index + 1 >= length {
raise casbin_error_at_offset(
MatcherSyntax,
start,
"unterminated string literal",
)
}
value.push(chars[index + 1])
index += 2
continue
}
if ch == quote {
return (StringView::from_iter(value.iter()).to_owned(), index + 1)
}
value.push(ch)
index += 1
}
raise casbin_error_at_offset(
MatcherSyntax,
start,
"unterminated string literal",
)
}
///|
fn slice_to_string(chars : Array[Char], start : Int, end : Int) -> String {
let slice : Array[Char] = []
for i in start.. Bool {
ch.is_ascii_alphabetic() || ch == '_'
}
///|
fn is_identifier_char(ch : Char) -> Bool {
is_identifier_start(ch) || ch.is_ascii_digit()
}
///|
fn power_of_ten(exponent : Int) -> Double {
let mut result = 1.0
for _i in 0..