среда, 28 декабря 2016 г.

Состояние правильности

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

Необходим механизм, который позволит сделать проверки синтаксически более легковесными. Для языка защищённого программирования таковым становится явно выведенное состояние правильности.

Многие языки программирования используют явное понятие некорректности, известное как механизм исключений, но такой вид неприемлем для наших целей. Исключения сами порождают некоторые проблемы. Например, усложняют отделение ошибок входных данных от ошибок в самой программе, что важно для упрощения устранения и ошибок и уменьшения последствий их проявления. Также они нарушают принцип структурного программирования, заставляя учитывать, что любая подпрограмма при правильном выполнени может завершиться иным способом. Поэтому в новом языке понятие правильности выведено иным способом.

Рассмотрим для примера сложение — так выглядит оно без явных проверок:

sum = a1 + a2;

При переполнении такая программа должна аварийно завершиться. Чтобы проверить возможность переполнения и выполнить сложение только при его отсутствии, можно было бы использовать явные проверки:

if (a2 >= 0) 
    ok = a1 <= max(int) - a2
else
    ok = a1 >= min(int) - a2
end;
if (correct)
    sum = a1 + a2
end

Или, при наличии специальной функции, такого кода:

ok = add(> sum, a1, a2);

Язык предлагает такую запись для выполнения действия и явного получения его правильности:

ok ?? sum = a1 +? a2;

Лексема «??» служит для обозначения присваивания состояния правильности, а «?» придаёт операции возможность передать это состояние в соответствующую логическую переменную. Без этого знака поведение операции будет обычным несмотря на наличие «??».

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

Необходимость же введения специальных знаков вместо использования функций, вроде указанной add, объясняется на более сложных примерах. Незащищенный проверками код:

a = b[i] * b[0] + c;

Код с проверками на функциях:

ok = (i >= 0) & (i < len(b)) & mul(> m, b[i], b[0]) & add(> a, m, c);

Новый подход:

ok ??  a = b[?i] *? b[0] +? c;

Этот способ лучше читается и позволяет обойтись без переписывания первоначального кода, обеспечивая непрерывность в его сопровождении — сначала можно написать незащищённый прототип, а затем небольшими правками защитить его от неправильных данных. Небольшие правки позволяют избежать ошибок переписывания.

Действия, для которых доступна проверка с помощью «?»:

  1. Арифметика: «+?», «-?», «*?», «/?», «%?»
  2. Арифметика совместно с присваиванием: «+?=», «-?=», «*?=», «/?=», «%?=»
  3. Обращение к элементу массива: array[? i], matrix[? y][? x]
  4. Выделение динамической памяти тоже можно сделать с показателем правильности для избежания лишних проверок на null: new(param)?
  5. Обращение к элементу структуры, заданной указателем: pstr .? item
  6. Вызов функции, которая может возвращать состояние неправильности: fun(param)?
  7. Вызов функции с проверкой правильности параметра: fun(? param)
  8. Вызов рекурсивной функции при исчерпании допустимой глубины вызовов recursive(? deep) fun(param)
  9. Приведение типа: int(? rational)
  10. Приведение динамического типа — переход от основы к расширению (от предка к наследнику): base.(? extended)

четверг, 28 июля 2016 г.

Объявления и области видимости

В этом вопросе язык для надёжного программирования ближе всего к Оберону, но с более жёсткими условиями по недопущению перекрытия.

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

Объявление-d, сделанное на уровне раздела-s доступно по имени от начала его появления в тексте до конца s. В других разделах-so d доступно только в том случае, если оно было помечено как экспортируемое в s, и только в тех из них, где s был импортирован. Доступ к d в so осуществляется черезимпортированное имя s - ins, следующей за ним разделяющей точкой и именем d

section s {
  const(c = d + 1; // ошибка - d ещё не объявлена
       +d = 0;     // пометка объявления "+" как экспортированное
        e = d + 1)
  
  proc p() {
    const(f = d;
          e = 6) // ошибка - е уже объявлено в этой области уровнем выше
    ...
  }
}.

section so {
  import (ins = so) // переименовываем, s недоступна для использования
  const (d = ins.d;
         e = so.d;  // ошибка, раздел s был переименован для использования
         f = ins.e) // ошибка, объявление е не было экспортировано в s
  
  proc p() {
    { const (a = 3)
      ...
    }
    { const (a = 0.6)// ошибка - хотя a=0.6 объявлена в другой области
      ...            // видимости, но в пределах одной процедуры,
    }                // что запрещено
  }
}.
Имя объявления, сделанного на уровне структуры должно быть уникальным лишь в пределах самой структуры. Имя может совпадать как с предопределёнными именами, так и с именами, объявленными на уровне раздела, функций и других структур, в том числе вложенных, так как из-за строго иерархического обращения к элементам структуры не может быть никакого кофликта имён.
section a {
  const (b = 1)
  var (r struct {
           r struct {
              r, b int
           }
         }
      )
  proc p() {
    r.r.r = 0;
    r.r.b = b
  }
}

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

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

