На пороге 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)


  1. rsashka
    24.08.2026 21:24

    Вы какой язык пытаетесь улучшить C, C++? И улучшить в чем? Синтаксис, скорость, безопасность, совместимость или что-то еще?


    1. Denis535 Автор
      24.08.2026 21:24

      Я просто размышлял каким могбы быть Си, если бы можно было вернуться в прошлое и исправить все его ошибки.


      1. Kyoki
        24.08.2026 21:24

        А что вас останавливает на пороге 2027 года? Есть кучка генераторов парсеров, есть llvm, к которому можно прицепить фронтэнд парсер, есть ии, который и парсер напишет, и компилятор целиком...


        1. Denis535 Автор
          24.08.2026 21:24

          Для начала было бы хорошо написать семантическую модель для языка. Но я слабо представляю как реализовать некоторые вещи.


  1. black_warlock_iv
    24.08.2026 21:24

    Rust: просто существует.


    1. Dmitri-D
      24.08.2026 21:24

      Единственный недостаток Раста - это порог входа (хотя, может, и преимущество - кому как).
      Начать кодировать C можно просто прочитав по диагонали первую главу - в сущности это язык, котоым должен был бы быть бейсик. А вот с Растом такой фокус не выйдет -- ровно потому, что он дает намного больше чем тут запрашивалось. К примеру, тут не запрашивалась асинхронность и многопоточность - это встроено в Раст. Начинаете сразу путаться со ссылками, с константыми и не константными, и не врубаетесь за что от нас хотят столько клонов.


      1. Seraphimt
        24.08.2026 21:24

        Начать кодировать C можно просто прочитав по диагонали первую главу

        На мой взгляд, это обманчиво. Синтаксис у С может и простой, но чтобы писать нормально вам понадобится изучить кучу всего сверх - зазубрить список ub, следовать различным best practices, помнить кучу различных тонких мест, четко следить за множеством неявных конструкций типа кастов и тд. Это не говоря о разных стандартах и система х сборки, что по сути отдельный язык в нагрузку.


      1. Lewigh
        24.08.2026 21:24

        Начать кодировать C можно просто прочитав по диагонали первую главу - в сущности это язык, котоым должен был бы быть бейсик. 

        Если цель написать Hello world то да. Но уже на втором шаге:

        • порядок объявления функций важен

        • на ровном месте словили UB

        • типы не типы

        • веселуха с заголовками

        • веселуха с указателями

        • на ровном месте словили UB

        • на ровном месте словили UB

        • сломали память

        • из стандартных структур данных только массив, строк нет а числовые типы вам вообще ничего не обещают, удачи с подключением библиотек

        В то время как в Rust порог действительно высокий, но явный. Основная сложность - побои и оскорбления со стороны компилятора. Если программа собралась - она будет работать. Плюс Rust современный и следовательно и стандартная библиотека богатая, и проект собирать не сложно и подключать библиотеки просто и не нужно думать как тип или библиотека будет себя вести на этой системе.

        Я думаю нет простых системных языков. Разница просто в чем сложность.


        1. ardraeiss
          24.08.2026 21:24

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


          1. becefi
            24.08.2026 21:24

            Это надо быть совсем безнадежным.


            1. ardraeiss
              24.08.2026 21:24

              Речь же про C. А там - достаточно пройтись в цикле по строке как массиву, промахнувшись в счётчике на единицу - и вот влезание в чужую область памяти. Пока изучал как программировать в такое наступал даже не знаю сколько раз.

              Или выделить памяти на нуль-терминейтед строку, забыв учесть хвостовой нуль, но потом попытаться его в строку всё же запихать.

              Или попытаться разыменовать переданный в функцию указатель, передав по ошибке неициализированный, когда только учишься с ними работать.

              Или начать разбираться "что за зверь void* и что он даёт?" и вместо char* его как int* обходить...

              В стародавние времена могло даже и не упасть. А могло сходу ребутнуться, по классике. (но да, это скорее "UB по порче памяти", чем просто порча памяти)


      1. Dhwtj
        24.08.2026 21:24

        Rust показывает явно сложность программирования.

        C, Go внешне просты, но держатся на такой дисциплине что у растоманов волосы на ж шевелятся.


      1. Akon32
        24.08.2026 21:24

        Единственный недостаток Раста - это порог входа

        Ничто не мешает вайбкодить, даже на расте.


    1. MinimumLaw
      24.08.2026 21:24

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


  1. SlFed
    24.08.2026 21:24

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


    1. Groramar
      24.08.2026 21:24

      Это для однопроходных типа Паскаля. Для многопроходных такой проблемы обычно нет.


      1. Byakaha
        24.08.2026 21:24

        forward просто существует


        1. Akon32
          24.08.2026 21:24

          Это для любителей излагать мысли дважды.


    1. Strijar
      24.08.2026 21:24

      Rust именно так и позволяет делать


    1. simplegamesstudio
      24.08.2026 21:24

      Однопроходный компилятор


  1. MrArtem222
    24.08.2026 21:24

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


  1. kipar
    24.08.2026 21:24

    У меня нет директив для инклюдов или импортов. На мой взгляд, такое лучше настраивать в конфигурации проекта, а не в каждом файле

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


  1. Biga
    24.08.2026 21:24

    Упомяну до кучи ещё одну большую боль C/C++ - это макроподстановки (#define). Они очень сильно портят жизнь IDE, усложняют автоматический рефакторинг.

    В то же время, макроподстановки имеют широкое применение для конфигурирования проекта, и не только.

    Для нового языка как минимум нужно предложить какие-то решения на замену макроподстановкам. Что-то для compile-time настроек.
    И даже что-то, частично заменяющее кодогенерацию (например, в D пытались реализовать миксины). Хотя подозреваю, что любая кодогенерация опять испортит рефакторинг. В общем, это непростое дело.


  1. pda0
    24.08.2026 21:24


  1. Akon32
    24.08.2026 21:24

    В наше время язык программирования скорее должен быть заточен под LLM...

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


  1. feelamee
    24.08.2026 21:24

    Возможно, проблема в том, что разработчики любят чрезмерно экспериментировать, мечтая придумать что‑то лучшее. […]

    Каким должен быть идеальный язык? В моем представлении идеальный системный язык должен быть классическим и простым. Чтобы потом не возникало сожалений о каких‑то сомнительных решений. И конечно же, чтобы каждый мог за час разобраться в нем.

    Мне кажется или вы в своем дизайне сделали все да наоборот? И поэксперементировали и ушли от классики и простоты.

    Вот если бы вы сделали точь в точь Си, но поправили ошибки, которые описали вначале. Вот тогда это было бы “классически” и “просто” и: каждый мог за час разобраться в нем


    1. Denis535 Автор
      24.08.2026 21:24

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


  1. damirstash34
    24.08.2026 21:24

    Разве вы не сделали то, за что осуждали другие языки? Поэкспериментировали. Во многом получился язык похожий на Java/Kotlin, но с добавлением ваших наработок или идей.

    Также, раз уж мы продумываем "идеальный системный язык", то важным аспектом будет backend составляющая. Как язык управляет памятью, как предотвращает use-after-free? Раз уж вы используете классы, наверняка нужны будут деструкторы, операторы копирования и movement-а. Подобных вопросов еще очень много может быть, системные языки очень сложные и обязывают иметь четкую модель компилятора.

    Не поймите меня неправильно, я не стараюсь осудить. Наоборот я очень рад что вы заинтересовались созданием языков программирования. Просто пытаюсь сказать, что делать сразу "идеальный" язык требует продумывания очень многих аспектов.


    1. Denis535 Автор
      24.08.2026 21:24

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