среда, 24 июля 2024 г.

Присваивание

Разновидности

  • Обычное var := expr — только устанавливает значение переменной
  • Завершающее var = expr — устанавливает значение и маркирует конец возможности изменения переменной ниже по тексту в тех же ветках кода, учитывая вложенность.
  • Обмен var1 <-> var2 — взаимный обмен переменных значениями
  • Передающее var1 <- var2 — подразумевает, что правая переменная теряет значение
  • Лишение значения X var — обозначение, что переменная утратила значение

Внутриструктурность присваивания — var.{a := b}. Аналогично var.a := var.b за исключением того, что позволяет удостоверять сохранение полноты графа. Внутриструктурность неприменима только к лишению значения.

proc go(x, y int) (ok bool) {
 var (last bool)

 ok := check(x);
 if (ok)
   do /* последнее присваивание только текстуально,
         но не обязательно во времени, особенно в циклах */
     ok = nextDo(<>x, >last)
   while (~last);
// ok = true // ошибка, ok уже зафиксирована в цикле выше
 else
   // позволена, ведь фиксация ok была выше, но в другой ветке
   ok := tryOther(y) 
 end;
//ok = true // ошибка, даже если выполнилась else с обычным присваиванием
}

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

понедельник, 15 июля 2024 г.

Подпрограммы

  • Процедуры — наиболее общие подпрограммы. Могут содержать выходные параметры и изменять внешнее состояние. Возвращаемое значение необязательно.
  • Функции — ограниченные подпрограммы без изменения внешнего состояния с входными неизменяемыми параметрами и возвращаемым значением. Могут содержать изменение внутреннего состояния
    • Константные функции — тривиально вычислимые на этапе трансляции для константых параметров. Не содержат циклов и рекурсивных вызовов.

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

section example0 {
 const (
  infoSize = 4;
  
  //константные функции объявляются в разделе констант
  func optimalAllocSize(main, x0 int) (x1 int) {
    var (total, fixed int)
    
    fixed := main + infoSize;
    total := x0 + fixed;
    if (total <= 16)
      total := 16
    else
      total := -(-total) / 32 * 32
    end;
    x1 := total - fixed
  }
  
  //такие функции можно использовать для получения констант
  dataSize = optimalAllocSize(33, 22)
  
 )
 
 //эта функция содержит цикл и не может возвращать константы 
 func min(a []int) (val int) {
  var (i int)

  val := a[0];
  for (i = 1; i < len(a); i += 1)
    if (val > a[i])
      val := a[i]
    end
  end
 }

 // процедура с изменяемыми параметрами 
 proc swap(<>a, <>b int) {
  var (t int)
  t := a;
  a := b;
  b := t
 }
 
 proc test(a, b int) {
  var (c, d int)
  // константные функции можно использовать и для переменных
  c := optimalAllocSize(a, b);
  d := 1;
  swap(<>c, <>d)
 }
 
}.

Разновидности параметров

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

  • По значению — без отметок.
  • Ссылочный параметр. Обозначается знаками «<>». Используется для чтения и записи в произвольном порядке.
  • Выходной параметр. Требует предшествующего знака «>». Перед чтением в самой процедуре требует предварительного присваивания. Если его не было, то после вызова процедуры значение параметра должно считаться неначализированным. Возможность отсутствия присваивания должна быть указана в объявлении, иначе это считается ошибкой.
  • Ссылочный параметр только для чтения. Предназначен только для параметров структурного типа (массивов и записей) для избежания избыточного копирования и для обобщённого кода. Предваряется знаком «<», но не обязателен для открытых массивов, так как они в общенм случае и так не могут передаваться по значению.
  • Закрываемый параметр. Обозначается буквой «Х»(необязательно «X>»). После вызова процедуры переменная-фактический параметр считается неначализированной.
