На пороге 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#, благодаря чему я смог хоть немного разобраться что к чему.

