Comments 38
немного критики:
Название для языка не слишком оригинальное: https://yandex.ru/search/?text=onyx+lang
Идея статьи интересная но читать ее я не смог из-за огромных полотен кода. В конце концов есть же гитхаб, почему бы не вставить вместо них ссылки на строки оттуда: https://github.com/SamirShef/onyxlang/blob/main/Src/Basic/ASTVal.cpp#L3-L27
Пример кода на языке я бы из конца в самое начало перенес, потому что это первое что интересует когда узнаешь о языке, да и читать проще когда понимаешь что мы пытаемся получить в итоге.
Впечатляет! Карма/статья/в закладки: +/+/+
Я посмотрел репозиторий - там с десяток языков.
Плюсы:
-- правильная тема
-- 1 на 10.000 "програмистов", который занят реальным делом
Минусы:
-- нет описания грамматики языков в BNF
-- кодовая лапша на Си++ (full ООП - не самый лучший вариант кодирования)
-- грамматика всех языков однотипна, С++ подобна
PS Кто учит и сколько лет ты в теме?
PPS Статьи - пиши.
Спасибо за оценку)
Никто не учит, я самоучка, а первый язык написал в конце февраля прошлого года. Языков у меня довольно много, каждый раз пытаюсь сделать лучше и вроде как даже получается. Описывать грамматику я пока не хотел, потому что был занят созданием результата, а не продукта для всех с документацией. Естественно когда проект будет доведен до MVP будет и дока и возможно даже книга
Описывать грамматику я пока не хотел
С неё надо начинать
Угу. Во всех смыслах)
Что до конкретики: на гитхабе имеем семейство Си-зеркально-подобных языков (калька с ЦПП), и вся их грамматика легко читается по таким структурам:
keywords
static std::unordered_map<std::string, TokenKind> keywords {
{ "false", TkBoolLit },
{ "true", TkBoolLit },
{ "var", TkVar },
{ "const", TkConst },
{ "bool", TkBool },
{ "char", TkChar },
{ "i16", TkI16 },
{ "i32", TkI32 },
{ "i64", TkI64 },
{ "f32", TkF32 },
{ "f64", TkF64 },
{ "noth", TkNoth },
{ "fun", TkFun },
{ "return", TkRet },
{ "if", TkIf },
{ "else", TkElse },
{ "for", TkFor },
{ "break", TkBreak },
{ "continue", TkContinue },
{ "struct", TkStruct },
{ "pub", TkPub },
};
var a: i32 = 10;
var b: i32; // инициализация нулем
const pi: f32 = 3.14159265359f;
fun get_pi() : f32 {
return pi;
}
fun main() : i32 {
var c: i32 = a + 2;
b += c;
a *= b + c;
var radius: i32 = 5;
var circle_sqr: f32 = radius * radius * get_pi();
return 0;
}И тут были крутые ребята, которые ваяли языки. Чел в 18 лет сделал Mash. Мы задружились даже, но он сдулся, и пошёл прикладником шаманить на Питушмане.
Язык программирования Mash. Там многопоточность, сбока мусора и много чего ещё из коробки.
https://habr.com/ru/articles/508096/
Untyped, lightweight, crossplatform OOP language.
https://habr.com/ru/users/RoPi0n/articles/
Вот блин, я тут же начал искать, что за "Питушман", поиск нашёл некоего "Питушман Егорович", только потом до меня дошло...)))). М-да, старость не радость. А паренёк молодец, в 16 лет пишет уже свои языки. Я в свои 16 даже слыхом не слыхивал о каких-то языках программирования (ну, может кроме Бейсика)), а в университете, книжка по С++, ничего кроме отвращения не вызывала, а вот Паскалем зачитывался)))
Что думаете насчёт того чтобы использовать готовый генератор парсеров который на выходе сразу даст вам строготипизированное AST на C++?
Это просто, но не дает полного контроля над созданием AST. Мне больше по душе свобода действий. К тому же создавая парсер с 0 ты сам понимаешь как он работает
Мне больше по душе свобода действий
Если вдруг кому-то/вам надоест писать парсеры вручную и захочется делегировать/разделить эту задачу с кем-то, то пишите мне я попробую помочь сделать эту часть работы на 5+(в плане архитектуры строготипизированного AST).
А у вас есть какие-то прорывные идеи в дизайне языков программирования? Если вам не жалко ими поделиться, то я бы с радостью хотел их увидеть одним из первых.
"Виситор" - это однозначно 5! Причём повторяется несколько раз, значит не опечатка
Очень неплохо!
Я читал статью вперемешку с кодом, который на гитхабе, код в статье выглядит получше. Например, в некоторых местах SemanticAnalyzer на гитхабе в случае ошибки возвращает ASTVal(ASTType(ASTTypeKind::I32, "i32", false), ASTValData { .i32Val = 0 }) , в статье nullopt, что получше. Наверное, запушить не успели.
Для 64-битного целого не завели литералы, дискриминация!
У меня такой вопрос про LLVM-кодогенератор. Я правильно понял, что у вас визитор один раз запускается? Я вижу, что VisitStructExpr()/VisitFieldAccessExpr() ожидают, что LLVM-тип для структуры уже будет сформирован в VisitStructStmt(). Это означает для языка, что структуры должны быть объявлены выше того места, где они используются. Про объявления и вызовы функций у меня тоже такое подозрение.
Да, кодогенератор уже ожидает созданную структуру из VisitStructStmt, также как и вызов функции ожидает явное определение выше. В будущем планируется допускать использование функций и структур до их явного определения. В текущей реализации семантики возвращается нулевой i32 для того, чтобы не ломалась семантика (а еще мне просто лень везде проверять выражения на .has_value(), где-нибудь я бы его точно потерял и узнал об этом через какое-то время)
Ещё одну штуку хотел сказать. Может я невнимательно смотрел, но у вас сейчас парсер считает, что исходник - это последовательность операторов, а объявления функций и переменных - это разновидности операторов. С одной стороны, это означает, что функции можно определять внутри функций, хотя они, похоже, попадут в глобальное пространство имён. С другой же стороны это значит, что на самом верхнем уровне можно не только определять глобальные переменные и функции, но и просто писать код, с присваиваниями, циклами, return-ами. Я не знаю, куда этот код вне функций после LLVM попадёт, наверное в какое-то пространство, но он не будет выполнен. Возможно, стоит это запретить.
Но я ни в коем случае не критикую, мне всё очень понравилось, и код, и статья. Портяночки кода в статье длинные, да, но меня это не раздражает (в отличие от хабровской подписи "объяснить код", лучше бы сделали в каждом листинге кнопочки "+"/"-", которыми можно было бы уменьшить размер шрифта).
Для 64-битного целого не завели литералы, дискриминация!
Он есть: TkI64Lit, в семантике и кодгене он тоже учитывается, а вот в лексере забыл сделать суффикс, спасибо, что нашли косяк
PS: Я уже исправил недоработку, ещё раз спасибо!
Наверняка статья очень интересная, но пока к сожалению столь длинное чтиво читать не очень хочется. Может быть потом прочитаю.
Можно было бы наверное разделить на несколько статей где подробно рассказывается отдельно лексер, отдельно парсер, отдельно синтаксическое дерево и т.д.. Так оно и психологически проще читать было бы.
А так я сюда спустился для того чтобы просто поделиться своей маленькой наработкой над которой работал недавно, но забросил пока из-за нехватки знаний.
Я тоже ради интереса писал свой C подобный язык программирования. Но смог реализовать пока лишь лексер. У меня была задумка модульности написания, так чтобы можно было добавлять в язык новые возможности постепенно без полного переписывания кода. Для этого я реализовал префиксное дерево для быстрого поиска ключевых слов. А для лексера сканеры. И у меня получилось нечто подобное:
Скрытый текст
#include <unordered_map>
#include <optional>
#include <string_view>
#include <iostream>
#include <deque>
#include <utility>
#include <memory>
#include <vector>
/*=============== Trie ===============*/
template<class Char, class T>
class Trie {
private:
using string = std::basic_string<Char>;
using string_view = std::basic_string_view<Char>;
template<class String>
class Result_impl;
using Result = Result_impl<string>;
using Result_view = Result_impl<string_view>;
struct Node {
std::unordered_map<Char, Node> nodes;
std::optional<T> value;
};
public:
Trie() = default;
Trie(Trie&&) = default;
Trie(const Trie&) = default;
Trie(std::initializer_list<std::pair<string_view, T>> ilist) {
for (auto& [key, value] : ilist) {
emplace(key, value);
}
}
Trie& operator=(Trie&&) = default;
Trie& operator=(const Trie&) = default;
public:
const std::optional<T>& find(string_view key) const {
const Node* current = &root;
for (auto& ch : key) {
auto it = current->nodes.find(ch);
if (it == current->nodes.end()) {
static const std::optional<T> empty;
return empty;
}
current = &it->second;
}
return current->value;
}
public:
std::deque<Result_view> find_prefix_of(string_view word) const {
std::deque<Result_view> results;
const Node* current = &root;
if (current->value) {
results.emplace_back({}, &*current->value);
}
for (size_t i = 0; i < word.size(); ++i) {
auto it = current->nodes.find(word[i]);
if (it == current->nodes.end()) {
break;
}
current = &it->second;
if (current->value) {
results.emplace_back(word.substr(0, i + 1), &*current->value);
}
}
return results;
}
std::deque<Result> find_by_prefix(string_view prefix) const {
std::deque<Result> results;
const Node* current = &root;
for (Char ch : prefix) {
auto it = current->nodes.find(ch);
if (it == current->nodes.end()) {
return results;
}
current = &it->second;
}
string key(prefix);
collect(*current, key, results);
return results;
}
public:
Result_view find_first_prefix(string_view word) const {
const Node* current = &root;
if (current->value) {
return { {}, &*current->value };
}
for (size_t i = 0; i < word.length(); ++i) {
auto it = current->nodes.find(word[i]);
if (it == current->nodes.end()) {
return {};
}
current = &it->second;
if (current->value) {
return { word.substr(0, i + 1), &*current->value };
}
}
return {};
}
Result_view find_last_prefix(string_view word) const {
const Node* current = &root;
Result_view result;
size_t length = 0;
if (current->value) {
result._value = &*current->value;
}
for (size_t i = 0; i < word.length(); ++i) {
auto it = current->nodes.find(word[i]);
if (it == current->nodes.end()) {
break;
}
current = &it->second;
if (current->value) {
result._value = &*current->value;
length = i + 1;
}
}
result._key = word.substr(0, length);
return result;
}
public:
void insert(string_view key, T&& value) {
emplace(key, std::move(value));
}
void insert(string_view key, const T& value) {
emplace(key, value);
}
template<class ... Args>
void emplace(string_view key, Args&& ... args) {
Node* current = &root;
for (auto& ch : key) {
current = ¤t->nodes[ch];
}
current->value.emplace(std::forward<Args>(args) ...);
}
public:
void clear() {
root.nodes.clear();
root.value.reset();
}
void erase(string_view key) {
erase_impl(root, key, 0);
}
private:
static void collect(Node& node, string& key, std::deque<Result>& results) {
if (node.value) {
results.emplace_back(key, &*node.value);
}
for (const auto& [ch, child] : node.nodes) {
key.push_back(ch);
collect(child, key, results);
key.pop_back();
}
}
bool erase_impl(Node& node, const string_view& key, size_t index) {
if (index == key.size()) {
if (!node.value) {
return false;
}
node.value.reset();
return node.nodes.empty();
}
auto it = node.nodes.find(key[index]);
if (it == node.nodes.end()) {
return false;
}
if (erase_impl(it->second, key, index + 1)) {
node.nodes.erase(it);
}
return node.nodes.empty() && !node.value;
}
private:
Node root;
};
template<class Char, class T>
template<class String>
class Trie<Char, T>::Result_impl {
public:
friend Trie;
public:
Result_impl()
: _value(nullptr) {
}
Result_impl(Result_impl&& other)
: _key(std::move(other._key))
, _value(std::exchange(other._value, nullptr)) {
}
Result_impl(const Result_impl& other)
: _key(other._key)
, _value(other._value) {
}
Result_impl(String key, const T* value)
: _key(key)
, _value(value) {
}
Result_impl& operator=(Result_impl&& other) {
if (this != &other) {
_key = std::move(other._key);
_value = std::exchange(other._value, nullptr);
}
return *this;
}
Result_impl& operator=(const Result_impl& other) {
if (this != &other) {
_key = other._key;
_value = other._value;
}
return *this;
}
public:
const String& key() const {
return _key;
}
const T& value() const {
if (_value == nullptr) {
throw std::logic_error("Result not have value");
}
return *_value;
}
const T& value_or(const T& value) const {
return _value ? *_value : value;
}
public:
operator bool() const {
return _value;
}
private:
String _key;
const T* _value;
};
/*=============== Token ===============*/
enum Token_type {
KEYWORD,
IDENTIFIER,
OPERATOR,
BINARY,
OCTE,
HEX,
INT,
FLOAT,
DOUBLE,
CHAR,
STRING,
PUNCTUATOR,
WHITESPACE,
COMMENT,
ERROR = 404
};
template<class Char>
struct Token {
Token() = default;
Token(Token&&) = default;
Token(const Token&) = default;
Token(Token_type token, std::basic_string<Char> lexeme, size_t line, size_t column)
: token(token)
, lexeme(std::move(lexeme))
, line(line)
, column(column) {
}
Token_type token;
std::basic_string<Char> lexeme;
size_t line;
size_t column;
};
/*=============== Lexer ===============*/
template<class Char>
struct Lexer_data {
Lexer_data(std::basic_string_view<Char> code)
: code(code) {
}
void new_line() {
++line;
column = 1;
}
void advance(size_t step) {
column += step;
code = code.substr(step);
}
std::deque<Token<Char>> tokens;
void emplace(Token_type token, std::basic_string_view<Char> value) {
tokens.emplace_back(token, std::basic_string<Char>(value), line, column);
}
std::basic_string_view<Char> code;
private:
size_t line = 1;
size_t column = 1;
};
template<class Char>
struct Scanner {
virtual bool read(Lexer_data<Char>& data) const = 0;
};
template<class Char>
struct Lexer {
std::deque<Token<Char>> tokenize(std::basic_string_view<Char> code) const {
Lexer_data<Char> data(code);
while (!data.code.empty()) {
size_t size = data.code.size();
for (auto& scanner : scanners) {
if (scanner->read(data)) break;
}
if (size == data.code.size()) {
data.emplace(Token_type::ERROR, data.code.substr(0,1));
data.advance(1);
}
}
return std::move(data.tokens);
}
template<class Scanner_t, class ... Args>
void register_scanner(Args&& ... args) {
scanners.emplace_back(std::make_unique<Scanner_t>(std::forward<Args>(args) ...));
}
template<class Scanner_t>
void register_scanner(std::unique_ptr<Scanner_t> scanner) {
if (scanner) {
scanners.emplace_back(std::move(scanner));
}
}
private:
std::vector<std::unique_ptr<Scanner<Char>>> scanners;
};
/*=============== Scanners ===============*/
template<class Char>
bool is_number(Char ch) {
return (ch >='0' && ch <= '9');
}
template<class Char>
bool is_letter(Char ch) {
return (
(ch >= 'a' && ch <= 'z')
||
(ch >= 'A' && ch <= 'Z')
);
}
template<class Char>
struct Whitespace : Scanner<Char> {
bool read(Lexer_data<Char>& data) const override {
switch(data.code.front()) {
case ' ':
case '\t':
data.advance(1);
break;
case '\n':
case '\r':
data.advance(1);
data.new_line();
break;
default:
return false;
}
return true;
}
};
template<class Char>
struct Keyword : Scanner<Char> {
Keyword(std::initializer_list<const Char*> ilist) {
for (auto& key : ilist) {
keywords.emplace(key, Token_type::KEYWORD);
}
}
bool read(Lexer_data<Char>& data) const override {
auto result = keywords.find_last_prefix(data.code);
if (result == false) return false;
auto& lexeme = result.key();
size_t i = lexeme.size();
if (i < data.code.size() && (is_letter(data.code[i]) || is_number(data.code[i]) || data.code[i] == '_')) return false;
data.emplace(Token_type::KEYWORD, lexeme);
data.advance(i);
return true;
}
Trie<Char, Token_type> keywords;
};
template<class Char>
struct Identifier : Scanner<Char> {
bool read(Lexer_data<Char>& data) const override {
if (
!is_letter(data.code.front())
&&
data.code.front() != '_'
) return false;
size_t i = 1;
while (
i < data.code.size()
&&
(
is_letter(data.code[i])
||
is_number(data.code[i])
||
data.code[i] == '_'
)
) {
++i;
}
data.emplace(Token_type::IDENTIFIER, data.code.substr(0, i));
data.advance(i);
return true;
}
};
template<class Char>
struct Operator : Scanner<Char> {
Operator(std::initializer_list<const Char*> ilist) {
for (auto& key : ilist) {
operators.emplace(key, Token_type::KEYWORD);
}
}
bool read(Lexer_data<Char>& data) const override {
auto result = operators.find_last_prefix(data.code);
if (result == false) return false;
auto& lexeme = result.key();
size_t i = lexeme.size();
data.emplace(Token_type::OPERATOR, lexeme);
data.advance(i);
return true;
}
Trie<Char, Token_type> operators;
};
template<class Char>
struct Number : Scanner<Char> {
bool read(Lexer_data<Char>& data) const override {
switch (
start_number
.find_last_prefix(data.code)
.value_or(Token_type::ERROR)
) {
case Token_type::INT:
if (is_decimal(data)) return true;
break;
case Token_type::DOUBLE:
if (is_double(data)) return true;
break;
case Token_type::BINARY:
if (is_binary(data)) return true;
break;
case Token_type::OCTE:
if (is_octe(data)) return true;
break;
case Token_type::HEX:
if (is_hex(data)) return true;
break;
default:
return false;
};
return false;
}
static bool is_binary(Lexer_data<Char>& data) {
size_t i = 2;
Token_type token = Token_type::ERROR;
while (i < data.code.size()) {
auto& ch = data.code[i];
if (ch == '0' || ch == '1') {
token = Token_type::BINARY;
}
else if (ch != '\'') {
break;
}
++i;
}
data.emplace(token, data.code.substr(0, i));
data.advance(i);
return true;
}
static bool is_octe(Lexer_data<Char>& data) {
size_t i = 1;
Token_type token = Token_type::ERROR;
while (i < data.code.size()) {
auto& ch = data.code[i];
if (ch >= '0' && ch <= '7') {
token = Token_type::OCTE;
}
else if (ch != '\'') {
break;
}
++i;
}
data.emplace(token, data.code.substr(0, i));
data.advance(i);
return true;
}
static bool is_hex(Lexer_data<Char>& data) {
size_t i = 2;
Token_type token = Token_type::ERROR;
while (i < data.code.size()) {
auto& ch = data.code[i];
if (
(ch >= '0' && ch <= '9')
||
(ch >= 'A' && ch <= 'F')
||
(ch >= 'a' && ch <= 'f')
) {
token = Token_type::HEX;
}
else if (ch != '\'') {
break;
}
++i;
}
data.emplace(token, data.code.substr(0, i));
data.advance(i);
return true;
}
static bool is_decimal(Lexer_data<Char>& data) {
return false;
}
static bool is_double(Lexer_data<Char>& data) {
return false;
}
static const Trie<Char, Token_type> start_number;
};
template<class Char>
const Trie<Char, Token_type> Number<Char>::start_number {
{"1", Token_type::INT},
{"2", Token_type::INT},
{"3", Token_type::INT},
{"4", Token_type::INT},
{"5", Token_type::INT},
{"6", Token_type::INT},
{"7", Token_type::INT},
{"8", Token_type::INT},
{"9", Token_type::INT},
{"0.", Token_type::DOUBLE},
{"0e", Token_type::DOUBLE},
{"0b", Token_type::BINARY},
{"0B", Token_type::BINARY},
{"0", Token_type::OCTE},
{"0x", Token_type::HEX},
{"0X", Token_type::INT},
};
template<class Char>
struct String : Scanner<Char> {
bool read(Lexer_data<Char>& data) const override {
if (data.code.empty() || data.code.front() != '"') {
return false;
}
size_t i = 1;
bool escape = false;
while (i < data.code.size()) {
Char c = data.code[i];
if (!escape && (c == '\n' || c == '\r')) {
data.emplace(Token_type::ERROR, data.code.substr(0, i));
data.advance(i);
if (c == '\n') {
data.new_line();
}
return true;
}
if (escape) {
escape = false;
} else if (c == '\\') {
escape = true;
} else if (c == '"') {
i++;
data.emplace(Token_type::STRING, data.code.substr(0, i));
data.advance(i);
return true;
}
i++;
}
data.emplace(Token_type::ERROR, data.code.substr(0, i));
data.advance(i);
return true;
}
};
template<class Char_>
struct Char : Scanner<Char_> {
bool read(Lexer_data<Char_>& data) const override {
if (data.code.empty() || data.code.front() != '\'') {
return false;
}
if (data.code.size() < 2) {
data.emplace(Token_type::ERROR, data.code.substr(0, 1));
data.advance(1);
return true;
}
size_t i = 1;
auto& c = data.code[i];
if (c == '\\') {
if (data.code.size() < 3) {
data.emplace(Token_type::ERROR, data.code.substr(0, 2));
data.advance(2);
return true;
}
i++;
auto& escape_char = data.code[i];
switch (escape_char) {
case '\'': case '\"': case '\\': case '0':
case 'n': case 't': case 'r': case 'b': case 'f': case 'v':
case 'a': case '?':
break;
default:
data.emplace(Token_type::ERROR, data.code.substr(0, i + 1));
data.advance(i + 1);
return true;
}
} else if (c == '\n' || c == '\r') {
data.emplace(Token_type::ERROR, data.code.substr(0, i));
data.advance(i);
if (c == '\n') data.new_line();
return true;
} else if (c == '\'') {
data.emplace(Token_type::ERROR, data.code.substr(0, i));
data.advance(i);
return true;
}
i++;
if (i >= data.code.size() || data.code[i] != '\'') {
data.emplace(Token_type::ERROR, data.code.substr(0, i));
data.advance(i);
return true;
}
i++;
data.emplace(Token_type::CHAR, data.code.substr(0, i));
data.advance(i);
return true;
}
};
/*=============== keywords ===============*/
std::initializer_list<const char*> keywords {
"int",
"float",
"double",
"bool",
"char",
"void",
"auto",
"if",
"else",
"for",
"do",
"while",
"continue",
"break",
"return",
"true",
"false",
"module",
"export",
"import",
};
/*=============== operators ===============*/
std::initializer_list<const char*> operators {
"+",
"-",
"*",
"/",
"%",
"=",
"+=",
"-=",
"*=",
"/=",
"%="
};
/*=============== punctuators ===============*/
std::initializer_list<const char*> punctuators {
"(",
")",
"{",
"}",
"[",
"]",
";",
".",
",",
"::",
"..."
};
std::string_view code = R"(
import std;
bool is_even(int num) {
return num % 2 == 0;
}
int main() {
for (int i = 0; i < 10; ++i) {
if (is_even(i)) {
std::print("is even number: ", i, '\n');
}
}
return 0;
}
)";
int main() {
std::cout << code;
Lexer<char> lexer;
//lexer.register_scanner<Comment<char>>();
lexer.register_scanner<Whitespace<char>>();
lexer.register_scanner<Keyword<char>>(keywords);
lexer.register_scanner<Identifier<char>>();
lexer.register_scanner<Operator<char>>(operators);
lexer.register_scanner<Number<char>>();
//lexer.register_scanner<Punctuator<char>>();
lexer.register_scanner<String<char>>();
lexer.register_scanner<Char<char>>();
auto tokens = lexer.tokenize(code);
for (auto& token : tokens) {
std::cout << "Token: " << token.lexeme
<< " Type: " << token.token
<< " Line: " << token.line
<< " Column: " << token.column
<< std::endl;
}
return 0;
}А потом я просто столкнулся с тем, что не понимаю как реализовать модульный парсер. Поэтому пока забросил
Проект интересный, явно надо продолжать!
Можно было бы наверное разделить на несколько статей где подробно рассказывается отдельно лексер, отдельно парсер, отдельно синтаксическое дерево и т.д.. Так оно и психологически проще читать было бы.
Не согласен, лексеры-парсеры в отрыве от остального непонятно зачем нужны. В этой статье всё от начала до конца. Автор пишет язык не слоями, а целиком, пусть и начал с маленького языка, так даже самому разработчику проще.
Я тоже ради интереса писал свой C подобный язык программирования. Но смог реализовать пока лишь лексер. У меня была задумка модульности написания, так чтобы можно было добавлять в язык новые возможности постепенно без полного переписывания кода.
Не понял проблему. У автора, например, компилятор не нужно переписывать, чтобы добавить новые возможности, и это не что-то необычное или сложное.
А потом я просто столкнулся с тем, что не понимаю как реализовать модульный парсер.
Что такое "модульный парсер", дайте определение? Ваш пример лексера - он тоже не особо модульный, оно только выглядит декларативно, но от порядка добавления сканеров будет меняться грамматика, попробуйте поменять местами Keyword и Identifier.
Я просто высказал своё мнение. И это нормально, что наши мнения не совпадают, так как каждому удобно по разному воспринимать информацию.
А про то что вы не поняли это ни как не связано со статьёй, так как он использует готовый фреймворк для написания языка программирования, а я свой с полного нуля, очень грубо говоря свой фреймворк.
Я лишь хотел поделиться своими наработками.
А дать определение модульного парсера к сожалению не могу так как это лишь идея проекта без какой либо конкретики. И вот из-за того что я не совсем понимаю как эту конкретику сформулировать я пока остановился.
И да согласен, что от порядка сканеров лексика может нарушиться. Но в моем представлении лексер выглядит модульным.
Не хочу [в наше время] показаться умным. Но и дружелюбным идиотом [как это принято... и не только в "ИТ"] прикидываться не хочу.
Поэтому прошу считать нижеследуюшее шуткой юмора [в которой, как известно...].
от порядка добавления сканеров будет меняться грамматика
а как она может меняться, если её нельзя менять? :)
читать полные определения лексем удобнее, чем код конечного автомата
это почему же? сделайте грамматику под конечный автомат и читайте в своё удовольствие.
Статья действительно впечатляет! Но к критике тоже добавлю:
"Перескоки". Вот эти: "Лексер", потом "Диагностика", потом "Лексер (ещё раз)". Читая, сосредотачиваешься на лексере, а потом раз, и перескочили к ошибкам. Думаю стоило рассказать о лексере, а потом об ошибках, а в части о лексере сделать ссылку, в духе: "Это для обработки ошибок, об этом ниже". Такой подход в голове создаёт условную "закладку", что об этом узнаешь позже, а на данном этапе можно принять как некоторую условность.
Как по мне, хватает лишнего. Например, отличие префиксного и постфиксного инкремента. Об этом тьма инфы, да и уважающие себя C++ программисты знают. Я не думаю, что её возьмётся читать начинающий C++ программист. Если всё же хочется подать статью и начинающим, то думаю лучше здесь же на хабре найти статью по теме инкремента и вставить на неё ссылку. Что-то в духе: "Здесь использую префиксный инкремент для оптимизации. Разницу между префиксным и постфиксным инкрементами можно узнать из этой статьи" (на "статьи" повесить ссылку).
Присоединюсь к большим кускам кода. Имхо не плохо это, но, если не ошибаюсь, на хабре был элемент для схлопывания больших кусков (не помню, как зовётся, типа accordion widget). Если нет, то возможно реально проще ссылки на гитхаб.
Теперь по теме. Сам тоже немного писал парсеры языков. В основном небольшие командные интерпретаторы. Есть старый проект HDL, но там я знатно повис на синтаксисе: сначала разрабатывал этот язык, вообще не зная о существовании HDL. Есть эмулятор логики Atanua, который поддерживает плагины. Изначальное назначение языка: инструмент для того, чтобы выносить какие-то блоки в некоторую нотацию, чтобы оно не мешалось в схеме (тащить какие-нибудь соединения через всю схему - то ещё приключение). А потом пошло-поехало: "хочу Z состояние"; "о, так же можно и электрические схемы описывать, надо добавить", "любая схема - это граф, а можно ли тогда сделать язык описания графов с примочками для схем" и т.д. Потом узнал про HDL (но в виду отсутствия каких-либо интересных проектов под это - не изучал), и уже появилась мысль сделать HDL язык, не подглядывая в существующие, а потом сравнить результаты. А сейчас работа, и уже как-то в целом не до этого: так потихоньку время от времени возвращаюсь.
Вот возник вопрос ещё. Замечал, что по меньшей мере лексер довольно удобно описывать (да и реализовывать) в виде конечного автомата. Не пробовали такой подход?
Спасибо за критику, я обязательно учту эти замечания ☺️
У вас на самом деле крутой проект, хорошая фантазия, я уверен, что он у вас получится
Что касаемо лексера как конечный автомат, то я не совсем понял, что вы имели ввиду. Если вы имеете ввиду сделать так, чтобы лексер возвращал список токенов, а не один токен как сейчас, то моя реализация экономически выгодна и более производительна, чем возврат списка, который до этого будет много раз реаллоцироваться и занимать больше места, чем реально необходимо. В принципе подход не важен, но мне было интересно попробовать сделать именно такой подход
Лексер неудобно описывать в виде явного конечного автомата. Но генераторы лексеров могут сформировать из читабельного описания реализацию в виде конечного автомата, и в ней не будет backtracking- а. Например, символ / может означать деление или быть началом комментария. Когда он встретился, лексер переходит в состояние divisionOrComment, и проверяет следующий символ. Определять совпадающие начальные символы у токенов гораздо проще автоматически, и читать полные определения лексем удобнее, чем код конечного автомата
Круто, ещё два-три месяца и я тоже буду заниматься с language design
Кажется в условии
if (LLVMLvl == llvm::OptimizationLevel::O3) {
Нет смысла.
Как для 16 лет - норм. Но попробуй сам сделать собственный простой компилятор. Информации надо знать поменьше чем в работе LLVM, зато задача очень на логику:
Распределение переменных (простых и сложных) по адресам, ветвления программы. LLVM многословен и ни на какие вопросы не отвечает в плане понимания настоящего компиляторостроения, этоткак писать игру с движком. Потому что самые трудные места компилятора (если и вправду делать свой компилятор) это memory layout, организовать логику контекста и стека, то есть, работа на прямую с памятью и регистрами. Все это LLVM делает за тебя , эту саммую лаконичную по смыслу, но самую логически сложную.
Вы безусловно правы, и я тоже понимаю, что всё самое интересное LLVM прячет за своей спиной. У меня в планах помимо разработки оникса написать ещё и ВМ и компилятор, который транспилируется сразу в асм. Возможно даже сделаю свою реализацию фреймворка, похожего на LLVM, но явно проще. Вполне возможно что-то из этого буду писать не на плюсах, а на си. У меня всё ещё впереди
Мысленно обнял, пожал руку, вытер слезу и пошел дальше. Молодец!!! Прошу Вас, не бросайте свое занятие.
Завидую, давно пытаюсь такое сделать, но времени не хватает, масштабно. Только не на базе LLVM, а все свое. Короче, очень круто.
Статья конечно длинная (читал около часа), но читается легко и интересно. Насчет больших блоков кода в статье, думаю их лучше вставить как скрытый текст, можно добавить и ссылки на github, но совсем убирать код не стоит. По-моему это удобно - читать код рядом с обяснением его работы, вместо того чтобы каждый раз переходить по ссылке.
Сам недавно принялся писать свой небольшой компилятор (хотя, скорее, фронтенд к LLVM), правда, на Rust (спокойно, вас я не отговариваю писать на C++), поэтому мне есть, что добавить.
Выше правильно заметили, что для определения языка по-хорошему нужна грамматика. Это фундамент, на котором строится весь компилятор. С ней гораздо проще писать парсер, так как грамматика строго определяет структуру любой программы на языке, который она описывает. К тому же с помощью грамматики можно автоматически генерировать парсер без необходимости, например, в случае её расширения добавлять в парсер новые функции. Да, выше вы писали, что вам нужен больший контроль над парсингом, чтобы парсер возвращал типы, определённые вами. И всё-таки попробуйте поискать подходящий вам генератор парсеров. В этой статье описаны многие популярные подходы к синтаксическому анализу, в том числе и метод рекурсивного спуска, на основе которого и написан ваш парсер. (Например, для Rust есть генератор LALRPOP, который позволяет описывать правила грамматики, возвращающие любой тип, в том числе и пользовательский. Думаю, нечто похожее должно быть и для C++)
Также рекомендую книгу "Crafting Interpreters". Она, как понятно из названия, посвящена именно интерпретаторам, но тем не менее в ней помимо прочего показано, как можно строить грамматику языка. Ещё стоит присмотреться к классической книге "Компиляторы: принципы, технологии и инструменатрий". В ней освещены, наверное, все стадии компилятора.
На гитхабе проекта в ридми и самом репозитории огорчило отсутствие каких-либо примеров программ. На мой взгляд, их наличие очень полезно для ознакомления с языком, его синтаксисом и т.д.
class Token {
TokenKind _kind;
llvm::StringRef _text;
llvm::SMLoc _loc;
_Зачем в именах членов класса писать "_"? _Чтобы визуальное зашумление _кода повысить?
Я написал компилятор на C++ при помощи LLVM