section example1 {
 type ( file { i int } )
  
 proc open(>f file) {
  f.i := 0
 }
  
 proc read(<>f) (val int) {
  val := f.i;
  f.i += 1
 }
  
 proc close(Х f file) {
  // X f // подразумевается неопределённость в f
 }
  
 proc test {
  var ( f file; i int )
  //i := read(<>f);// ошибка трансляции
  open(>f);
  i := read(<>f);
  close(X f);
  // i := read(<>f); // ошибка трансляции
  open(>f)
 }
}.

Утверждения в подпрограммном типе. Пред- и пост- условия

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

section range {

 type ( +t { +@min, +@max int } )

 proc +init(>r t, mn, mx int)
 [ // предусловие
  ! mn <= mx
 post: // постусловие
  ! t.min = mn
  ! t.max = mx
 ]
 {
  t.min := mn;
  t.max := mx
 }

}.

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

Утверждения, которые верны и для пред-, и для пост- условий, можно не дублировать, а записать один раз после «inv:», находящегося между их секциями.

  • Пред- и пост- условия состоят из утверждений, разрешений и ветвлений
  • В каждом утверждении обязан присутствовать хотя бы один из параметров
  • Из подпрограмм в выражениях утверждений могут использоваться только константные функции. Механизм предназначен для сравнительно легковесных проверок
  • Выходные параметры «>» не могут присутствовать в предусловиях
  • Закрываемые параметры «X» не могут присутствовать в постусловиях
  • Нарушение утверждений в вызовах подпрограмм считается ошибочным состоянием, кроме случая явного учёта правильности
  • Применение в утверждениях непрозрачных предикатов тоже считается ошибочным состоянием, хотя в общем случае и сложно диагностируемым

Проверка параметров

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

section example2 {

 import ( range, log )
 
 var (a, b int)

 proc +test {
  var (r range:t; ok bool)

  /* мягко проверяются только утверждения, 
    куда входят параметры, помеченные «?» */
  ok ?? range:init(>r, ? a, b);
  log.b(ok);// false 
  
  ok ?? range:init(>r, ? b, ? a);
  log.b(ok);// true 
 }

 { a := 32; b := -23 }

}.

Разрешения

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

 proc example3(>out []int) (ok bool) [ 
 post:
  allow uninit (out)
 ] {
  var ( i int )
  ok := false;
  if (ok)
    for (i := 0; i < len(out); i += 1)
      out[i] := len(out) - i
    end
  end
 }

Разрешение на неначализированность выходного параметра после вызова процедуры позволено, если

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

Разрешение на неначализированность входного или ссылочного параметра после вызова процедуры позволено, если

  1. есть входной или ссылочный параметр без возможности неначализированности

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

Для входных и ссылочных параметров есть разрешение на неиспользование — allow unuse name, что может быть необходимо для соблюдения сигнатуры, например, для совместимости.

Полнота требований

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

func add3(a, b int) (sum int) [[
 ! min(int) <= a + b <= max(int)
post:
 ! sum = a + b
]] {
 // переполнение невозможно при выполнении предусловий
 sum := a + b
}

Неявные требования и их отсутствие

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

func add1(a, b int) (sum int) {/* Законно предполагать, что add1 вызывается
   с параметрами, не вызывающими переполнение, но и гарантий этого нет */ 
 sum := a + b // поэтому нужен контроль во время выполнения
}

func add2(a, b int) (sum int) [[]] {
 /* Это ошибочное состояние, потому что возможно переполнение, 
    так как a и b — любые */
 sum := a + b
}

вторник, 18 июня 2024 г.

Маршрутизация разделов

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

