Info Guide #12
31 декабря 2017

Системки - NedoLang: Проклятие языка Си (часть 3).

     Проклятие языка Си
Alone Coder

   Язык Си - ужасный язык. По всем виртов─
ским  канонам. Но огромная и беспрецедент─ 
ная поддержка его синтаксиса в каждом утю─ 
ге при отсутствии такой же поддержки у лю─ 
бого из его конкурентов обрекает разработ─ 
чиков новых языков копировать и копировать 
этот синтаксис. 
   Проблемы  Си (здесь  и про C++), в двух
словах, сводятся  к тому, что  он сложен в 
синтаксическом  разборе  и при этом чреват 
ошибками. 

   Грамматика  построена  так, что требует
знать правый контекст: 
  - даже  если  парсер  знает  все  типы у
идентификаторов,  он  всё  равно  вынужден
смотреть  вправо  на ширину скобки, потому
что левая часть присваивания разрешает ис─
пользовать скобки.
  - даже  если мы запомним контекст начала
скобки, уже в лексере разбор выражений по─
требует  смотреть  на два символа вперёд -
так  устроены  двухсимвольные  операции  и
комментарии. ( NedoLang  смотрит только на
один  символ,  поэтому  у него нет++/-- в
выражениях,а комментарии запрещены в неко─
торых местах, например,перед[ в массиве.)
  - мы  не  узнаем, что объявление функции
не  имеет  тела (то есть что не надо выде─
лять  память под перечисленные параметры),
пока не дочитаем объявление до конца.
   Плюс  куча зарезервированных слов (ведь
они  могут встречаться в позиции идентифи─
катора) и множество случаев неопределённо─
го поведения.

   Ошибки из-за неочевидности:
  - тернарный оператор. У него низкий при─
оритет,но его постоянно забывают заключать
в скобки.
  - то же  с операторами сравнения. Причём
они любезно возвращаютint, чтобы вы могли
накосячить, а  компилятор  вам  ничего  не
сказал. В  предке  Си  не было типов. Но в
первом  NedoLang тоже не было типов, а тем
не менее,bool есть.
  - мы знаем,что в хорошем языке поведение
не  должно  зависеть от порядка объявления
("Дизайн и эволюция C++", с. 265).  Однако
же  поведение if с логическими операциями
&& и|| зависит от порядка операндов (ана─ 
логично AND THEN / OR  ELSE  в языке Ада,
которые  являются  не операциями, а частью
командыIF ). Кстати,поэтому их не рекоме─
ндуется  перегружать, потому что такое по─
ведение нельзя повторить своим кодом(Сат─
тер, Александреску "Стандарты программиро─
вания на C++", #30).
  - присваивание в выражении - оно на пра─
ктике не требуется,но постоянно встречает─
ся там,где имели в виду== . Некоторые из-
за этого даже пишут<число>==<выражение>.
  -int  и  unsigned int неявно приводятся
друг к другу, и не только они. Много сюрп─
ризов, например, при обработке звука.
  - вы пишете:
unsigned char c1, c2, c3;
void f(void)
{
 if (c1 == c2 + c3)
 c1=3;
}
   и думаете, что вычисления идут в байто─
вом  диапазоне? Вы просчитались!(Derek M.
Jones. The New C Standard: An Economic and
Cultural Commentary - далее cbook1_0b.pdf,
p. 206)
  - вы  не  знаете, как  разадресуется ваш
указатель, пока  не промотаете исходник до
его  определения. Более  того, вы  даже не
знаете, куда  он сдвинется, если вы к нему
прибавите 1.
  - вы  не знаете знаковостьchar(Страус─
труп  "Язык программирования C++", с. 51).
Кстати, надо  было  догадаться так назвать
байтовый  тип. Наверно, авторы  не думали,
что есть языки помимо английского.
  - ошибки типаwhile();{}
  - ошибки типаif();{}
  - ошибки типаif()if(){}else ...; - else
не к той ветке.
  - ошибки типаfunc; вместо func(); - ко─
мпилируется, но ничего не делает!
  - вы пишете:
#include <stdio.h>
#include <sys/types.h>
int main(void)
{
 printf("Startedn");
 if (fork() == 0)
   printf("Childn");
}
   и ожидаете:
Started
Child
   Но  на  некоторых  системах  вас  может
ждать сюрприз:
Started
Started
Child
   Это  потому, что процесс может клониро─
ваться   вместе   с   файловыми   буферами
(cbook1_0b.pdf, p. 202).
  - так и тянет написать без скобок:
#define A(x) x+1
   Может даже работать. Иногда.
  -010 - это  не  10.  (Я  сделал,  чтобы
NedoAsm  ругался на это. Пишите восьмерич─ 
ные  константы  в  виде0o10 или не пишите
вообще!)

   Плохая читаемость исходника:
  -int *a, b - сколько здесь указателей?
  -*a++ - это *(a++) или (*a)++ ?
  -for (i=0; i<5; ++i){} - когда происхо─
дит++i ?
  -((void(*)(void))0)(); - это вызов про─
цедуры  с  адресом 0. Можете повторить, не
подглядывая? Даже fk0  не смог, пропустил
скобку.  Посоветовал  сайт cdecl, который
переводит с English на C gibberish и обра─
тно, например:
  int (*(*foo)(void ))[3]
declare foo as pointer to function (void)
returning pointer to array 3 of int
  - одинаково  оформляемый  тайпкаст может
означать  как  реинтерпретацию  значения в
регистре, так и его сужение-расширение,или
даже  преобразование в/из формат(а) с пла─
вающей точкой, ещё и с непредсказуемым ок─
руглением!А вот усечения по диапазону нет.
Ни в каком виде. Фиксированной точки  тоже
нет.
  - нельзя  найти объявление функции прос─
тым  поиском ("Дизайн и эволюция C++", с.
316), все  приведения  типов тоже не найти
(там же, с. 334).
  - Пишем:
#define a b (возможно, внутри #include)
#include ..
   и макрос будет действовать во вложенной
библиотеке, что есть для неё сюрприз("Ди─
зайн  и эволюция C++", с. 429). А вот нао─
борот вполне надо: инклюдим макросы, даль─
ше работаем с макросами.

  Прочие бяки:
  -a >> n, a << n, p + n - не алгебраиче─
ские операции, т.к. их  операнды - разного
типа.
  - вычитание  указателей,  сравнения - не
алгебраические операции, т.к. их результат
другого типа.
  - строки  без длины - замечательная дыра
в системе. А ещё они очень "быстро" конка─
тенуются.
  - язык  дьявольски  подталкивает  читать
структуры из файла и писать их в файл. Да─
же  предлагает типshort int для 16-битных
значений и union  для многозначных полей.
НЕ НАДО ТАК ДЕЛАТЬ. Это непортируемо.
  - и т.д., см. стандарты  типаMISRA C (я
собрал даже целую коллекцию таких стандар─
тов)  и  книгу отPVS-Studio"The Ultimate
Question  of Programming" (она же"Главный 
вопрос  программирования,  рефакторинга  и
всего такого" ).

   Чего ещё нет:
  - поддержки  разных  типов памяти в ука─
зателях (ПЗУ, встроенное ОЗУ, внешнее ОЗУ;
в  случае  Z80 ещё банкируемая/небанкируе─
мая). Обходят  в  некоторых  компиляторах,
например, черезconst (остальные указатели
проверяются при попытке доступа) или через
far.
  - более  того, мы даже не знаем, в какую
область  памяти  попадёт  интересующая нас
метка. Например,секция инициализации может
быть  или  не  быть, даже в разных версиях
одного и того же SDCC.
  - runtime  range  check, который  есть в
Паскале.
  - вложенных комментариев. Предполагается
писать#ifdef, тело  которого не подсвечи─
вается  в  редакторах. Поиск  пропущенного
#endif в файле - очень  весёлое  развлече─
ние.
  - экспорта  меток. Обходят  черезstatic
(остальные метки по умолчанию экспортирую─
тся - бедный линкер).
  - массивов, не  адресуемых  указателями.
Ме-е-едленных  таких  банкируемых массивов
размером  в мегабайты. Проблема в том, что
в  Си  массив  и указатель - это одно и то
же. Не обходится. Если вы пишете графичес─
кий  редактор, вам придётся самому разбить
вашу  картинку  на  множество  банкируемых
объектов.
  - длины   строковых   литералов.  Нельзя
сгенерировать  строковый литерал с длиной.
Вы только можете  вычислить отдельно длину
черезconstexpr в C++11 :https://
stackoverflow.com/questions/25890784/
computing-length-of-a-c-string-at-compile-
time-is-this-really-a-constexpr
  - сишник не позволяет сгенерировать мас─
сив  с символическими именами  и индексами
процедур (с адресами можно:https://
gcc.gnu.org/onlinedocs/cpp/
Concatenation.html ), потому что нет разо─
рванного enum и разорванной инициализации
константного массива.Для сравнения,в ALASM
всё это возможно.
   По  той  же причине нет возможности, не
описывая по два раза, сгенерировать массив
индексов для строковых констант, использу─
емых в программе (для задачи типа хранили─
ща данных, доступных по названию).
  - функция не возвращает результат и оши─
бку (что  мы  видели  в функциях iS-DOS, в
языке  Go  и  ждём  в  C++17 под названием
"structured bindings"). В результате обыч─
но  часть значений резервируется под ошиб─
ку.Особенно шикарно-1 в getc(), ради чего
функция  возвращает int вместо байта - но
вы поняли,пацаны всё равно кладут вchar и
обрубают  файл в кодировкеCP1251 по букве
"я". А в C++ появляются exceptions на каж─
дый чих, из которых какое-нибудь вы обяза─
тельно  забудете обработать (особенно если
оно  прибежало  к вам из библиотеки). Спе─
цификации исключений не проверяются стати─
чески(Саттер, Александреску, #75).  Кста─
ти,деление на0 может в некоторых системах
дать  exception  даже  для  флоатов,  нес─
мотря  на  наличие  бесконечностей  и NaN
(cbook1_0b.pdf,p.267). Более того, except─
ion может возникнуть не при самом делении,
а, например,при сравнении(там же, p.343).
А ещё exceptions передаются по значению,но
должны  перехватываться  по  ссылке (иначе
производный  класс обрезается по базовому,
который мы ловим - см.Misra C++ 2008 Rule
15-3-5 ).
  - нет отслеживания чистых функций (кото─
рые  не  зависят от окружения  и не меняют
его). А ведь  по  уму  в выражениях  можно
использовать только их.
  - нет  операции передачи объекта с унич─
тожением  оригинала  (передать  указатель,
вызвать  функцию,  уничтожить  указатель).
При  функциональном стиле программирования
в большинстве случаев уничтожение объектов
нужно  только  здесь,  и  не нужно было бы
вспоминать, уничтожили ли мы исходный объ─
ект (в  ряде  других языков для этого есть
сборщик мусора).
  - невозможно   сделать  общий else  для
группы условий. (Хотя это есть даже в ква─
дратно-гнездовом языке ДРАКОН .)
  - вmain() не передаётся системное окру─
жение (PATH и т.п.), его приходится читать
через глобальную переменнуюextern char **
environ(cbook1_0b.pdf,p.168). Более того,
в main()  вообще  не передаётся объект(ы)
состояния системы (пути,консоль, настройки
режима запуска, ФС, описатель  задачи...).
Это  всё  тоже  приходится доставать через
одно  место, причём  поддержка  доставания
через это место нужна на уровне ОС.И вы не
сможете  мокать (mock) эти объекты при на─
писании тестов.
  - в  стандарте  C (как  минимум C90) и в
стандарте  C++ не определено понятие кодо─
вых  таблиц  исходника  и среды исполнения
(cbook1_0b.pdf, p. 213). В  итоге  в среду
исполнения попадает ровно то, что вы напи─
сали в кавычках, без всякого перекодирова─
ния. Вам  будет  нелегко  писать на  Linux
программы   под  ZX Spectrum  в  кодировке
CP866.

   И вот это зло надо было реализовать.

   Я решил  пойти на компромисс - оставить
хотя бы  строгую  типизацию и необязатель─ 
ность;в конце  строки, запретить арифме─ 
тику для типа char,аifиwhileзащитить 
обязательной  фигурной  скобкой и точкой с 
запятой. 

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

  20.12.2016 - провёл эксперимент с дина─
мической памятью. 85% заполнение.Не дегра─
дирует.

  10.01.2017 - сделал    тайпкаст   через
+(<тип>)<терм> . До этого был только тайп─
каст вint через префиксный + ,в беззнако─
вые типы через бинарный& , а в long - че─
рез0L+<uintexpression> .

  11.01.2017 - внезапно понял, что префи─
ксные ++/-- нельзя реализовать без обяза─
тельных  точек  с запятой в конце команды,
или  же  придётся  считать число пробелов.
Записал в памятку.

  17.01.2017 - в  вызовах  вместо  типа в
/*...*/  сделал тайпкаст, и название пара─
метра  не пишется (для этого пришлось сде─
лать отдельный стек строк):
 LET _barr[.vas]=
   +(byte)byter(
     recursive(byte)+(byte)shmyter2(
       recursive (byte)0b1
     )
   )
   Это  не стековая и не регистровая пере─
дача  параметров. Куда писать параметр при
вызове, определяется  по номеру параметра.
К  имени функции приклеивается номер пара─
метра. А  внутри  функции та же ячейка ис─
пользуется по имени. То есть у одной и той
же ячейки две метки.

  18.01.2017 - начал  переводить исходник
компилятора  на  язык NedoLang. Для начала
расставил PROC,  FUNC  и +(BOOL)0xff/0x00
вместоtrue/false .

  19.01.2017 - расставил в исходнике ком─
пилятора VAR (и убрал запятые, потому что
PINT a,b -это int *a,b ,а что это значит, 
вы  уже читали выше),LET, REPEAT...UNTIL,
а  модуль syscalls  полностью  перевёл на
NedoLang. 

  20.01.2017 - ещё одно озарение:
  - можно  сделать  вызов  без call, если
проверять скобку( после команды - но тог─
да нельзя имена с точкой.
  - можно сделать  присваивание (не масси─
вов) безlet, если проверять = после кома─
нды - но тогда нельзя имена с точкой.
  - можно  сделать  присваивание  массивов
без let, если проверять [ после команды -
но тогда нельзя имена с точкой.

  23.01.2017 - примерно   перевёл  модуль
reader  на NedoLang. Для этого добавил оп─
ределение константы (командаconst ).Заод─
но  запретил арифметику указателей с нару─
шением  типизации, аtrue и false разрешил
писать как+true , +false .

  24.01.2017 - примерно  перевёл на Nedo─
Lang основную часть компилятора. 

  27.01.2017 - пришла идея сделать + пре─
фиксом для всех констант,а заодно для под─
сказок  компилятору  типа regfast . Но не
сделал.

  08.02.2017 - выделил  GUI  в  отдельный
проект.

  09.02.2017 - доделал forward для проце─
дур/функций.

  10.02.2017:
  - убрал  ручное указание типов функций и
параметров  при вызове. Эта информация бе─
рётся из таблицы меток.
  - рекурсивность  функции хранится в таб─
лице  меток  и помечается словомRECURSIVE
после её названия в определении.
  - глобальные  переменные теперь начинаю─
тся  с__ , а с одним _ мы только попадаем
на уровень выше.

  13.02.2017 - идеи по указателям на про─
цедуры и функции. Но не сделал. Экспериме─
нтировал  с файлами последовательного дос─
тупа в TR-DOS (до этого единственный раз я
их пробовал, наверно, году в 1995).

  14.02.2017:
  - добавлены  константные массивы (не для
строк, потому что для строк надо генериро─
вать отдельные метки). Но строки допускаю─
тся в обычных константах.
  - непопулярная  мера: разность  указате─
лей даётint.

  16.02.2017:
   - убралLET, CALL и THEN.
   - придумал  в общих чертах, как сделать
far pointer'ы. Но не far call'ы. И не сде─
лал. Надо  провести  проект ещё через нес─
колько бутылочных горлышек,прежде чем рас─
ширять.

  17.02.2017:
  - добавилEXTERN для внешних переменных.
  - убрал ENDIF. Вместо  него защищать от
ошибки будет конечная точка с запятой.

  20.02.2017 - убрал  некоторые  ворнинги
от gcc, которые прислалLVD.

  21.02.2017 - багфикс  таблицы  меток  в
ассемблере.

  22.02.2017 - выработал стиль оформления
исходника и привёл к нему все модули.

  23.02.2017 - уже  каждый  модуль по от─
дельности компилируется (без ассемблирова─
ния).

  25.02.2017 - смог  скомпилировать и ас─
семблировать  основную  часть компилятора.
Для  этого написал .bat-скрипт. Первый раз
получилось 60491 байт. Немного подкрутил,
получилось58515 байт.




Темы: Игры, Программное обеспечение, Пресса, Аппаратное обеспечение, Сеть, Демосцена, Люди, Программирование

Похожие статьи:
Юмор - анекдоты.
Железо - Интерфейсы принтеров.
Почта - письма читателей: Sergio, Macros, Батько Архей, moroz1999, Oleg Groskow/Arhon.

В этот день...   20 сентября