В этом посте мне хотелось бы порассуждать на тему того что я бы поменял/убрал/добавил в язык Си. Си является моим основным языком, более того уже больше 4 лет я занимаюсь разработкой оптимизирующего компилятора Си, который пишется на Си. За это время у меня накопились некоторые мысли на тему того что должен и чего не должен современный язык, какие проблемы есть в Си, как их можно было бы решить.
Для начала объясню чем мне нравится Си - он простой, предсказуемый, быстрый язык, который позволяет взять и сделать то что тебе нужно. Если писать по стандарту, то вероятность огрести неадекватную плоходиагностируемую проблему мала. Но к сожалению, Си позволяет писать не по стандарту, что приводит к большому количеству проблем. Более того некоторые пункты стандарта написаны не лучшим образом, некоторые его пункты не отвечают современным требованиям. Поэтому мне хотелось бы сохранить простоту и скорость языка, устранив из него устаревшие или неправильные на мой взгляд моменты. Возможно при этом добавить ещё несколько простых возможностей.
Сначала пройдёмся по уже существующим возможностям. Чтобы упростить чтение подчёркиванием я выделил основной вердикт по тому или иному пункту.
Начнём с не самого очевидного пункта, но от него надо избавиться вообще полностью. Почему это вдруг мешает? Во-первых результат таких действий implementation-defined, т.е. зависит от реализации компилятора. Здесь очень хорошо расписано к чему могут приводить такие операции. Есть ещё одна вещь, о которой почти никто не задумывается - потеря информации о типе указателя. В Эльбрусе, например, существует аппаратная возможность контроля типов, но такое приведение указателя полностью перечёркивает её.
Это очень частый источник ошибок, которые игнорируются программистами. Кратко: в Си нельзя к переменной типа
К сожалению мало того что компилятор позволяет творить такое безобразие, его ещё часто используют на практике. От него надо также полностью избавиться.
Более сложная проблема - указатели на
Неявные приведения типов необходимо полностью запретить. Они служат источником неочевидностей и ошибок. На всякий случай - описание правил приведения типов в Си: вот и вот. Я хочу видеть статический строго типизированный язык, это позволит устранить ошибки и ускорить исполнение.
На данный момент он ужасен. Как только нам надо сделать alias для указателя на функцию жизнь превращается в боль. Сейчас это выглядит примерно так:
Каждый раз приходится вспоминать что как и за чем следует. На мой взгляд подобный синтаксис должен быть примерно таким:
Также я уже писал про проблемы с typedef и const, это ещё один пример совершенно безумного синтаксиса, и его нужно переделать.
Сейчас если функция не содержит аргументов, то необходимо писать
Это историческое наследие, от которого давно пора избавиться.
С ними ситуация тоже сложная. К сожалению в некоторых случаях Си позволяет использовать функции без прототипа, достраивая его самостоятельно. Я уже писал про проблемы, к которым это приводит. Как минимум нужно всегда обязывать писать прототипы функций. Но я бы пошёл дальше и вообще запретил бы их :) Ниже будет понятно почему, если в кратце, то компилятор должен видеть всю компилируемую программу, соответственно всегда должна быть видна реализация функции.
Ещё один источник проблем - то что
Есть с ними интересная проблема. Так по стандарту
В c99 были введены variable length arrays - массивы переменной длины. Это объект, память под который выделяется на стеке, но при этом его размер неизвестен во время компиляции. Особо много проблем они доставляют если помещать их в середине структуры (непонятно как считать её размер, как вообще это обрабатывать). Да и просто работа оптимизаций с ним крайне затруднительна. В нормальном языке VLA быть не должно.
Тоже очень больная тема для Си. Для них стандарт прописан очень криво и муторно, основная проблема с ними в том что в одной области памяти могут лежать данные разных типов, и во время компиляции мы не знаем конкретный тип на данный момент. Совсем кошмар начинается если на union внезапно берут указатель (а ещё хуже если на его поле). Тогда компилятор полностью теряет возможность отслеживать происходящее. У меня есть понимание что union'ы нужны, но пока нет понимания как их грамотно сделать.
Глобалы нужно запретить. Никаких
Тоже довольно интересный момент. Она даёт большую гибкость в работе с памятью, но в реальности выливается в совершенно уродливое хаккерство, нарушающее стандарт и убивающее переносимость. Для всех объектов и типов (кроме, возможно char) её следует запретить.
Сейчас она ужасна и приводит к ошибкам, её нужно полностью переделать. Во-первых каждый case должен быть отдельным лексическим блоком, окончанием которого должен быть break. Во-вторых имеет смысл добавить нормальный синтаксис для перечисления диапазонов значений switch. Нужно всегда явно требовать default ветки.
Убрать. Сейчас компиляторы всё равно по дефолту игнорируют это ключевое слово. В реальности же программист сам не может знать нужно делать подстановку функции или нет, часто это приводит к деградациям. Этим вопросом должен заведовать компилятор. Также неплохо бы избавиться от других устаревших ключевых слов (register, auto и т.п.).
С ними тоже очень неоднозначная ситуация. Макросы очень полезны для условной компиляции, поэтому в том или ином виде я бы оставил
С другой - она является источником ошибок. Не являясь конструкцией языка, она не делает проверку типов своих аргументов, здесь довольно много пунктов как с ними следует обращаться. Поэтому я скорее склоняюсь к тому что
Убрать. Я знаю что есть техники, в которых goto может быть красив и полезен. Но это не отменяет вреда от его использования. Более того я знаю что есть техники где без longjump не обойтись, но всё же он доставляет больше проблем, а места его использования следует переписать.
Текущая система сборки Си не отвечает современным требованиям. В Си есть "единица трансляции" - один модуль, т.е. .c файл. Компилятор генерирует из них объектный файл, потом линкует. Такая схема приводит к множеству проблем. Про проблемы с сигнатурами функций я уже писал, более того это приводит к зависимости от порядка линковки! Ну и как бонус - такая система не позволяет делать межмодульные оптимизации, что не позволяет нормально оптимизировать программы. Современный компилятор для современного языка должен собирать всё в режиме "вся программа". Это более продвинутая (и более сложная) техника чем lto, но только так можно обеспечить качественные и быстрые приложения. Тут есть проблемы с библиотеками (особенно подключением динамических библиотек), пока что я не знаю как их разрешить.
Как следствие из предыдущего пункта в языке должен действовать ODR. Это правило есть в C++, оно говорит о том что в лексическом блоке одному имени может соответствовать только одна реализация класса. Это правило должно быть обязательно.
На данный момент все переменные вне функций и сами функции неявно считаются extern'ами, т.е. видны другим модулям. По умолчанию функции должны быть static, глобалы вообще могут быть только static.
Сейчас подобные вещи реализиуютсячерез #pragma или через __attribute__. Я бы убрал оба варианта и сделал унифицированный способ подачи метаинформации. Пока сложно сказать как это должно выглядеть, потому как метаинформация может быть нужна для типов, для объектов, для синтаксических конструкций.
В Си существует три типа неопределённого поведения: unspecified, implementation-defined и undefined. Первые два типа я бы убрал полностью. Если же компилятор может статически доказать undeifned behavior, программа не должна собираться.
Выше были пункты, которые я бы убрал/переделал. А теперь хотелось бы показать то что я в язык добавил бы. Некоторые пункты можно легко добавить без накладных расходов на реализацию и изменения концептов языка, некоторые могут противоречить моим требованиям, поэтому я не уверен на сколько их стоит добавлять.
Под jit может подразумеваться несколько вещей, поэтому поясню. Во-первых мне кажется интересной возможность выполнить eval в языке. Т.е. скомпилировать строку прямо во время исполнения и обращаться к фунциям и неё. Ещё одной возможностью является перекомпиляция функций если во время исполнения выясняется что они были соптимизированы неоптимально. Это довольно сложная фича и у меня пока нет понимания возможно ли её реализовать "малой кровью", т.е. без переноса исполнения в виртуальную машину.
В Си есть некоторые проблемы с полиморфизмом. Наименьшая - его отсутствие, но она тянет все другие. Разделим проблему на две части. Первая - это полиморфизм по отношению к вложенным структурам. На самом деле его можно делать на вполне законных основаниях (тут strict-aliasing нарушаться не будет), но т.к. я хочу запретить адресную арифметику, с этим будут проблемы. Вторая проблема - каждый тип данных требует реализации отдельной функции, например если мы делаем список, то у нас будет отдельная функция для добавления целого, отдельная для плавающего и т.д.
Это заставляет задумать о механизме обобщённых функций (или перегрузке), которые избавят нас от всех этих проблем. Но тут возникнет другая сложность - я хочу избежать манглирования. Основная идея в том что имя функции из дизассемблера должно легко находиться в исходнике. Поэтому перед введением такой вкусной фичи надо много думать и хорошенько всё взвесить.
Большие и сложные проекты на Си в любом случае сводятся к написанию собственной системы объектов и классов, иногда даже с наследованием. Такие вещи хотелось бы иметь из коробки. Т.е. как минимум хотелось бы уметь создавать методы объектов, конструкторы/деструктры. Но методы опять же усложняют язык, что противоречит моей изначальной цели. Поэтому тут тоже следует всё хорошенько обдумать.
Очень полезным было бы добавление в функцию параметров по умолчанию и именованных параметров. По идее это не должно сильно усложнять компилятор и язык, но при этом является весьма полезной возможностью.
Хотелось бы иметь возможность делать так:
Хочется уметь навешивать признак
Можно спокойно жить и без них, но мне кажется это было бы удобно.
В python есть отличная возможность создавать много строчные литералы:
Хотелось бы иметь такою же возможность в своём языке.
В C++11 был добавлен специальный синтаксис для описания регулярных выражений:
В он был бы крайне полезен.
В этом посте я поразмышлял над тем каким я хотел бы видеть Си, что поменял бы в нём. Это очень субъективный пост, на которой во многом повлияло то что я занимаюсь разработкой компилятора. Когда я только начинал думать на этот счёт, казалось что я получу просто более строгий Си, но в реальности получается принципиально другой язык.
Введение
Для начала объясню чем мне нравится Си - он простой, предсказуемый, быстрый язык, который позволяет взять и сделать то что тебе нужно. Если писать по стандарту, то вероятность огрести неадекватную плоходиагностируемую проблему мала. Но к сожалению, Си позволяет писать не по стандарту, что приводит к большому количеству проблем. Более того некоторые пункты стандарта написаны не лучшим образом, некоторые его пункты не отвечают современным требованиям. Поэтому мне хотелось бы сохранить простоту и скорость языка, устранив из него устаревшие или неправильные на мой взгляд моменты. Возможно при этом добавить ещё несколько простых возможностей.
Сначала пройдёмся по уже существующим возможностям. Чтобы упростить чтение подчёркиванием я выделил основной вердикт по тому или иному пункту.
Убрать или изменить
Преобразование указателя в целое и наоборот
Начнём с не самого очевидного пункта, но от него надо избавиться вообще полностью. Почему это вдруг мешает? Во-первых результат таких действий implementation-defined, т.е. зависит от реализации компилятора. Здесь очень хорошо расписано к чему могут приводить такие операции. Есть ещё одна вещь, о которой почти никто не задумывается - потеря информации о типе указателя. В Эльбрусе, например, существует аппаратная возможность контроля типов, но такое приведение указателя полностью перечёркивает её.
Приведение типов указателей
Это очень частый источник ошибок, которые игнорируются программистами. Кратко: в Си нельзя к переменной типа
int
обращаться через указатель типа float
:int i;
float * f = (float *) &i;
*f = 5.0; // Undefined behaviour
К сожалению мало того что компилятор позволяет творить такое безобразие, его ещё часто используют на практике. От него надо также полностью избавиться.
Более сложная проблема - указатели на
void
и char
. От них также необходимо избавиться, но на данный момент я не понимаю получится ли сделать это без противоречий к последующим пунктам того что я хочу.Неявные приведения типов
Неявные приведения типов необходимо полностью запретить. Они служат источником неочевидностей и ошибок. На всякий случай - описание правил приведения типов в Си: вот и вот. Я хочу видеть статический строго типизированный язык, это позволит устранить ошибки и ускорить исполнение.
Синтаксис typedef
На данный момент он ужасен. Как только нам надо сделать alias для указателя на функцию жизнь превращается в боль. Сейчас это выглядит примерно так:
typedef void (*SignalHandler)(int);
Каждый раз приходится вспоминать что как и за чем следует. На мой взгляд подобный синтаксис должен быть примерно таким:
alias SignalHandler = * (void)(int);
Также я уже писал про проблемы с typedef и const, это ещё один пример совершенно безумного синтаксиса, и его нужно переделать.
void в сигнатуре функции
Сейчас если функция не содержит аргументов, то необходимо писать
void
в её сигнатуре, иначе компилятор будет считать что это K&R стиль и множество оптимизаций от неё тупо отвалят:int foo() {return 1;} // K&R - плохо
int bar(void) {return 1} // Си - хорошо
Это историческое наследие, от которого давно пора избавиться.
Прототипы функций
С ними ситуация тоже сложная. К сожалению в некоторых случаях Си позволяет использовать функции без прототипа, достраивая его самостоятельно. Я уже писал про проблемы, к которым это приводит. Как минимум нужно всегда обязывать писать прототипы функций. Но я бы пошёл дальше и вообще запретил бы их :) Ниже будет понятно почему, если в кратце, то компилятор должен видеть всю компилируемую программу, соответственно всегда должна быть видна реализация функции.
Проблемы с enum
Ещё один источник проблем - то что
enum
не является отдельным типом. На самом деле это int
, что тоже приводит к ошибкам. В частности, есть возможность присвоения типа int
объекту типа enum
, и, что ещё хуже, есть возможность присвоения значения объекта одного enum'а объекту другого enum'а. Такие вещи должны быть запрещены. Сам enum - самостоятельный тип со всеми вытекающими.Signed и unsigned типы
Есть с ними интересная проблема. Так по стандарту
signed int
не может переполняться, значит компилятор всегда подразумевает что поведение таких переменных предсказуемо и всегда справедливо неравенство: (i+1) > i
. С unsigned
всё не так, и мы не можем исходить из такого предположения. Это не позволяет применяться некоторым оптимизациям. Сейчас мне видится что их поведение должно быть унифицировано и переполнения должны быть исключены.Массивы переменной длины (vla)
В c99 были введены variable length arrays - массивы переменной длины. Это объект, память под который выделяется на стеке, но при этом его размер неизвестен во время компиляции. Особо много проблем они доставляют если помещать их в середине структуры (непонятно как считать её размер, как вообще это обрабатывать). Да и просто работа оптимизаций с ним крайне затруднительна. В нормальном языке VLA быть не должно.
union
Тоже очень больная тема для Си. Для них стандарт прописан очень криво и муторно, основная проблема с ними в том что в одной области памяти могут лежать данные разных типов, и во время компиляции мы не знаем конкретный тип на данный момент. Совсем кошмар начинается если на union внезапно берут указатель (а ещё хуже если на его поле). Тогда компилятор полностью теряет возможность отслеживать происходящее. У меня есть понимание что union'ы нужны, но пока нет понимания как их грамотно сделать.
Глобалы
Глобалы нужно запретить. Никаких
extern int
. Самое глобальное что только можно делать - static
объекты, которые видны только внутри модуля. Если кому-то понадобится прочитать/записать глобальное значение, то это очень легко реализуется через extern
функции, меняющие static
объект.Арифметика указателей
Тоже довольно интересный момент. Она даёт большую гибкость в работе с памятью, но в реальности выливается в совершенно уродливое хаккерство, нарушающее стандарт и убивающее переносимость. Для всех объектов и типов (кроме, возможно char) её следует запретить.
Конструкция switch
Сейчас она ужасна и приводит к ошибкам, её нужно полностью переделать. Во-первых каждый case должен быть отдельным лексическим блоком, окончанием которого должен быть break. Во-вторых имеет смысл добавить нормальный синтаксис для перечисления диапазонов значений switch. Нужно всегда явно требовать default ветки.
inline
Убрать. Сейчас компиляторы всё равно по дефолту игнорируют это ключевое слово. В реальности же программист сам не может знать нужно делать подстановку функции или нет, часто это приводит к деградациям. Этим вопросом должен заведовать компилятор. Также неплохо бы избавиться от других устаревших ключевых слов (register, auto и т.п.).
Макросы
С ними тоже очень неоднозначная ситуация. Макросы очень полезны для условной компиляции, поэтому в том или ином виде я бы оставил
#ifdef
и #if
. Я бы полностью избавился от #include
. Ещё необходимо полностью запретить конкатенацию макросов - генерация имени функции в compile time это сущий ад, за такое хочется убивать. Далее #define
. С одной стороны он позволяет делать функции высшего порядка. Например мы в качестве аргумента можем подавать участки кода:#define debug(actions) \
{ \
if ( enablePrint ) \
{ \
actions; \
} \
}
С другой - она является источником ошибок. Не являясь конструкцией языка, она не делает проверку типов своих аргументов, здесь довольно много пунктов как с ними следует обращаться. Поэтому я скорее склоняюсь к тому что
#define
необходимо убрать.goto, longjump
Убрать. Я знаю что есть техники, в которых goto может быть красив и полезен. Но это не отменяет вреда от его использования. Более того я знаю что есть техники где без longjump не обойтись, но всё же он доставляет больше проблем, а места его использования следует переписать.
Система сборки
Текущая система сборки Си не отвечает современным требованиям. В Си есть "единица трансляции" - один модуль, т.е. .c файл. Компилятор генерирует из них объектный файл, потом линкует. Такая схема приводит к множеству проблем. Про проблемы с сигнатурами функций я уже писал, более того это приводит к зависимости от порядка линковки! Ну и как бонус - такая система не позволяет делать межмодульные оптимизации, что не позволяет нормально оптимизировать программы. Современный компилятор для современного языка должен собирать всё в режиме "вся программа". Это более продвинутая (и более сложная) техника чем lto, но только так можно обеспечить качественные и быстрые приложения. Тут есть проблемы с библиотеками (особенно подключением динамических библиотек), пока что я не знаю как их разрешить.
One Definition Rule
Как следствие из предыдущего пункта в языке должен действовать ODR. Это правило есть в C++, оно говорит о том что в лексическом блоке одному имени может соответствовать только одна реализация класса. Это правило должно быть обязательно.
static
На данный момент все переменные вне функций и сами функции неявно считаются extern'ами, т.е. видны другим модулям. По умолчанию функции должны быть static, глобалы вообще могут быть только static.
Подсказки компилятору
Сейчас подобные вещи реализиуютсячерез #pragma или через __attribute__. Я бы убрал оба варианта и сделал унифицированный способ подачи метаинформации. Пока сложно сказать как это должно выглядеть, потому как метаинформация может быть нужна для типов, для объектов, для синтаксических конструкций.
Unspecified и Implementation-defined behavior
В Си существует три типа неопределённого поведения: unspecified, implementation-defined и undefined. Первые два типа я бы убрал полностью. Если же компилятор может статически доказать undeifned behavior, программа не должна собираться.
Добавить
Выше были пункты, которые я бы убрал/переделал. А теперь хотелось бы показать то что я в язык добавил бы. Некоторые пункты можно легко добавить без накладных расходов на реализацию и изменения концептов языка, некоторые могут противоречить моим требованиям, поэтому я не уверен на сколько их стоит добавлять.
JIT
Под jit может подразумеваться несколько вещей, поэтому поясню. Во-первых мне кажется интересной возможность выполнить eval в языке. Т.е. скомпилировать строку прямо во время исполнения и обращаться к фунциям и неё. Ещё одной возможностью является перекомпиляция функций если во время исполнения выясняется что они были соптимизированы неоптимально. Это довольно сложная фича и у меня пока нет понимания возможно ли её реализовать "малой кровью", т.е. без переноса исполнения в виртуальную машину.
Обобщённые функции
В Си есть некоторые проблемы с полиморфизмом. Наименьшая - его отсутствие, но она тянет все другие. Разделим проблему на две части. Первая - это полиморфизм по отношению к вложенным структурам. На самом деле его можно делать на вполне законных основаниях (тут strict-aliasing нарушаться не будет), но т.к. я хочу запретить адресную арифметику, с этим будут проблемы. Вторая проблема - каждый тип данных требует реализации отдельной функции, например если мы делаем список, то у нас будет отдельная функция для добавления целого, отдельная для плавающего и т.д.
Это заставляет задумать о механизме обобщённых функций (или перегрузке), которые избавят нас от всех этих проблем. Но тут возникнет другая сложность - я хочу избежать манглирования. Основная идея в том что имя функции из дизассемблера должно легко находиться в исходнике. Поэтому перед введением такой вкусной фичи надо много думать и хорошенько всё взвесить.
Классы
Большие и сложные проекты на Си в любом случае сводятся к написанию собственной системы объектов и классов, иногда даже с наследованием. Такие вещи хотелось бы иметь из коробки. Т.е. как минимум хотелось бы уметь создавать методы объектов, конструкторы/деструктры. Но методы опять же усложняют язык, что противоречит моей изначальной цели. Поэтому тут тоже следует всё хорошенько обдумать.
Параметры по умолчанию, именованные параметры
Очень полезным было бы добавление в функцию параметров по умолчанию и именованных параметров. По идее это не должно сильно усложнять компилятор и язык, но при этом является весьма полезной возможностью.
Инициализация полей структуры
Хотелось бы иметь возможность делать так:
typedef struct {
int a = 1;
float b = 2.0;
} MyStruct_t;
Неизменяемые поля
Хочется уметь навешивать признак
immutable
на поля структуры, чтобы показать что они не будут меняться в течении работы программы. Вообще это некоторого рода синтаксический сахар, но иметь такою возможность было бы полезно, благо её легко поддержать в оптимизаторе.Вложенные комментарии
Можно спокойно жить и без них, но мне кажется это было бы удобно.
Многострочные строки
В python есть отличная возможность создавать много строчные литералы:
"""
текст
текст
текст
"""
Хотелось бы иметь такою же возможность в своём языке.
Синтаксис для регулярных выражений
В C++11 был добавлен специальный синтаксис для описания регулярных выражений:
regex integer("(\\+|-)?[[:digit:]]+");
В он был бы крайне полезен.
Заключение
В этом посте я поразмышлял над тем каким я хотел бы видеть Си, что поменял бы в нём. Это очень субъективный пост, на которой во многом повлияло то что я занимаюсь разработкой компилятора. Когда я только начинал думать на этот счёт, казалось что я получу просто более строгий Си, но в реальности получается принципиально другой язык.
Комментариев нет:
Отправить комментарий
Примечание. Отправлять комментарии могут только участники этого блога.