sections some {
  
  /* Необязательное перечисление имён как оглавление
  name ( math, path, io, sandbox/io, storage, random.pseudo, editor, 
         random, cypher, untrusted/editor, fileman, compress)
  */

  // расположение доступных разделов
  locate (
    //простейшее указание по одному
    math = file("base/math.def");
    
    //группа разделов по маске через переменную name
    (path, io, sandbox/io, storage, random.pseudo, editor) 
      = file("base/"-name-".def");
    
    //файл может содержать несколько разделов
    (random, cypher) in file("base/cypher.defs") as section(name-".2");
    
    untrusted/editor := file("trash/editor.def");
    fileman := file("files/fileman.def");

    //заглушка для раздела
    compress := stub of file("base/compress.def")
  )
  
  // перечисляет ограниченные по умолчанию разделы 
  restricted ( path )
  
  /* указание доступности для импорта одних разделов другими.
    отсутствие прямого импорта не ограничивает косвенный импорт 
    и косвенные вызовы */
  allow (
    // редактор может получить доступ только к указанному файлу
    (path) for (editor)
    /* хранилище предоставляет ограниченный доступ к системе,
      но само имеет полный доступ для его воплощения */
    (path+all) for (storage)
    
    /* под видом предоставления недоверенному редактору
      «полного» доступа к произвольным файлам даёт путь в изолированную
      песочницу, подменив раздел ввода-вывода */
    (path+all, io = sandbox/io) for (untrusted/editor)
    
    /* те разделы, что не указаны, видны всем (other for all)
      разделы с подразделами не могут входить в эту группу,
      так как подразделы и нужны для разграничения ответственности */
  )
  
  // отсутствие опциональных разделов
  disable (
    (compress) for (fileman)
  )
  
  // строгий запрет на прямое и косвенное использование разделов 
  deny (
    // настоящий ГПСЧ не должен использовать ввод 
    (io) for (random/pseudo)
  )

}

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

среда, 5 июня 2024 г.

Ошибочное состояние

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

Ближайшим аналогом из других языков является термин «неопределённое поведение»[0] с поправкой на правильно выставленный приоритет в отношении понимания и обработки.

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

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

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

  • Обращение к массиву по индексу, выходящему за его пределы
  • Обращение по пустому указателю
  • Обращение по недопустимому адресу в низкоуровневых операциях с данными
  • Арифметическое переполнение допустимого диапазона чисел — и целых, и дробей
  • Деление на 0
  • Чтение значения переменной с ошибочным значением (не- или де-начализированной)
  • Преобразование типа для недопустимого значения
  • Получение ложного значения в утверждении
  • Возможность достижения потоком выполнения места, помеченного как недостижимое
  • Фактическое зацикливание или превышение ограничения количества вычислений
  • Неявные константные выражения
  • Бесполезные выражения и инструкции, не документирующие и не оказывающие влияния на состояние

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

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

Примеры возможного воплощения поведения при ошибочном состоянии:

  • Статическое выявление как ошибки трансляции для простых случаев или при продвинутом анализе. В отличий от опциональных предупреждений, которые могут быть ложно-положительными, выявление ошибки допустимо только для достоверных нарушений правил языка с учётом контекста применения. Поиск нарушений ведётся во все ветках кода без учёта фактической достижимости их выполнения.
  • Аварийное завершение подзадачи в итоге проверок во время исполнения программы. Подходит для отладки и для быстрых проверок хорошо отлаженного кода в выпускной версии.
  • Регистрация ошибки в системе и пометка выходного значения как ошибочного. Другие вычисления с ошибочными значением тоже порождают ошибочное значение, возможно, кроме тех, что приводят к определённому значению вне зависимости от другого значения (e * 0, e & false). Если ошибочное значение добирается до существенной развилки или вывода данных, то происходит аварийное завершение, так как дальнейший учёт ошибочности не представляется возможным. Если ошибочное значение оказывается незадействованным или уничтоженным, то программа может продолжить нормальное выполнение. Этот подход позволяет предотвратить некоторые избыточные падения, оставляя возможность исправления ошибки после получения отчёта о зарегистрированных ошибках.
  • Максимизация случайного поведения. Подходит для отладки того, что сложней диагностировать аварийной остановкой, например, работу с неначализированными переменными. Изменчивое поведение свидетельствует об ошибках и препятствует закреплению случайных характеристик текущей версии исполнителя в ошибочном коде как чего-то ожидаемого[1].
  • Максимизация детерминированности поведения, которое даёт наиболее вероятную правильность. Подходит для выпускной версии, но будет полезен только в паре с предыдущим. Если нужно выбрать что-то одно, лучше выбрать предыдущий.
  • Приостановка выполнения и ожидание ответа пользователя, который может выбрать наиболее подходящее решения на основе анализа конкретной ситуации. Подходит для длительных задач личного характера.
  • Предотвращение нарушений, но без аварийной остановки кода-нарушителя, а возвратом значения ошибки, то есть преобразования ошибки кода в специальное значение, например, ошибку ввода. Подходит для повышения устойчивости кода, менее критичного к последствиям ошибок кода, и в тех подзадачах, где допустимо некоторое значение по умолчанию.
  • Отсутствие специальной реакции, как и необходимости обеспечения детерминированности. Такая возможность — это лишь признание сложности диагностики некоторых ошибок, например, зацикливания, но без требования отказа от их диагностики. Применимо не только к простым трансляторам, но и тем, что работают в связке с системами доказательства правильности, позволяющих, как минимум, доказать отсутствие в коде возможности достижения ошибочного состояния. В таком случае самому транслятору нет необходимости в собственной диагностике ошибок, и потому может быть проще, что важно, если и правильность самого транслятора должна быть доказана.

