- - -
Всем привет,
На данный момент я работаю над своим языком программирования (Системный язык, Plasm, но сейчас не об этом). Пока я работал над HIR->MIR->LLVM-IR преобразованиями, я начал задумываться о фундаментальной асимметрии в том как мы проектируем языки.
Практически во всех языках программирования у нас есть следующие определяемые пользователем категории первого порядка:
* Данные: структуры, классы, перечисления.
* Функции: вход->выход логика.
* Переменные: хранение и псевдонимы привязок.
* Операторы: Мы можем переопределять поведение
<<, +, ==.Однако операторы управления потоком (такие как if-elif-else, do-while, for-in, switch-case) строго зашиты в семантику языков. Другими словами, вы не можете переопределить, например, понятие "цикл" на фундаментальном уровне.
Вы можете возразить: "На самом деле мы можем это все делать в Swift/Kotlin/Scala".
Хотя эти языки позволяют использовать синтаксис, который выглядит словно кастомные операторы управления потоком, это на самом деле просто синтаксический сахар вокруг обычных функций, и под капотом они все равно опираются на жестко зашитые примитивы (вроде while или if).
Например, в Swift
@autoclosure позволяет нам передать условие как неявное замыкание, а блок кода мы можем передать как замыкание, написанное вне списка аргументов. Это выглядит симпатично, но внутренне это просто обертка вокруг стандартного цикла:func until(_ condition: @autoclosure () -> Bool, do action: () -> Void) {
while !condition() {
action()
}
}
var i = 0
until(i == 5) {
print("Iter \(i)")
i += 1
}
Хотя подобные трюки можно проделать и в Kotlin (через
inline в функции) или в Scala (через : => синтаксис), это всегда просто абстрагирование уже имеющихся примитивов.У меня есть вот такая фантазия: "Что если вместо синтаксического сахара мы введем новую категорию
flow первого порядка?"Это могла бы быть, например, конструкция со специфичными синтаксическими правилами, которые позволили бы нам описать любой оператор управления потоком через явное определение как он будет транслироваться в LLVM CFG (Граф управления потоком). Это вовсе не означает, что мы должны позволять пользователю открыто использовать сырые
goto операторы везде, это скорее про структурированный способ определить как совершаются прыжки между блоками кода.Представьте, что вы можете определить цикл
while не просто как ключевое слово языка, а как импортируемую структуру с внутренней реализацией через более простые фундаментальные сущности с возможностью их прочитать через "Go to definition", которая явно определяет точки входа, выхода и логику переходов.Это все приводит меня к нескольким вопросам, которые мне бы хотелось тут обсудить:
1. Реально ли определить такой набор правил описания потока, который был бы достаточно гибким для описания таких сложных операторов как pattern matching? (обработка именованных привязок, множества путей выхода и структурное сопоставление с compile time гарантиями)?
2. Можем ли мы описать
async/await/yield полностью как кастомные операторы потока? Когда мы делаем if/else, мы производим прыжки между двумя локальными блоками кода, а когда мы делаем await, мы по сути тоже делаем прыжок, просто не локально, а за пределы текущего контекста функции (в событийный цикл или планировщик) и объявляем точку возврата. Вместо того чтобы рассматривать асинхронность как преобразование конечного автомата, жестко заданное в компиляторе, можем ли мы определить асинхронность просто как межконтекстные прыжки?Кто нибудь пытался создать строго-типизированные, определяемые пользователем операторы управления потоком, которые строго ложатся в узлы CFG, а не как макросы/замыкания/сахар? Буду рад почитать критику, референсы к существующим попыткам или причины почему это ужаснейшая идея:)
- - -
В принципе на все эти вопросы мы нашли ответы, но если кто то захочет написать свои идеи в комментах, то я буду рад их почитать;)