Введение

На GitHub лежит репозиторий Сфера — это полностью кириллический язык программирования. Компилятор написан на Rust, код генерируется через LLVM 18 в нативный машинный код. Не интерпретатор, не транспайлер в C — а настоящий компилятор: лексер → парсер → LLVM IR → машинный код.

Язык прошёл путь от «вывести строку» до собственного HTTP‑сервера, хранилища данных, системы логов/метрик/трейсинга, графики через SDL2, полноценного отладчика с DWARF, пакетного менеджера со своим публичным индексом и — последнее по времени добавление — опционального детерминированного планировщика потоков. Эта статья — это честный разбор на данный промежуток времени: что работает, как устроено и чего пока нет.


Синтаксис

Полностью кириллический — все ключевые слова, имена встроенных функций и типов пишутся кириллицей. Не «русский Pascal» и не «русский Python» — свой синтаксис, по духу ближе к Swift/Rust.

Концепция

Сфера

Swift

Rust

Функция

функция

func

fn

Переменная

целое х = 5;

var x = 5

let x = 5;

Константа

неизменно целое х = 5;

let x = 5

const x = 5;

Если

если (х > 0) { }

if x > 0 { }

if x > 0 { }

Иначе

иначе { }

else { }

else { }

Цикл while

пока (х < 10) { }

while x < 10 { }

while x < 10 { }

Возврат

вернуть х;

return x

return x;

Структура

структура

struct

struct

Интерфейс

контракт

protocol

trait

Ошибка

результат<T>

Result<T, E>

Result<T, E>

Self

это

self

self

Bool

верно (да/нет)

Bool (true/false)

bool (true/false)

Программа «Привет, мир»:

функция главная() -> целое {
    вывести("Привет, мир!");
    вернуть 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. Счётчик партии не сбрасывался между батчами — вторая партия зависала навсегда.

  2. Запись родителя в общий флаг активности потока — без мьютекса, редкая (~1 из 5 запусков) гонка видимости памяти, тоже дедлок.

  3. Самый тонкий: быстрый поток мог полностью отработать и покинуть эстафету ДО того, как родитель успевал зарегистрировать следующего участника — не зависание, а молчаливая потеря самого чередования (Г0 Г1 Г2 Д0 Д1 Д2 вместо честного Г0 Д0 Г1 Д1 Г2 Д2). Исправлено барьером регистрации всей партии.

Честные ограничения v1: для каждого не является точкой передачи (обходной путь — пока со счётчиком); статический барьер регистрации партии работает только для последовательности параллельно детерминировано, идущей сразу друг за другом в одном блоке (не внутри если/цикла — там действует более слабый, задокументированный запасной вариант); максимум 256 потоков на партию; главный поток в эстафете не участвует.


Управление памятью (ARC)

Структуры управляются автоматически через подсчёт ссылок — free/delete писать не нужно. Текст в подавляющем большинстве практических случаев тоже под управлением ARC: локальные переменные, конкатенация, текстовые поля структур, результат функции/метода без промежуточной переменной (вывести(целое_в_текст(42)); теперь не течёт), результаты Список<текст>/Словарь<К,З>, элементы, переданные напрямую в .добавить()/.положить(), и текст, полученный через канал<текст>.получить().

✅ Ранее здесь был честно задокументирован отдельный случай — структура, упакованная в контракт (не крах, только утечка). С тех пор исправлен (см. раздел про контракты выше) — сейчас известных дыр в ARC не осталось.


Модули

подключить утилиты;

Поиск по трём уровням:

  1. {имя}.код рядом с подключающим файлом

  2. {корень_проекта}/пакеты/{имя}.код (в т.ч. составное имя пакеты/ИМЯ_ПАКЕТА/файл.код — см. пакетный менеджер ниже)

  3. Системная стандартная библиотека (при установке через собрать.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 → Список<текст>

Низкоуровневые

Функция

Описание

прочитать_целое32(буфер, смещение)

Чтение 4 байт из буфера

записать_целое32(буфер, смещение, значение)

Запись 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)


  1. ZamirHa
    05.10.2026 07:23

    А зачем все это?


  1. Diversus
    05.10.2026 07:23

    Такое уже давно есть https://oscript.io/, но это для 1Сников


  1. a-tk
    05.10.2026 07:23

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


  1. qrasik
    05.10.2026 07:23

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


  1. Diacut
    05.10.2026 07:23

    1. Лексика это не синтаксис.

    2. Не взлетит

    3. Даже лексику заменить на привычную, всё равно не взлетит


  1. atues
    05.10.2026 07:23

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