В 2025-м году язык Ü продолжил своё развитие. Сам язык был заметно улучшен, был исправлен ряд ошибок, существенно прибавила в объёме его стандартная библиотека а также инфраструктура языка заметно обогатилась. В данной статье я хотел бы рассказать, что было сделано и что изменилось.
Сборочная система Ü
Написание сборочной системы Ü началось ещё в конце 2024-го года, но более-менее законченный вид она приобрела только в 2025-м году. Посему целесообразно будет её описать в данной статье.
Сначала коротко поясню, что это такое и зачем это нужно. Долгое время существовал только компилятор Ü. Его использование не отличается особо от компиляторов многих других языков (например C++) - компилятору передаются пути к файлам исходного кода, директории включений, опции оптимизации, тип выходного файла и т. д. Компилятор выполняет работу по сборке согласно переданным опциям - ни больше и ни меньше. Но для комфортной разработки одного компилятора мало. Нужно, например, выполнять такие важные вещи как сборка программ, состоящих из более чем одного файла исходного кода, подключение зависимостей, генерация кода внешними скриптами, инкрементальная сборка и т. д. Поэтому во многих языках существуют надстройки над их компилятором, как например CMake для C++. Сборочная система Ü является подобной надстройкой.
Технически сборочная система - это отдельный исполняемый файл. Пользователь должен взаимодействовать только с нею (а не с компилятором напрямую). Она сама вызывает исполняемый файл компилятора, когда и как надо, пользователю не надо об этом заботиться.
Для того, чтобы указать, что нужно собирать, пользователь должен написать специальный файл по имени build.u, который описывает проект. В отличие от других систем сборки, где существует специальный отдельный язык для описания проектов, в Ü с этим проще - сам же язык Ü используется для этих целей, так что файл описания проекта это обычный исходник на Ü. В данном файле должна присутствовать функция GetPackageInfo, которая возвращает описание проекта в декларативном виде. Формат описания задаётся сборочной системой - она предоставляет включаемые файлы, в которых объявлены структуры результата, которая должна возвращать функция GetPackageInfo. Сборочная система компилирует этот файл в нативную разделяемую библиотеку, загружает её и вызывает вышеназванную функцию, тем самым получая описание проекта и далее может собрать проект должным образом.
Проект состоит из нуля или более сборочных целей. С��орочная цель имеет тип - библиотека, исполняемый файл, объектный файл, нативная разделяемая библиотека. Обычно у сборочной цели присутствует список файлов исходного кода (которые компилируются). Также могут быть указаны директории со включаемыми файлами.
Одни сборочные цели могут зависеть от других. Например, исполняемый файл может зависеть от цели-библиотеки или даже от нескольких библиотек. Библиотеки тоже могут зависеть от других библиотек. Зависимости бывают публичным и внутренними. Публичные зависимости транзитивно наследуются.
Существует возможность указать зависимости от внешних библиотек и объектных файлов. Обычно такое нужно для компоновки с системными библиотеками. Но это может быть полезно также для случаев, когда используется какая-либо библиотека, собираемая не сборочной системой Ü, например, если она написана на C++.
Кроме сборочных целей существуют особые сборочные шаги - команды, которые на основе указанных входных файлов создают выходные файлы. В том числе возможно использование особых сборочных шагов для генерации файлов исходного кода и/или включаемых файлов. Сборочная система умна и умеет запускать особые сборочные шаги в нужном порядке - дабы шаг, требующий некий файл, запускался после другого шага, генерирующего этот файл.
Сборочная система Ü поддерживает пакеты. Пакет определяется файлом build.u в файловой системе. Один пакет может указывать зависимость от других пакетов. Созданы пакеты главным образом для случаев вроде библиотек, которые потенциально могут быть использованы более чем в одном проекте. Но пока что Ü не обладает централизованным репозиторием пакетов и пока не понятно, насколько таковой нужен.
Сборочная система Ü поддерживает указание целевой платформы, что может означать в том числе и кросс-компиляцию. При этом возможна и сборка некоторых пакетов под хост-систему, что может быть необходимо для сборки вспомогательных инструментов, использующихся для сборки под целевую систему (через особые сборочные шаги).
Само собою разуется, что сборочная система Ü осуществляет инкрементальную сборку (пересобирается только то, что изменилось). Также сборочная система Ü осуществляет то, что в C++ называется оптимизацией времени компоновки - исполняемый код целевого процессора генерируется только при генерации итоговых файлов с прогоном оптимизации промежуточного кода перед этим. Такой подход может несколько замедлять инкрементальную сборку на больших проектах, но я не считаю это проблемой - скоростью сборки не страшно пожертвовать ради итоговой скорости выполнения собранного кода.
Существуют различные типы сборок, в соответствии с которыми выбираются опции компилятора. Существуют типы сборки debug, release, min_size_release. Кроме того существуют опции для изменения некоторых параметров соответствующих этим типам (можно release собирать в режиме O2 или O3).
Ещё сборочная система имеет команду для сборки программ, состоящих из одного исходного файла и не имеющих зависимостей кроме стандартной библиотеки. Данная команда полезна для сборки чего-то простого. Примеры использования Ü теперь используют эту команду.
Сборочная система Ü теперь используется для сбор��и самой себя. Также она используется для сборки частей Компилятора1 (с фронтендом, написанным на Ü).
Сборочная система также может в некоторой степени взаимодействовать с языковым сервером. Она создаёт специальный файл, в котором описывается, какие директории каким образом должны использоваться для импорта. Языковой сервер читает данный файл и может должным образом разрешать импорты.
Расширение документации
Долгое время существовала документация только для самого языка Ü. Это было хорошо, но не достаточно. Особенно недостаточным это положение стало после написания сборочной системы. Посему была написана документация для самого компилятора, для сборочной системы, языкового сервера и преобразователя заголовочных файлов C++. Она не так чтобы очень подробная, но даёт возможность примерно понять что и как надо использовать.
nodiscard типы и функции
В Ü были введены структуры/классы с nodiscard флагом. nodiscard типами считаются структуры/классы с этим флагом а также массивы и кортежи, их содержащие. Значение такого типа (например, результат вызова функции) не может быть проигнорировано и должно быть использовано. В стандартной библиотеке контейнер result был сделан nodiscard типом.
Чуть позже был ещё добавлен флаг nodiscard для функций. Результат вызова таких функций тоже нельзя игнорировать. Отдельный флаг для функций необходим, если тип результата сам по себе не может быть сделан nodiscard, например, если это фундаментальный тип.
Доступ ко внешним функциям и переменным
В ходе написания сборочной системы возникла необходимость взаимодействия со внешним C кодом (с системными API). В целом то это и раньше работало, но вскрылся ряд проблем. Например, оказалось, что нельзя из Ü вызывать внешние функции, имена которых не являются валидными идентификаторами в Ü, например, если они являются ключевыми словами или начинаются с _. Также оказалось, что иногда надо получать доступ ко внешним глобальным переменным, что в Ü оказалось невозможным.
Решением вышеописанных проблем стало введение в язык операторов доступа ко внешним символам, которые начинаются с import fn и import var. В этих операторах указывается тип символа, к которому нужно получить доступ и имя в виде строки (что позволяет использовать произвольное имя, а не только валидный идентификатор).
Пример использования этих операторов:
// Получаем доступ к функции glibc, которая возвращает адрес переменной errno.
auto f= import fn</ fn() unsafe call_conv( "C" ) : $(i32) /> ( "__errno_location" );
// Получаем доступ к глобальной переменной "environ" из glibc.
auto env= import var</ $($(char8)) />( "environ" );
Собираемая стандартная библиотека
Долгое время стандартная библиотека состояла только из импортируемых файлов и тем самым не нуждалась в сборке. Это было даже логично, ибо она состояла в основном только из кода шаблонных контейнеров и ряда вспомогательных функций.
Но уже долгое время я хотел реализовать в стандартной библиотеке функционал для взаимодействия с операционной системой, вроде работы с файлами, переменными окружения, создания потоков и примитивов синхронизации и т. д. Меня от это удерживал тот факт, что добавление такого функционала требует наличия типов и прототипов функций, которые зависят от целевой системы, но делать доступными эти объявления во включаемых файлах мне не хотелось. Спас бы ситуацию перенос этих объявлений в файлы, которые пользователю не доступны (он их не включает) - по сути в отдельные файлы, которые требуют сборки. Но этого я не хотел делать, ибо это бы усложнило использование стандартной библиотеки, т. к. пользователю надо было бы как-то организовывать сборку этих файлов самостоятельно.
С написанием системы сборки вышеописанная ситуация изменилась. Теперь пользователь не вызывает компилятор Ü напрямую, а вместо этого делегирует это сборочной системе. Сборочная система может взять на себя ответственность за сборку стандартной библиотеки. Это то я и реализовал. В стандартной библиотеке были выделены некоторые файлы, которые требуют сборки. Сборочная система собирает эти файлы и компонует код из них с другими сборочными целями.
Когда стало возможным делать файлы стандартной библиотеки собираемыми, я воспользовался этой возможностью чтобы реализовать больше функционала. Например, было добавлено больше функций преобразования между строками и целыми числами. Также были добавлены функции для работы с UTF-8 и UTF-16.
Работа с файловой системой
В стандартную библиотеку Ü были добавлены классы файлов - для их чтения и записи. Также были добавлены вспомогательные функции для чтения/записи файлов целиком. Кроме того реализованы другие функции для работы с файловой системой, вроде создания/удаления/чтения директорий, получения метаданных, запроса текущей директории и т. д.
Внутри стандартной библиотеки работа с файловой системой реализована системозависимым способом. Есть отдельная реализация под Windows (через WinAPI) и под Unix. Некоторый функционал даже на разных версиях Unix реализован немного по-разному (вроде копирования файлов).
Также были реализованы вспомогательные функции для манипуляции с путями. Этот функционал не взаимодействует напрямую с системой, но он реализован системозависимым способом (под Windows и Unix формат путей различен).
Потоки, примитивы синхронизации и атомарные переменные
Ранее в стандартной библиотеке Ü были реализованы класс потока и контейнеры на основе rw_lock. Но они использовали функции библиотеки pthread напрямую и были посему непереносимыми. После введения поддержки компилируемых файлов стандартной библиотеки стало возможным это дело улучшить. Я реализовал класс потока системозависимым образом - через потоки WinAPI под Windows и через pthread на остальных системах.
Потоки это хорошо, но мало. Поэтому я ещё реализовал примитивы синхронизации, такие как mutex (точнее говоря, контейнер, содержащий mutex внутри), condition variable, семафор, барьер, атомарные переменные (в качестве высокоуровнего контейрена). Последнее потребовало также доработки низкоуровневых функций для работы с атомарными переменными. Теперь есть поддержка 64-битных атомарных переменных, но пока только на 64-битных платформах.
Всё вышеописанное делает возможным написание на Ü многопоточного кода с нужной синхронизаций и без необходимости велосипедить что-то своё. Единственное исключение - в стандартной библиотеке нету lock-free контейнеров, кому они нужны, должен их сам реализовать на основе существующего функционала.
Переменные среды
Теперь в стандартной библиотеке Ü есть функция для чтения переменных среды. Нечасто, но иногда бывает необходимо их читать. Писать эти переменные тоже можно, но только это не безопасно из-за дефектного дизайна libc.
Отдельный синтаксис для символьных литералов
Удивительно, но долгое время в Ü отсутствовал синтаксис для задания одиночных символьных литералов. Можно было или задать строковый литерал и обратиться к его начальному элементу ("q"[0]), или же воспользоваться ещё более неудобным способом с суффиксом литерала ("q"c8). Но теперь этот позор в прошлом и Ü поддерживает символьные литералы в одинарных кавычках, как это есть в каком-нибудь C++ ('q'). Старый способ ("q"c8) был при этом вообще удалён за ненадобностью.
Дедупликация глобальных изменяемых переменных
В ходе разработки системы сборки был принят способ компоновки приватных зависимостей, следствием которого стало потенциальное дублирование кода некоторых библиотек (если сборочная цель зависит от библиотек A и B и они обе внутри используют библиотеку C). Дублирование кода само по себе не страшно и компилятор может его оптимизировать. Страшнее оказался тот факт, что дублирующимися могли оказаться и глобальные изменяемые переменные.
С вышеизложенной проблемой пришлось разбираться. Был реализован механизм дедупликации глобальных изменяемых переменных на основе comdat. При этом закодированным именам переменных пришлось навешивать суффиксы с хешом пути файла исходного кода, чтобы не происходило слияния двух переменных из различных файлов, которые случайно имеют одно и то же имя.
thread-local глобальные переменные
Они иногда полезны и даже необходимы, посему я решил их добавить. Синтаксис их объявления отличается от такового обычных переменных. Объявляются они через синтаксис вроде thread_local i32 x= 123;. Модификатор изменяемости указывать не надо, ибо такие переменные всегда изменяемые (да и зачем нужны неизменяемые thread-local переменные)?
В целом они аналогичны обычным изменяемым глобальным переменным. Всё также для доступа к ним нужно использовать unsafe. Единственное различие - каждый поток имеет свой собственных экземпляр такой переменной.
Разбор композитов на части
В Ü давно существовала некая асимметрия работы с композитными значениями. Можно было скомпоновать множество значений в композитный тип (массив, кортеж, структуру). Но вот извлечь часть из композитного значения было не возможно. Можно было только оставить значения как есть внутри композита или же копировать их. Чтобы эту асимметрию побороть, был реализован механизм разбора композитных значений на части.
Для кортежей и массивов синтаксис следующий:
var [ i32, 3 ] mut arr[ 88, 999, 10101010 ];
// arr разбирается на части и объявляются переменные для этих частей.
auto [ x, y, z ] = move(arr);
var tup[ f32, u64 ] mut t[ 0.5f, 656u64 ];
// t разбирается на части и объявляются переменные для этих частей.
auto [ first, second ] = move(t);
Для структур синтаксис несколько иной:
struct S{ i32 x; f32 y; }
// ...
var S mut s{ .x= 7867, .y= 123.45f };
// s разбирается н�� части и объявляются переменные для этих частей.
auto { x, y } = move( s );
Стоит подчеркнуть, что такой разбор не имеет накладных расходов - части композитов не копируются, а перемещается. Это особенно важно для типов, которые дорого копировать или же которые вообще не копируются.
Оказалось, что это нововведение упрощает использование функций, возвращающих больше одного значения (через кортеж):
fn Foo() : tup[ i32, bool ];
// ...
// Сразу разбираем на части результат вызова функции.
auto [ x, y ]= Foo();
typeinfo для шаблонных методов
Оператор typeinfo в Ü предоставляет различную информацию о типе. Для структур/классов в том числе предоставляется информация о их методах. Это бывает местами полезно, особенно в шаблонном коде - можно проверить наличие метода у класса и если он есть - вызвать его. К сожалению шаблонные методы через typeinfo получить было нельзя, что несколько ограничивало такую инспекцию.
Немного подумав, я решил, что этот недостаток надо устранить. Я добавил информацию о шаблонных методах в typeinfo структур/классов. Но эта информация несколько ограничена в сравнении с обычными методами. Типы аргументов и возвращаемого значения не доступны, ибо они могут определяться аргументами шаблона. Также не проверяется, что шаблонный метод не отключён через enable_if, ибо это свойство тоже требует инстанцирования шаблона. Но даже несмотря на эти ограничения эта информация всё-же полезна. Иногда хватает знания, что метод с нужным именем есть, а его сигнатура и тип результата могут неявно предполагаться.
Двоичный поиск, куча и сортировка
В стандартной библиотеке Ü были реализованы различные вспомогательные функции для двоичного поиска. Этот алгоритм встречается достаточно часто, чтобы имело смысл его иметь в стандартной библиотеке, к тому же он не столь тривиальный, чтобы рядовой программист мог его с первого раза без ошибок написать.
Кроме того были реализованы функции для работы с двоичной кучей и функции сортировки кучей. Двоичная куча сама по себе весьма полезна для реализации очередей с приоритетом. Сортировка кучей полезна там, где надо иметь гарантированную максимальную временную сложность O( n * log(n) ), чего т. н. "быстрая сортировка" (которая, кстати, давно уже есть в стандартной библиотеке Ü) не гарантирует (она в худшем случае имеет квадратичную сложность).
Поддержка Ü в ecode
Нативная поддержка Ü была добавлена в IDE ecode. Эта поддержка включает подсветку синтаксиса, работу с языковым сервером Ü и отладку.
Самое интересное, что я даже не сам это сделал. Пользователь Github под псевдонимом Curculigo реализовал это по одному ему ведомым причинам. Я в последствии лишь немного доработал подсветку синтаксиса.
Запрет циклов изменяемых логических ссылок
В этом году был обнаружен весьма хитрый дефект механизма контроля ссылок. Он заключался в том, что иногда в графе ссылок (отслеживаемом статически во время компиляции) можно было создать циклы из изменяемых ссылок. Такие циклы позволяли в некоторых случаях нарушить правило единственности изменяемой ссылки - со всеми вытекающими последствиями.
Пример кода, который выявил дефект:
struct S
{
i32 &mut @('a') x;
i32 &mut @('a') y;
// В конструкторе создаётся производная от аргумента "a" ссылка и записывается в ссылочное поле "x".
// Потом производная уже от поля "x" ссылка записывается в ссылочное поле "y".
// Проблема тут в том, что оба эти поля помечены ссылочным тегом 'a' и логически представляют из себя одну ссылку, но по факту их две и это может вести к проблемам.
// Во время компиляции этого кода создавался цикл в графе ссылок - ссылочный тег 'a' ссылался на самого себя.
fn constructor( i32 &mut a ) @(pollution)
( x= a, y= x )
{}
var [ [ [char8, 2], 2 ], 1 ] pollution[ [ "0a", "1_" ] ];
}
Данный дефект был исправлен и введён новый код ошибки компиляции, который выдаётся в случае, если создаётся цикл из изменяемых логических ссылок. Удивительно, но ни в коде стандартной библиотеки, ни в коде Компилятора1 этот дефект ни разу не был использован и соответственно там не пришлось вносить исправлений.
Расширение возможностей оператора .
Раньше в Ü через оператор . можно было обращаться только к полям структур/классов и их методам. С некоторых пор можно таким образом обращаться ещё и к внутренним типам и статическим переменным. Особо необходимым это нововведение назвать нельзя, но оно иногда полезно - оператор . короче писать, чем ::, если присутствует экземпляр структуры/класса.
Продвинутое хеширование и улучшения хеш-таблицы
В стандартной библиотеке Ü был существенно переработан код вычисления хешей. Во-первых, были реализованы функции хеширования семейства MurmurHash (в некотором виде). Во-вторых, было реализовано автоматическое хеширование значений композитных типов, так что теперь реализовывать свою хеш-функцию нужно весьма нечасто. В-третьих, был изменён подход к хешированию пользовательских типов - теперь тип, требующий особого подхода к хешированию, только лишь предоставляет сырые данные, которые должны быть хешированы, а не реализует саму функцию хеширования.
Хеш-таблица стандартной библиотеки была существенно переработана. Она теперь использует вышеизложенный подход к хешированию ключей. Кроме того были починены случаи существенной деградации производительности, которые были иногда возможны, например, если из некогда большой хеш-таблицы удалили почти все значения. Также было реализовано разделение таблицы-хранилища и служебной таблицы, как это принято в т. н. "швейцарских" хеш-таблицах, что по идее должно ускорять поиск в сильно-заполненной таблице.
Стало также возможным делать выборку из хеш-таблицы с типом ключа, не совпадающим с хранимым типом ключа, но только если эти типы совместимы. Это полезно, например, если нужно делать поиск в таблице с ключом типа ust::string8, используя ключи типа ust::string_view8. Теперь при таком поиске не происходит конструирования временного значения типа ust::string8.
Расширение для Visual Studio
В качестве развлечения я написал расширение для Microsoft Visual Studio. Оно необходимо, если хочется использовать эту IDE для разработки на Ü, без расширения это было бы сложновато (в других IDE и текстовых редакторах с этим проще). Функционал расширения не богат, но достаточен - он включает работу с языковым сервером Ü, а также предоставляет файл подсветки синтаксиса. Разработка этого расширения к тому же помогла обнаружить и устранить пару мелких недостатков в языковом сервере.
Оптимизация размера контейнеров shared_ptr
Контейнеры вроде shared_ptr были оптимизированы - теперь внутри хранится только один указатель (раньше было два). Память под хранимый объект и контрольный блок теперь выделяется одним вызовом memory_allocate и соответственно нужно хранить только один указатель на этот блок памяти. Для полиморфных типов при этом пришлось несколько исхитриться, дабы поддержать хранение указателей различных базовых типов на одно и то же значение. Но благодаря возможности вычисления адреса выделенного объекта на основе указателя на его базовый класс стало возможным хранить только один указатель и для таких типов. Данная оптимизация дала заметный прирост производительности фронтенда Компилятора1 - около 3%, что кажется весьма неплохим результатом.
Запрет чтения переменных, на которые есть изменяемые ссылки
Механизм контроля ссылок в Ü предотвращает ошибки памяти, вроде доступа вне разрешённого диапазона, чтения памяти после её освобождения, ошибки несинхронизированного доступа. Оказалось, что в реализации этого механизма был один небольшой изъян. В некоторых случаях допускалось читать переменные, на которые есть изменяемые ссылки. Такое происходило правда только в случаях, когда код чтения генерировал компилятор, передать ссылку куда-то в функцию чтобы там производить чтение и так уже было нельзя. В чём же проблема с чтением? Суть её заключалась в том, что наличие изменяемой ссылки означает потенциальную возможность изменения переменной из другого потока. В таком случае при чтении может возникнуть гонка.
Оказалось, что мест, где компилятор читает значения, не проверяя наличия изменяемых ссылок, достаточно много. Но все они относились к чтениям простых скаляров в случаях вроде оператора присваивания, условных операторов/циклов, чтении индекса при обращении к элементу массива, инициализации скаляров и т. д. В итоге я во все эти места вставил проверки, что на читаемые переменные нету изменяемых ссылок. К тому же пришлось исправить несколько мест в коде стандартной библиотеки и Компилятора1, где такое поведение встречалось.
Больше поддерживаемых систем
Долгое время компилятор Ü работал и генерировал код только под GNU/Linux и Windows. С целевой архитектурой было не сильно лучше - хоть LLVM и поддерживает множество архитектур и компилятор Ü их теоретически тоже поддерживает, но по факту это не тестировалось, код работал только на x86 и x86_64. Но в этом году ситуация сильно улучшилась.
Сначала я попробовал на Github Actions собирать проект под OS X с AArch64. Кое-что пришлось менять, но полного успеха я не достиг. Особенно тяжело было отлаживать сборку не имея ничего кроме этих Github Actions - прямого доступа к компьютеру с OS X у меня не было и нету. Посему я решил временно сделать паузу.
С чем было проще, так это с FreeBSD. Я поставил себе виртуалку с этой системой и принялся собирать проект. Потребовались некоторые изменения, но в целом не очень много. Больше всего потребовалось изменять код стандартной библиотеки - нужны были другие биндинги к Unix функциям/типам нежели к GNU/Linux и нужно было отказаться от Linux-специфичного функционала. За несколько дней я управился с этой задачей и добился работы Ü на FreeBSD.
Далее я взялся за сборку проекта на GNU/Linux с архитектурой AArch64. С виртуалкой это было не возможно и пришлось использовать только Github Actions. Сборка падала и я долго провозился, выясняя, что же там не так. Как оказалось, генерация кода во фронтенде компилятора работала не совсем корректно. На инструкциях вызова не проставлялся атрибут sret, что влияло на ABI под этой архитектурой. Эту ошибку я исправил и дальше код на Ü больше не падал. Остальные изменения в сравнении с x86_64 были несущественными.
После починки сборки под AArch64 я вернулся к сборке под OS X. Пришлось сделать много изменений, дабы поддержать эту систему. Например, формат исполняемых файлов и разделяемых библиотек отличается от такового на GNU/Linux и FreeBSD. Также формат объектного файла Mach-O не поддерживает comdat, который используется в паре мест компилятора, из-за чего пришлось реализовывать тот-же функционал альтернативным способом. Необходимые изменения в стандартной библиотеке тоже оказались немаленькими. Ну и в целом потребовалось внести много правок, связанных главным образом с особенностями системных инструментов сборки. В итоге сборки и работоспособности на OS X я добился, но не в полной мере - пара тестов стандартной библиотеки падает по неясной причине и в целом поддержка версий OS X кроме той, что есть в Github Actions, находится под вопросом.
Также я реализовал поддержку GNU/Linux с архитектурой x86 (32-битной). Это тоже оказалось несколько нетривиальным. Пришлось возиться в различиях системных библиотек и опять дорабатывать биндинги к Unix функциям в стандартной библиотеке. Также пришлось реализовывать обходные пути для реализации арифметики со 128-битными целыми числами.
В итоге всех этих изменений поддержка различных систем в Ü значительно расширилась. С одной стороны это конечно хорошо, но с другой стороны добавляет работы, если нужно вносить изменения в системозависимый функционал.
Починки числовых литералов
Код парсинга числовых литералов в Ü был написан весьма давно и с тех пор существенно не менялся. Однако он был проблематичным. Во-первых, могла генерироваться ошибка, дескать литерал переполнился, если задавался вещественный литерал вроде 1000000000000000000000000.0. Во-вторых, литералы обрезались молча до типа i32, если не был указан суффикс типа. В-третьих, зачем-то была реализована поддержка вещественных литералов с недесятичным основанием, что вносило больше проблем, чем давало выгоды.
Посему я решил таки код компилятора по работе с числовыми литералами существенно переработать. Теперь недесятичные литералы бывают только целочисленными. При переполнении целой части десятичного литерала парсинг продолжается как для вещественного литерала, но если в конце не оказалось дробного разделителя/экспоненты (что требуется для вещественных литералов) генерируется ошибка. Также целочисленные литералы теперь расширяются до 64-битных или даже 128-битых типов, если в 32 бита они не умещаются.
В целом я доволен этим улучшением, но оно пока что не достаточно. Проблемы существуют с вещественными литералами - они парсятся простым, но не всегда корректным способом, что в редких случаях может вести к небольшой потере точности (получается число на пару младших битов отличающееся от того, что должно быть). А как делать правильно, я пока точно не знаю. Насколько я могу судить, парсинг вещественных чисел это целая наука и надо быть очень внимательным, чтобы соблюсти стандарт и также нужно где-то раздобыть достаточное количество тестов, покрывающих потенциально-проблемные случаи.
Контейнеры any
В стандартную библиотеку добавлена пара контейнеров - any и any_non_sync. Они позволяют хранить типобезопасным способом значения произвольного типа. any может хранить любой тип, кроме того, который помечен как non_sync. any_non_sync может хранить вообще любой тип, но сам при этом является non_sync.
Значение из контейнера any можно прочитать специальным шаблонным методом, если в качестве аргумента шаблона указан тот тип, значение которого в данный момент хранится в контейнере. Предназначен этот контейнер для тех случаев, когда надо по каким-то причинам скрыть реальный тип объекта, но в то же время сделать это без использования небезопасных механизмов языка.
Соглашение о вызове C
В Ü поначалу не было никаких соглашений о вызове кроме стандартного. Потом (несколько лет назад) оказалось, что нужно как-минимум уметь вызывать функции WinAPI под 32-битными версиями Windows, что требует соглашения о вызове stdcall. Поэтому в языке была реализована нотация соглашения о вызове и соглашения типов default, C, Ü, system, fast и cold. default, C и Ü были при этом синонимами, system был равен default везде, кроме 32-битной версии Windows (где оно было равно stdcall), fast и cold просто представляли соответствующие соглашения о вызове, которые предоставляет библиотека LLVM.
Но в этом году я поразмыслил и обнаружил, что данное разделение несколько проблемно. Во-первых я решил, что негоже языку Ü иметь ровно то же соглашение о вызове, что использует C и разделил их. Теперь default и Ü это одно соглашение, а C - другое. Во-вторых, я сделал так, что соглашения о вызове внутри компилятора хранятся в их логической форме - как они задаются правилами языка, а не в виде перечисления, которое используется в библиотеке LLVM. Тем самым соглашение о вызове system стало всегда отличным от Ü и C.
Далее я обнаружил (меня товарищ @Makcimka132 надоумил), что собственно говоря соглашение о вызове C у меня не работает должным образом во всех случаях. Оказалось, например, что иногда целочисленные аргументы/возвращаемые значения должны иметь атрибуты zext или sext, коих компилятор Ü не проставлял. Без них передача 8-битных/16-битных значений не работала корректно во всех случаях. Также оказалось, что соглашение о вызове C также регламентирует передачу аргументов композитных типов, чего я вообще не ожидал. Раньше я исходил из предположения, что в C композиты по значению не передаются. Оказалось, что передаются, хоть и не часто (в системных API такого не встретить).
Так что мне пришлось заняться полной поддержкой соглашения о вызове C. Оказалось, что это соглашение зависит от операционной системы/архитектуры. И библиотека LLVM ни капельки не помогает эту зависимость абстрагировать. Например, если указать тип аргумента - композит, LLVM "любезно" распихнёт его части по отдельным регистрам при передаче, что не всегда правильно. Так что мне пришлось во фронтенде компилятора писать код, который определяет, как надо передавать аргументы/возвращать значение для каждой поддерживаемой архитектуры и операционной системы, что местами потребовало даже подсчёта регистров (что по моему мнению фронтенд компилятора делать не должен). К тому же пришлось писать какое-то умопомрачительное количество тестов, которые покрывают все хитрые случаи.
После вышеописанных изменений я также добавил в typeinfo для типов функций информацию о их соглашении о вызове. Там оно указывается в строковом виде - так же, как и при объявлении функции.
Поддержка C++ в CppHeaderConverter
В проекте Ü уже давно существует компонент под названием CppHeaderConverter. Это вспомогательная утилита, которая позволяет генерировать Ü биндинги для C кода. Но при чём тут Cpp? А так исторически сложилось. По-началу был план поддержать C++, но он был быстро отвергнут и эта утилита работает нормально только с C кодом.
В этом году я это недоразумение несколько исправил. Теперь CppHeaderConverter умеет работать с небольшим подмножеством заголовочных файлов C++. Поддерживаютс�� функции с extern "C", ссылки, классы C++, глобальные неизменяемые переменные, перечисления в стиле C++ (scoped enums). Что-то более сложное не работает, включая пространства имён и шаблоны.
Починка перевода перечислений в CppHeaderConverter
Раньше в CppHeaderConverter некоторые перечисления C (с последовательными значениями) переводились как перечисления Ü. Но как оказалось, делать это не совсем корректно. В C вполне допускается в значение типа перечисления записать любое целое число. В Ü же такое не допускается, перечисления могут принимать только указанные значения. Я эту оплошность исправил и теперь в CppHeaderConverter перечисления C переводятся просто как псевдонимы для нижележащих типов. Для значений перечислений создаются константы - в глобальном пространстве имён или же в отдельном пространстве для scoped enums.
Отладочная информация для глобальных переменных
Поначалу в Ü не было глобальных изменяемых переменных, были только глобальные константы. Соответственно не было нужды в отладочной информации для глобальных переменных - какой смысл, если они не меняются? Потом глобальные изменяемые переменные были добавлены, но они остались без отладочной информации. В этом году я этот недостаток исправил и теперь в отладчике можно видеть значения глобальных изменяемых переменных. Но с этим бывают всё-же проблемы в некоторых IDE - они больше заточены на показ только локальных переменных.
Работа с сетью
В стандартную библиотеку Ü был добавлен функционал для работы с сетью. Он включает в себя класс UDP сокета, класс TCP потока, класс TCP-слушателя, структуры IP-адресов и адресов сокетов. Кроме того была реализована функция разрешения сетевых имён (получение IP адреса по доменному имени). Всё вышеописанное работает с адресами IPv4 и IPv6.
Стоит отметить, что это лишь базовый функционал. Реализации HTTP, например, нету, да и нужна ли она в стандартной библиотеке? Также пока что нету кода для сетевой криптографии (TLS и прочее).
Запрет non_sync переменных, существующих в момент приостановки выполнения корутины
В какой-то момент я обнаружил, что подобный код компилятор Ü компилирует без проблем:
struct S non_sync {}
fn generator Foo()
{
var S s;
yield;
}
Но по сути тут содержится потенциальная ошибка. Оператор yield приостанавливает выполнение генератора, позже его выполнение может быть возобновлено. При этом внутри генератора существует локальная переменная non_sync типа. Такие переменные запрещено передавать в другие потоки выполнения, но в случае с приостановленным генератором (или асинхронной функцией) передача в другой поток в принципе возможна.
Посему я добавил в компилятор проверку, что в местах с yield и await не существует локальных переменных non_sync типов. Исключение составляют только корутины, которые сами помечены как non_sync - в них такое разрешено, но при этом такую корутину нельзя передать в другой поток. Также дозволено иметь локальные non_sync переменные, которые создаются и разрушаются между точками останова.
Arena allocator
В стандартную библиотеку был добавлен класс линейного (аренного) аллокатора. Он позволяет выделять память большими блоками и освобождать их разом при разрушении экземпляра аллокатора. Стоит отметить, что этот аллокатор - это не просто замена аллокатору, который используется в обычных контейнерах стандартной библиотеки, а отдельный класс, который используется особыми контейнерами вроде arena_allocated_array и arena_allocated_box. При этом взаимодействие этих контейнером с аллокатором сделано хитрым образом - класс контейнера конструируется с передачей экземпляра аллокатора и логическая ссылка на него удерживается внутри контейнера. Это позволяет на этапе компиляции отследить, что все контейнеры, использующие данный экземпляр аллокатора, разрушаются до того, как разрушается сам аллокатор.
Честно говоря, я сам не считаю такой аллокатор очень то необходимым. Польза от него может быть заметна только в нишевых случаях. Но реализовать его я всё равно посчитал необходимым, ибо такая реализация доказывает, что Ü способен на нечто большее, чем просто управление памятью на основе деструкторов на индивидуальной основе для каждого экземпляра контейнера.
Экспериментальная библиотека для асинхронной работы с сетью
В качестве эксперимента была написана отдельная библиотека для работы с сетью в асинхронном режиме по имени sm_async_net. Она реализует аналоги классов для работы с сетью стандартной библиотеки с тем отличаем, что методы чтения/записи в них сделаны async. Эти методы можно вызывать из асинхронных функций при помощи await. Корневые асинхронные функции можно запускать при помощи специального класса по имени runner. Этот класс может выполнять множество асинхронных функций, переключаться между ними и ожидать сетевых событий при помощи системной функции poll.
Библиотека пока что весьма сырая. Она работает только под Unix-системами (не под Windows). Она может быть не очень оптимальной на большом числе одновременных сетевых соединений, ибо вызов poll требует передачи всех активных сокетов при каждом вызове. Тем не менее эта библиотека уже послужила и помогла найти пару ошибок в коде компилятора Ü, который компилирует/оптимизирует асинхронные функции.
Глобальные переменные - сырые указатели с нулевым инициализатором
В Ü долгое время глобальные переменные не могли быть типа сырого указателя. Логика тут была следующая: сырые указатели - это типы для низкоуровневой работы с памятью, в обычном коде они использоваться не должны и операции с сырыми указателями дозволены только в unsafe коде. По этой причине сырые указатели считаются типами, которые нельзя использовать в constexpr контексте. Но для глобальных переменных дозволены только constexpr типы, ибо не-constexpr типы могут иметь нетривиальные конструкторы/деструкторы, что для глобальных переменных неприемлемо.
Но недавно я обнаружил, что в некоторых случаях полезно иметь глобальные переменные-сырые указатели. Это в основном случаи низкоуровневого кода, где указатель должен указывать на какой-нибудь объект-синглтон. Так что я решил вышеописанное ограничение смягчить. Теперь глобальные переменные типов сырых указателей допустимы, но только если они инициализируются нулём. Иная инициализация была бы слишком опасной/проблемной в реализации. При этом сырые указатели по прежнему остаются не-constexpr типами и по прежнему нельзя, например, сделать глобальную переменную типа структуры, у которой лежит внутри сырой указатель.
Битовый сдвиг со знаковой константой сдвига
В Ü операции битового сдвига требуют в качестве правого аргумента значение целого беззнакового типа. Беззнаковый тип требуется, ибо сдвиг на отрицательное количество бит не имеет смысла. Но, как оказалось, такое требование несколько усложняет код в некоторых случаях.
Вот такой код раньше не собирался:
auto y= x << 3; // Ошибка - литерал "3" имеет знаковый тип "i32", а для сдвига нужен беззнаковый.
Приходилось писать так:
auto y= x << 3u; // Суффикс литерала "u" делал значение беззнаковым.
Я подумал "Доколе?" и решил эту проблему исправить. Теперь значение сдвига может быть знакового типа, но только если это значение - неотрицательная константа времени сборки. Теперь код из первого примера компилируется без ошибок.
Заключение
По моему мнению введение сборочной системы и добавление функционала по взаимодействию с операционной системой в стандартной библиотеке кардинально упростили использование языка Ü для написания типичных программ. Данные нововведения заметно снизили т. н. "потери на трение", связанные с использованием Ü, что особенно заметно в относительно простых программах, где организация сборки и взаимодействия с внешним миром занимают относительно большое время разработки. Остальные улучшения тоже упростили жизнь, пусть и не так значительно.
Что будет дальше? На этот вопрос у меня пока нету точного ответа. Пока я могу экстраполировать текущий тренд и предположить, что Ü ожидает ещё ряд несущественных изменений, главным образом направленных на упрощение типичных паттернов использования. Ну и наверняка будут ещё обнаружены и устранены какие-нибудь ошибки.
Ссылки
Github проекта
Сайт проекта - с документацией, загрузками, блогом и прочим
