feat(formula): add formula lexer
This commit is contained in:
1 parent
728beb5013
commit
5adf469beb
1 file changed
+219
@@ -0,0 +1,219 @@
|
|||||||
|
import 'tokens.dart';
|
||||||
|
import 'values.dart';
|
||||||
|
|
||||||
|
class LexException implements Exception {
|
||||||
|
final String message;
|
||||||
|
const LexException(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'LexException: $message';
|
||||||
|
}
|
||||||
|
|
||||||
|
class Lexer {
|
||||||
|
final String source;
|
||||||
|
int _pos = 0;
|
||||||
|
|
||||||
|
Lexer(this.source);
|
||||||
|
|
||||||
|
List<Token> tokenize() {
|
||||||
|
final tokens = <Token>[];
|
||||||
|
while (!_atEnd) {
|
||||||
|
final ch = _peek();
|
||||||
|
if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n') {
|
||||||
|
_pos++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch == '#') {
|
||||||
|
tokens.add(_readErrorLiteral());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (_isDigit(ch) || (ch == '.' && _isDigit(_peekAt(1)))) {
|
||||||
|
tokens.add(_readNumber());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch == '"') {
|
||||||
|
tokens.add(_readText());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch == "'") {
|
||||||
|
tokens.add(_readQuotedName());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch == r'$' || _isLetter(ch)) {
|
||||||
|
tokens.add(_readWord());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tokens.add(_readOperator());
|
||||||
|
}
|
||||||
|
tokens.add(const Token(TokenType.eof, ''));
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
Token _readErrorLiteral() {
|
||||||
|
for (final kind in ErrorKind.values) {
|
||||||
|
if (source.startsWith(kind.symbol, _pos)) {
|
||||||
|
_pos += kind.symbol.length;
|
||||||
|
return Token(TokenType.errorLit, kind.symbol, kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw LexException('Unknown error literal at position $_pos');
|
||||||
|
}
|
||||||
|
|
||||||
|
Token _readNumber() {
|
||||||
|
final start = _pos;
|
||||||
|
while (!_atEnd && _isDigit(_peek())) {
|
||||||
|
_pos++;
|
||||||
|
}
|
||||||
|
if (!_atEnd && _peek() == '.') {
|
||||||
|
_pos++;
|
||||||
|
while (!_atEnd && _isDigit(_peek())) {
|
||||||
|
_pos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final lexeme = source.substring(start, _pos);
|
||||||
|
return Token(TokenType.number, lexeme, double.parse(lexeme));
|
||||||
|
}
|
||||||
|
|
||||||
|
Token _readText() {
|
||||||
|
_pos++;
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
while (!_atEnd) {
|
||||||
|
final ch = _peek();
|
||||||
|
if (ch == '"') {
|
||||||
|
if (_peekAt(1) == '"') {
|
||||||
|
buffer.write('"');
|
||||||
|
_pos += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_pos++;
|
||||||
|
return Token(TokenType.text, buffer.toString(), buffer.toString());
|
||||||
|
}
|
||||||
|
buffer.write(ch);
|
||||||
|
_pos++;
|
||||||
|
}
|
||||||
|
throw const LexException('Unterminated string literal');
|
||||||
|
}
|
||||||
|
|
||||||
|
Token _readQuotedName() {
|
||||||
|
_pos++;
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
while (!_atEnd) {
|
||||||
|
final ch = _peek();
|
||||||
|
if (ch == "'") {
|
||||||
|
if (_peekAt(1) == "'") {
|
||||||
|
buffer.write("'");
|
||||||
|
_pos += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_pos++;
|
||||||
|
return Token(TokenType.quotedName, buffer.toString(), buffer.toString());
|
||||||
|
}
|
||||||
|
buffer.write(ch);
|
||||||
|
_pos++;
|
||||||
|
}
|
||||||
|
throw const LexException('Unterminated sheet name');
|
||||||
|
}
|
||||||
|
|
||||||
|
Token _readWord() {
|
||||||
|
final start = _pos;
|
||||||
|
if (_peek() == r'$') _pos++;
|
||||||
|
while (!_atEnd && _isLetter(_peek())) {
|
||||||
|
_pos++;
|
||||||
|
}
|
||||||
|
final hasDollar = _peek() == r'$';
|
||||||
|
final afterLetters = hasDollar ? _peekAt(1) : _peek();
|
||||||
|
if (_isDigit(afterLetters)) {
|
||||||
|
if (hasDollar) _pos++;
|
||||||
|
while (!_atEnd && _isDigit(_peek())) {
|
||||||
|
_pos++;
|
||||||
|
}
|
||||||
|
final lexeme = source.substring(start, _pos);
|
||||||
|
if (_peek() == '!' || _peek() == '(') return Token(TokenType.identifier, lexeme);
|
||||||
|
return Token(TokenType.cellRef, lexeme);
|
||||||
|
}
|
||||||
|
final lexeme = source.substring(start, _pos);
|
||||||
|
return Token(TokenType.identifier, lexeme);
|
||||||
|
}
|
||||||
|
|
||||||
|
Token _readOperator() {
|
||||||
|
final ch = _peek();
|
||||||
|
switch (ch) {
|
||||||
|
case '+':
|
||||||
|
_pos++;
|
||||||
|
return const Token(TokenType.plus, '+');
|
||||||
|
case '-':
|
||||||
|
_pos++;
|
||||||
|
return const Token(TokenType.minus, '-');
|
||||||
|
case '*':
|
||||||
|
_pos++;
|
||||||
|
return const Token(TokenType.star, '*');
|
||||||
|
case '/':
|
||||||
|
_pos++;
|
||||||
|
return const Token(TokenType.slash, '/');
|
||||||
|
case '^':
|
||||||
|
_pos++;
|
||||||
|
return const Token(TokenType.caret, '^');
|
||||||
|
case '%':
|
||||||
|
_pos++;
|
||||||
|
return const Token(TokenType.percent, '%');
|
||||||
|
case '&':
|
||||||
|
_pos++;
|
||||||
|
return const Token(TokenType.amp, '&');
|
||||||
|
case '(':
|
||||||
|
_pos++;
|
||||||
|
return const Token(TokenType.lparen, '(');
|
||||||
|
case ')':
|
||||||
|
_pos++;
|
||||||
|
return const Token(TokenType.rparen, ')');
|
||||||
|
case ',':
|
||||||
|
_pos++;
|
||||||
|
return const Token(TokenType.comma, ',');
|
||||||
|
case ':':
|
||||||
|
_pos++;
|
||||||
|
return const Token(TokenType.colon, ':');
|
||||||
|
case '!':
|
||||||
|
_pos++;
|
||||||
|
return const Token(TokenType.bang, '!');
|
||||||
|
case '=':
|
||||||
|
_pos++;
|
||||||
|
return const Token(TokenType.eq, '=');
|
||||||
|
case '<':
|
||||||
|
_pos++;
|
||||||
|
if (_peek() == '=') {
|
||||||
|
_pos++;
|
||||||
|
return const Token(TokenType.lte, '<=');
|
||||||
|
}
|
||||||
|
if (_peek() == '>') {
|
||||||
|
_pos++;
|
||||||
|
return const Token(TokenType.neq, '<>');
|
||||||
|
}
|
||||||
|
return const Token(TokenType.lt, '<');
|
||||||
|
case '>':
|
||||||
|
_pos++;
|
||||||
|
if (_peek() == '=') {
|
||||||
|
_pos++;
|
||||||
|
return const Token(TokenType.gte, '>=');
|
||||||
|
}
|
||||||
|
return const Token(TokenType.gt, '>');
|
||||||
|
default:
|
||||||
|
throw LexException('Unexpected character "$ch" at position $_pos');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _atEnd => _pos >= source.length;
|
||||||
|
|
||||||
|
String _peek() => _atEnd ? '' : source[_pos];
|
||||||
|
|
||||||
|
String _peekAt(int offset) {
|
||||||
|
final index = _pos + offset;
|
||||||
|
return index >= source.length ? '' : source[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isDigit(String ch) => ch.length == 1 && ch.codeUnitAt(0) >= 0x30 && ch.codeUnitAt(0) <= 0x39;
|
||||||
|
|
||||||
|
bool _isLetter(String ch) {
|
||||||
|
if (ch.length != 1) return false;
|
||||||
|
final code = ch.codeUnitAt(0);
|
||||||
|
return (code >= 0x41 && code <= 0x5A) || (code >= 0x61 && code <= 0x7A) || code == 0x5F;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user