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

И в первую очередь, поскольку отладка занимает гораздо больше времени, чем написание кода — нужна защита от дурака (в т.ч.себя) на этапе написания. И это хотелось бы получить от используемого языка программирования (ЯП).

Конечно же, надо изобрести новый, самый лучший ЯП!
Нет, сначала попробуем выразить свои пожелания и посмотреть на то, что уже наизобретали.

Итак, что бы хотелось получить:

  • Устойчивость к ошибкам человека, исключение неоднозначностей при компиляции
  • Устойчивость к входным данным
  • Устойчивость к повреждению программы или данных — сбой носителя, взлом
  • При этом всем — более-менее терпимый синтаксис и функциональность

Область желательного применения — машинерия, транспорт, промышленные системы управления, IoT, эмбеддед включая телефоны.

Вряд ли это нужно для Веб, он построен (пока) на принципе “бросил и перезапустил” (fire and forget).

Достаточно быстро можно прийти к выводу, что язык должен быть компилируемым (как минимум Пи-компилируемым), чтобы все проверки максимально были выполнены на этапе компиляции без VS (Версус, далее по тексту негативное противопоставление) “ой, у этого объекта нет такого свойства” в рантайме. Даже скриптование описаний интерфейса уже приводят к обязательности полного покрытия тестами таких скриптов.

Лично я не хочу платить своими ресурсами (в т.ч. деньгами за более быстрое дорогое железо) за интерпретацию, потому итого желательно иметь минимум JIT-компиляцию.

Итак, требования.

Устойчивость к ошибкам человека


Старательно полистав талмуды от PVS-Studio, я выяснил, что самые распространенные ошибки — это опечатки и недоправленная копипаста.

Еще добавлю чуточку казусов из своего опыта и встреченных в различной литературе, как негативные примеры. Дополнительно обновил в памяти правила MISRA C.

Чуть позже обдумав, пришел к выводу, что линтеры, примененные постфактум страдают от “ошибки выжившего”, поскольку в старых проектах серьезные ошибки уже исправлены.

а) Избавляемся от похожих имен

— должна проводится жесткая проверка видимости переменных и функций. Опечатавшись, можно использовать идентификатор из более общей области видимости, вместо нужного
— использоваться регистронезависимые имена. (VS) «Давайте функцию назовем как переменную, только в Кэмелкейз» и потом с чем нибудь сравним — в С так сделать можно (получим адрес ф-ции, который вполне себе число)
— имена с отличием на 1 букву должны вызывать предупреждение (спорно, можно выделять в IDE) но очень распространенная ошибка копипасты .x, .y, .w, .h.
— не допускаем одинаково именовать разные сущности — если есть константа с таким названием, то не должно быть одноименной переменной или имени типа
— крайне желательно, именование проверять по всем модулям проекта — легко перепутать, особенно если разные модули пишут разные люди

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

— Исключить возможность дублей линковки (компоновки).

