Оператор для организации диалога с пользователем в языках программирования это оператор ввода

В этой серии мы разработаем новый язык сценариев и опишем этот процесс шаг за шагом. Первый вопрос. Который спонтанно приходит в голову любому удивленному читателю. Скорее всего. Будет: “Действительно ли нам нужен новый язык программирования?”

Действительно Ли Нам Нужен Новый Язык Программирования?

Чтобы ответить на этот вопрос. Я позволю себе небольшое отступление. Представьте. Что вы архитектор (настоящий архитектор. А не программист). И вам не повезло родиться в очень бюрократической стране. У вас есть отличная идея для торгового центра в вашем слаборазвитом родном городе. Поэтому вы создаете проект и подаете заявку на разрешение на строительство.

Конечно, они сразу же отказывают вам на том основании. Что у вас нет зарегистрированной компании. Итак, вы регистрируете компанию. Для этого вам нужно внести немного денег. Затем вы придумываете название для своей компании. Которое отвергается. С пятой попытки она принимается. И ваше заявление отправляется на самое дно кучи. В конце концов. Вы либо сдаетесь. Либо понимаете. Что кто-то другой построил торговый центр за это время.

Но мы не настоящие архитекторы. Мы инженеры-программисты . И у нас есть привилегия воплощать наши идеи в жизнь без каких-либо разрешений или бюрократии.

Единственное. Что нам нужно. — это свободное время и желание тратить его на программирование. А не на головоломки судоку.

Если вы все еще настаиваете на том. Что мотивом программирования не может быть чистое любопытство. Позвольте мне просто упомянуть. Что первый язык программирования. Который я разработал. Был разработан в результате необходимости. А не просто любопытства. Однако это не должно быть первой мотивацией для чтения этого блога. Я думаю, что многие идеи. С которыми вы столкнетесь здесь. Достаточно интересны и инновационны. Чтобы заинтересовать вас. Даже если вам на самом деле не нужно создавать свой собственный язык программирования.

Наша цель разработки скриптового языка небольшого размера вдохновила меня назвать его “Аистом”; и, к счастью. Нам не нужно убеждать какого-либо бюрократа одобрить это название.

Я собираюсь разработать язык программирования по мере прохождения серии. Поэтому есть вероятность. Что я также разработаю некоторые ошибки. Они должны быть сглажены по мере приближения к концу серии.

Полный исходный код всего описанного здесь находится в свободном доступе на GitHub.

Наконец, чтобы ответить на вопрос из названия этого параграфа—нет. Нам на самом деле не нужен новый язык программирования. Но поскольку мы пытаемся продемонстрировать. Как сделать язык программирования в C++. Мы создадим его для демонстрационных целей.

Маленькие помощники токенизатора

Я не знаю, регулярно ли другие программисты сталкиваются с той же проблемой. Но я сталкиваюсь с этой проблемой довольно часто:

Мне нужен контейнер ключ-значение. Который должен извлекать значения быстро. В логарифмическое время. Однако, как только я инициализирую контейнер. Я не хочу добавлять в него новые значения. Поэтому std::map(или std::unordered_map) — это излишество. Так как оно также позволяет быстро вставлять данные.

Я полностью против ненужной оптимизации. Но в этом случае я чувствую. Что много памяти тратится впустую. Не только это. Но и позже нам нужно будет реализовать алгоритм максимального мунка поверх такого контейнера, и mapэто не лучший контейнер для этого.

Второй вариантstd::vector<:pair> >-сортировка после вставок. Единственная проблема с этим подходом-меньшая читаемость кода. Поскольку нам нужно иметь в виду. Что вектор отсортирован. Поэтому я разработал небольшой класс. Который гарантирует это ограничение.

(Все функции. Классы и т. Д. объявляются в пространстве storkимен . Я опущу это пространство имен для удобства чтения.)

