Пока индустрия движется в сторону усложнения компиляторов, я задался вопросом: можно ли создать инструмент, который дает безопасность Rust, гибкость C и при этом не весит сотни мегабайт?
Так появился Flame — системный язык с фронтэндом в 260 КБ, который реализует управление памятью через статический анализ AST и предлагает альтернативный взгляд на обработку ошибок через патчинг дерева токенов.
Почему это не «очередной клон Си»?
Основные проблемы системных языков сегодня — это либо риск утечек (C/C++), либо избыточная сложность анализаторов (Rust). В Flame я реализовал три концепции, которые решают эти задачи иначе:
1. Автоудаление помеченных переменных в куче
Вместо сложного Borrow Checker как в Rust, Flame использует статический анализ AST по специальному алгоритму и вставляет инструкцию удаления. Алгоритм работает так:
Компилятор сканирует с конца;
Когда находит инструкцию, где использованная помеченная
autodel— вставляет после этой инструкции удаление;
Циклы считаются как одна инструкция, если переменная используется в последний раз в цикле и объявлен вне цикла — удалится после цикла. Если объявлена внутри цикла — удалится внутри цикла. А ветвления в виде if/else проверяются, и вставляется удаление в каждый блок где использована в последний раз. При передачи переменной по возврату функции или метода — наследуется autodel, но если при передаче используется только значение а не сам адрес — то создастся промежуточная переменная, потом вставится удаление, потом промежуточная передастся в возврат. Данная возможность не до конца проверена, и будет укрепляться и развиваться.
2. Meta-exceptions: патчинг токенов
Вместо тяжёлых try/catch, Flame полуил свой подход. В языке исключение описывается в специальной структуре, где объявляются: места где может появиться исключение, и проверка/замена. Когда компилятор видит последовательность токенов которые описаны каким то исключением — он обращается к этой структуре, и либо откатывается до завершаения прошлой инструкции (';' или '{' или '}'), и вставляет проверку, либо полностью вырезает эту инструкцию и вставляет замену (при описании замены можно вставлять исходную инструкцию с помощью $source). Это позволяет исключить необходимость try/catch и не засорять код if/else, но сохранить возможность перехватывать любые исключения.
Устройство компилятора
Компилятор (по словам GitHub) содержит: 83.6% кода на Си, и 16.4% Shell и LLVM (отладка и тестирование). Сам фронтэнд весит 260 КБ.
В процесс компиляции входят несколько этапов:
1. Препроцессор (#include, #ifdef и прочее)
2. Лексер (разбирает текст на токены)
3. Препарсер (работает с исключениями)
4. Парсер (разбирает токены на AST)
5. Прегенератор (работает с безопасностью памяти)
6. Генератор (разбирает AST на LLVM IR и компилирует в.o, линкует через GCC)
Cсылка на GitHub с исходным кодом
Примеры кода
В основном синтаксис один в один как в Си, но переменные и поля классов объявляются с ключевым словом var, а методы и функции через func.
Прмер объявления класса:
class Name { var int a; //числа имеют свой суффикс типа: 5 - int; 5s - short; 5l - long; var short b = 55s; func void c(int ab, int bb) { self->a = ab + bb; } new() { //конструктор self->a = 5 + 5; } delete() { //деструктор self->a = 0; } }
Пример автоудаления:
func int main() { var autodel Name *obj = new(); obj->c(5, 5); // <- тут будет delete() obj; return 0; }
Пример описания исключений:
exception DivByZero { var int op; instruction { %n / %:op //%n - любое число, %i - идентификатор } //%k - ключевое слово, %: - присваивание значения в переменную check { //есть replace, он полностью заменяет инструкцию на то что if (op == 0) { //op инициализируется в коде автоматически //... } } }
Остальные можно найти в README.md в GitHub
Будущее Flame
Flame будет активно разваиваться, улучшать совместимость с другими языками (через настройки компилятора @callconv, который позволит менять последовательность регистров для вызова функции) и добавлять новые фичи (Hot Reload или Restart через функцию из стандартной библеотеки которая через общую память и родительский процесс прогонит данные в новый процесс через таблицу имён). Скоро выйдет документация по языку.
Заключение
Flame — это язык, где я реализовал концепцию язык, который удобный, но мощный системный язык. Язык решает проблемы которые есть в других языках: перегруженность, сложность, медлительность, опасность. Но я не могу сказать, что язык не имеет минусов. Впереди ещё много работ над стабильностью и развитием!
Комментарии (56)

Nagdiel
07.03.2026 17:56Чем autodel семантически отличается от использования std::uniue_ptr из С++? Как реализовано разделяемое владение ресурсами?

mentin
07.03.2026 17:56Одно отличие я заметил, в С++ объект освобождается по выходу из области видимости, а тут иногда раньше, по месту "последнего" использования. Это память в каких-то случаях экономит, но редко. Зато ломает RAII паттерн.
Но разница как-то явно не достаточная чтобы избежать нормального borrow checker. А что компилятор делает если этот указатель передается в другие функции из статьи совсем не ясно.

rsashka
07.03.2026 17:56borrow checker и RAII, это разные концепции

mentin
07.03.2026 17:56Выше это и не утверждалось, лишь что небольшая смена семантики не позволит избежать borrow checker'а (при декларируемой memory safety).

rsashka
07.03.2026 17:56Выше это и не утверждалось ...
Я вами согласен. Более того, мой комментарий наверно следует адресовать автору, а не вам, так как именно автор связал данные термины.

willy2312 Автор
07.03.2026 17:56У Flame своя концепция безопасности, удаление просиходит после последнего использования, а не при выходе из scope. Это снижает нагрузку на программиста.
Если передать указатель в аргументы функций, удалится после выходе из функции

kotan-11
07.03.2026 17:56Если мы говорим об языке с автоматическим управлением временем жизни объектов, сразу становится интересно, как он справляется вот с этим бенчмарком, который как раз это и тестирует: https://habr.com/en/articles/955158/

willy2312 Автор
07.03.2026 17:56Я поленился, и решил просто дать ИИ искходники языка, и скинуть эту статью, вот что выдал:
Это как раз то, для чего Flame делался.
var Node* node = new Node()— autodel вставитdeleteавтоматически после последнего использования. Иерархия владения выражается естественно: документ владеет карточками, карточки — элементами. Деструкторы вызываются по цепочке. Это уже работает.Неизменяемые ресурсы (стили, битмапы) — copy-on-write
Здесь Flame пока ничего специального не предлагает. Copy-on-write придётся писать руками — завернуть стиль в структуру со счётчиком ссылок и клонировать при изменении. Это решаемо, но языковой поддержки нет. Можно сделать через
notdelуказатели для "слабого" хранения общих ресурсов.Перекрёстные ссылки с автообрывом
Вот здесь пока больная точка. В Flame нет слабых ссылок в смысле
weak_ptr. Коннектор, ссылающийся на другой элемент, получит висячий указатель после удаления цели. Нужно будет либо добавить в язык концепциюweak*, либо делать это через явный реестр/ID и проверку.Удаление карточки из метода элемента этой карточки
autodel здесь может конфликтовать — если
selfудаляется в процессе выполнения метода, это UB. Это общая проблема даже в C++. Flame наследует её.Копирование с сохранением топологии
Нужен явный "deep copy с remapping" — обойти дерево, создать копии, построить таблицу
старый указатель → новый, пройти заново и перешить ссылки. Языковой магии нет, но класс сnew-конструктором это выразит чисто.
С недавнего времени autodel автоматический, его писать не надо. Можно написать notdel чтобы предотавратить удаление компилятором...

vsting
07.03.2026 17:56А зачем добавлять суффикс там где вы уже явно указали что это short? Я понимаю в Java там суффиксы или префиксы но там они указываются в приведениях или там где явно не видно что это за тип. Но зачем вам в явном указаннии типа еще и суффикс добавлять и ладно бы это было название переменной но тут аж к значению да еще и в объявлении.

willy2312 Автор
07.03.2026 17:56Ну это облегчает компилятор, например когда передаётся в агументы, нужно будет искать какой там аргумент и подставлять тип short. Вместо этого пока что указывается суффикс s. Это может быть в дальнейшем удалено, так же как и var и func для объявлений. Это только для облегчения компилятора в стадии ранней разработки.

vsting
07.03.2026 17:56И зачем точка с запятой опять, когда ее можно сделать не обязательной? Например в некоторых языках она пишется только если нужно несколько коротких конструкций записать в одну строку, что бы компилятор понял что это разные конструкции.

willy2312 Автор
07.03.2026 17:56Потому что check {} в исключении опирается на ; как завершение прошлой инструкции. Это может временно, со временем точки с запятой могут стать необязательным.

mentin
07.03.2026 17:56А какой нибудь практический пример использования таких исключений есть? Не очень понятно, что в примере с делением на ноль можно написать внутри
if (op == 0), кромеterminate(), а это не совсем то что обычно понимают под обработкой исключений. Ну может быть альтернативно установить какой-нибудь глобальный флаг ошибки, вернуть какую-нибудь ерунду, и продолжить вычисления? Тоже так себе идея. Да и сделать оба варианта можно эффективнее без лишнихifчерез системные исключения.
mentin
07.03.2026 17:56Я бы даже сказал, что возможно стоит это позиционировать как механизм макросов, на уровне AST, может быть интересным, хотя видны и проблемы. Но это не про исключения.

willy2312 Автор
07.03.2026 17:56Нет, отличие от других макросов - это то, что вносятся правки на уровне токенов, а не AST (если в тексте написано что это в AST, извиняюсь, статья была отправлена на модерацию 2 недели назад, с тех пор многое изменилось). Это позволяет менять грамматику гибко, например:
exception Repeat { var int count; instruction { repeat %n:count } replace { for (int i = 0; i < count; i++) } } // Этого в грамматике нет, но исключение позволяет так писать repeat 5 { //это превратится в for цикл log("Hello"); }Это называются исключениями, потому что изначально задумывалось как автоматическое выявление опасных мест и вставка проверки...

ProMix
07.03.2026 17:56И в чём отличия от препроцессора?

willy2312 Автор
07.03.2026 17:56В отличии от препроцессора, исключения могут и вставлять проверки, захватывать значения, и проводить с ними действия. Если написать
#define, препроцессор заменит всё что совпадает, не проверяя последующие или предыдущие токены
ProMix
07.03.2026 17:56Можно пример?

willy2312 Автор
07.03.2026 17:56Вот пример:
exception DivByZero { int op; instruction { %n / %"("^")":op } check { if (op == 0) { out("FATAL"); exit(1); } } } int main() { // Здесь вставится: //int op = (6 - 6); //if (op == 0) { // out("FATAL"); // exit(1); //} int a = 5 / op; return 0; }Насколько я знаю, так препроцессор не умеет

VyacheslavHere
07.03.2026 17:56Интересная задумка с мемори сейфети, но к сожалению в ней куча подводных камней. Например: что если я создам переменную, передам значение в поле структуры, а затем последний раз использую переменную в цикле? Когда "произойдет" удаление? Как такое обрабатывает компилятор?

willy2312 Автор
07.03.2026 17:56Удаление будет после выхода из цикла. Если передать значение через разыменовывание - это не изменит поведение. А если передать в поле структуры указатель... то autodel сработает криво. Спасибо за замечание, можете писать такое в issues в GitHub. Поработаю над этим!

Martianov
07.03.2026 17:56Как язык разрешает проблему циклической зависимости во избежание утечек? Или модель языка не позволяет такое городить? Я так и не понял)

willy2312 Автор
07.03.2026 17:56autodel на такое не расчитан, если говорить про висячую ссылку. В будущем что нибудь придумаю, спасибо за замечание! Но в Flame это не проблема потому что
autodelне считает ссылки — он просто вызываетdeleteпо области видимости.Bудалится, потомAудалится. Висячий указатель внутри уже удалённогоB— это проблема программиста, но утечки не будет.
rsashka
07.03.2026 17:56autodel на такое не расчитан, если говорить про висячую ссылку. В будущем что нибудь придумаю, ....
Циклические зависимости и висячие ссылки, это не одно и тоже. Циклические (перекрестные) ссылки не висят, так как ими владеют валидные объекты. И если про это не думать на этапе проектирования языка, а оставить как проблему для программиста, то вы быстро скатитесь к джентльменским соглашениям Rust, которые циклические ссылки решили не считать ошибкой работы с памятью.

willy2312 Автор
07.03.2026 17:56Имел в виду что если поле структуры которая ещё сама не удалена указывает на уже удалённый объект - поле останется висячим

kmatveev
07.03.2026 17:56В С утечка памяти - забыл вызвать free(), в Flame утечка памяти - забыл поставить autodel.
И ещё вопрос про такой сценарий: могу ли я аллоцировать массив на стеке, и потом пройтись в цикле по элементам, вызывая функцию, в которую передаю указатель на элемент, а в этой функции сделаю autodel, оно освободит память внутри?

willy2312 Автор
07.03.2026 17:56autodel работает только для переменных в куче. Но спасибо за замечание, логичнее сделать notdel чтоб говорить компилятору самому не удалять...

kmatveev
07.03.2026 17:56А как оно понимает, что память выделена в куче? В голову приходят только такие варианты: 1. нельзя получить указатель на что-то, выделенное на стеке 2. есть разные типы указателей.

willy2312 Автор
07.03.2026 17:56Ну, там в имени в начале есть символ звёздочки, а со звёздочкой аллоцировать в стеке нельзя через alloca().

asya_mozgoff
07.03.2026 17:56удачи бро в разработке, идея реально прикольная, было бы интересно наблюдать за разработкой в телеграм канале

lovvercases
07.03.2026 17:56Если мы объявляем класс который в конструкторе выделяет память через оператор new и присваивает его значение полю класса, освобождение ресурсов происходит в деструкторе считая его последним местом использования переменной? До деструктура в последнем месте использования переменной или где?

willy2312 Автор
07.03.2026 17:56Нет, поля класса нужно вручную чистить в деструкуторе. Пока что, автоудаляемые поля не изобретены, но скоро будут

lovvercases
07.03.2026 17:56Как эксепшены работают в следующих случаях:
Происходит выделение памяти в конструкторе: operator new выбрасывает исключение(закончилась память для выделения) откат происходит во внешний для конструктора скоуп?Дальше мы будем пытаться по второму разу вызвать тот же конструктор класса? Можем ли мы что то захватить в эксепшен из внешнего скоупа для корректного завершения программы? (Условно освободить всю выделенную память). Или будем выделять память с надеждой что ос предоставит свободное место для выделения?
Что если идёт создание массива объектов через new в цикле и происходит исключение new? Оператор new в цикле будет заменена на инструкции описанные в exception? Получается массив будет незаполненный до конца и не валиден? Как вообще язык собирается решать эксепшены рантайма?

willy2312 Автор
07.03.2026 17:56Нет, это сегфолт. В языке исключения пока созданы чтоб их предотвращать проверками, а не обрабатывать. И исключение вставляет и заменяет проверки ещё на этапе дерева токенов, поэтому будет исключение или не будет, если это местечко описано в исключении - проверка ставится или заменяется

lovvercases
07.03.2026 17:56Это не segmentation fault. Это исключение. Один из важнейших случаев. Segfault - когда вы обращайтесь к невалидной памяти или невалидным способом. Выделение памяти - валидный способ общения, но результат операции закончился не так как мы ожидали т.е оставил программу в невалидном состоянии - память не выделилась.

willy2312 Автор
07.03.2026 17:56Ну если new() не выделил память, указатель не валидный, и обращаться по нему - сегфолт. new() магическим образом не будет пытаться ещё раз пробовать выделение в памяти.

lovvercases
07.03.2026 17:56Т.е компилятор заранее вычисляет места где могут произойти исключения? Или все держится на том что программист сам опишет все возможные места в которых произойдут эксепшены? Это же нереально, особенно при росте кода. Ну вот у вас рядом с каждым делением будет проверка и ветвление. Оверхед на проверку и ветвление при каждом делении?
Или при каждом выделении памяти оверхед на проверку? Не догадаться же в какой момент закончится память. В итоге производительность страдает не кажется?

willy2312 Автор
07.03.2026 17:56Исключения будут описаны заранее в стандартной библеотеке, вручную ничего описывать не пртидётся. Если у вас очень много делений, то да, на каждое действие будет немногие оверхед на несколько примерно 20-35 тактов процессора. Но никто не обязывает пользоваться исключениями если вам нужна максимальная скорость. В теории это может компенсироваться оптимизациями LLVM, но я не проверял. Слышал что с оптимизациями прирост от 50% до 200%, не проверял

lovvercases
07.03.2026 17:56Вопрос все ещё остаётся открытым, собственно какую проверку для предотвращения возникновения исключения new? Как должны отработать инструкции при положительной проверки т.е это освобождение всей памяти программы или продолжим биться в new? И как инструкции в эксепшене корректно завершат программу? Т.е могут ли они получить ссылки на внешний контекст и вызвать деструкторы или delete для выделенных переменных, чтобы корректно отработать завершения программы?

willy2312 Автор
07.03.2026 17:56Для предотвращения исключения new(), можно целиком заменить инструкцию, примерно так:
replace { $source; if (!$2) { out("FATAL"); } }Программа при исключении завершится только если там такое прописано. Пока что в стандартной библеотеке нет одной такой функцией, поэтому пока завершать программу вне main() нужно вручную через ASM syscall или ExitProcess.
И в исключении можно писать всё что угодно, потому что на этапе вставки проверок и прочего пройден только синтаксический анализ, но ошибки вылезут при парсинге. Так как код вставляется - обработчик в исключении имеет доступ ко всему, что имеют остальные в этом скоупе.

lovvercases
07.03.2026 17:56И собственно, если эксепшен произойдет во вложенной функции ну например main() -> foo() -> bar() {
Код приводящий к ошибке и ветвлению для ее обработки
} -> foo() -> main(), как собственно в данной ситуации мы проверим валидность полученных данных? В bar() отработало исключение, но валидны ли данные что вернула bar в foo, а потом foo в main - очевидно нет. Но механизма перевыбросить ошибку во внешний скоуп нет. Или мы будем возвращать магическое число об ошибки в return внутри кода функции? (Спойлер - после каждого использования функции вы должны постоянно проверять не равен ли возвращаемый результат магическому числу ошибки), что если это число об шишибки было получено корректными вычислениями?

willy2312 Автор
07.03.2026 17:56Какого то значения для ошибки как например в Golang - нет. И вложенных функций и вложенных классов тоже нет, и не будут. Ну можно конечно сделать какой то соглашение, что число -123456 будет означать что завершилось что то ошибкой

guamoko995
07.03.2026 17:56Статья + комментарии выше производят впечатление, что простота и отсутствие концепции владения -- это прямое следствие того, что задачи, ради которых концепция живет (там, где она жива), тут "ещё не закрыты". Веет обещаниями, а есть намек, что результат достижим?

willy2312 Автор
07.03.2026 17:56Да, спустя время верится, что результат будет. Уже сейчас неплохие результаты. Не хватает сообщества, чтоб быстро выявлять баги и недочёты. Пока концепция справляется с любыми припятствами, в том числе и цикличных ссылок, но не уверен что на 100%....

Veyyr
07.03.2026 17:56Очередная попытка внедрить ООП в системный язык?

willy2312 Автор
07.03.2026 17:56Ну вообще язык делает упор не в ООП. Реализовано это все без оверхеда (по типу vtable), классы это те же структуры, методы это функции которые неявно получают объект как аргумент. new() и delete() - под капотом те же malloc/free, но вызывают конструктор или деструктуор (delete() автоматически выставляет указатель на null)
rsashka
Если весь вопрос в безопасной работы с памятью, то зачем делать для этого новый язык не совместимый ни с каким другим и не проще ли взять уже существующий (например С++) и дополнить его тем же самым статическим анализом AST?
V1tol
У меня есть немного другой вопрос. Если создавать новый язык, зачем опять вот эти вот * и -> ?
rsashka
С этим то все как раз понятно. Это широко распространенные операторы доступа по ссылке, которые понятны практически всем. И не нужно учиться использовать какие-то новые способы.
vsting
Современные языки уходят от таких Сишных штук. Почему не точка, она же корочке?
willy2312 Автор
Ну вообще я согласен, и в языке вроде как абсолюно без разницы что использовать, -> просто существует
ProMix
Потому что важно различать разыменование ссылки и прибавление смещения.
Возьмём, к примеру, такие два обращения к полю:
В первом случае к указателю на структуру прибавляется одно смещение, известное на этапе компиляции. Во втором случае четыре раза прибавляется смещение и по этому смещению вычитывается новый адрес. Кратная разница в количестве операций, плюс второй вариант может четыре раза упасть из-за некорректного значения указателя. И считывать всю эту информацию одним взглядом очень удобно
willy2312 Автор
В языке так и есть, там в парсере прямо так и написано, что "->" и "." делают одно и то же
willy2312 Автор
Ну вообще то язык совместимы с Си. И причниой того что С++ не стал основой языка - это раздутость. Я сам недолюлиавю С++, и в разработке я бы использовал С++ из-за ООП и шаблонов, но простота Си мне больше нравится. Можно сказать что за основу был взят Си, потому что синтаксис один в один (за исключением var и func для объявлений функций и переменных...