На пороге 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 Ident() type {} method Ident(type ident) type {} } template[T] struct Ident { type Ident; struct {} Ident; union {} Ident; struct {} union {} prop Ident() type {} method Ident(type ident) type {} } template[T] union Ident { type Ident; struct {} Ident; union {} Ident; struct {} union {} prop Ident() type {} method Ident(type ident) type {} } template[T] enum Ident : type { Ident = literal, Ident(type) = literal, } template[T] extension Ident { prop Ident() type {} method Ident(type ident) type {} } // const const type Ident; // field type Ident; // prop template[T] prop Ident() type {} // method template[T] method Ident(type ident) type {} // parameter type ident; variadic type ident; type* ident; out type* ident; inout type* ident;
Directives
С помощью директив, вы можете давать компилятору дополнительные инструкции.
template[T] typealias type = type[T];
Statements
С помощью инструкций, вы можете описывать алгоритмы или данные. Стейтменты — это инструкции, которые не возвращают никакого значения.
Заметьте, сейчас в некоторых языках делают, чтобы любая инструкция могла возвращать значение. Но, на мой взгляд, это приводит лишь к зоопарку в коде, поэтому я бы предпочел избежать этого.
// Declarations const type ident = literal; let type ident = expression; _ = expression; prop ident() type {} method ident(type ident) type {} // Operators if (expression) {} else {} switch (expression) {} case literal {} default {} while (expression) {} do {} while (expression) for (statement; expression; statement) {} foreach (statement in expression) {} continue; break; try {} finally {} return; return expression; // Utils scope {}
Expressions
Выражения — это инструкции, которые возвращают какое‑либо значения. Надо сказать, что приоритет операторов — это самая сложная часть моей статьи. И только благодаря ИИ я смог разобраться с этим.
// 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 0:native 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 0u:native 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 // Operators/Postfix x++ x-- x!! x. x-> x?-> // should it return default or option? x[[index]] x?[[index]] // should it return default or option type? 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() // Operators/Prefix ++x --x +x -x ~x !x ref x deref x // Operators/Multiplicative x * y x / y x % y // Operators/Additive x + y x – y // Operators/Shift x << y x >> y x >>> y // Operators/Logical AND x & y // Operators/Logical XOR x ^ y // Operators/Logical OR x | y // Operators/Relational And Cast x < y x > y x <= y x >= y x is type x as type // Operators/Equality x == y x != y // Operators/Conditional AND x && y // Operators/Conditional OR x || y // Operators/Null-coalescing x ?? y // Operators/Conditional c ? x : y // Operators/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 // Utils typeof(expression) 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/member public — type/member is exposed internal — type/member is exposed only 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 // method/prop static — static method/prop abstract — abstract method/prop open — open method/prop override — override method/prop final — final method/prop inline — inline method/prop mut — mutable method/prop // local variable persistent — persistent variable volatile — volatile variable // local method/prop static — static method/prop inline — inline method/prop mut — mutable method/prop // parameter out — out parameter inout — in out parameter // type mut — mutable type // reference mut — mutable reference // literal persistent — persistent literal
Built‑in types
Bool Int8 Int16 Int32 Int64 Int128 NativeInt UInt8 UInt16 UInt32 UInt64 UInt128 NativeUInt Float32 Float64 Char8 Char16 Char32 mut Any mut Array[mut type, *] mut List[mut type]
Built‑in references
type mut*? Proc[type] mut*? Func[type, type] mut*? [type] mut*? // array (slice) reference
Built‑in attributes
@Deprecated @Terminal @AllowUnused @AllowDiscard @Conditional(expression) @Check(expression) @Assert(expression) @Before(statement) @After(statement)
Example
Теперь давайте соберем это все в один пример.
namespace Namespace; namespace Namespace { } public static class Class { public const type Value = type {}; public static type Value = type {}; public static struct {} Value = {}; public static union {} Value = {}; } public abstract class Class : type { public type Value; public struct {} Value; public union {} Value; struct { } union { } public prop Value() type { return this->Value; } public static method New(type value) Class { return Class { Value = value }; } public static method New(type value) mut Class* { return ...; } public mut method Free() { } public method Method() {} public abstract method Method(); public open method Method() {} public override final method Method() { super(); } } public struct Struct { } public union Union { } public enum Enum : int32 { Value_0 = 0, Value_1(type) = 1, } extension Enum { } public const type Const = type {}; public type Field = type {}; @Check(expression) @Assert(expression) public prop Prop() type { return type {}; } @Check(expression) @Assert(expression) public method Method() { { const type value = type {}; let type value = type {}; persistent let type value = type {}; let type value = persistent type {}; _ = type {}; public prop Prop() type { return type {}; } public method Method() { } } { let type value = type {}; let type* value = ref value; // let type* value = value[[0]]; } { let Array[type, *] array = Array[type, *] { ... }; // let unint length = array.Length(); // let type item = array[0]; // let type* item = ref array[0]; // array[0] = item; let Array[type, *]* array = ref array; // let unint length = array->Length(); // let type item = array->[0]; // let type* item = ref array->[0]; // array->[0] = item; } { let [type]* array = array[start..<=end]; let [type]* array = array->[start..<=end]; // let unint length = array->Length(); // let type item = array->[0]; // let type* item = ref array->[0]; // array->[0] = item; } { let List[type] list = List[type] { ... }; // let unint size = list.Size(); // let type item = list.Get(0); // list.Set(0, item); let List[type]* list = ref list; // let unint size = list->Size(); // let type item = list->Get(0); // list->Set(0, item); } { let Proc[type]* proc = lambda(type ident) {} let Func[type, type]* proc = lambda(type ident) type {} // proc->Invoke(...); // _ = func—>Invoke(...); } { let Proc[type]* proc = ref Namespace::Type::Proc; let Func[type, type]* func = ref Namespace::Type::Func; // proc->Invoke(...); // _ = func—>Invoke(...); } if (true) { } else if (true) { } else { } switch (0) { case 0 { } case 1 { } default { } } 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 type item in array) { continue; break; } try { let type* value = Namespace::Type::New() return; } finally { value->Free() } @Check(expression) @Assert(expression) @Before(let type* value = Namespace::Type::New()) @After(value->Free()) scope { } }
Заключение
Я хотел придумать простой язык, который не будет ничем раздражать. Надеюсь мне это удалось, хотя у вас может быть иное мнение.
Отмечу несколько оставшихся вопросов:
Нужны ли конструкторы?
Нужна ли перегрузка методов? Слышал, что в С++ это очень сложная часть языка. Но без этого нельзя реализовать конструкторы.
Нужна ли директива using для упрощенного использования пространств имен? Я хотел, чтобы синтаксис имел полную (в разумных пределах) семантику. То есть, чтобы все идентификаторы были полными. В теории IDE могла бы скрывать лишнее.
Нужно ли автоматическое определение типов переменных? Опять же, IDE могла бы скрывать лишнее.
Нужны ли вариативные шаблоны? Или лучше было бы обойтись вообще без шаблонов?
Нужен ли препроцессор? Некоторые новые языки вообще не имеют препроцессора, но, мне кажется, что это очень спорное решение.
Отмечу несколько важных деталей:
У меня нет директив для инклюдов или импортов. На мой взгляд, такое лучше настраивать в конфигурации проекта, а не в каждом файле.
И еще отмечу, что качественно описать дизайн языка оказалось не так просто. Очень сложно хорошо классифицировать и структурировать все синтаксические единицы. Я старался как мог, но все же у меня осталось чувство, что можно было бы и лучше. К сожалению, никакой информации на это тему я не видел. Только sharplab.io который может показывать синтаксическое дерево C#, благодаря чему я смог хоть немного разобраться что к чему.

