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