section a {
  const(+a = 10)// обращение к собственному модулю не предусмотрено, 
                // поэтому имя объявления может совпадать с именем модуля.
  proc +b() {
    const(c = a;
          d = 4)// d объявлена ниже, поэтому не входит в текущую 
    ...         // область видимости
  }
  
  proc +d() { // область видимости d = 4 закончилась внутри b()
    const(c = a + 1)// хотя с уже встречается в b(), но так как области 
                    // b() и d() не пересекаются, то объявление правильно. 
    ...
  }
}

Запрет на перекрытие имён и возможность иметь несколько конкретизаций синтаксисов создают трудности, требующие разрешения:

  1. Невозможность использования подходящего имени, если оно уже занято предопределённым идентификатором, что может быть неприятным, если их будет много.
  2. Невозможность использования объявления с именем, совпадающим с ключевым словом, от модуля, написанного под другой синтаксис.

Для разрешния этого можно использовать спец-символ, обозначающий, что помеченное имя имеет пользовательское происхождение.

section @const { // @ как в C#
  const (+@section = 1)
}

Можно рассмотреть и другую возможность — отказ от ключевых слов и ограниченное число предопределённых имён. При необходимости добавления новых в обновлённой версии языка, они добавляются в псевдораздел, чтобы исключить пересечение с именами в разделе, написанном ранее. Но это решение пока кажется сложней и проблематичней. Вариант, в котором наоборот — все ключевые слова и предопределённые идентификаторы помечены, кажется слишком неудобным и уродливым.

@section m {
  @const (c = 1)
  @type  (t @int)
  @var   (v t)
  
  @func f(i t) (res @bool) {
     res = i < @max(t) / 2
  }
}

понедельник, 18 июля 2016 г.

Разделяемость (модульность)

Организующей программной единицей является раздел. В ряде языков, например, в Oberon их называют модулями [0].

section name {
 /* Это место перечисления используемых разделов 
    и упорядоченных объявлений в строгом порядке:
    констант, типов, переменных, функций,
    отдела начализации переменных */
}.
Oberon
MODULE name;
 (* Это место перечисления используемых разделов 
    и упорядоченных объявлений в строгом порядке:
    констант, типов, переменных, функций,
    отдела начализации переменных *)
END name.

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

Для обеспечения доступности объявлений раздела другим разделам, они должны быть экспортированы. Упорядоченная совокупность экспортируемых объявлений составляет интерфейс раздела. Для работы с экспортированными объявлениями требуется произвести импорт разделов, в которых они объявлены.

section m1 {
  const (+a = 0;/* экспорт обозначен «+» перед именем */
          b = a + 1)
  ...
}.

section m2 {
  import (m1) /* доступ к элементам раздела через двоеточие */
  const (a = m1:a;/* a = 0 */
         b = m1:b)/* ошибка - константа "b" не экспортирована в "m1" */
  ...
}.

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

section m1 { import (m2) }.

section m2 {
  import (m1)/* ошибка - m1 уже ссылается на m2 */
}.

Назначение разделов

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

  • Раздел может использоваться исключительно как вспомогательный, не влияя на интерфейс импортирующего раздела. Использование такого раздела может быть заменёно другим кодом, выполняющим те же задачи.
  • Раздел, являющийся частью интерфейса импортирующего его раздела. Объявленные в нём типы могут становиться видимой частью экспортируемых объявлений импортирующего.
  • Обобщённый раздел-заготовка, чьё определение задаётся не только его непосредственным содержимым, но также и импортированными разделами-параметрами.
  • Опциональные разделы, наличие которых можно проверить в коде. Могут использоваться либо для создания более ограниченной версии раздела, либо для задействования более эффективных средств, не приводя к жёсткой зависимости от них.
  • Декоративные разделы, служащие для удобства использования. Они каким-либо образом перестраивают взаимодействие с основным функционалом, практически не меняя его основной сути, например, объединяя интерфейсы разных разделов.

Поскольку разделов, особенно с учётом сторонних библиотек, может быть много, то возникает необходимость в создании иерархий:

section a/m  { const (+c1 = 1) }.
section a/m2 { const (+c2 = 2) }.
section b/m  { const (+c3 = 3) }.

