суббота, 23 сентября 2017 г.

Ловушки для создателя языка программирования

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

вторник, 20 июня 2017 г.

Целочисленные типы

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

Целочисленные типы должны быть разделены на две группы:
  1. Ограниченное число предопределённых типов, подходящих для большинства задач, и к которым применимы встроенные арифметические операции: +, -, *, /, %.
  2. Типы из обычных или псевдо-разделов, для работы с которыми необходимо использовать функции из тех же разделов с соответствующими названиями: add, sub, mul, div, mod и т.д.

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

Предопределённые типы

В первую группу должны войти:
НазваниеДиапазон допустимых значений
byte 0 .. (28 - 1)
int -(231 - 1) .. (231 - 1)
longint -(263 - 1) .. (263 - 1)

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

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

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

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

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

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

Защитный языкJava
var (a, b, c int; d longint; f bool)

a = max(int) / 2; 
b = a + 2; 
c = 300;

f = a + b < c; // false
d = a * b; // 1152921504606846975
c = b * 2 / 3 // 715827883
int a, b, c; long d; boolean f;

a = Integer.MAX_VALUE / 2;
b = a + 2;
c = 300;

f = a + b < c; // true
d = a * b;     // -1
c = b * 2 / 3; // -715827882

Типы предопределённых разделов

Вторую группу целочисленных типов должны представить типы из состава специальных разделов, отличающихся знаковостью и разрядностью гарантированных диапазонов. Разделы должны предоставлять почти одинаковые наборы функций для работы с собственными целочисленными типами. Имена разделов можно вывести из синтаксического уравнения - имя = [u]int(8|16|32|64), где u обозначает беззнаковость, а число, естественно - разрядность типа. Диапазоны допустимых значений определяются по формулам - [0 .. 2n-1] для беззнаковых и [-2n-1 .. 2n-1-1] для чисел со знаком, так как они должны быть закодированы через двоичное дополнение.

Объявления разделов:
type (t) // сам целочисленный тип

var (min, max t) // минимальное и максимальное значение

// Группа функций, которые трактуют переполнение как ошибку.
// Если при их выполнении не было явно получено состояние корректности,
// то в случае переполнения они завершают выполнение программы.
func add(addend1,  addend2 t)    (sum t);
func sub(minuend,  subtrahend t) (difference t);
func mul(factor1,  factor2 t)    (product t);
func div(divident, divisor t)    (ratio t);
func mod(divident, divisor t)    (remainder t);

// Группа процедур, для которых переполнение сопровождается воображаемым
// отбросом старших разрядов.
proc mod_add(addend1, addend2,    > sum t)        (overflow bool);
proc mod_sub(minuend, subtrahend, > difference t) (overflow bool);
proc mod_mul(factor1, factor2,    > product t)    (overflow bool);

// Функции для преобразования к основным типам и обратно. Поскольку при
// этом может возникнуть переполнение, то функции также подразумевают
// явное взятие корректности, либо завершение работы.
func to_byte   (value? t) (result byte);
func to_int    (value? t) (result int);
func to_longint(value? t) (result longint);

func from_byte   (value byte)    (result t);
func from_int    (value int)     (result t);
func from_longint(value longint) (result t);

// Функции интерпретации стандартных типов как соответствующих их
// диапазону типов из разделов противоположной знаковости и наоборот.

// в разделе int8:
func as_byte(value t) (result byte);
func as_t(value byte) (result t);

// в разделе uint32:
func as_int(value t) (result int);
func as_t(value int) (result t);

// в разделе uint64:
func as_longint(value t) (result longint);
func as_t(value longint) (result t);

Битовые операции для встроенных целочисленных типов не предусмотрены как неуместные. Вместо них вводятся:

  1. Встроенная функция для возведения в степень func pow(v, n int) (res int) или отдельная операция v^n. Выбор будет сделан позднее.
  2. Типы-множества, которые можно приводить к целочисленным и обратно и о которых подробней будет рассказано в отдельной заметке.

Дополнительные материалы:

  1. Danger – unsigned types used here!
  2. INT14-C. Avoid performing bitwise and arithmetic operations on the same data.