На пороге 2027 год, и с момента появления языка Си прошло уже более 50 лет… Но, что удивительно, за все эти годы так и не появилось идеальной альтернативы этому языку. Конечно, попытки создать новые системный язык были, но каждый раз что‑то шло не так. Возможно, проблема в том, что разработчики любят чрезмерно экспериментировать, мечтая придумать что‑то лучшее. Но, к сожалению, это не всегда приводит к чему‑то лучшему. Поэтому, ради интереса, я попробовал разработать дизайн своего «идеального» языка. И теперь я хочу рассказать о том, что у меня получилось.
Каким должен быть идеальный язык? В моем представлении идеальный системный язык должен быть классическим и простым. Чтобы потом не возникало сожалений о каких‑то сомнительных решений. И конечно же, чтобы каждый мог за час разобраться в нем.
Для начала нужно понять какие были проблемы в Си:
Неявная деградация массива в указатель на первый элемент. Когда мы передаем массив в метод, то было бы ожидаемо, чтобы массив копировался как структура. Но синтаксис откровенно обманывает нас.
Неправильный приоритет операторов. Проблема в том, что компилятор будет рассматривать выражение
a & b == cкакa & (b == c), хотя мы точно ожидали другого.Символ минуса не является частью литерала. Компилятор будет рассматривать число
-2,147,483,648фактически как-1 * 2,147,483,648. Проблема в том, что2,147,483,648превышает максимально значение (2,147,483,647) целого числа. К сожалению, это тот случай, когда идеального решения проблемы не существует.Использование препроцессора для констант. Проблема в том, что такие константы не имеют типов. В итоге, когда Котлин (или другой язык) создает биндигни для Си библиотеки, то он не всегда правильно угадывает типы этих констант.
Также нужно понять, что могло бы быть лучше:
Null‑безопасность.
Иммутабельность по умолчанию. На мой взгляд, было бы логичнее писать mut, а не const или readonly.
Теперь я постараюсь учесть эти замечания и не добавить новых проблем.
Compilation unit
Единица компиляции может содержать декларации и директивы, которые в свою очередь, состоят из других синтаксических единиц.
Declarations
С помощью деклараций, вы можете объявлять сущности (symbols): пространства имен, типы и свободные члены. А внутри типов, вы можете объявлять члены этих типов.
Заметьте, что у меня есть классы, но нет интерфейсов. Я считаю, что абстрактные классы, свойства и методы необходимы. А вот вместо интерфейсов можно использовать адаптеры.
namespace Ident; namespace Ident {} template[T] class Ident : type { type Ident; struct {} Ident; union {} Ident; struct {} union {} prop type Ident() {} func type Ident(type ident) {} } template[T] struct Ident { type Ident; struct {} Ident; union {} Ident; struct {} union {} prop type Ident() {} func type Ident(type ident) {} } template[T] union Ident { type Ident; struct {} Ident; union {} Ident; struct {} union {} prop type Ident() {} func type Ident(type ident) {} } template[T] enum Ident : type { Ident = literal, Ident(type) = literal, } template[T] extension Ident { prop type Ident() {} func type Ident(type ident) {} } const type Ident; // field type Ident; template[T] prop type Ident() {} template[T] func type Ident(type ident) {} // parameter type ident; type* ident; out type* ident; inout type* ident; ...
Directives
С помощью директив, вы можете давать компилятору дополнительные инструкции.
template[T] typealias type = type[T];
Statements
С помощью инструкций, вы можете описывать алгоритмы или данные. Стейтменты — это инструкции, которые не возвращают никакого значения.
Заметьте, сейчас в некоторых языках делают, чтобы любая инструкция могла возвращать значение. Но, на мой взгляд, это приводит лишь к зоопарку в коде, поэтому я бы предпочел избежать этого.
// Declarations const type ident = literal; let mut type ident = expr; prop type ident() {} func type ident(type ident) {} // Operators if (expr) {} else {} while (expr) {} do {} while (expr) for (statement; expr; statement) {} foreach (statement in expr) {} switch (expr) {} case literal {} default {} continue; break; return; return expr; // Utils scope {} _ = expr;
Expressions
Выражения — это инструкции, которые возвращают какое‑либо значения. Надо сказать, что приоритет операторов — это самая сложная часть моей статьи. И только благодаря ИИ я смог разобраться с этим.
Заметьте, что для удобства принято считать, что void это тоже значение.
// Literals true / false 0 // 0b_0000_0000_0000_0000, 0x00_00_00_00 0:8 0:16 0:32 // by default 0:64 // by default 0:128 // by default 0u // 0b_0000_0000_0000_0000u, 0x00_00_00_00u 0u:8 0u:16 0u:32 // by default 0u:64 // by default 0u:128 // by default 0.0 // 0e1 0.0:32 0.0:64 // by default '...' // \u0000, \U0000_0000 and other '...':8 '...':16 '....':32 // by default "..." "...":8 // by default "...":16 "....":32 """...""" """...""":8 // by default """...""":16 """...""":32 null // Literals / Type { ... } type { ... } type[type] { ... } // Literals / Lambda lambda(type ident) type {} // Identifiers identifier // more details about this below. this base super // Operators Postfix x++ x-- x!! x. x-> x?-> // should it return default or option? x[index] x->[index] x?->[index] // should it return default or option? x[start..<=end] x->[start..<=end] x?->[start..<=end] // should it return default or option? x() Prefix ++x --x +x -x ~x !x ref x deref x Multiplicative x * y x / y x % y Additive x + y x – y Shift x << y x >> y x >>> y Logical AND x & y Logical XOR x ^ y Logical OR x | y Relational And Cast x < y x > y x <= y x >= y x is type x as type Equality x == y x != y Conditional AND x && y Conditional OR x || y Null-coalescing x ?? y Conditional c ? x : y Assignment x = y x *= y x /= y x %= y x += y x -= y x <<= y x >>= y x >>>= y x &= y x ^= y x |= y x ??= y // Expressions / Utils typeof(expr) nameof(type) sizeof(type)
Types
С помощью типов, вы можете ссылаться на сущности типов.
type type[...] ::type ::type[...] namespace::type namespace::type[...]
Identifiers
С помощью идентификаторов, вы можете ссылаться на другие сущности.
identifier ::identifier ::type::identifier namespace::identifier namespace::type::identifier
Modifiers
С помощью модификаторов, вы можете предоставлять компилятору дополнительную информации о ваших сущностях.
// type public — type is exposed internal — type is exposed within its module // member public — member is exposed internal — member is exposed within its module protected — member is exposed for its subclasses protected internal — member is exposed for its subclasses and its module private — member is hidden inside its type // class static — static class abstract — abstract class open — open class // field static — static field volatile — volatile field // func/prop static — static func/prop abstract — abstract func/prop open — open func/prop override — override func/prop final — final func/prop inline — inline func/prop mut — mutable func/prop // parameter out — out parameter inout — in out parameter // local variable static — static variable volatile — volatile variable // local func/prop static — static func/prop // literal static — static literal // misc mut — mutable type or reference
Built‑in types
bool int8 int16 int32 int64 int128 nint uint8 uint16 uint32 uint64 uint128 unint float32 float64 char8 char16 char32 void Array[type, *] List[type]
Built‑in references
type*? // reference on type Proc[type]*? // reference on proc Func[type, type]*? // reference on func [type]*? // reference on array
Built‑in attributes
@Deprecated @Terminal @AllowUnused @AllowDiscard @Check(expr) @Assert(expr) @Before(statement) @After(statement)
Example
Теперь давайте соберем это все в один пример.
namespace Namespace; namespace Namespace { } public abstract class Class : type { public prop type Value() mut { } public static func Class New(type value) { } public func void Free() mut { } public func void Func() mut { } public abstract func void Func() mut; public open func void Func() mut { } public override func void Func() mut { super(); } public override final func void Func() mut { super(); } } public struct Struct { public const type Value = type {}; public static mut type Value; public mut type Value; public mut struct { } Value; public mut union { } Value; struct { } union { } public prop type Value() { } public func void Func() { } } public union Union { public const type Value = type {}; public static mut type Value; public mut type Value; public mut struct { } Value; public mut union { } Value; struct { } union { } public prop type Value() { } public func void Func() { } } public enum Enum : int32 { Value_0 = 0, Value_1(type) = 1, } extension Enum { } public const type Const = type {}; public static type Field = type {}; @Check(expr) @Assert(expr) public static prop type Prop() { return type {}; } @Check(expr) @Assert(expr) public static func void Func() { scope { static let mut type value = type {}; let mut type value = static type {}; let mut type value = type {}; let mut type mut*? value = ref value; // let mut type mut*? value = value[0]; } scope { let mut Array[mut type, *] array = Array[mut type, *] { ... }; // let unint length = array.Length(); // let type item = array[0]; // let type* item = ref array[0]; // array[0] = item; let mut Array[mut type, *] mut*? array = ref array; // let unint length = array!!->Length(); // let type item = array!!->[0]; // let type* item = ref array!!->[0]; // array!!->[0] = item; } scope { let [mut type] mut*? array = array[start..<=end]; let [mut type] mut*? array = array!!->[start..<=end]; // let unint length = array!!->Length(); // let type item = array!!->[0]; // let type* item = ref array!!->[0]; // array!!->[0] = item; } scope { let mut List[mut type] list = List[mut type] { ... }; // let unint size = list.Size(); // let type item = list.Get(0); // list.Set(0, item); let mut List[mut type] mut*? list = ref list; // let unint size = list!!->Size(); // let type item = list!!->Get(0); // list!!->Set(0, item); } scope { let Proc[mut type] mut*? proc = ref Namespace::Type::Proc; let Func[mut type, type] mut*? func = ref Namespace::Type::Func; // proc!!->Invoke(...); // _ = func!!—>Invoke(...); } @Before(let mut type mut* value = Namespace::Type::New()) @After(value->Free()) scope { } if (true) { } else if (true) { } else { } while (true) { continue; break; } do { continue; break; } while (true); for (let mut int32 i = 0, let mut int32 j = 0; i < 10; i++, j++) { continue; break; } foreach (let mut type item in array) { continue; break; } switch (0) { case 0 { } case 1 { } default { } } return; }
Заключение
Я хотел придумать простой язык, который не будет ничем раздражать. Надеюсь мне это удалось, хотя у вас может быть иное мнение.
Отмечу несколько оставшихся вопросов:
Нужны ли конструкторы?
Нужна ли перегрузка методов? Слышал, что в С++ это очень сложная часть языка. Но без этого нельзя реализовать конструкторы.
Нужна ли директива using для упрощенного использования пространств имен? Я хотел, чтобы синтаксис имел полную (в разумных пределах) семантику. То есть, чтобы все идентификаторы были полными. В теории IDE могла бы скрывать лишнее.
Нужно ли автоматическое определение типов переменных? Опять же, IDE могла бы скрывать лишнее.
Нужны ли вариативные шаблоны? Или лучше было бы обойтись вообще без шаблонов?
Нужен ли препроцессор? Некоторые новые языки вообще не имеют препроцессора, но, мне кажется, что это очень спорное решение.
Отмечу несколько важных деталей:
У меня нет директив для инклюдов или импортов. На мой взгляд, такое лучше настраивать в конфигурации проекта, а не в каждом файле.
И еще отмечу, что качественно описать дизайн языка оказалось не так просто. Очень сложно хорошо классифицировать и структурировать все синтаксические единицы. Я старался как мог, но все же у меня осталось чувство, что можно было бы и лучше. К сожалению, никакой информации на это тему я не видел. Только sharplab.io который может показывать синтаксическое дерево C#, благодаря чему я смог хоть немного разобраться что к чему.
Комментарии (29)

black_warlock_iv
24.08.2026 21:24Rust: просто существует.

Dmitri-D
24.08.2026 21:24Единственный недостаток Раста - это порог входа (хотя, может, и преимущество - кому как).
Начать кодировать C можно просто прочитав по диагонали первую главу - в сущности это язык, котоым должен был бы быть бейсик. А вот с Растом такой фокус не выйдет -- ровно потому, что он дает намного больше чем тут запрашивалось. К примеру, тут не запрашивалась асинхронность и многопоточность - это встроено в Раст. Начинаете сразу путаться со ссылками, с константыми и не константными, и не врубаетесь за что от нас хотят столько клонов.
Seraphimt
24.08.2026 21:24Начать кодировать C можно просто прочитав по диагонали первую главу
На мой взгляд, это обманчиво. Синтаксис у С может и простой, но чтобы писать нормально вам понадобится изучить кучу всего сверх - зазубрить список ub, следовать различным best practices, помнить кучу различных тонких мест, четко следить за множеством неявных конструкций типа кастов и тд. Это не говоря о разных стандартах и система х сборки, что по сути отдельный язык в нагрузку.

Lewigh
24.08.2026 21:24Начать кодировать C можно просто прочитав по диагонали первую главу - в сущности это язык, котоым должен был бы быть бейсик.
Если цель написать Hello world то да. Но уже на втором шаге:
порядок объявления функций важен
на ровном месте словили UB
типы не типы
веселуха с заголовками
веселуха с указателями
на ровном месте словили UB
на ровном месте словили UB
сломали память
из стандартных структур данных только массив, строк нет а числовые типы вам вообще ничего не обещают, удачи с подключением библиотек
В то время как в Rust порог действительно высокий, но явный. Основная сложность - побои и оскорбления со стороны компилятора. Если программа собралась - она будет работать. Плюс Rust современный и следовательно и стандартная библиотека богатая, и проект собирать не сложно и подключать библиотеки просто и не нужно думать как тип или библиотека будет себя вести на этой системе.
Я думаю нет простых системных языков. Разница просто в чем сложность.

ardraeiss
24.08.2026 21:24Память, скорее, сломали при первой же попытке работе с нею

becefi
24.08.2026 21:24Это надо быть совсем безнадежным.

ardraeiss
24.08.2026 21:24Речь же про C. А там - достаточно пройтись в цикле по строке как массиву, промахнувшись в счётчике на единицу - и вот влезание в чужую область памяти. Пока изучал как программировать в такое наступал даже не знаю сколько раз.
Или выделить памяти на нуль-терминейтед строку, забыв учесть хвостовой нуль, но потом попытаться его в строку всё же запихать.
Или попытаться разыменовать переданный в функцию указатель, передав по ошибке неициализированный, когда только учишься с ними работать.
Или начать разбираться "что за зверь void* и что он даёт?" и вместо char* его как int* обходить...
В стародавние времена могло даже и не упасть. А могло сходу ребутнуться, по классике. (но да, это скорее "UB по порче памяти", чем просто порча памяти)

Dhwtj
24.08.2026 21:24Rust показывает явно сложность программирования.
C, Go внешне просты, но держатся на такой дисциплине что у растоманов волосы на ж шевелятся.

Akon32
24.08.2026 21:24Единственный недостаток Раста - это порог входа
Ничто не мешает вайбкодить, даже на расте.

MinimumLaw
24.08.2026 21:24... и дай бог ему здоровья и удачи. Оба ему еще неоднократно понадобятся. Особенно когда в коммунити фанатов в разы больше, чем мастеров.

SlFed
24.08.2026 21:24С институтских пор меня напрягала одна особенность компиляторов - почему функция должна быть обязательно описана ДО её вызова. Почему нельзя вставить вызов вот здесь и сейчас, а саму функцию написать позже в конце файла.

MrArtem222
24.08.2026 21:24Насчёт альтернатив C я поспорю, ведь есть Rust, Zig. Но вот задумка языка интересная.

kipar
24.08.2026 21:24У меня нет директив для инклюдов или импортов. На мой взгляд, такое лучше настраивать в конфигурации проекта, а не в каждом файле
Возможность инкрементальной компиляции (когда при изменении одного файла мы пересобираем не весь проект а только зависящие от него файлы) лучше заложить с самого начала, потом добавлять будет больно. И как раз для этого импорты и нужны.

Biga
24.08.2026 21:24Упомяну до кучи ещё одну большую боль C/C++ - это макроподстановки (#define). Они очень сильно портят жизнь IDE, усложняют автоматический рефакторинг.
В то же время, макроподстановки имеют широкое применение для конфигурирования проекта, и не только.
Для нового языка как минимум нужно предложить какие-то решения на замену макроподстановкам. Что-то для compile-time настроек.
И даже что-то, частично заменяющее кодогенерацию (например, в D пытались реализовать миксины). Хотя подозреваю, что любая кодогенерация опять испортит рефакторинг. В общем, это непростое дело.

Akon32
24.08.2026 21:24В наше время язык программирования скорее должен быть заточен под LLM...
Каким он должен быть? Не знаю. Наверно, производительным, как Rust. Надёжность Rust, в т.ч. статическая типизация, похоже (по некоторым отзывам), и для LLM полезна. Хотя, наверно, LLM могли бы использовать и что-то типа ассемблера (могут, но не знаю, насколько это будет эффективно для больших проектов). Есть плюс и в интерпретируемости Python - LLM очень легко пишут скрипты для проверок своих гипотез, и лучше, когда скрипты выполняются мгновенно. Возможно, такой "следующий" язык будет трудночитаемым для человека, но легкочитаемым для ИИ.

feelamee
24.08.2026 21:24Возможно, проблема в том, что разработчики любят чрезмерно экспериментировать, мечтая придумать что‑то лучшее. […]
Каким должен быть идеальный язык? В моем представлении идеальный системный язык должен быть классическим и простым. Чтобы потом не возникало сожалений о каких‑то сомнительных решений. И конечно же, чтобы каждый мог за час разобраться в нем.
Мне кажется или вы в своем дизайне сделали все да наоборот? И поэксперементировали и ушли от классики и простоты.
Вот если бы вы сделали точь в точь Си, но поправили ошибки, которые описали вначале. Вот тогда это было бы “классически” и “просто” и: каждый мог за час разобраться в нем

Denis535 Автор
24.08.2026 21:24Я добавил объектный стиль и аспекты. Получилось что-то среднее между Си и С++. На мой взгляд это золотая середина для многих задач.

damirstash34
24.08.2026 21:24Разве вы не сделали то, за что осуждали другие языки? Поэкспериментировали. Во многом получился язык похожий на Java/Kotlin, но с добавлением ваших наработок или идей.
Также, раз уж мы продумываем "идеальный системный язык", то важным аспектом будет backend составляющая. Как язык управляет памятью, как предотвращает use-after-free? Раз уж вы используете классы, наверняка нужны будут деструкторы, операторы копирования и movement-а. Подобных вопросов еще очень много может быть, системные языки очень сложные и обязывают иметь четкую модель компилятора.
Не поймите меня неправильно, я не стараюсь осудить. Наоборот я очень рад что вы заинтересовались созданием языков программирования. Просто пытаюсь сказать, что делать сразу "идеальный" язык требует продумывания очень многих аспектов.

Denis535 Автор
24.08.2026 21:24Единственное, что у меня можно было бы назвать эксперементальным - это аспекты. Многие другие языки выглядят гораздо более экспериментальными. Управление памятью - ручное. Конструкторов и деструкторов - нет. Возможно стоило бы добавить операторы для явного копирования и перемещения, но, мне кажется, в Си и без них хорошо. В любом случае, я не планирую реализовывать эти идеи.

rsashka
Вы какой язык пытаетесь улучшить C, C++? И улучшить в чем? Синтаксис, скорость, безопасность, совместимость или что-то еще?
Denis535 Автор
Я просто размышлял каким могбы быть Си, если бы можно было вернуться в прошлое и исправить все его ошибки.
Kyoki
А что вас останавливает на пороге 2027 года? Есть кучка генераторов парсеров, есть llvm, к которому можно прицепить фронтэнд парсер, есть ии, который и парсер напишет, и компилятор целиком...
Denis535 Автор
Для начала было бы хорошо написать семантическую модель для языка. Но я слабо представляю как реализовать некоторые вещи.