Сноски

[0] Не исключено, что неудачно выбранное название и привело к тому, что большинство разработчиков понимают эту тему неточно в той или иной степени.
[1] Противодействие закону Хирума. Именно этот закон приводит многих разработчиков к ошибочному выводу о том, что ошибочное состояние/неопределённое поведение является причиной дополнительных ошибок, ведь у них раньше работало, что достигалось ненадёжным подходом правки кода, пока не заработает, вместо устранения всех потенциально выявляемых ошибок.

понедельник, 21 ноября 2022 г.

Ансамбль языков

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

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

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

Языки:

  1. Разъёмно-интерфейсный
    1. Описания типов
  2. Разметка данных
  3. Низкоуровневый системный
    1. Версия для генерации кода
  4. Среднеуровневый системный
  5. Высокоуровневый надёжный
  6. Спецификационный-верифицирующий
  7. Преобразующий
  8. Архитектурный
  9. Быстрокодовый(скриптовый)
    1. Для гибкой записи преимущественно поверхностного кода
    2. Интерфейсный интерактивный

Разъёмно-интерфейсный

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

Описание типов данных нужно не только для активных интерфейсов, но и пассивных данных.

Разметка данных

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

Низкоуровневый системный

Этот язык нужен для представления кода на уровне, близком к машинному, в то же время сохраняя важные защиты высокоуровневых языков. Язык позволяет распространять код между недоверенными системами. Может служить промежуточным представлением обычных трансляторов.

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

Среднеуровневый системный

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

Высокоуровневый надёжный

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

Спецификационный-верифицирующий

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

Преобразующий[0]

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

Быстрокодовый(скриптовый)

Язык для поверхностного кодирования, для ситуативных задач. Для них требуется лёгкость ввода, и не требуется ни высокая проработанность, ни высокая производительность. В пределе такие программы могут терять актуальность даже после единственного исполнения. Уровень критичности к ошибкам сравнительно низок. Тем не менее при аккуратном проектировании возможна куда более вменяемая логика работы, чем для многих современных скриптовых языков, например, bash.

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


Сноски:
[0] Преобразования — это часть концепции поддержки наследия без раздувания сложности

пятница, 10 июня 2022 г.

Другие языки

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

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

четверг, 24 декабря 2020 г.

Исполнители языка

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

Можно выделить 4-е основные свойства, задающие назначение инструмента:

  1. Определяющий
  2. Юркий
  3. Доказанный
  4. Многоцелевой

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

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

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

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

  1. Поддержка нескольких исходных языков-спутников.
  2. Трансляция в разнообразные машинные языки и промежуточные представления.
  3. Языковые и машиноспецифичные оптимизации кода.
  4. Статический анализ для выявления ошибок и, желательно, проверка доказательства правильности.
  5. Преобразования исходного кода для перехода на новые решения.
  6. Отслеживание связей в коде.
  7. Подсчёт метрик кода.

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

воскресенье, 20 декабря 2020 г.

Свойства. Сопровождаемый

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

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