Пишем интерпретатор на PascalABC.NET. Лексический анализатор. Часть 3
Данный текст - продолжение https://teletype.in/@pascalabcnet/Lex2
Построим лексический анализатор для языка, содержащего следующие лексемы: ключевые слова (if, then, else, while, do), идентификаторы, целые и вещественные константы, строковые константы, заключенные в "", операции присваивания, операции сравнения и логические операции в стиле C, арифметические операции, лексемы { } для выделения блока кода, комментарии // до конца строки.
Напомним, что лексема (токен) - это неделимая конструкция в тексте программы.
Базовые типы
Определим вначале все типы токенов в виде перечислимого типа TokenType:
type TokenType = (Int, DoubleLiteral, StringLiteral, Id, Plus, Minus, Multiply, Divide, Dot, Semicolon, LPar, RPar, LBrace, RBrace, Comma, Colon, Assign, AssignPlus, AssignMinus, AssignMult, AssignDiv, Equal, Less, LessEqual, Greater, GreaterEqual, NotEqual, tkAnd, tkOr, tkNot, Eof, tkTrue, tkFalse, tkIf, tkThen, tkElse, tkWhile, tkDo );
Далее определим класс Token, производный от TokenBase и включающий в себя тип токена как поле:
type Token = class(TokenBase) public Typ: TokenType; constructor (Typ: TokenType; Pos: Position; Value: Object := nil); begin (Self.Typ, Self.Pos, Self.Value) := (Typ, Pos, Value); end; end;
Словарь ключевых слов
Кроме того, определим словарь, ставящий в соответствие ключевым словам типы их токенов:
var Keywords := Dict( ('True', TokenType.tkTrue), ('False', TokenType.tkFalse), ('if', TokenType.tkIf), ('then', TokenType.tkThen), ('else', TokenType.tkElse), ('while', TokenType.tkWhile), ('do', TokenType.tkDo) );
Класс Lexer
Теперь всё готово для создания конкретного лексического анализатора, который определим в виде класса, производного от LexerBase:
type Lexer = class(LexerBase) ... end;
Метод NextToken
Приведем полный код главного метода этого класса - NextToken - возвращающего следующий токен:
function NextToken: Token; begin var c := NextChar; while c in |#13,#7,' ',#10| do c := NextChar; var pos := CurrentPosition; start := cur-1; case c of #0 : Result := new Token(TokenType.Eof,pos,'Eof'); ',': Result := new Token(TokenType.Comma,pos); ')': Result := new Token(TokenType.RPar,pos); '(': Result := new Token(TokenType.LPar,pos); '}': Result := new Token(TokenType.RBrace,pos); '{': Result := new Token(TokenType.LBrace,pos); '+': Result := new Token(IsMatch('=') ? TokenType.AssignPlus : TokenType.Plus,pos); '-': Result := new Token(IsMatch('=') ? TokenType.AssignMinus : TokenType.Minus,pos); '*': Result := new Token(IsMatch('=') ? TokenType.AssignMult : TokenType.Multiply,pos); '/': if IsMatch('/') then while (PeekChar <> #10) and not IsAtEnd do NextChar else Result := new Token(IsMatch('=') ? TokenType.AssignDiv : TokenType.Divide,pos); ';': Result := new Token(TokenType.Semicolon,pos); '!': Result := new Token(IsMatch('=') ? TokenType.NotEqual : TokenType.tkNot,pos); '=': Result := new Token(IsMatch('=') ? TokenType.Equal : TokenType.Assign,pos); '>': Result := new Token(IsMatch('=') ? TokenType.GreaterEqual : TokenType.Greater,pos); '<': Result := new Token(IsMatch('=') ? TokenType.LessEqual : TokenType.Less,pos); '&': if IsMatch('&') then Result := new Token(TokenType.tkAnd,pos) else raise new LexerException(#39;Неверный символ {c.Code} после &',CurrentPosition); '|': if IsMatch('|') then Result := new Token(TokenType.tkOr,pos) else raise new LexerException(#39;Неверный символ {c.Code} после |',CurrentPosition); '"': Result := GetString(pos) else if c.IsDigit then Result := GetNumber(pos) else if IsAlpha(c) then Result := GetIdentifier(pos) else raise new LexerException(#39;Неизвестный символ {c} в позиции {pos.line},{pos.col}',pos); end; if Result.Value = nil then Result.Value := code[start:cur]; end;
В начале этого метода мы пропускаем пробельные символы и считываем первый не пробельный. После этого запоминаем текущую позицию этого символа (это будет позиция начала токена) и в переменной start - индекс начала токена в строке code.
Далее в коде метода следует большой оператор case, который по текущему считанному символу c
пытается определить лексему и вернуть её в качестве значения этого метода.
Рассмотрим несколько примеров.
#0 : Result := new Token(TokenType.Eof,pos,'Eof');
обрабатывает ситуацию, когда достигнут конец строки (). В этом случае возвращается токен с типом TokenType.Eof и строковым представлением Eof
.
',': Result := new Token(TokenType.Comma,pos); ')': Result := new Token(TokenType.RPar,pos); '(': Result := new Token(TokenType.LPar,pos); '}': Result := new Token(TokenType.RBrace,pos); '{': Result := new Token(TokenType.LBrace,pos);
обрабатывают односимвольные токены, с которых не могут начинаться многосимвольные токены. Обратим внимание, что каждый токен сохраняет позицию своего начала.
Далее рассмотрим ситуацию определения одного из токенов + или +=. Заметим, что оба токена имеют префикс + и более не существует токенов с этим префиксом. Обрабатывается подобная ситуация так: если после символа + мы встречаем символ =, то это токен с типом TokenType.AssignPlus, в противном случае это токен TokenType.Plus и следующий символ не считывается:
'+': Result := new Token(IsMatch('=') ? TokenType.AssignPlus : TokenType.Plus,pos);
Далее рассмотрим случай обработки двухсимвольного токена на примере &&. После считывания первого символа мы анализируем второй - он также должен быть & - если это не так, то кидаем исключение:
'&': if IsMatch('&') then Result := new Token(TokenType.tkAnd,pos) else raise new LexerException(#39;Неверный символ {c.Code} после &',CurrentPosition);
Нам осталось выделить такие составные лексемы как литеральные строки, числа и идентификаторы.
Метод GetString
Наконец, если встретился символ "
, то перед нами - начало литеральной строки вида "Hello" - соответствующий токен возвращается вызовом метода Result := GetString(pos), где метод GetString описывается так:
function GetString(startPos: Position): Token; begin while (PeekChar <> '"') and not IsAtEnd do NextChar; NextChar; // пропустить " var value := code[start + 1: cur - 1]; Result := new Token(TokenType.StringLiteral, startPos, value); end;
Нетрудно видеть, что здесь пропускаются все символы до второго " и затем также пропускается сам символ ". После этого возвращается токен TokenType.StringLiteral, значение которого - это литеральная строка без кавычек, расположенная в срезе строки code[start + 1: cur - 1]
.
В ветви else оператора case обработаем все остальные случаи, включающие не отдельные символы, а классы символов.
Выделение чисел и идентификаторов
Если далее встречается символ из класса цифр, то возвращается токен Число (целое или вещественное), а если - символ из класса букв, то токен Идентификатор. В любом из оставшихся случаев генерируется исключение:
if c.IsDigit then Result := GetNumber(pos) else if IsAlpha(c) then Result := GetIdentifier(pos) else raise new LexerException(#39;Неизвестный символ {c} в позиции {pos.line},{pos.col}',pos);
В заключительной части метода NextToken мы заполняем поле Value у токена текстовым представлением токена - если это значение отсутствует:
if Result.Value = nil then Result.Value := code[start:cur];
Исключение LexerException
Отдельно поясним класс исключения LexerException. Он вводится следующим кодом:
type BaseCompilerException = class(Exception) public Pos: Position; constructor(msg: string; p: Position); begin inherited Create(msg); Pos := p; end; end; LexerException = class(BaseCompilerException) end;Нетрудно видеть, что этот класс наследуется от базового класса BaseCompilerException ,
Нетрудно видеть, что этот класс наследуется от базового класса BaseCompilerException, хранящего помимо сообщения ошибки её позицию. Обработке этого исключения будет посвящен отдельный пункт.
Методы GetNumber и GetIdentifier
Теперь разберем оставшиеся методы - GetNumber
и GetIdentifier
.
Если мы считали букву, то это - идентификатор, и считываем далее буквы или цифры:
function GetIdentifier(startPos: Position): Token; begin while IsAlphaNumeric(PeekChar) do NextChar; var value := code[start:cur]; var typ := TokenType.Id; if value in Keywords then typ := Keywords[value]; Result := new Token(typ, startPos, value); end;
Текст идентификатора находится как обычно в code[start:cur]
.
Есть только небольшое исключение, которое обрабатывается аналогичным образом во всех языках, имеющих ключевые слова: если данный идентификатор является ключевым словом, то мы находим соответствующий ему тип токена в словаре Keywords:
if value in Keywords then typ := Keywords[value];
Наиболее сложно обрабатывается число - вот код метода GetNumber:
function GetNumber(startPos: Position): Token; begin while PeekChar.IsDigit do NextChar; if (PeekChar = '.') and PeekNextChar.IsDigit then begin NextChar; while PeekChar.IsDigit do NextChar; var value := code[start : cur]; var re: real := value.ToReal; Result := new Token(TokenType.DoubleLiteral, startPos, value.ToReal); exit; end; var val := code[start : cur]; Result := new Token(TokenType.Int,startPos,val.ToInteger); end;
Основная сложность здесь - в том, что число может быть либо целым, либо вещественным (содержать точку).
Вначале мы пропускаем все цифры:
while PeekChar.IsDigit do NextChar;
Затем если текущий символ - точка и следующий символ - цифра, то мы пропускаем точку и вновь пропускаем все цифры:
if (PeekChar = '.') and PeekNextChar.IsDigit then begin NextChar; while PeekChar.IsDigit do NextChar;
Затем получаем строковое представление числа, преобразуем его в вещественное и именно этим значением инициализируем возвращаемый токен:
var value := code[start : cur]; var re: real := value.ToReal; Result := new Token(TokenType.DoubleLiteral, startPos, value.ToReal); exit; end;
Наконец, если мы не встретили точку, то это - целое число:
var val := code[start : cur]; Result := new Token(TokenType.Int,startPos,val.ToInteger);
Итоговый класс Lexer
Итак, мы написали класс Lexer нашего будущего языка. Вот его полный код:
Lexer = class(LexerBase) private function GetIdentifier(startPos: Position): Token; begin while IsAlphaNumeric(PeekChar) do NextChar; var value := code[start:cur]; var typ := TokenType.Id; if value in Keywords then typ := Keywords[value]; Result := new Token(typ, startPos, value); end; function GetString(startPos: Position): Token; begin while (PeekChar <> '"') and not IsAtEnd do NextChar; NextChar; // пропустить " var value := code[start + 1: cur - 1]; Result := new Token(TokenType.StringLiteral, startPos, value); end; function GetNumber(startPos: Position): Token; begin while PeekChar.IsDigit do NextChar; if (PeekChar = '.') and PeekNextChar.IsDigit then begin NextChar; while PeekChar.IsDigit do NextChar; var value := code[start : cur]; var re: real := value.ToReal; Result := new Token(TokenType.DoubleLiteral, startPos, value.ToReal); exit; end; var val := code[start : cur]; Result := new Token(TokenType.Int,startPos,val.ToInteger); end; public function NextToken: Token; begin var c := NextChar; while c in |#13,#7,' ',#10| do c := NextChar; var pos := CurrentPosition; start := cur-1; case c of #0 : Result := new Token(TokenType.Eof,pos,'Eof'); ',': Result := new Token(TokenType.Comma,pos); ')': Result := new Token(TokenType.RPar,pos); '(': Result := new Token(TokenType.LPar,pos); '}': Result := new Token(TokenType.RBrace,pos); '{': Result := new Token(TokenType.LBrace,pos); '+': Result := new Token(IsMatch('=') ? TokenType.AssignPlus : TokenType.Plus,pos); '-': Result := new Token(IsMatch('=') ? TokenType.AssignMinus : TokenType.Minus,pos); '*': Result := new Token(IsMatch('=') ? TokenType.AssignMult : TokenType.Multiply,pos); '/': if IsMatch('/') then while (PeekChar <> #10) and not IsAtEnd do NextChar else Result := new Token(IsMatch('=') ? TokenType.AssignDiv : TokenType.Divide,pos); ';': Result := new Token(TokenType.Semicolon,pos); '!': Result := new Token(IsMatch('=') ? TokenType.NotEqual : TokenType.tkNot,pos); '=': Result := new Token(IsMatch('=') ? TokenType.Equal : TokenType.Assign,pos); '>': Result := new Token(IsMatch('=') ? TokenType.GreaterEqual : TokenType.Greater,pos); '<': Result := new Token(IsMatch('=') ? TokenType.LessEqual : TokenType.Less,pos); '&': if IsMatch('&') then Result := new Token(TokenType.tkAnd,pos) else raise new LexerException(#39;Неверный символ {c.Code} после &',CurrentPosition); '|': if IsMatch('|') then Result := new Token(TokenType.tkOr,pos) else raise new LexerException(#39;Неверный символ {c.Code} после |',CurrentPosition); '"': Result := GetString(pos) else if c.IsDigit then Result := GetNumber(pos) else if IsAlpha(c) then Result := GetIdentifier(pos) else raise new LexerException(#39;Неизвестный символ {c} в позиции {pos.line},{pos.col}',pos); end; if Result.Value = nil then Result.Value := code[start:cur]; end; end;
Тестирующая программа
Простая тестирующая программа:
uses LexUnit; begin var lex := new Lexer('a = 35; if a > 40 then a = 40 else { a = 0; print(a * 223 + 10) }'); var t: Token; repeat t := lex.NextToken; Print(t.Value); until t.Typ = TokenType.Eof; end.
a = 35 ; if a > 40 then a = 40 else { a = 0 ; print ( a * 223 + 10 ) } Eof
Немного модифицированная тестирующая программа:
uses LexUnit; begin var lex := new Lexer('a = 35; if'#10'a > 40 && True'#10#10'then a = 40 else '#10'{ a = 0; print(a * 223 + 10) }'); var t: Token; repeat t := lex.NextToken; Println(t); until t.Typ = TokenType.Eof; end.
((1,1),a,Id) ((1,3),=,Assign) ((1,5),35,Int) ((1,7),;,Semicolon) ((1,9),if,tkIf) ((2,1),a,Id) ((2,3),>,Greater) ((2,5),40,Int) ((2,8),&&,tkAnd) ((2,11),True,tkTrue) ((4,1),then,tkThen) ((4,6),a,Id) ((4,8),=,Assign) ((4,10),40,Int) ((4,13),else,tkElse) ((5,1),{,LBrace) ((5,3),a,Id) ((5,5),=,Assign) ((5,7),0,Int) ((5,8),;,Semicolon) ((5,10),print,Id) ((5,15),(,LPar) ((5,16),a,Id) ((5,18),*,Multiply) ((5,20),223,Int) ((5,24),+,Plus) ((5,26),10,Int) ((5,28),),RPar) ((5,30),},RBrace) ((5,30),Eof,Eof)
Здесь следует обратить внимание, что для каждого токена выводится позиция его начала, его значение и тип.
Поздравляем! Мы создали лексический анализатор простого языка. Он занимает около 250 строк кода вместе со всеми модулями.