section m {
  import (
    a/m;  /* в качестве идентификатора раздела после импорта     */
    a/m2; /* используется только вторая часть имени после точки */
    bm = b/m/* переименование, чтобы избежать ошибки совпадения имён */
  )
  const (c4 = m:c1 + m2:c2 + bm:c3)
}.

Версии разделов

Система в целом может одновременно поддерживать разные версии одного и того же раздела. С её точки зрения это просто разные разделы, их типы считаются разными даже при полном совпадении объявлений. Таким образом разрешается проблема несовместимых зависимостей. Возможна и более тонкая настройка, когда учитывается не столько раздел целиком, сколько его отдельные объявления. Если объявления совпадают полностью, включая поведение, обеспечиваемое тем же самым исполняемым кодом, то их всё же можно считать совместимыми, не переводя на них несовместимость других объявлений.

В примере версии разделов указаны напрямую, но также их можно задать в проекте маршрутизации разделов[1], продолжая оперировать только именами в коде самих разделов.

section lib.0.1 { ... }
section lib.0.2 { ... }

section a { import (lib.0.1) }
section b { import (lib.0.2) }

section u { import (a; b; lib1 = lib.0.1; lib2 = lib.0.2) }

Разграничение, подразделы

Для удобства разграничения возможностей, предоставляемых импортирующим разделам, раздел может быть разбит на части, ограниченно владеющие функционалом. Раздел может сохранять высокую связность, поэтому такие части не могут быть разнесены по разным разделам.

/* path — это имя раздела, а name, clear и all — имена подразделов, 
  открывающие дополнительный функционал пользователям path, 
  которым дан доступ к этим подразделам и если 
  они их указывают при импорте */
section path +name+clear+all {

  type (+t) /* можно обозначить, не раскрывая содержимое */

section +name+clear+all:

  type (+t = { +name string })

section +clear+all:

  type (+t = { +up (*)t })

section +all:
  /* Только в этом подразделе позволено задавать путь, и без доступа 
    к нему путь к произвольным ресурсам закрыт по определению */
  proc +new(name string, indir (*)t) (path *t) { ...;
    path.name = name; path.up = indir }
  proc +str(spath string) (path *t) { ... }

} path.

section editor {
  import (
    /* path+all; */ /* после импорта должен быть доступен по имени path, 
                        но у editor нет доступа к полному разделу, 
                       и здесь была бы ошибка трансляции */

    path; /* эта часть позволяет получать доступ к ресурсам, пути к
            которым явно переданы параметрами, но не позволяет 
            самостоятельно указывать, с какими ресурсами можно работать, 
            как и не даёт доступа к самому имени */
    io 
  )
  proc +do(file path:t) {
    ... io.open(file)
    ...
    // io.open(path.str("~/.ssh/id_rsa")) // ошибка трансляции
  }
}.

Подразделы могут вносить следующие изменения относительно предыдущих подразделов:

  • Добавление объявлений, включая элементы в ранее объявленные типы.
  • Добавление экспортированности ранее закрытым объявлениям, включая отдельные элементы.

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

Трансляция

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

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

В операционных системах, где были бы воплощены идеи хотя бы 80-х, а не только 60-70-х, раздел может служить главной и единственной единицей загрузки вместо россыпи таких понятий как программа, статическая и динамическая библиотеки. В морально устаревших же операционных системах нужно будет выполнить наподобие этого:

$ sc build util.do

Что приведёт к созданию исполнимого файла util, полученного из одноименного раздела с точкой входа do — экспортированной процедуры без параметров.


Сноски:

[0] Почему раздел — это не модуль/unit?

Необходимо ответить на вопрос: «Если в разделе несовместимо изменилось экспортированное объявление, должна ли потеряться совместимость с разделами, использующими только неизменившиеся его объявления?»

Если ответ — «не должен», значит истинными модулями являются отдельные объявления раздела, а не сам раздел. Раздел же служит организационной цели, так как отдельные объявления слишком мелки для этого. Объявления пронизаны неявным импортом других объявлений раздела для избежания чрезмерного перечисления используемых связей, что было бы нужно в случае раздельного их оформления.

[1] Маршрутизация разделов в защитном языке.
[2] Оберон умер, да здравствует Оберон! Часть 2. Модули
[3] Михаэль Франц Динамическая кодогенерация: ключ к разработке переносимого программного обеспечения
[4] Webassembly: Design Rationale

воскресенье, 17 июля 2016 г.

Синтаксис

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

