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