Комментарии 56
Если весь вопрос в безопасной работы с памятью, то зачем делать для этого новый язык не совместимый ни с каким другим и не проще ли взять уже существующий (например С++) и дополнить его тем же самым статическим анализом AST?
У меня есть немного другой вопрос. Если создавать новый язык, зачем опять вот эти вот * и -> ?
С этим то все как раз понятно. Это широко распространенные операторы доступа по ссылке, которые понятны практически всем. И не нужно учиться использовать какие-то новые способы.
Современные языки уходят от таких Сишных штук. Почему не точка, она же корочке?
Ну вообще я согласен, и в языке вроде как абсолюно без разницы что использовать, -> просто существует
Потому что важно различать разыменование ссылки и прибавление смещения.
Возьмём, к примеру, такие два обращения к полю:
struct.f1.f2.f3.value
...
struct->f1->f2->f3->valueВ первом случае к указателю на структуру прибавляется одно смещение, известное на этапе компиляции. Во втором случае четыре раза прибавляется смещение и по этому смещению вычитывается новый адрес. Кратная разница в количестве операций, плюс второй вариант может четыре раза упасть из-за некорректного значения указателя. И считывать всю эту информацию одним взглядом очень удобно
Ну вообще то язык совместимы с Си. И причниой того что С++ не стал основой языка - это раздутость. Я сам недолюлиавю С++, и в разработке я бы использовал С++ из-за ООП и шаблонов, но простота Си мне больше нравится. Можно сказать что за основу был взят Си, потому что синтаксис один в один (за исключением var и func для объявлений функций и переменных...
Чем autodel семантически отличается от использования std::uniue_ptr из С++? Как реализовано разделяемое владение ресурсами?
Одно отличие я заметил, в С++ объект освобождается по выходу из области видимости, а тут иногда раньше, по месту "последнего" использования. Это память в каких-то случаях экономит, но редко. Зато ломает RAII паттерн.
Но разница как-то явно не достаточная чтобы избежать нормального borrow checker. А что компилятор делает если этот указатель передается в другие функции из статьи совсем не ясно.
borrow checker и RAII, это разные концепции
У Flame своя концепция безопасности, удаление просиходит после последнего использования, а не при выходе из scope. Это снижает нагрузку на программиста.
Если передать указатель в аргументы функций, удалится после выходе из функции
Если мы говорим об языке с автоматическим управлением временем жизни объектов, сразу становится интересно, как он справляется вот с этим бенчмарком, который как раз это и тестирует: https://habr.com/en/articles/955158/
Я поленился, и решил просто дать ИИ искходники языка, и скинуть эту статью, вот что выдал:
Это как раз то, для чего 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 чтобы предотавратить удаление компилятором...
А зачем добавлять суффикс там где вы уже явно указали что это short? Я понимаю в Java там суффиксы или префиксы но там они указываются в приведениях или там где явно не видно что это за тип. Но зачем вам в явном указаннии типа еще и суффикс добавлять и ладно бы это было название переменной но тут аж к значению да еще и в объявлении.
Ну это облегчает компилятор, например когда передаётся в агументы, нужно будет искать какой там аргумент и подставлять тип short. Вместо этого пока что указывается суффикс s. Это может быть в дальнейшем удалено, так же как и var и func для объявлений. Это только для облегчения компилятора в стадии ранней разработки.
И зачем точка с запятой опять, когда ее можно сделать не обязательной? Например в некоторых языках она пишется только если нужно несколько коротких конструкций записать в одну строку, что бы компилятор понял что это разные конструкции.
А какой нибудь практический пример использования таких исключений есть? Не очень понятно, что в примере с делением на ноль можно написать внутри if (op == 0), кроме terminate(), а это не совсем то что обычно понимают под обработкой исключений. Ну может быть альтернативно установить какой-нибудь глобальный флаг ошибки, вернуть какую-нибудь ерунду, и продолжить вычисления? Тоже так себе идея. Да и сделать оба варианта можно эффективнее без лишних if через системные исключения.
Я бы даже сказал, что возможно стоит это позиционировать как механизм макросов, на уровне AST, может быть интересным, хотя видны и проблемы. Но это не про исключения.
Нет, отличие от других макросов - это то, что вносятся правки на уровне токенов, а не 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");
}Это называются исключениями, потому что изначально задумывалось как автоматическое выявление опасных мест и вставка проверки...
И в чём отличия от препроцессора?
В отличии от препроцессора, исключения могут и вставлять проверки, захватывать значения, и проводить с ними действия. Если написать #define, препроцессор заменит всё что совпадает, не проверяя последующие или предыдущие токены
Можно пример?
Интересная задумка с мемори сейфети, но к сожалению в ней куча подводных камней. Например: что если я создам переменную, передам значение в поле структуры, а затем последний раз использую переменную в цикле? Когда "произойдет" удаление? Как такое обрабатывает компилятор?
Как язык разрешает проблему циклической зависимости во избежание утечек? Или модель языка не позволяет такое городить? Я так и не понял)
autodel на такое не расчитан, если говорить про висячую ссылку. В будущем что нибудь придумаю, спасибо за замечание! Но в Flame это не проблема потому что autodel не считает ссылки — он просто вызывает delete по области видимости. B удалится, потом A удалится. Висячий указатель внутри уже удалённого B — это проблема программиста, но утечки не будет.
autodel на такое не расчитан, если говорить про висячую ссылку. В будущем что нибудь придумаю, ....
Циклические зависимости и висячие ссылки, это не одно и тоже. Циклические (перекрестные) ссылки не висят, так как ими владеют валидные объекты. И если про это не думать на этапе проектирования языка, а оставить как проблему для программиста, то вы быстро скатитесь к джентльменским соглашениям Rust, которые циклические ссылки решили не считать ошибкой работы с памятью.
В С утечка памяти - забыл вызвать free(), в Flame утечка памяти - забыл поставить autodel.
И ещё вопрос про такой сценарий: могу ли я аллоцировать массив на стеке, и потом пройтись в цикле по элементам, вызывая функцию, в которую передаю указатель на элемент, а в этой функции сделаю autodel, оно освободит память внутри?
autodel работает только для переменных в куче. Но спасибо за замечание, логичнее сделать notdel чтоб говорить компилятору самому не удалять...
удачи бро в разработке, идея реально прикольная, было бы интересно наблюдать за разработкой в телеграм канале
Если мы объявляем класс который в конструкторе выделяет память через оператор new и присваивает его значение полю класса, освобождение ресурсов происходит в деструкторе считая его последним местом использования переменной? До деструктура в последнем месте использования переменной или где?
Нет, поля класса нужно вручную чистить в деструкуторе. Пока что, автоудаляемые поля не изобретены, но скоро будут
Как эксепшены работают в следующих случаях:
Происходит выделение памяти в конструкторе: operator new выбрасывает исключение(закончилась память для выделения) откат происходит во внешний для конструктора скоуп?Дальше мы будем пытаться по второму разу вызвать тот же конструктор класса? Можем ли мы что то захватить в эксепшен из внешнего скоупа для корректного завершения программы? (Условно освободить всю выделенную память). Или будем выделять память с надеждой что ос предоставит свободное место для выделения?
Что если идёт создание массива объектов через new в цикле и происходит исключение new? Оператор new в цикле будет заменена на инструкции описанные в exception? Получается массив будет незаполненный до конца и не валиден? Как вообще язык собирается решать эксепшены рантайма?
Нет, это сегфолт. В языке исключения пока созданы чтоб их предотвращать проверками, а не обрабатывать. И исключение вставляет и заменяет проверки ещё на этапе дерева токенов, поэтому будет исключение или не будет, если это местечко описано в исключении - проверка ставится или заменяется
Это не segmentation fault. Это исключение. Один из важнейших случаев. Segfault - когда вы обращайтесь к невалидной памяти или невалидным способом. Выделение памяти - валидный способ общения, но результат операции закончился не так как мы ожидали т.е оставил программу в невалидном состоянии - память не выделилась.
Т.е компилятор заранее вычисляет места где могут произойти исключения? Или все держится на том что программист сам опишет все возможные места в которых произойдут эксепшены? Это же нереально, особенно при росте кода. Ну вот у вас рядом с каждым делением будет проверка и ветвление. Оверхед на проверку и ветвление при каждом делении?
Или при каждом выделении памяти оверхед на проверку? Не догадаться же в какой момент закончится память. В итоге производительность страдает не кажется?
Исключения будут описаны заранее в стандартной библеотеке, вручную ничего описывать не пртидётся. Если у вас очень много делений, то да, на каждое действие будет немногие оверхед на несколько примерно 20-35 тактов процессора. Но никто не обязывает пользоваться исключениями если вам нужна максимальная скорость. В теории это может компенсироваться оптимизациями LLVM, но я не проверял. Слышал что с оптимизациями прирост от 50% до 200%, не проверял
Вопрос все ещё остаётся открытым, собственно какую проверку для предотвращения возникновения исключения new? Как должны отработать инструкции при положительной проверки т.е это освобождение всей памяти программы или продолжим биться в new? И как инструкции в эксепшене корректно завершат программу? Т.е могут ли они получить ссылки на внешний контекст и вызвать деструкторы или delete для выделенных переменных, чтобы корректно отработать завершения программы?
Для предотвращения исключения new(), можно целиком заменить инструкцию, примерно так:
replace {
$source;
if (!$2) {
out("FATAL");
}
}Программа при исключении завершится только если там такое прописано. Пока что в стандартной библеотеке нет одной такой функцией, поэтому пока завершать программу вне main() нужно вручную через ASM syscall или ExitProcess.
И в исключении можно писать всё что угодно, потому что на этапе вставки проверок и прочего пройден только синтаксический анализ, но ошибки вылезут при парсинге. Так как код вставляется - обработчик в исключении имеет доступ ко всему, что имеют остальные в этом скоупе.
И собственно, если эксепшен произойдет во вложенной функции ну например main() -> foo() -> bar() {
Код приводящий к ошибке и ветвлению для ее обработки
} -> foo() -> main(), как собственно в данной ситуации мы проверим валидность полученных данных? В bar() отработало исключение, но валидны ли данные что вернула bar в foo, а потом foo в main - очевидно нет. Но механизма перевыбросить ошибку во внешний скоуп нет. Или мы будем возвращать магическое число об ошибки в return внутри кода функции? (Спойлер - после каждого использования функции вы должны постоянно проверять не равен ли возвращаемый результат магическому числу ошибки), что если это число об шишибки было получено корректными вычислениями?
Статья + комментарии выше производят впечатление, что простота и отсутствие концепции владения -- это прямое следствие того, что задачи, ради которых концепция живет (там, где она жива), тут "ещё не закрыты". Веет обещаниями, а есть намек, что результат достижим?
Очередная попытка внедрить ООП в системный язык?
Ну вообще язык делает упор не в ООП. Реализовано это все без оверхеда (по типу vtable), классы это те же структуры, методы это функции которые неявно получают объект как аргумент. new() и delete() - под капотом те же malloc/free, но вызывают конструктор или деструктуор (delete() автоматически выставляет указатель на null)

Flame: Системный язык программирования на C и LLVM с мета-исключениями и Memory Safety без Borrow Checker