в) Неоднозначности
— Должен быть определенный порядок вызовов функций. При записи X = funcA() + fB() или Fn(funcA(), fB(), callC()) — надо понимать, что человек рассчитывает получить вычисления в записанном порядке, (VS) а не как себе надумал оптимизатор.
— Исключить похожие операторы. А не как в С: + ++, < <<, | ||, & &&, = ==
— Желательно иметь немного понятных операторов и с очевидным приоритетом. Привет от тернарного оператора.
— Переопределение операторов скорее вредно. Вы пишете i := 2, но (VS) на самом деле это вызывает неявное создание объекта, для которого не хватает памяти, а диск дает сбой при сваппинге и ваш спутник падает на Марс :-(

На самом деле из личного опыта наблюдал вылет на строке ConnectionString = “DSN”, это оказалось сеттером, который открывал БД (а сервер не был виден в сети).

— Нужна инициализация всех переменных дефолтными значениями.
— Также ООП подход спасает от забывчивости переназначения всех полей в объекте в какой-нибудь новой сотой функции.
— Система типов должна быть безопасной — нужен контроль за размерностями присваиваемых объектов — защита от затирания памяти, арифметическим переполнением типа 65535+1, потерями точности и значимости при приведении типов, исключение сравнения несравнимого — ведь целое 2 не равно 2.0 в общем случае.

И даже типовое деление на 0 может давать вполне определенный +INF, вместо ошибки — нужно точное определение результата.

Устойчивость к входным данным


— Программа должна работать на любых входных данных и желательно, примерно одинаковое время. (VS) Привет Андроиду с реакцией на кнопку трубки от 0.2с до 5с; хорошо, что не Андроид управляет автомобильной ABS.

Например, программа должна корректно обрабатывать и 1Кб данных и 1Тб, не исчерпав ресурсы системы.

— Очень желательно иметь в ЯП RAII и надежную и однозначную обработка ошибок, не приводящую к побочным эффектам (утечкам ресурсов, например). (VS) Очень веселая вещь — утечка хендлов, проявиться может через многие месяцы.
— Было бы неплохо защититься от переполнения стека — рекурсию запретить.
— Проблема превышения доступного объема требуемой памятью, неконтролируемый рост потребления из-за фрагментации при динамическом выделении/освобождении. Если же язык имеет рантайм, зависимый от кучи, дело скорее всего плохо — привет STL и Phobos. (VS) Была история со старым C-рантаймом от Микрософт, который неадекватно возвращал память системе, из-за чего msbackup падал на больших объемах (для того времени).
— Нужна хорошая и безопасная работа со строками — не упирающаяся в ресурсы. Это сильно зависит от реализации (иммутабельные, COW, R/W массивы)
— Превышение времени реакции системы, не зависящее от программиста. Это типичная проблема сборщиков мусора. Хотя они и спасают от одних ошибок программирования — привносят другие — плохо диагностируемые.
— В определенном классе задач, оказывается, можно обойтись совсем без динамической памяти, либо однократно выделив ее при старте.
— Контролировать выход за границы массива, причем вполне допустимо написать предупреждение рантайма и игнорировать. Очень часто это некритичные ошибки.
— Иметь защиту от обращений к неинициализованному программой участку памяти, в т.ч к null-области, и в чужое адресное пространство.
— Интерпретаторы, JIT — лишние прослойки снижают надежность, есть проблемы с сборкой мусора (очень сложная подсистема — привнесет свои ошибки), и с гарантированным временем реакции. Исключаем, но есть в принципе Java Micro Edition (где от Явы отрезано так много, что остается только Я, была интересная статья dernasherbrezon (жаль, пристрелили) и .NET Micro Framework с C#.

Впрочем, по рассмотрению, эти варианты отпали:

  • .NET Micro оказался обычным интерпретатором (вычеркиваем по скорости);
  • Java Micro — пригодна только для внедряемых приложений, поскольку слишком сильно кастрирована по API, и придется для разработки переходить хотя бы на SE Embedded, которую уже закрыли или обычную Java’у, слишком монструозную и непредсказуемую по реакции.
    Впрочем, есть еще варианты, и хотя это и не выглядит заготовкой для работоспособного фундамента, но можно сравнить с другими языками, даже устаревшими или обладающими определенными недостатками.


— Устойчивость к многопоточной работе — защита приватных данных потока, и механизмы гарантированного обмена между потоками. Программа в 200 потоков может работать совсем не так, как в два.
— Контрактное программирование плюс встроенные юниттесты тоже весьма помогают спать спокойно.

Устойчивость к повреждению программы или данных — сбой носителя, взлом


— Программа должна целиком загружаться в память — без подгрузки модулей, особенно удаленно.
— Очищаем память при освобождении (а не только выделении)
— Контроль переполнения стека, областей переменных, особенно строки.
— Перезапуск после сбоя.

Кстати, подход, когда рантайм имеет свой логгинг, а не только выдает, что северный песец и стектрейс, мне очень импонирует.

Языки — и таблица соответствия


На первый взгляд, для анализа возьмем специально разработанные безопасные ЯП:

  1. Active Oberon
  2. Ada
  3. BetterC (dlang subset)
  4. IEC 61131-3 ST
  5. Safe-C

И пройдемся по ним с точки зрения вышеприведенных критериев.

Но это уже объем для статьи-продолжения, если карма позволит.

С выделением в таблицу вышеупомянутых факторов, ну и возможно — еще что то толковое почерпнется из комментариев.

Что же касается прочих интересных языков — C++, Crystal, Go, Delphi, Nim, Red, Rust, Zig (добавьте по вкусу) то заполнять таблицу соответствия по ним оставлю желающим.

Дисклеймеры:

  • В принципе, если программа, скажем на Питоне, потребляет 30Мб, а требования к реакции- секунды, а микрокомпьютер имеет 600 Мб свободной памяти и 600 МГц проц — то почему нет? Только надежной такая программа будет с некоторой вероятностью (пусть и 96%), не более.
  • Кроме того, язык должен стараться быть удобным для программиста — иначе никто его использовать не будет. Такие статьи «я придумал идеальный язык программирования, чтобы мне и только мне было удобно писать» — не редкость и на Хабре тоже, но это совсем о другом.

Комментарии (38)


  1. knstqq
    28.01.2019 14:24

    Очень сложно читать статью, потому что вместо текста — списки-списки, бесконечные списки.
    в трети мест список хорошо бы превратить в текст, в трети — написать через запятую. Оставшуюся треть можно оставить. Например, в «Устойчивость к входным данным» первый список из одного пункта — это просто ух!


    1. Siemargl Автор
      28.01.2019 20:17

      Точно. Нечаянно переключил редактор в Маркдаун режим и без единой правки все поехало. Так лучше?


  1. kaleman
    28.01.2019 14:34
    +1

    А разве Oberon это не мертвый академический язык? Как можно говорить о безопасности инструмента, который не тестировался на реальных проектах?


    1. hoegni
      28.01.2019 14:57

      Ну вот тут можно взглянуть на применения.

      Среди прочего управление беспилотными ЛА, расчет фазированной антенны для радара Еврофайтера, моделирование в физике высоких энергий.


    1. batyrmastyr
      28.01.2019 18:45
      -2

      То, что академики сразу после компилятора зафигачили себе ОСь — среду разработки — Word Pad — подобием ActiveX в одном флаконе и всё это в 1988 году — это, разумеется, семечки, а не реальный проект.


  1. andreymal
    28.01.2019 16:34
    +1

    рекурсию запретить.

    Смело


    Иметь защиту от обращений… в чужое адресное пространство.

    Встроено в каждую современную ОС


    Контролировать выход за границы массива, причем вполне допустимо написать предупреждение рантайма и игнорировать. Очень часто это некритичные ошибки.

    Heartbleed передаёт привет, лол


    1. Siemargl Автор
      28.01.2019 20:30
      -1

      Встроено в каждую современную ОС
      1. не везде они современные
      2. не везде даже есть ОС
      3. ОС плевать что ты делаешь со своими потокам
      Heartbleed передаёт привет, лол
      Логи то читать все же надо, и править. А вот крешить программу зря — не надо.


  1. amarao
    28.01.2019 16:40

    Очень, очень советую посмотреть Rust. Это первый индустриальный (не академический) универсальный язык (т.е. от веб-фреймворков до модулей ядра), у которого вместо Си-подобной модели языка «язык позволяет сделать, а пользователь думает как не ошибиться», применяется другая: «язык старается не допустить ошибок, а пользователь думает, как бы сделать».


    1. andreymal
      28.01.2019 16:48

      1. amarao
        28.01.2019 17:00
        +1

        А, я понял. Вам нужен язык, в котором гарантировано нет ошибок, зависаний и т.д. Такой язык есть, и он работает с скоростью машинного кода. Называется eBPF, встроен во все ядра линукса уже несколько лет.

        Как он работает? Программа на eBPF проверяется, если она подходит, то ядро загружает не eBPF, а результат его компиляции в машинные коды (очень быстрые).

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

        Подробнее про eBPF: lwn.net/Articles/740157


        1. andreymal
          28.01.2019 17:02

          (не мне, я не автор поста)


        1. Gryphon88
          28.01.2019 18:20

          Почитал, но из этого краткого введения не понял несколько вещей, может, подскажете?
          1. Не совсем понял про sanity checks

          The second stage is more involved and requires the verifier to simulate the execution of the eBPF program one instruction at a time. The virtual machine state is checked before and after the execution of every instruction to ensure that register and stack state are valid.
          В рантайме проверяется состояние кждый раз, или единожды на этапе компиляции?
          2. Возможно ли работать с eBFP без ОС? Иначе говоря, перейти от 10 виртуальных регистров и интерпретации к реальным 8/32битным регистрам аппаратуры?


          1. amarao
            28.01.2019 18:25

            verifier проверяет программу перед тем, как «принять» (загрузить) её в ядро. Он симулирует исполнение и смотрит, не попортили ли стек и регистры.

            К регистрам аппаратуры, наверное, перейти можно, но после этого никто не может гарантировать адекватность вашей программы. Вдруг, вы dma-контроллер перепрограммируете чтобы всё перезатереть.


            1. Gryphon88
              28.01.2019 21:15

              Насколько «проверяет»? Полная проверка подразумевает конечное число состояний, что требует достаточно сильного допущения о однопоточности и гранулярности общения песочницы с ядром. А МК (в статье речь в первую очередь о них) не нужен без периферии, т.е. без асинхронности: даже если по переполнению таймера, считающего на частоте ядра, читать его значение, то оно будет от 6 до 14 в зависимости от модели, компилятора и качества кода. eBPF поможет избавиться от зависания и зацикливания, но вот использовать его при асинхронности и модификации глобального состояния…


        1. worldmind
          28.01.2019 18:55

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


          1. amarao
            28.01.2019 19:03

            Там история возникновения. Сначала это был «packet filter» для сетевиков. Потом его начали использовать для трассировки приложений. Сейчас это почти универсальная lightinig speed виртуальная машина в ядре, которая гарантирует, что программа не зависнет в бесконечном цикле (и не попортит всё вокруг) перед тем, как её запускать.


            1. worldmind
              28.01.2019 19:55

              Может напишете пост о том как напрогать какую-нибудь консольную утилиту на нём?


      1. tgz
        28.01.2019 19:18

        Там еще и есть слово unsafe. Но только вот для реальной жизни ничего лучше нет.


      1. 0xd34df00d
        28.01.2019 19:29

        Ну тогда остается идрис или агда с проверкой тотальности всех функций.


        1. worldmind
          28.01.2019 19:55

          проверка тотальности?


        1. worldmind
          28.01.2019 19:56

          Или это про это?


          1. 0xd34df00d
            28.01.2019 20:02

            Да, про это.


            1. worldmind
              28.01.2019 20:18

              А вышеупомянутые языки предоставляют какие-то специальные инструменты для гарантии того что функция тотальная?


              1. 0xd34df00d
                28.01.2019 21:04

                Да, там есть totality checker. Консервативный, но не всесильный: для некоторых функций приходится постараться, чтобы объяснить тотальность. А, скажем, как изложить известное мне доказательство тотальности интерпретатора просто типизированного лямбда-исчисления, я совсем не знаю.


    1. Siemargl Автор
      28.01.2019 20:32

      См последний абзац перед дисклеймерами. Вполне можно и Раст прогнать через прокрустово ложе _всех_ требований.


  1. vvmtutby
    28.01.2019 21:19

    Отсутствие рекурсии — необязательное требование? ( ADA )
    Аналогично — сборка мусора? ( Active Oberon )

    Итого: стоит добавить в список Modula-3
    ( да и Modula-2 стандарта ISO )


    1. Siemargl Автор
      28.01.2019 21:49

      Насколько я в курсе, идеального языка не существует =)

      Так что ничего абсолютно обязательного быть не может, но какой то критерий конечно важнее.


      1. vvmtutby
        29.01.2019 14:09

        ничего абсолютно обязательного быть не может, но какой то критерий конечно важнее
        А не использовать ли нам «плечи титанов»?
        Например, «список некоторых ограничений, вводимых SPARK» для того, что бы мы могли применять инструментарий формальной верификации:

        1. Все выражения (в том числе вызовы функций) не производят побочных эффектов. Хотя функции в Ада 2012 могут иметь параметры с режимом in out и out, в SPARK это запрещено. Функции не могут изменять глобальные переменные. Эти ограничения помогают гарантировать, что компилятор волен выбирать любой порядок вычисления выражений и подчеркивают различие между процедурами, чья задача изменять состояние системы, и функциями, которые лишь анализируют это состояние.
        2. Совмещение имен (aliasing) запрещается. Например, нельзя передавать в процедуру глобальный объект при помощи out или in out параметр, если она обращается к нему напрямую. Это ограничение делает результат работы более прозрачным и позволяет убедиться, что компилятор волен выбрать любой способ передачи параметра (по значению или по ссылке).
        3. Инструкция goto запрещена. Это ограничение облегчает статический анализ.
        4. Использование контролируемых типов запрещено. Контролируемые типы приводят к неявным вызовам подпрограмм, генерируемых компилятором. Отсутствие исходного кода для этих конструкций затрудняет использование формальных методов.


        1. Siemargl Автор
          29.01.2019 20:29

          Спарк это уже более жесткие ограничения. Пока остановимся на Аде.
          Но с идеей, что какую то особо ответственную часть программы/системы можно писать с более жестким контролем (ака СПАРК), а часть — с послаблениями (Ада), я согласен.

          Аналогично с формальной верификацией — не везде ее применение стоит свеч.


  1. lingvo
    28.01.2019 23:31

    Вы только что описали требования к ответственным или встраиваемым программам, исполняемым в жестком или мягком реальном или времени. Все уже изобретено: Codesys, IEC61131-3, Matlab Simulink, Embedded Coder


    1. Siemargl Автор
      29.01.2019 11:11

      А остальные программы недостойны стабильной и быстрой работы и разработки?

      Мягкое реальное время это как осетрина второй свежести. Не нужно злоупотреблять этим термином, да и я же не внес РТ в перечень требований.

      Codesys — редкое поделие (в сравнении, конечно), но вот если была аналогичная среда для разработки прикладных приложений — было бы неплохо. Это одна из идей для 2й части статьи.


  1. lingvo
    29.01.2019 11:59
    +1

    А остальные программы недостойны стабильной и быстрой работы и разработки?

    Да, но вы не забывайте, что подход к проектированию таких программ и требования будут совершенно неадекватными. Например такая вещь, как динамическое выделение памяти. В программах реального времени статическое выделение памяти — это Маст хев. Но представьте себе Firefox выделяющий себе при запуске память под все. Я думаю ему и 16ГБ не хватит. Windows с определенной реакцией на события? Приготовьтесь, что на стандартном процессоре 3,0 Ггц это будет в пределах 2с, но зато всегда и везде. Это хорошо?


    Не нужно злоупотреблять этим термином, да и я же не внес РТ в перечень требований.

    Вы написали, что требуете определенного времени обработки данных — это одно из требований для программ, исполняемым в реальном времени.


    1. Siemargl Автор
      29.01.2019 12:55

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

      Откуда такие данные про замедление до 2с?
      Ни в каких тестах не наблюдал даже 10-кратного замедления от проверок.

      Для человека время реакции программы 100-200мс — вполне достаточно, если говорить о прикладной области.


      1. vvmtutby
        29.01.2019 14:14

        Откуда такие данные про замедление до 2с?
        Про FF не уверен, но если уменьшим free RAM до 3Gb ( например, стартовав 1-2 VM Hyper-V) и откроем побольше окон в IE, то получим нечто похожее


      1. AlexanderG
        29.01.2019 18:22

        Для человека время реакции программы 100-200мс — вполне достаточно, если говорить о прикладной области.

        Недостаточно. Если машина каждый клик будет обрабатывать по 100 мс, пользователь выкинет её в окно через пять минут.


        1. Siemargl Автор
          29.01.2019 20:21

          Реакция программы — это отработать все действия и выдать результат. Например, отрисовать другое окно по клику.

          VS вот переход на новую страницу в мэйл.ру на сайте, за ~1с уже раздражает.


          1. lingvo
            29.01.2019 21:14

            Ну пожалуйста — действие — запустить программу. Длительность выполнения может достигать и 5с и выше. Это комфортно? Думаю не очень, но нард придумывает что-то. Критично? Вряд ли.
            А как вы можете ускорить это, с вашим подходом?


            1. Siemargl Автор
              29.01.2019 23:59

              Это действие (запуск приложения) относится не к прикладной программе, о которых идет речь в данной статье, а к операционной системе.

              Впрочем об ускорении этого см.мои предыдущие публикации.