Язык не обязан иметь лишь одну терминальную часть синтаксиса, которая отвечает за условную кодировку программы - вполне возможна такая реализация средств программирования, которые позволяли бы конкретные детали синтаксиса выбирать по усмотрению разработчика, оставляя неизменной лишь ту центральную часть, которая ответственна за определения конструкций языка. Хотите си-подобный вид программ — пожалуйста. Больше по вкусу паскалевский подход — и это без проблем. Нужны ключевые слова на родном языке для обучения — всё для вас. Более того, исходный код не обязан быть представлен в виде традиционного печатного формата, а может использовать более богатое представление, наподобие того, которым оперируют текстовые процессоры, а также и не обязан вообще иметь строковое представление, а быть оформленным, к примеру, в виде диаграмм. В развитых средствах программирования возможен учёт особых потребностей людей с инвалидностью, например, слепых или страдающих ДЦП.

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

Необходимость угодить среднестатистическому кодировщику не оставляет выбора — основная лексика должна быть Би-подобный как ныне наиболее популярная. Именно в языке B Кен Томпсон заложил основы вида кодировки, которая сейчас известна благодаря С.

Обсуждение хороших свойств некоторых языков в половине случаев застревает в обсуждении чего-то подобного begin и end, поэтому можно просто дать программистам их любимые {}, и вместо ненужных споров сосредоточиться на главном. Единственное, что нужно сделать, учитывая особенности Си — это привести его синтаксис к более понятному и ошибкоустойчивому виду, наподобие того, как это получилось у создателей Go. Собственно, ради экономии энергии в первом приближении можно взять синтаксис Go за основу, но без трепетного отношения ко всем решениям его создателей, так как иная семантика связана с иным синтаксисом.

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

понедельник, 11 июля 2016 г.

Архитектура языка

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

  1. Правильность исходного кода
  2. Простота использования языка
  3. Лёгкость воплощения транслятора
  4. Эффективность выходного кода

Учитывается, что:

  1. Язык ценен не только тем, что в нём есть, но и тем, чего в нём нет.
  2. Добавить новую возможность гораздо легче, чем удалить старую.
  3. И маловероятные ошибки могут становиться большими проблемами.
  4. Возможность негарантированного обнаружения ошибки лучше гарантированного необнаружния.

Если какие-то возможности нужны для оптимизации или обеспечения совместимости с внешним миром, но могут приводить к появлению ошибок или сложностям в понимании, то предпочтение будет отдаваться подходу, в котором такие возможности будут перемещены из ядра языка в разновидность раздела. Это будет стимулировать использовать более простые средства, не злоупотребляя преждевремнной оптимизацией кода.

Конструктивные особенности:

  1. Синтаксис
  2. Разделяемость
  3. Маршрутизация разделов
  4. Объявления и области видимости
  5. Состояние правильности
  6. Ошибочное состояние
  7. Целочисленные типы

Другое:

воскресенье, 10 июля 2016 г.

Свойства. Цельный

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

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

Современный разработчик имеет то преимущество перед первопроходцами, что основные потребности давно ясны и опробованы в разных вариантах. Всё что остаётся - это правильно распорядиться этими знаниями и не повторять ошибок предшественников.

четверг, 7 июля 2016 г.

Свойства. Эффективный

Разработчик языка для защищённого программирования не может поддаться соблазну пренебречь необходимостью учитывать ограничения вычислительных возможностей современных устройств, провозглася, что закон Мура давно решил все проблемы программистов и теперь можно использовать почти сколь угодно прожорливые инструменты - главное, чтобы разработчику было приятно, а об остальном позаботятся гигагерцы с гигабайтами. Увы, это не так. Всегда будут ниши, где будет требоваться максимальная производительность, и если язык защищённого программирования не будет употребим для этих ниш из-за тяжеловесности, то это будет стимулировать к использованию старых низкоуровневых языков, оставляя программы всё такими же незащищёнными, как и раньше. Чтобы не стать ещё одним ненужным узкоспециальным языком, ему необходимо позволять писать код с такой эффективностью, с которой он мог бы приблизиться к низкоуровневым языкам.

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

понедельник, 20 июня 2016 г.

Свойства. Дисциплинирующий

Язык должен способствовать использованию хороших практик программирования и усложнять вплоть до невозможности использование плохих. К плохим практикам можно отнести неструктурные переходы потока управления, низкоуровневое, а, значит, и незащищённое использование памяти, злоупотребление глобальным состоянием и создание слишком объёмных функций.

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

воскресенье, 19 июня 2016 г.

Свойства. Ошибкоустойчивый

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

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

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

Также в языке не может быть однозначной реакции на ошибки. Часть ошибок вылавливать чисто статически слишком сложно, поэтому нельзя волюнтаристски назначить такой, в остальном лучший, вариант. Гарантированный перенос отлова ошибок на время исполнения приводит к нежелательным последствиям. В первую очередь это может привести к встраиванию ошибок кода в дизайн самого кода, а также послужить уменьшению надёжности и даже общей применимости.

