Найти в Дзене

Написание компилятора своего языка. Часть 2.

Всем привет в этой части я расскажу вам каким будет наш язык и напишем лексический анализатор ( лексер ). Для начала нужно определится что будет уметь наш язык. Уметь он будет достаточно для простого языка, а именно:
Типы данных: программа(programm), функция (func) , целые числа (byte (1 byte) , word (2 byte) , int (4 byte), long (8 byte) ), числа с плавающий точкой ( float (4 byte), double ( 8 byte ) ), строковой тип (string), bool, структуры. Переменные: будут доступны как глобальные переменные, так и локальные. Математические операции: будут доступны +, -, *, /,= также знаки сравнения < > == != . Конструкции языка нам будут доступны: программы (programm) , функции, if/else, циклы for, while. Возможности языка: динамическая и статическая типизация (если пользователь хочет динамически типизировать переменную то просто нужно написать i = 5 а если статически i:int = 5 . Парсер будет определять тип переменной. ).Препроцессор ( из директив у нас будут #define, #include, #ifdef, #ifndef, #

Всем привет в этой части я расскажу вам каким будет наш язык и напишем лексический анализатор ( лексер ).

Для начала нужно определится что будет уметь наш язык. Уметь он будет достаточно для простого языка, а именно:
Типы данных: программа(programm), функция (func) , целые числа (byte (1 byte) , word (2 byte) , int (4 byte), long (8 byte) ), числа с плавающий точкой ( float (4 byte), double ( 8 byte ) ), строковой тип (string), bool, структуры.

Переменные: будут доступны как глобальные переменные, так и локальные.

Математические операции: будут доступны +, -, *, /,= также знаки сравнения < > == != .

Конструкции языка нам будут доступны: программы (programm) , функции, if/else, циклы for, while.

Возможности языка: динамическая и статическая типизация (если пользователь хочет динамически типизировать переменную то просто нужно написать i = 5 а если статически i:int = 5 . Парсер будет определять тип переменной. ).Препроцессор ( из директив у нас будут #define, #include, #ifdef, #ifndef, #endif ), точка с запятой будет обозначать конец выражения , ООП у нас так такого не будет но можно будет имитировать структурами и функциями.

Лексический анализатор.

Как помните из прошлой статьи, я говорил что лексический анализ нужен для преобразования текста исходного в поток токенов. То есть если передать лексическому анализатору подобный код "

a = 5;

b:int = 7;

c:word = a + b;

print(c);

То лексер нам выдаст вот такой поток токенов

{

var_t, assigm_t, number_t, end_expr_t, var_t, colon_t, int_t ,assigm, number_t, end_expr_t, var_t, colon_t, word_t, assigm_t, var_t, plus_t, var_t, end_expr_t, var_t, open_round_bracket_t, var, close_round_bracket, end_expr_t

}

окончание _t указывает что это токен.

Также кроме токена нам нужно запоминать строковое значение. Например:

var_t, "a"

Теперь я думаю мы разобрались что делает лексер, так что давайте его напишем на C#.

Для начала создадим файл Token.cs в нашем проекте.

В файле будет перечисление Token. Оно будет таким:

public enum Token

{

programm_t,

func_t,

byte_t,
word_t,
int_t,
long_t,

float_t,
double_t,

string_t,

bool_t,

struct_t,



//math_op
plus_t,
minus_t,
mul_t,
div_t,
assigm_t,

less_t,
more_t,
equal_t,
no_equal_t,

//

//keys

end_expr_t,

//

//con_statement

if_t,
else_t,
for_t,
while_t,

//

//conditional symbols

open_round_bracket_t,
close_round_bracket_t,
open_curly_bracket_t,
close_curly_bracket_t,

//

//other
var_t,
number_t

}

Теперь нужно создать файл в котором будет класс Node который будет хранить в себе токен , string значение, и List экземпляров класса Node (это будет потом для парсера).

В этом классе нам нужно подключить пространство имен System.Collections.Generic, для использования List.

Вот таким будет класс Node:

public class Node

{

private Token typeNode;
private string value_n;

private List<Node> childs;

public Token TypeNode
{
get
{
return typeNode;
}
}

public string ValueNode
{
get
{
return value_n;
}
set
{
value_n = value;
}
}

public IEnumerable<Node> Childs
{
get
{
return childs;
}
}

public int ChildsCount
{
get
{
return childs.Count;
}
}

public Node(Token tn, string val)
{
childs = new List<Node>();
typeNode = tn;
value_n = val;
}

public void AddChild(Node node)
{
childs.Add(node);
}

public Node GetChild(int i)
{
return childs[i];
}

}

Данный класс инкапсулирует в себе эти 3 переменные.:

Ну теперь самое вкусное лексер. Для начала создадим файл Lexer.cs и напишем в нем класс Lexer. Также нужно будет подключить System.Collections.Generic и System.Text.RegularExpressions. Он будет вот таким:

class Lexer

{

protected List<Node> nodes;

public IEnumerable<Node> Nodes
{
get
{

return nodes;

}
}

public Lexer()
{

nodes = new List<Node>();

}

public void LexCode(string code)
{

string[] strTokens = Regex.Split(code, @"([\ \n(){};+*/=\-])");

for (int i =0; i < strTokens.Length; i++)
{
if( strTokens[i] != " "

&& strTokens[i] != string.Empty

&& strTokens[i] != "\r"

&& strTokens[i] != "\n")
{

nodes.Add(new Node( AnalyzeStrToken( strTokens[i]), strTokens[i]) );

}
}
}

public Token AnalyzeStrToken(string strToken)
{

Token token = Token.int_t;

switch (strToken)
{
case "func":
token = Token.func_t;
break;
case "programm":

token = Token.programm_t;
break;

case "byte":
token = Token.byte_t;
break;
case "word":
token = Token.word_t;
break;
case "int":
token = Token.int_t;
break;
case "long":
token = Token.long_t;
break;

case "float":
token = Token.float_t;
break;
case "double":
token = Token.double_t;
break;

case "string":
token = Token.string_t;
break;

case "bool":
token = Token.bool_t;
break;

case "struct":
token = Token.struct_t;
break;

case "+":
token = Token.plus_t;
break;
case "-":
token = Token.minus_t;
break;
case "*":
token = Token.mul_t;
break;
case "/":
token = Token.div_t;
break;
case "=":
token = Token.assigm_t;
break;

case "<":
token = Token.less_t;
break;
case ">":
token = Token.more_t;
break;

case ";":
token = Token.end_expr_t;
break;

case "(":
token = Token.open_round_bracket_t;
break;
case ")":
token = Token.close_round_bracket_t;
break;
case "{":
token = Token.open_curly_bracket_t;
break;
case "}":
token = Token.close_curly_bracket_t;
break;

default:

double number = 0;

if(double.TryParse(strToken, out number))
{

token = Token.number_t;

break;
}

token = Token.var_t;
break;
}

return token;

}

}

Вот тут нужно прояснить некоторые моменты в коде.

Сначала Regex с помощью метода Split в который закладывается паттерн с сепараторами ( пробел, переход на новую строку , круглые скобки, фигурные скобки, точка с запятой, плюс, минус, умножение, деление, равно), делит строку коду на множество строк которые можно уже обработать.

Обработка строк происходит следующим способом.

Сначала создаем экземпляр класса Node который принимает 2 аргумента в конструктор это токен и строку. Токен вычесляется в методе AnalyzeStrToken(string strToken). Данный метод сравнивает строку с другими строками в switch и возращает нужный токен.

Вот теперь мы можем протестировать данный лексер.

Lexer lexer = new Lexer();

string code = "int i = 0 ; func main { testf(); }";

lexer.LexCode(code);

foreach(Node node in lexer.Nodes)
{
Console.WriteLine(node.TypeNode);
}

И нам на консоль выдаст нужные токены.

Сами токены.
Сами токены.

На этом пока все. В следующей части мы будем писать уже парсер. Спасибо за прочтение.