Введение
На GitHub лежит репозиторий Сфера — это полностью кириллический язык программирования. Компилятор написан на Rust, код генерируется через LLVM 18 в нативный машинный код. Не интерпретатор, не транспайлер в C — а настоящий компилятор: лексер → парсер → LLVM IR → машинный код.
Язык прошёл путь от «вывести строку» до собственного HTTP‑сервера, хранилища данных, системы логов/метрик/трейсинга, графики через SDL2, полноценного отладчика с DWARF, пакетного менеджера со своим публичным индексом и — последнее по времени добавление — опционального детерминированного планировщика потоков. Эта статья — это честный разбор на данный промежуток времени: что работает, как устроено и чего пока нет.
Синтаксис
Полностью кириллический — все ключевые слова, имена встроенных функций и типов пишутся кириллицей. Не «русский Pascal» и не «русский Python» — свой синтаксис, по духу ближе к Swift/Rust.
Концепция |
Сфера |
Swift |
Rust |
|---|---|---|---|
Функция |
|
|
|
Переменная |
|
|
|
Константа |
|
|
|
Если |
|
|
|
Иначе |
|
|
|
Цикл while |
|
|
|
Возврат |
|
|
|
Структура |
|
|
|
Интерфейс |
|
|
|
Ошибка |
|
|
|
Self |
|
|
|
Bool |
|
|
|
Программа «Привет, мир»:
функция главная() -> целое { вывести("Привет, мир!"); вернуть 0; }
Компиляция и запуск (одной командой, как go run):
запустить привет.код # соберёт и сразу запустит запустить привет.код -c # только соберёт, без запуска
Флаг -c можно писать и кириллицей — -с. Оба варианта равнозначны, и сделано это сознательно: кириллическая «с» и латинская «c» выглядят неотличимо на экране, но для компьютера это разные символы. Поддержка обоих — чтобы привычная латинская -c продолжала работать без сюрпризов.
Типы данных
Целые числа
8 типов от целое8 (8 бит) до целое64 / целое (64 бита), плюс беззнаковые варианты беззнак8—беззнак64.
Важное правило: типы разной разрядности не смешиваются неявно. целое32 и целое в одном выражении требуют явного приведения. Даже инициализация литералом — целое32 а = целое32(5);, не целое32 а = 5;. Строго, но безопасно.
Дробные числа
дробное (64 бита по умолчанию), дробное32, дробное64. Полностью рабочая арифметика (+, -, *, /, сравнения, унарный минус), вывод (вывести(дробное)) и дробное_в_текст.
Текст
текст — строка в двойных кавычках. Конкатенация через +. Экранирование: \n, \r, \t, \", \\. Сравнение по содержимому, не по адресу.»
Логический тип
верно — булев тип. Литералы — да и нет, не true/false.
Переменные, вывод типа и неизменяемость
Объявление с явным типом: тип имя = значение;. Но тип можно и опустить — если имя ещё не объявлено, имя = значение; компилятор понимает как объявление с выводом типа, а не присваивание:
возраст = 25; // выведено: целое имя = "Иван"; // выведено: текст итог = удвоить(21); // выведено из типа возврата функции
Работает для литералов, вызовов функций/методов, конструкторов структур — для подавляющего большинства реальных объявлений. Не работает для Список<T>/Словарь<К, З> (нужен явный тип параметра) и не сочетается с неизменно (та по‑прежнему требует явного типа).
Неизменяемость — через неизменно (только с явным типом):
неизменно целое максимум = 100;
Явное приведение типов — имя типа как функция: целое32(значение), дробное(значение).
Операторы
Арифметика: +, -, *, /, % (остаток от деления).
Сравнение: ==, !=, <, >, <=, >=.
Логические: &&, ||, !.
Побитовые: &, |, ^, ~ — только для целых чисел, оба операнда одного типа. Приоритет ниже, чем у сравнения (как в C/Rust) — а & б == 8 разбирается как а & (б == 8). При смешивании используйте скобки.
Сдвиги: << и >> тоже есть, несмотря на то, что язык использует </> для обобщённых типов (Список<целое>). Риск был в том, что вложенные обобщённые (Список<Список<целое>>) дают два подряд идущих < — та же ловушка, что была в ранних версиях C++. Решение: лексер не трогали (он по‑прежнему выдаёт </> раздельно) — сдвиг распознаётся только на уровне разбора выражений, где два соседних токена </> не могут означать ничего, кроме сдвига; закрытие вложенных обобщённых типов — совсем другой, не пересекающийся с этим код. Строгость та же, что у остальных побитовых: только целые, оба операнда одного типа.
Строковая интерполяция
Внутри текстового литерала $имя подставляет значение переменной, а ${выражение} — значение любого выражения формируемого типа (текст, любые целые, дробное, верно):
целое возраст = 25; текст имя = "Никита"; вывести("Привет, $имя! Тебе $возраст лет."); вывести("Через год будет ${возраст + 1} лет."); // Результат интерполяции — обычное текстовое значение: можно сохранить // в переменную, вернуть из функции, склеить через "+". текст сообщение = "Итог: ${площадь(3, 4)} м²";
Честное ограничение: строковые литералы внутри ${...} не поддерживаются — лексер разбирает внешний литерал целиком до того, как парсер узнаёт о вложенном выражении.
Управляющие конструкции
Условие
если (х > 10) { вывести("Больше десяти"); } иначе если (х > 0) { вывести("Положительное"); } иначе { вывести("Ноль или отрицательное"); }
Цикл с условием
целое счётчик = 0; пока (счётчик < 5) { вывести(целое_в_текст(счётчик)); счётчик = счётчик + 1; }
Цикл по коллекции
целое[3] числа = [1, 2, 3]; для каждого элемент в числа { вывести(целое_в_текст(элемент)); }
для каждого работает и с Список<T> — тип переменной цикла выводится автоматически из типа элемента списка, работает и с пустым списком, и со структурами‑элементами, прервать/продолжить — как обычно.
прервать и продолжить
Обычные break/continue — работают в любом цикле, включая вложенные и тело параллельно { ... }.
Функции
функция сложить(целое а, целое б) -> целое { вернуть а + б; }
Тип возврата указывается через ->. Компилятор проверяет количество и типы аргументов при каждом вызове — и для обычных функций, и для функций из подключённых модулей.
Обобщённые функции
Поддержаны, с честными ограничениями:
функция макс_из_двух<Т>(Т а, Т б) -> Т { если (а > б) { вернуть а; } вернуть б; } функция главная() -> целое { целое итог_целое = макс_из_двух<целое>(3, 7); дробное итог_дробное = макс_из_двух<дробное>(2.5, 1.5); вернуть 0; }
Ограничения: только один обобщённый параметр, тип указывается явно при вызове, только функции главного файла.
Массивы
Фиксированного размера, размер — часть типа. Требуют явной инициализации литералом:
целое[5] числа = [10, 20, 30, 40, 50]; числа[0] = 99; вывести(целое_в_текст(числа[0]));
Структуры
структура Точка { целое х; целое у; функция сумма_координат() -> целое { вернуть это.х + это.у; } } Точка точка = Точка(3, 4); вывести(целое_в_текст(точка.сумма_координат())); // 7
Поля — без значений по умолчанию
Методы — функции внутри тела структуры
это— ссылка на текущий экземпляр (аналогself/this)Конструктор генерируется автоматически по всем полям в порядке объявления
Структуры — всегда объекты в куче, передаются по ссылке, с ARC
Слабые ссылки
Поле, объявленное как слабая Тип, не увеличивает счётчик ссылок — нужно для разрыва циклических ссылок:
структура Держатель { текст имя; слабая Цель цель; }
Проверка живости — метод .жива(). Работает безопасно даже после того, как цель «логически» умерла: пока жива хотя бы одна слабая ссылка, компилятор откладывает реальное освобождение памяти, чтобы жива() могла безопасно проверить.
Инкапсуляция
публично/приватно — поля и методы могут быть скрыты внутри структуры.
Обработка ошибок — результат
Вместо исключений — обёртка результат<T> с тремя полями: .успешно (верно), .значение (тип T), .сообщение (текст ошибки).
функция разделить(целое а, целое б) -> результат<целое> { если (б == 0) { вернуть ошибка("деление на ноль"); } вернуть успех(а / б); } функция главная() -> целое { результат<целое> итог = разделить(10, 2); если (итог.успешно) { вывести(целое_в_текст(итог.значение)); } иначе { вывести("Ошибка: " + итог.сообщение); } вернуть 0; }
Многие встроенные функции (подстрока, найти, прочитать_файл) возвращают результат<T> — .успешно нужно проверять перед использованием .значение.
Обобщённые типы
Список
Список<целое> числа = Список<целое>(); числа.добавить(10); числа.добавить(20); целое первое = числа.получить(0); целое количество = числа.размер(); числа.удалить(0); // удаляет элемент по индексу
Словарь
Словарь<текст, целое> возрасты = Словарь<текст, целое>(); возрасты.положить("Аня", 25); результат<целое> возраст = возрасты.получить("Аня"); если (возраст.успешно) { вывести(целое_в_текст(возраст.значение)); } возрасты.освободить(); // Словарь не участвует в ARC — освобождать явно
Важное: Словарь.получить(ключ) возвращает результат<Значение>, а не значение напрямую — ключа может не быть. Словарь не участвует в ARC — .освободить() нужен для явного освобождения памяти; текстовые ключи и значения при этом безопасно освобождаются как самостоятельные копии.
Пользовательские обобщённые структуры
структура Коробка<Т> { Т содержимое; } коробка = Коробка<целое>(42); // вывод типа Коробка<целое> другая = Коробка<целое>(7); // явный тип — тоже работает
Параметров может быть и несколько — структура Пара<А, Б> { А первый; Б второй; }, Пара<целое, текст>(42, "привет") — работает и с выводом типа, и с явным типом слева, так же, как одно‑параметрический случай выше.
Список<T> и Словарь<K, V> можно использовать как поле структуры, с цепочками доступа это.поле.метод(...) — в том числе параметризованные СОБСТВЕННЫМИ параметрами структуры (структура ПараСписков<А, Б> { Список<А> первые; Список<Б> вторые; }).
Контракты и полиморфизм
Контракт — аналог интерфейса. Компилятор проверяет, что структура, заявляющая : Контракт, реально имеет все требуемые методы с совпадающими сигнатурами.
контракт Печатаемый { функция напечатать() -> текст; } структура Кот : Печатаемый { функция напечатать() -> текст { вернуть "Мяу"; } } структура Собака : Печатаемый { функция напечатать() -> текст { вернуть "Гав"; } } функция озвучить(Печатаемый существо) -> целое { вывести(существо.напечатать()); вернуть 0; } функция главная() -> целое { Кот кот = Кот(); Собака пёс = Собака(); озвучить(кот); // "Мяу" озвучить(пёс); // "Гав" — та же функция, другая реализация вернуть 0; }
Список<Контракт> — коллекция разнотипных объектов через общий интерфейс:
Список<Печатаемый> зоопарк = Список<Печатаемый>(); зоопарк.добавить(кот); зоопарк.добавить(пёс);
Контракт как тип возврата — функция может быть объявлена как функция ... -> ИмяКонтракта, возвращая любую структуру, реализующую этот контракт; вызывающий код не знает заранее, какая структура за ним стоит.
Оба сценария доведены до рабочего состояния (в т.ч. исправлен настоящий сегфолт при упаковке структуры в контракт внутри списка). ✅ Ограничение из более ранних версий этого материала — что ARC не «видит насквозь» структуру, упакованную в толстый указатель контракта, — с тех пор устранено: элементы Список<Контракт> теперь корректно захватываются и освобождаются (рантайм‑сравнение адреса vtable с известными vtable контракта), и отдельно исправлена утечка локальных переменных контрактного типа при выходе из области видимости (вместе с риском двойного освобождения, который эта же правка могла бы создать при копировании контрактных переменных). Проверено под valgrind — ноль ошибок, ноль утечек.
Многопоточность
параллельно
Запускает блок кода в отдельном потоке ОС. Тело блока не может использовать переменные из окружающей области видимости — защита от обращения к освобождённой памяти.
параллельно { вывести("Работаю в фоне"); }
дождаться_все_потоки() и дождаться_поток(номер)
Ждёт завершения всех потоков — или одного конкретного по номеру.
параллельно { вывести("Поток 1"); } параллельно { вывести("Поток 2"); } дождаться_все_потоки(); вывести("Оба потока точно закончили работу");
канал
Безопасная передача одного значения из фонового потока:
канал<целое> канал_результата = создать_канал(); параллельно { целое вычислено = 2 + 2; канал_результата.отправить(вычислено); } целое значение = канал_результата.получить(); // ждёт, пока не придёт канал_результата.освободить();
Каналы с текстом (канал<текст>) тоже поддержаны — по пути найдена и исправлена настоящая гонка данных (use‑after‑free между потоками), не просто утечка: теперь отправить(...) всегда делает независимую копию значения. У канал<T> появился .освободить() для самого объекта канала.
Остающееся ограничение: только целочисленные, указательные и текстовые типы, одностороннняя разовая передача одного значения.
параллельно детерминировано {... } — детерминированный планировщик («эстафета»)
Обычный параллельно — это честные, независимые потоки ОС: относительный порядок их побочных эффектов (кто и когда напечатал строку) непредсказуем и меняется от запуска к запуску — так и должно быть для настоящих потоков без собственного планировщика.
Для задач, которым нужен ВОСПРОИЗВОДИМЫЙ порядок (встроенные/промышленные сценарии, для которых язык и задумывался — станки с ЧПУ, конвейеры, тестирование), добавлен опциональный модификатор, не меняющий поведение обычного параллельно:
функция главная() { параллельно детерминировано { целое i = 0; пока (i < 5) { вывести("А" + целое_в_текст(i)); i = i + 1; } } параллельно детерминировано { целое i = 0; пока (i < 5) { вывести("Б" + целое_в_текст(i)); i = i + 1; } } дождаться_все_потоки(); }
Вывод этой программы бит‑в‑бит одинаков при любом числе повторных запусков: А0 Б0 А1 Б1 А2 Б2 А3 Б3 А4 Б4.
Механизм — «эстафета» (baton over real threads): все детерминировано‑потоки одной партии (партия — это потоки между соседними вызовами дождаться_все_потоки(), тот же принцип, что уже был у обычных потоков) по кругу передают друг другу право выполняться, как эстафетную палочку, на заранее определённых точках: начало каждой итерации пока, после вывести(...), после канал.отправить/.получить. Честно: реальной параллельности между такими потоками поэтому нет — в каждый момент физически работает ровно один из них. Обычный параллельно можно свободно запускать рядом — он в эстафете не участвует и её не блокирует.
По пути найдены и исправлены три отдельных, по‑настоящему воспроизводимых гонки/дедлока — ни один не был виден по чтению кода, только многократным живым прогоном (сотни повторов, в т.ч. под valgrind):
Счётчик партии не сбрасывался между батчами — вторая партия зависала навсегда.
Запись родителя в общий флаг активности потока — без мьютекса, редкая (~1 из 5 запусков) гонка видимости памяти, тоже дедлок.
Самый тонкий: быстрый поток мог полностью отработать и покинуть эстафету ДО того, как родитель успевал зарегистрировать следующего участника — не зависание, а молчаливая потеря самого чередования (
Г0 Г1 Г2 Д0 Д1 Д2вместо честногоГ0 Д0 Г1 Д1 Г2 Д2). Исправлено барьером регистрации всей партии.
Честные ограничения v1: для каждого не является точкой передачи (обходной путь — пока со счётчиком); статический барьер регистрации партии работает только для последовательности параллельно детерминировано, идущей сразу друг за другом в одном блоке (не внутри если/цикла — там действует более слабый, задокументированный запасной вариант); максимум 256 потоков на партию; главный поток в эстафете не участвует.
Управление памятью (ARC)
Структуры управляются автоматически через подсчёт ссылок — free/delete писать не нужно. Текст в подавляющем большинстве практических случаев тоже под управлением ARC: локальные переменные, конкатенация, текстовые поля структур, результат функции/метода без промежуточной переменной (вывести(целое_в_текст(42)); теперь не течёт), результаты Список<текст>/Словарь<К,З>, элементы, переданные напрямую в .добавить()/.положить(), и текст, полученный через канал<текст>.получить().
✅ Ранее здесь был честно задокументирован отдельный случай — структура, упакованная в контракт (не крах, только утечка). С тех пор исправлен (см. раздел про контракты выше) — сейчас известных дыр в ARC не осталось.
Модули
подключить утилиты;
Поиск по трём уровням:
{имя}.кодрядом с подключающим файлом{корень_проекта}/пакеты/{имя}.код(в т.ч. составное имяпакеты/ИМЯ_ПАКЕТА/файл.код— см. пакетный менеджер ниже)Системная стандартная библиотека (при установке через
собрать.shилиустановить-windows.ps1)
Доступ к содержимому — напрямую по имени или через квалификацию модуля:
целое итог = удвоить(21); // обычный вызов целое итог2 = утилиты.удвоить(21); // с указанием модуля
Как это устроено: не раздельная компиляция с изоляцией, а текстовое склеивание перед семантическим анализом. Дублирующиеся имена дают понятную ошибку компиляции, а не молчаливую подмену. Циклические подключения корректно обрабатываются — файл, уже разобранный, пропускается при повторной встрече.
Пакетный менеджер (сфера_пакеты)
Отдельный инструмент для установки стороннего кода — git как реестр, без своей инфраструктуры вроде crates.io/npm: зависимость — это ссылка на git‑репозиторий.
Манифест сфера.зависимости в корне проекта:
геометрия = git https://github.com/кто-то/геометрия-сфера.git графика_доп = git https://github.com/кто-то/графика-доп.git ветка=разработка описание="Доп. примитивы рисования"
сфера_пакеты установить # клонирует и ставит все зависимости из манифеста сфера_пакеты список # показывает установленные пакеты и как их подключать сфера_пакеты удалить ИМЯ # убирает пакет целиком
Изоляция пространств имён. Каждый пакет ставится в свою папку — пакеты/ИМЯ_ПАКЕТА/. Два разных пакета могут содержать одноимённые файлы без конфликта — физически они в разных папках, подключаются по составному имени подключить ИМЯ_ПАКЕТА.файл;.
Транзитивные зависимости. Если у устанавливаемого пакета внутри его собственного репозитория тоже есть сфера.зависимости, сфера_пакеты установить подхватывает и его — обходом в ширину по всему графу, не только один уровень. Циклы (А зависит от Б, Б — от А) корректно обрываются.
Разрешение версионных конфликтов — настоящий constraint solving. Необязательное поле версия=X.Y.Z (вместо ветка=) даёт сравнимую семантическую версию, с поддержкой диапазонов в стиле npm/cargo — ^1.2.0 (совместимые версии) и ~1.2.0 (только патчи). Если один и тот же пакет транзитивно запрошен несколько раз с разными требованиями, они не «побеждают по старшинству», а пересекаются как интервалы версий — итоговая версия обязана удовлетворять всем требованиям дерева одновременно; если пересечение пусто, установка честно падает с ошибкой, называющей оба требования и через какие пакеты каждое пришло.
Файл блокировки (сфера.блокировка). Диапазоны ^/~ нестабильны во времени — у пакета могут появиться новые теги. После каждого успешного установить блокировка перезаписывается с уже разрешёнными точными тегами (как Cargo.lock/package-lock.json) — следующий установить берёт версию из неё напрямую, без похода в сеть, пока она всё ещё удовлетворяет диапазону манифеста.
Публичный индекс пакетов. Раньше, чтобы поставить чужой пакет, нужно было заранее знать его git‑адрес. Индекс закрывает это — один git‑репозиторий с каталог‑файлом, который можно искать и добавлять:
сфера_пакеты индекс-настроить URL_ИНДЕКСА # один раз на машину сфера_пакеты индекс-обновить сфера_пакеты искать математика # ищет по имени и описанию сфера_пакеты добавить математика # дописывает строку в сфера.зависимости сфера_пакеты установить
Честно: сам индекс — сознательно временное решение, только git, без сервера и базы данных — следующий шаг (когда будет домен и сервер) заменит его полноценным веб‑сервисом. Разрешение версий ограничено форматом X.Y.Z (без пререлизов/build‑метаданных и без >=/<=/hyphen‑диапазонов из npm) и работает только для пакетов с одного и того же git‑адреса.
Внешние функции (FFI)
Прямой вызов функций из C‑библиотек:
внешняя функция получить_переменную_окружения(текст имя) -> текст как "getenv";
Строка после как — настоящее имя символа в библиотеке. Если совпадает с именем в Сфере, можно опустить.
Кириллические имена FFI‑функций — ключевая возможность для полностью кириллического проекта:
внешняя функция инициализировать(беззнак32 флаги) -> целое32 как "SDL_Init";
Программист вызывает инициализировать(32) — полностью по‑русски. Латинское имя "SDL_Init" существует только в одной строке объявления, которую пишет автор привязки, а не пользователь библиотеки.
Флаг --линковать ИМЯ добавляет -lИМЯ к линковке.
Отладчик
Флаг -g (или --отладка) встраивает в объектный файл настоящую DWARF‑отладочную информацию через LLVM — тот же механизм, которым пользуется clang для C/C++. Отдельного инструмента отладки не появляется — запускается обычный lldb или gdb, как для любой скомпилированной программы:
bash
запустить программа.код -g -c lldb ./программа # b программа.код:10 — точка останова по номеру строки # run / next / step / bt / continue
Работает без ограничений: точки останова по номеру строки для любой функции/метода (включая тело параллельно), пошаговое выполнение, полноценный стек вызовов с настоящими именами функций «Сферы», просмотр исходного кода в отладчике. Проверено напрямую и lldb, и gdb.
Честное ограничение: просмотр значений локальных переменных по имени (print х) пока не поддержан — не описан точный DWARF‑тип для каждого типа языка. lldb/gdb всё равно показывают сырые байты по адресу.
Компиляция под другую архитектуру процессора
Кросс‑компиляция под другие архитектуры (ARM64 и др.) через LLVM.
✅ Компилятор полностью работает внутри Android‑приложения — теперь это подтверждённый факт, не осторожное предположение. Кросс‑компиляция под Android (--целевая-платформа aarch64-linux-android21 + --android-sysroot) проверена с реальным запуском результата на физическом Android‑устройстве через Termux. Но интереснее другое: сам компилятор запустить, линковщик ld.lld и небольшая Sfera‑замена clang кросс‑собраны под ARM64 и зашиты в APK как файлы lib/arm64-v8a/*.so — Android распаковывает всё из lib/<abi>/*.so с правом на исполнение, это легальный, документированный приём (не эксплойт), который снимает необходимость в root или Termux для запуска. Внутри приложения запустить вызывается как обычная команда ОС — на Android это работает, вопреки более раннему предположению в этом материале, что прямого доступа к оболочке нет. Подтверждено adb logcat, многократно: полный цикл компиляция → линковка → запуск результата, целиком на устройстве, с настоящим выводом собранной программы.
⚠️ Единственное, что по‑прежнему честно не проверено вживую — сборка этой возможности ВМЕСТЕ с интерфейсом панели‑приложения (компиляция и запуск по отдельности проверены, собранные воедино — ещё нет).
Экспериментальная поддержка bare‑metal
Запуск без операционной системы, на голом железе. Раньше программа работала без libc, но запускалась ядром Linux. Здесь — прошивка (BIOS/UEFI) загружает программу напрямую.
Архитектурное решение — Multiboot вместо своего загрузчика. Проверено через QEMU.
Сеть — TCP‑сокеты и HTTP‑сервер
Прямой доступ к POSIX‑сокетам через FFI. Не встроенный HTTP‑клиент — а «кирпичики» (socket, connect, send, recv, close, bind, listen, accept, setsockopt), из которых HTTP собирается на самой Сфере.
HTTP‑клиент
подключить сеть; целое32 сокет = создать_сокет(целое32(2), целое32(1), целое32(0)); // connect, send, recv — всё на самой Сфере
Проверено: настоящая программа на Сфере подключилась к локальному HTTP‑серверу, получила 240 байт — точно совпало с суммой заголовков (185) и тела (55).
HTTP‑сервер
Полноценный сервер: принимает подключение, читает запрос, извлекает первую строку через найти/подстрока, отвечает валидным HTTP‑ответом. Маршрутизация — разные ответы для разных путей. Параллельная обработка подключений через параллельно.
Проверено объективно: curl получил корректные ответы от всех маршрутов. Три параллельных запроса — три разных ответа, одновременно.
Ограничения
Только TCP, нет UDP
Нет DNS‑резолвинга — только числовой IP
Нет HTTPS/TLS
Нет URL‑декодирования
Графика и UI (SDL2)
Библиотека графика.код — обёртка над SDL2 через FFI. Подключается как обычный .код‑файл: подключить графика;.
Что работает:
Создание окна, холста, закраска экрана
Обработка событий — клики мыши, ввод текста, нажатия клавиш
Кнопки, панели, поле ввода текста с поддержкой кириллицы
Шрифты через SDL_ttf (DejaVuSans, поддержка кириллицы)
Отрисовка BMP‑картинок
удалить_символ()корректно удаляет один символ, а не байт — важно для UTF-8 (кириллица — 2 байта на символ)
Проверено: через xdotool под виртуальным дисплеем отправлены клики мыши — программа поймала оба, координаты совпали байт‑в‑байт.
Все имена функций — кириллические: инициализировать, создать_окно, создать_холст, задать_цвет, опросить_событие, выделить_память.
SQLite через FFI
база_данных.код — обёртка над SQLite через FFI. Параметризованные запросы, подготовленные выражения, привязка параметров через ?:
подключить база_данных; результат<текст> бд_результат = открыть_базу("пример.db"); текст бд = бд_результат.значение; выполнить_sql(бд, "CREATE TABLE IF NOT EXISTS пользователи (id INTEGER PRIMARY KEY, имя TEXT);"); // параметризованный запрос — безопасно, без инъекции
Стратегическая цель — собственное хранилище, без внешних зависимостей (см. ниже). SQLite — временное, стартовое решение.
Собственное хранилище данных
хранилище.код — полностью написано на Сфере, без единой внешней библиотеки. Формат — append‑only лог (как в Bitcask): каждое изменение дописывается в конец файла, ничего не перезаписывается на месте.
подключить хранилище; сохранить("данные.db", "имя", "Никита"); результат<текст> имя = получить("данные.db", "имя"); если (имя.успешно) { вывести(имя.значение); } // "Никита"
Поиск сканирует файл целиком (
O(n)) — простое первое решениеИндекс в памяти — ускоряет повторные запросы
Компактификация (
сжать_хранилище) — пересканирует файл, оставляет только свежие версии ключей, удаляет tombstone‑записи
Проведён честный аудит памяти всего проекта под valgrind: обнаружена одна маленькая (22 байта / 2 блока) утечка именно в сжать_хранилище, сознательно оставленная неисправленной — любая ручная попытка её устранить провоцировала настоящий double‑free из‑за скрытой эвристики автоматического освобождения памяти в компиляторе. Задокументировано честно, а не тихо исправлено «на глаз».
Не полноценная реляционная база — нет SQL, индексов, транзакций. Честное, стартовое решение.
Отслеживание: логи, метрики, трейсинг
Система наблюдаемости языка — три модуля.
Логи (лог.код) — уровни (отладка/инфо/предупреждение/ошибка/критическая), вывод в консоль и/или файл, метка времени (собственная реализация без вызова strftime/localtime, тем же алгоритмом, что и в <chrono> C++20):
подключить лог; Логгер логгер = создать_логгер(уровень_инфо(), да, "приложение.log"); лог_ошибка(логгер, "не удалось подключиться к базе данных"); // [2026-09-29 19:42:46] ОШИБКА: не удалось подключиться к базе данных
Метрики (метрики.код) — счётчики и измерители, модель как в Prometheus/StatsD:
подключить метрики; Метрики м = создать_метрики(); счётчик_увеличить_на_единицу(м, "запросы_всего"); измеритель_установить(м, "открытых_соединений", 5.0); метрики_напечатать(м);
Трейсинг (трейсинг.код) — именованные «спаны» с вложенностью, для измерения длительности участков кода:
подключить трейсинг; спан_начать(т, "обработать_запрос"); спан_начать(т, "прочитать_бд"); спан_завершить(т); спан_завершить(т);
Честно, узко (v1): время всегда UTC, без часовых поясов; поддержаны только даты после 1970 года; нет структурированных полей и вывода в JSON — только человекочитаемая строка.
Стандартная библиотека
Текст
Функция |
Описание |
|---|---|
|
Длина строки в байтах |
|
Часть строки → |
|
Проверка вхождения → |
|
Позиция вхождения → |
|
В верхний регистр |
|
В нижний регистр |
|
Разбор числа → |
Преобразование в текст
Функция |
Описание |
|---|---|
|
Число → текст |
|
Дробное → текст |
|
|
Математика
Функция |
Описание |
|---|---|
|
Модуль числа |
|
Меньшее из двух |
|
Большее из двух |
Ввод‑вывод
Функция |
Описание |
|---|---|
|
Чтение строки из stdin |
|
Чтение файла → |
|
Запись в файл → |
|
Выполнение команды ОС → |
|
Размер файла в байтах |
|
Чтение части файла |
|
Переименование файла |
|
Аргументы CLI → |
Низкоуровневые
Функция |
Описание |
|---|---|
|
Чтение 4 байт из буфера |
|
Запись 4 байт в буфер |
|
Чтение строки по адресу |
|
Чтение одного байта (0–255) |
|
Чтение 8-байтового указателя |
Сайт на самом себе
В репозитории есть сайт/ — первый сайт языка, работающий на самой Сфере. HTTP‑сервер + хранилище данных, три страницы, CSS, счётчик посещений. Никакой внешней веб‑инфраструктуры — только код на Сфере.
Первая программа, подключающая несколько модулей стандартной библиотеки одновременно (сеть.код + хранилище.код). По пути обнаружен и исправлен конфликт повторных объявлений FFI‑функций — создан общий модуль память.код.
Инструменты разработчика
Помимо самого компилятора и пакетного менеджера, есть полноценный языковой сервер (LSP) для VS Code — диагностика ошибок, автодополнение, переход к определению — переиспользующий тот же лексер/парсер/семантику, что и сам компилятор (не отдельная, рассинхронизирующаяся копия правил языка), а также готовое расширение подсветки синтаксиса (sfera-vscode).
Известные ограничения
Честный список — что пока не работает или работает не полностью:
Каналы: только целые/указатели/текст, одноразовая передача, без закрытия (
.освободить()есть, но неполученный текст на момент освобождения всё равно утечёт)параллельно детерминировано: нет настоящей параллельности между потоками партии (строго по очереди);для каждогоне точка передачи хода; барьер регистрации партии — только для «плоской» последовательности в одном блоке; максимум 256 потоков на партиюМногопараметрические generic‑структуры (
Пара<А, Б>) — методы шаблона типизируются сразу для всех инстанциаций: метод, годный только для части конкретных типов, провалит компиляцию, даже если он не вызывается на «плохой» инстанциации (то же ограничение уже было и уКоробка<Т>с одним параметром)Опциональный тип (
тип?) — только скалярные типы (Tне может быть текстом/структурой), работает только для локальных переменных (не параметр, не поле, не тип возврата), нет?‑оператора автораспространенияМассив массивов (
тип[N][M]) — только скалярные элементы,Точка[2][2]не поддержанНет DNS‑резолвинга и URL‑декодирования для сети
Отладчик не показывает значения переменных по имени (только сырые байты) — нужен полноценный DWARF‑тип для каждого типа языка, отдельная крупная задача
Пакетный менеджер: разрешение версий работает только в формате
X.Y.Z(без пререлизов и диапазонов>=/<=/hyphen из npm) и только для пакетов с одного git‑адреса; публичный индекс — пока только git‑репозиторий со списком, не полноценный веб‑сервис (осознанно временное решение)Windows‑установщик (
установить-windows.ps1) написан, но не проверен вживую на реальной машине (разработка идёт без доступа к Windows); то же — для сборки самого компилятора под Android ВМЕСТЕ с интерфейсом панели‑приложения (кросс‑компиляция обычных программ под Android уже проверена на устройстве, см. выше)Логи/метрики/трейсинг: время только UTC, нет структурированных полей и JSON‑вывода
Дорожная карта развития
Ближайшее
Задача |
Что даёт |
Сложность |
|---|---|---|
Ленивая мономорфизация методов generic‑структур (по факту вызова) |
Меньше ложных ошибок компиляции |
Высокая |
Каналы: структуры, закрытие |
Полноценная многопоточность |
Высокая |
|
Полнее покрытый детерминированный планировщик |
Средняя |
Барьер регистрации партии вне «плоских» блоков (внутри |
Надёжнее детерминизм в динамических сценариях |
Высокая |
DNS‑резолвинг |
Реальная сетевая работа |
Средняя |
URL‑декодирование |
Корректный HTTP‑сервер |
Низкая |
Linux‑установщик |
Доступ для разработчиков |
Низкая |
Дальше
Задача |
Что даёт |
Сложность |
|---|---|---|
Парсер CLI‑флагов |
CLI‑приложения |
Средняя |
Просмотр переменных в отладчике по имени |
Производственная разработка |
Высокая |
Вывод типа обобщённых функций |
Удобство generics |
Средняя |
Полноценный веб‑сервис для реестра пакетов |
Настоящая экосистема, как crates.io/npm |
Высокая (нужен домен и сервер) |
Структурированные логи (JSON, часовые пояса) |
Продакшн‑наблюдаемость |
Средняя |
Тестирование на Эльбрус/Байкал |
Господдержка |
Высокая |
Telegram‑канал |
Обратная связь |
— |
Итог
«Сфера» — не игрушка и не академический эксперимент. Это рабочий компилятор с LLVM‑бэкендом, типизацией, ARC, обобщёнными типами, контрактами, многопоточностью (включая опциональный детерминированный планировщик), FFI, сетью, графикой, собственным хранилищем данных, логами/метриками/трейсингом, отладчиком с полноценным DWARF и своим пакетным менеджером с публичным индексом. Язык, на котором написан собственный сайт (скоро будет со своим доменом в публичном доступе).
Ограничения честно задокументированы — ARC не покрывает упаковку в контракт, каналы ограничены, версии пакетов пока не разрешаются автоматически. Но фундамент серьёзный, и темп развития — впечатляющий.
Если интересно — репозиторий: github.com/nikitosklim/Sfera. Лицензия MIT.
Комментарии (6)

a-tk
05.10.2026 07:23Зачем нужен русский язык, если для ввода всё равно надо постоянно переключаться на английскую раскладку?

qrasik
05.10.2026 07:23К слову, фигурные скобки — это хорошо, но для их набора приходится переключать раскладку, и не только ради них одних. Понятно, что можно сказать, будто это код только для чтения, но нет: если бы нужен был код только для чтения, то можно было бы русифицировать Питон или Ржавчину.

Diacut
05.10.2026 07:23Лексика это не синтаксис.
Не взлетит
Даже лексику заменить на привычную, всё равно не взлетит

atues
05.10.2026 07:23Конечно, "понос" - термин, понятный всем русскоговорящим. Но врач все равно напишет: "диарея". Так что за усердие можно похвалить, но использовать это поделие как инструмент - извините )))
ZamirHa
А зачем все это?