Отладка как неотъемлемая часть процесса разработки должна найти соответствующее отображение в языке и инструментарии. В то же время она сама не должна служить источником дополнительных ошибок.

суббота, 18 июня 2016 г.

Свойства. Мощный

Мощность - это то свойство языка, что позволяет писать довольно сложный код с минимальными затратами разработчика, что при соблюдении других принципов способствует улучшению обозримости и понятности алгоритмов. Для того же, чтобы правило всё же не входило в противоречие с простотой и понятностью, необходимо из всех возможных средств отобрать только самые важные. Иными словами, нужно взять те 10% возможностей, что небходимы для написания 90% кода, смирившись, если понадобится, с неудобством языка для более редких случаев.

вторник, 14 июня 2016 г.

Свойства. Однозначный

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

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

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

среда, 8 июня 2016 г.

Свойства. Простой

Полное описание языка*, не содержащее неоднозначных мест, должно быть по возможности кратким. В идеале, средний разработчик должен иметь возможность усвоить описание за один рабочий день.

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

* не следуеть путать описание языка и стандартной библиотеки

понедельник, 6 июня 2016 г.

Свойства. Простой для инструментария

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

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

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

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

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

воскресенье, 5 июня 2016 г.

Свойства. Понятный

Понятность — это главный способ достижения защиты от ошибок. Человек должен глубоко понимать то, что он пишет и читает, а не приблизительно догадываться о смысле. Любые инструменты разработчика должны быть лишь вспомогательным средством.

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

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

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

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

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

* не следуеть путать описание языка и стандартной библиотеки

Свойства языка

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

  1. Понятный
  2. Простой для разработчика
  3. Простой для инструментария
  4. Однозначный
  5. Ошибкоустойчивый
  6. Дисциплинирующий
  7. Мощный
  8. Эффективный
  9. Цельный
  10. Сопровождаемый

Введение

Автоматизация на основе электроники всё глубже проникает в человеческую жизнь, занимая новые ниши и всё сильней на неё влияя: бытовая техника обзаводится прилагательным "умная", автомобили примеряют на себя автопилот, компании доставки испытывают квадрокоптеры и, вообще, каждая мало-мальски уважающая себя вещь стремится обзавестись айпишником и беззастенчиво устроить интернет вещей. Это, конечно, замечательно и создаёт новые прекрасные возможности, но одновременно и несёт новые угрозы. Одной из причин таких угроз являются ошибки программного обеспечения, которые и сами по себе способны нанести вред, так ещё и зачастую служат инструментом в руках злоумышленников. А уж автоматизация вредительской работы ускоряет её ничуть не хуже полезной.

Важнейшим инструментом в руках программиста, способном существенно влиять на свойства приложений как в лучшую, так и худшую сторону, является язык программирования. И сейчас, в 21 веке по-прежнему широко используются языки, созданные десятилетия назад без современного уровня понимания и опыта для куда более скромных задач чем те, что ставятся сейчас. Это способствует появлению специфических ошибок в ПО, которых могло бы не быть при использовании более продуманных средств. Несмотря на многолетнее развитие старых языков, требование обратной совместимости обязало сохранить многочисленные недочёты и было закреплено в стандартах.

Особенно наивно выглядят попытки использования этих языков для приложений, связанных с противодействием злому умыслу. Безупречные с математической точки зрения механизмы защиты нередко оказываются бесполезны, так как будучи реализованы в незащищённой среде, могут быть обойдены через "чёрные ходы" ошибок.

Более новые языки, пришедшие в промышленность за последние 20 лет, несмотря на похвальные попытки исправить ситуацию, не смогли в полной мере искоренить недостатки старых решений. Во-первых, отнесясь с пренебрежением к требовательности среды исполнения, они не смогли удовлетворить две противоположные ниши - маломощные устройства и область высокопроизводительных программ. Во-вторых, авторы новинок не смогли ограничиться исправлениями ошибок, и, стремясь облагодетельствовать и побаловать разработчика, внесли немало новых возможностей вместе с сопутствующими проблемами.

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

Задача этой работы заключается в составлении требований c последующим воплощением нового языка программирования среднего уровня, являющегося, с одной стороны, нацеленным на создании правильных в широком смысле, защищённых и устойчивых систем, а с другой - по прежнему универсальным, применимым к широкому кругу задач. Я попытаюсь учесть опыт разных поколений составителей языков, приняв во внимание разработки как промышленных компаний, так и академических кругов.