template typename Key, typename Value> class lookup { public: using value_type = std::pair; using container_type = std::vector; private: container_type _container; public: using iterator = typename container_type::const_iterator; using const_iterator = iterator; lookup(std::initializer_list init) : _container(init) { std::sort(_container.begin(), _container.end()); } lookup(container_type container) : _container(std::move(container)) { std::sort(_container.begin(), _container.end()); } const_iterator begin() const { return _container.begin(); } const_iterator end() const { return _container.end(); } template typename K> const_iterator find(const K& key) const { const_iterator it = std::lower_bound( begin(), end(), key, [](const value_type& p, const K& key) { return p.first return it != end() && it->first == key ? it : end(); } size_t size() const { return _container.size(); } }; 

Как видите, реализация этого класса довольно проста. Я не хотел реализовывать все возможные методы, только те. Которые нам понадобятся. Базовый контейнер является a vector, поэтому он может быть инициализирован с предварительно заполненным vector, или с.initializer_list

Токенизатор будет считывать символы из входного потока. На этом этапе проекта мне трудно решить. Каким будет входной поток. Поэтому я буду использовать std::functionвместо него.

using get_character = std::functionint()>; 

Я буду использовать хорошо известные соглашения из потоковых функций C-стиля. Такие как getc, который возвращает intвместоchar, а также отрицательное число. Чтобы сигнализировать конец файла.

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

class push_back_stream { private: const get_character& _input; std::stackint> _stack; size_t _line_number; size_t _char_index; public: push_back_stream(const get_character& input); int operator()(); void push_back(int c); size_t line_number() const; size_t char_index() const; }; 

Чтобы сэкономить место. Я опущу детали реализации. Которые вы можете найти на моей странице GitHub.

Как видите, push_back_streamинициализируется из get_characterфункции. Перегруженный operator()используется для извлечения следующего символа и push_backиспользуется для возврата символа обратно в поток. line_number и char_numberявляются удобными методами. Используемыми для отчетов об ошибках.

Имейте в виду. Что char_indexэто не индекс символа в текущей строке. А общий индекс; в противном случае нам пришлось бы хранить все прошлые символы в каком-то контейнере. Чтобы push_backправильно реализовать функцию.

Зарезервированные Токены

Зарезервированные Токены

Токенизатор-это компонент компилятора самого низкого уровня. Он должен считывать входные данные и “выплевывать” токены. Существует четыре типа токенов которые представляют для нас интерес:

  • Зарезервированные токены
  • Идентификаторы
  • Числа
  • Строки

Комментарии нас не интересуют. Поэтому токенизатор просто “съест” их. Никого не уведомив.

Для обеспечения привлекательности и планетарной популярности этого языка мы будем использовать известный C-подобный синтаксис. Он довольно хорошо работал для C, C++. JavaScript, Java. C# и Objective-C. Поэтому он должен работать и для Stork. Если вам нужен курс повышения квалификации. Вы можете обратиться к одной из наших предыдущих статей. Посвященных учебным ресурсам C/C++.

Вот список зарезервированных токенов:

enum struct reserved_token { inc, dec, add, sub, concat, mul, div, idiv, mod, bitwise_not, bitwise_and, bitwise_or, bitwise_xor, shiftl, shiftr, assign, add_assign, sub_assign, concat_assign, mul_assign, div_assign, idiv_assign, mod_assign, and_assign, or_assign, xor_assign, shiftl_assign, shiftr_assign, logical_not, logical_and, logical_or, eq, ne, lt, gt, le, ge, question, colon, comma, semicolon, open_round, close_round, open_curly. Close_curly. Open_square. Close_square, kw_if. Kw_else, kw_elif. Kw_switch, kw_case. Kw_default. Kw_for, kw_while. Kw_do, kw_break. Kw_continue, kw_return. Kw_var, kw_fun. Kw_void, kw_number. Kw_string, }; 

Члены перечисления с префиксом “kw_” являются ключевыми словами. Мне пришлось поставить перед ними префикс. Поскольку они обычно совпадают с ключевыми словами C++. Те, у которых нет префикса. Являются операторами.

Почти все они следуют конвенции Си. Те, которые этого не делают:
concat и concat_assign(.. и..=), которые будут использоваться для конкатенации
idivи idiv_assign(\ и\=), которые будут использоваться для целочисленного деления
kw_varдля объявления переменных
kw_funдля объявления функций
kw_numberдля числовых переменных
kw_stringдля строковых переменных

Мы добавим дополнительные ключевые слова по мере необходимости.

Есть одно новое ключевое слово. Которое заслуживает описания: kw_elif. Я твердо верю. Что блоки с одним утверждением (без фигурных скобок) не стоят того. Я не использую их (и я не чувствую. Что что-то пропало). За исключением двух случаев:

  1. Когда я случайно нажал точку с запятой сразу после forоператора , while, или ifперед блоком. Если мне повезет. Он возвращает ошибку времени компиляции. Но иногда это приводит к фиктивному if-оператору и блоку. Который всегда выполняется. К счастью, за эти годы я научился на своих ошибках. Поэтому такое случается очень редко. Собака Павлова тоже в конце концов научилась.
  2. Когда у меня есть “цепные” операторы if, поэтому у меня есть блок if, затем один или несколько блоков else-if и, возможно, блок else. Технически, когда я пишу else if, это elseблок только с одним оператором. Который является этим if-оператором.

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

Есть две вспомогательные функции. Возвращающие зарезервированные токены:

std::optional get_keyword(std::string_view word); std::optional get_operator(push_back_stream& stream); 

Функция get_keywordвозвращает необязательное ключевое слово из переданного слова. Это “слово” представляет собой последовательность букв. Цифр и подчеркиваний. Начиная с буквы или подчеркивания. Он вернет areserved_token, если слово является ключевым словом. В противном случае токенизатор будет считать. Что это идентификатор.

Функция get_operatorпытается прочитать как можно больше символов. Пока последовательность является допустимым оператором. Если он читает больше. То он будет читать все дополнительные символы. Которые он прочитал после самого длинного распознанного оператора.

Для эффективной реализации этих двух функций нам нужны два поиска между string_viewи reserved_keyword.

const lookupstd::string_view. Reserved_token> operator_token_map { {"++", reserved_token::inc}, {"--", reserved_token::dec}, {"+", reserved_token::add}, {"-", reserved_token::sub}, {"..", reserved_token::concat}, }; const lookupstd::string_view. Reserved_token> keyword_token_map { {"if", reserved_token::kw_if}, {"else", reserved_token::kw_else}, {"elif", reserved_token::kw_elif}, {"switch", reserved_token::kw_switch}, {"case", reserved_token::kw_case}, {"default", reserved_token::kw_default}, {"for", reserved_token::kw_for}, {"while", reserved_token::kw_while}, {"do", reserved_token::kw_do}, {"break", reserved_token::kw_break}. {"continue", reserved_token::kw_continue}, {"return", reserved_token::kw_return}, {"var", reserved_token::kw_var}, {"fun", reserved_token::kw_fun}, {"void", reserved_token::kw_void}, {"number", reserved_token::kw_number}, {"string", reserved_token::kw_string} }; 

get_keywordРеализация полностью проста. Но для get_operatorэтого нам нужен пользовательский компаратор . Который будет сравнивать данный символ с операторами-кандидатами. Принимая во внимание только n-й символ.

class maximal_munch_comparator{ private: size_t _idx; public: maximal_munch_comparator(size_t idx) : _idx(idx) { } bool operator()(char l, char r) const { return l bool operator()( std::pairstd::string_view. Reserved_token> l, char r ) const { return l.first.size() bool operator()( char l, std::pairstd::string_view. Reserved_token> r ) const { return r.first.size() > _idx && l bool operator()( std::pairstd::string_view. Reserved_token> l, std::pairstd::string_view. Reserved_token> r ) const { return r.first.size() > _idx && ( l.first.size() 

Это обычный лексический компаратор . Который учитывает только символ в позицииidx, но если строка короче . То он обрабатывает ее так. Как если бы она имела нулевой символ в позицииidx, который меньше любого другого символа.

Это реализация get_operator, которая должна сделать maximal_munch_operatorкласс более понятным:

std::optional get_operator(push_back_stream& stream) { auto candidates = std::make_pair( operator_token_map.begin(), operator_token_map.end() ); std::optional ret; size_t match_size = 0; std::stackint> chars; for (size_t idx = 0; candidates.first != candidates.second; ++idx) { chars.push(stream()); candidates = std::equal_range( candidates.first. Candidates.second, char(chars.top()). Maximal_munch_comparator(idx) ); if ( candidates.first != candidates.second && candidates.first->first.size() == idx + 1 ) { match_size = idx + 1; ret = candidates.first->second; } } while (chars.size() > match_size) { stream.push_back(chars.top()); chars.pop(); } return ret; } 

В принципе. Мы рассматриваем всех операторов как кандидатов с самого начала. Затем мы читаем символ за символом и фильтруем текущих кандидатов по вызовуequal_range, сравнивая только n-й символ. Нам не нужно сравнивать предыдущие символы. Поскольку они уже сравниваются. И мы не хотим сравнивать следующие символы. Поскольку они все еще не имеют отношения к делу.

Всякий раз, когда диапазон непуст. Мы проверяем. Не содержит ли первый элемент в диапазоне больше символов (если такой элемент существует. Он всегда находится в начале диапазона при сортировке поиска). В таком случае у нас есть соответствующий законный оператор. Самый длинный такой оператор-это тот. Который мы возвращаем. Мы будем читать все символы. Которые мы в конечном итоге читаем после этого.

Токенизатор

Поскольку токены гетерогенны. Токен-это удобный класс. Который обертывает std::variantразличные типы токенов, а именно:

  • Зарезервированный токен
  • Идентификатор
  • Номер
  • Строка
  • Конец файла
class token { private: using token_value = std::variantdouble, std::string, eof>; token_value _value; size_t _line_number; size_t _char_index; public: token(token_value value, size_t line_number, size_t char_index); bool is_reserved_token() const; bool is_identifier() const; bool is_number() const; bool is_string() const; bool is_eof() const; reserved_token get_reserved_token() const; std::string_view get_identifier() const; double get_number() const; std::string_view get_string() const; size_t get_line_number() const; size_t get_char_index() const; }; 

Это identifierпросто класс с одним членом std::stringтипа. Он есть для удобства, так как. На мой взгляд, std::variantчище. Если все его альтернативы разных типов.

Теперь мы можем написать токенизатор. Это будет одна функция. Которая примет push_back_streamи вернет следующий токен.

Хитрость заключается в использовании различных ветвей кода. Основанных на типе символа первого символа. Который мы читаем.

  • Если мы прочитаем символ конца файла. То вернемся из функции.
  • Если мы прочитаем пробел. Мы его пропустим.
  • Если мы читаем буквенно-цифровой символ (букву. Цифру или подчеркивание). Мы будем читать все последовательные символы этого типа (мы также будем читать точки. Если первый символ является цифрой). Затем, если первый символ является цифрой. Мы попытаемся разобрать последовательность как число. В противном случае мы будем использовать эту get_keywordфункцию. Чтобы проверить. Является ли она ключевым словом или идентификатором.
  • Если мы читаем кавычку. Мы будем рассматривать ее как строку. Освобождая из нее экранированные символы.
  • Если мы читаем символ косой черты (/), мы проверим. Является ли следующий символ косой чертой или звездочкой (*), и в этом случае мы пропустим комментарий строки/блока.
  • В противном случае мы будем использовать эту get_operatorфункцию.

Вот реализация функции токенизации. Я опущу детали реализации функций. Которые он вызывает.

token tokenize(push_back_stream& stream) { while (true) { size_t line_number = stream.line_number(); size_t char_index = stream.char_index(); int c = stream(); switch (get_character_type(c)) { case character_type::eof: return {eof(). Line_number. Char_index}; case character_type::space: continue; case character_type::alphanum: stream.push_back(c); return fetch_word(stream); case character_type::punct: switch (c) { case '"': return fetch_string(stream); case '/': { char c1 = stream(); switch(c1) { case '/': skip_line_comment(stream); continue; case '*': skip_block_comment(stream); continue; default: stream.push_back(c1); } } default: stream.push_back(c); return fetch_operator(stream); } break; } } } 

Вы можете видеть. Что он отодвигает символы. Которые он читает. Прежде чем вызвать функцию более низкого уровня. Штраф за производительность почти не существует. А код функции более низкого уровня намного чище.

Исключения

В одной из своих тирад против исключений мой брат однажды сказал::

“Есть два типа людей: те. Кто бросает исключения, и те. Кто должен их поймать. Я всегда в этой печальной. Второй группе.”

Я согласен с духом этого утверждения. Я не особенно люблю исключения. И их выбрасывание может значительно усложнить обслуживание и чтение любого кода. Почти всегда.

Я решил сделать исключение (неудачный каламбур) из этого правила. Это действительно удобно-выбросить исключение из компилятора. Чтобы раскрутить его из глубин компиляции.

Вот реализация исключения:

class error: public std::exception { private: std::string _message; size_t _line_number; size_t _char_index; public: error(std::string message, size_t line_number, size_t char_index) noexcept; const char* what() const noexcept override; size_t line_number() const noexcept; size_t char_index() const noexcept; }; 

Тем не менее. Я обещаю поймать все исключения в коде верхнего уровня. Я даже добавил line_numberи char_indexэлементы для pretty-printing, и функцию. Которая это делает:

void format_error( const error& err. Get_character source, std::ostream& output ); 

Сворачивание

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

Я надеюсь, что вы получили несколько хороших идей из этого поста. И если вы хотите изучить детали. Перейдите на мою страницу GitHub.