Немножко об основах компиляции программ, компиляцию можно разбить на 3 фазы:
- Разбиение на Token'ы
- Создание AST (абстрактного синтаксического дерева)
- Генерация кода
Немного поподробнее о каждой из фаз:
Разбиение на Token'ы - процесс преобразования кода в определенные единицы компилятора. Как правило их можно разделить на несколько типов:
- Ключевые слова - keyword (`if`, `else`, `while`, `struct`, `class`...)
- Операторы - operator (`+`, `-`, `=`, `-=`, `+=`)
- Литералы - literal (нет, не либералы, а: `"строка"`, `1488`, `true`, `false`)
- Пунктуация - punctuation (`{`, `}`, `[`, `,`)
- Идентификаторы - identifier (имена функций, переменных, `main`, `x`)
Например, если разбить на токены следующий код на C
int main() {
return 1;
}
Мы получим:
Identifier int
Identifier main
Punctuation (
Punctuation )
Punctuation {
Punctuation }
Keyword return
Punctuation ;
Именно эту фазу мы реализуем в этой части.
Потом строится
абстрактное синтаксическое дерево - такая приколюха, уже содержащая иерархию (структуру) нашей программы. Единица этого дерева называется узлом, некоторые из возможных разновидностей узлов имеют дочерние узлы. Дерево, кода выше, похоже на что то подобное:
decl function
name "main"
arguments []
body
statement return
value 1
После, генератор проходится по каждому из узлов и создает код,
объектный файл, который может быть скомпонован с аналогичными файлами C/C++ итд.
На самом деле фаза генерации очень сложна, так как там надо учитывать архитектуру, операционную систему, слава Аллаху, кто-то из программистов подумал:
А что если создать платформу, для создания компиляторов, которая будет предоставлять разработчикам интерфейс по созданию программ, а всю грязную низкоуровневую работу будет выполнять библиотека?
И так появился
LLVM.
Вдаваться в подробности не буду, лишь скажу, что LLVM имеет собственный язык программирования, напоминающий ассемблер, я нашел такой код, не знаю что он делает, но выглядет номрыч:
table entries. Here is an example of the “hello world” module:
; Declare the string constant as a global constant.
@.str = private unnamed_addr constant [13 x i8] c"hello world\0A\00"
; External declaration of the puts function
declare i32 @puts(i8* nocapture) nounwind
; Definition of main function
define i32 @main() { ; i32()*
; Convert [13 x i8]* to i8*...
%cast210 = getelementptr [13 x i8], [13 x i8]* @.str, i64 0, i64 0
; Call puts function to write out the string to stdout.
call i32 @puts(i8* %cast210)
ret i32 0
}
; Named metadata
!0 = !{i32 42, null, !"string"}
!foo = !{!0}
Так вот, теперь мы будем создавать свой ЯП.
Писать код придется на Rust'е, потому, что я так захотел.
Напишем основные перечисления:
//Думаю, тут все предельно ясно. Перечисления в Rust - декларация нового типа,
// который может принимать одно из указанных значений. Как и в C.
// Только в Rust у перечислений могут быть аргументы, что довольно круто.
enum Keyword {
// `func`
Func,
// `let`
Let,
// `ret`
Ret
}
enum Literal {
// `123456`
// Самое класнное, что в Rust'е у перечисления есть аргументы.
Int(i32),
// `123456.0`
// `.0`
Float(f64)
}
// Перечисление Token
enum Token {
// Идентификатор
Ident(String),
// Ключевое слово
Keyword(Keyword),
// Литерал
Literal(Literal),
// Конец файла
Eof
}
Теперь создадим класс (в Rust нет классов поэтому это будет структура) Lexer, отвечающий за преобразование кода в токены.
// Так объявляются структуры, Вас может смутить кака в ``,
// но это нормально, это так называемое время жизни, т. к. в Rust нет
// сборщика мусора, фактически мы говорим, что переменная code
// существует столько же, или дольше чем объект структуры Lexer
struct Lexer,
// Последний считанный символ
last: char
}
// Так задаются метода структур
impl {
// В rust тип возвращаемого значения указывается после `->`
// Что-то типа конструктора нашего лексера
fn new(code: &str) -> Lexer {
Lexer {
code: code.chars(),
last: ' '
}
}
// В качестве аргумента фукции вы можете видеть `&mut self` - это
// означает, изменяемую ссылку на себя, то есть, что функция нестатическая.
// Прочесть следующий символ
fn next(&mut self) {
// Метод next() типа Chars возвращает следующий символ или None,
// если символов больше не осталось.
match self.code.next() {
Some(val) => self.last = val,
None => self.last = '\0'
}
}
// Получить новый токен
fn tokenize(&mut self) -> Token {
// Пропускаем все проблемы
while self.last == ' ' || self.last == '\n' || self.last == '\t' {
self.next();
}
// Если текущий символ - символ алфавита, то
if self.last.is_alphabetic() {
// Скорее всего это идентификатор или ключевое слово
// Строка со всем словом
let mut val = String::new();
// Теперь, мы считываем символы пока они являются цифрой или буквой,
// чтобы идентификаторы типа `x1` тоже были верны
while self.last.is_alphanumeric() {
val.push(self.last);
self.next();
}
match val.as_str() {
"func" => Token::Keyword(Keyword::Func),
"Let" => Token::Keyword(Keyword::Let),
"Ret" => Token::Keyword(Keyword::Ret),
_ => Token::Ident(val)
}
}
// Если текщий символ - цифра
else if self.last.is_numeric() {
// Скорее всего, это литерал числа
// Считываем аналогично цифры и собираем итоговое число,
// Float пока не реализован, ибо мудро.
let mut val = String::new();
while self.last.is_numeric() {
val.push(self.last);
self.next();
}
let val = val.parse::().unwrap();
Token::Literal(Literal::Int(val))
}
// Иначе
else {
let val = self.last;
self.next();
match val {
'\0' => Token::Eof,
'(' => Token::Punct(Punct::OpenParen),
')' => Token::Punct(Punct::CloseParen),
_ => Token::Symbol(val)
}
}
}
}
Теперь все это надо проверить, благо Rust поддерживает тесты
// Rust поддерживает тесты
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple_code() {
// Задаем переменные
let code =
"func main() 10";
let mut lexer = Lexer::new(code);
// Сравниваем получаемые значения
assert_eq!(lexer.tokenize(), Token::Keyword(Keyword::Func));
assert_eq!(lexer.tokenize(), Token::Ident("main".to_string()));
assert_eq!(lexer.tokenize(), Token::Punct(Punct::OpenParen));
assert_eq!(lexer.tokenize(), Token::Punct(Punct::CloseParen));
assert_eq!(lexer.tokenize(), Token::Literal(Literal::Int(10)));
}
}
Что-ж, мы создали простой лексер, в следущем гайде мы создадим парсер и небольшое дерево,
всем спасибо,
тавьте 5 мем это моя первая новость не судите строго