Анализ состояния типа , иногда называемый анализом протокола , является формой анализа программы , используемой в языках программирования . Чаще всего он применяется к объектно-ориентированным языкам. Состояния типа определяют допустимые последовательности операций, которые могут быть выполнены над экземпляром заданного типа. Состояния типа, как следует из названия, связывают информацию о состоянии с переменными этого типа. Эта информация о состоянии используется для определения во время компиляции, какие операции допустимы для вызова над экземпляром типа. Операции, выполняемые над объектом, которые обычно выполняются только во время выполнения, выполняются над информацией о состоянии типа, которая изменяется для совместимости с новым состоянием объекта.
Typestates способны представлять поведенческие уточнения типов, такие как «метод A должен быть вызван до вызова метода B , а метод C не может быть вызван между ними». Typestates хорошо подходят для представления ресурсов, которые используют семантику открытия/закрытия, обеспечивая семантически допустимые последовательности, такие как «открыть, затем закрыть», в отличие от недопустимых последовательностей, таких как оставление файла в открытом состоянии. Такие ресурсы включают элементы файловой системы, транзакции, соединения и протоколы. Например, разработчики могут захотеть указать, что файлы или сокеты должны быть открыты перед тем, как они будут прочитаны или записаны, и что они больше не могут быть прочитаны или записаны, если они были закрыты. Название «typestate» происходит от того факта, что этот вид анализа часто моделирует каждый тип объекта как конечный автомат . В этом автомате каждое состояние имеет четко определенный набор разрешенных методов/сообщений, а вызовы методов могут вызывать переходы состояний. Сети Петри также были предложены в качестве возможной поведенческой модели для использования с типами уточнения . [1]
Анализ состояния типа был введен Робом Стромом в 1983 году [2] в Network Implementation Language (NIL), разработанном в Watson Lab IBM . Он был формализован Стромом и Йемини в статье 1986 года [3] , в которой описывалось, как использовать состояние типа для отслеживания степени инициализации переменных, гарантируя, что операции никогда не будут применяться к неправильно инициализированным данным, и далее обобщен в языке программирования Hermes .
В последние годы различные исследования разработали способы применения концепции состояния типа к объектно-ориентированным языкам. [4] [5]
Strom и Yemini (1986) потребовали, чтобы набор состояний типов для данного типа был частично упорядочен, так что более низкое состояние типа можно было получить из более высокого, отбросив некоторую информацию. Например, переменная int
в C обычно имеет состояния типов " uninitialized " < " initialized ", а FILE*
указатель может иметь состояния типов " unallocated " < " allocated, but uninitialized " < " initialized, but file not opening " < " file opening ". Более того, Strom и Yemini требуют, чтобы каждые два состояния типов имели наибольшую нижнюю границу, т. е. чтобы частичный порядок был даже meet-semilattice ; и чтобы каждый порядок имел наименьший элемент, всегда называемый "⊥".
Их анализ основан на упрощении, что каждой переменной v назначается только одно состояние типа для каждой точки в тексте программы; если точка p достигается двумя различными путями выполнения и v наследует различные состояния типа через каждый путь, то состояние типа v в точке p принимается за наибольшую нижнюю границу унаследованных состояний типа. Например, в следующем фрагменте кода на языке C переменная n
наследует состояние типа " initialized " и " uninitialized " из частей then
и (пустой) else
соответственно, поэтому она имеет состояние типа " uninitialized " после всего условного оператора.
int n ; // здесь n имеет состояние типа "неинициализировано" if (...) { n = 5 ; // здесь n имеет состояние типа "неинициализировано" } else { /*ничего не делаем*/ // здесь n имеет состояние типа "неинициализировано" } // здесь n имеет состояние типа "неинициализировано" = greatest_lower_bound("initialized","uninitialized")
Каждая базовая операция [примечание 1] должна быть снабжена переходом typestate , т. е. для каждого параметра требуемым и гарантированным typestate до и после операции соответственно. Например, операция fwrite(...,fd)
требует fd
иметь typestate " file opening ". Точнее, операция может иметь несколько результатов , каждому из которых нужен свой переход typestate. Например, код C FILE *fd=fopen("foo","r")
устанавливает fd
typestate в " file opening " и " unallocated ", если открытие успешно и не удалось, соответственно.
Для каждых двух typestates t 1 <· t 2 необходимо предоставить уникальную операцию приведения typestate , которая при применении к объекту typestate t 2 снижает его typestate до t 1 , возможно, освобождая некоторые ресурсы. Например, fclose(fd)
приводит fd
typestate из " file opening " в " initialized, but file not opening ".
Выполнение программы называется typestate-correct, если
Текст программы называется согласованным по состоянию типа , если его можно преобразовать, добавив соответствующие приведения состояния типа, в программу, точки которой могут быть статически помечены состояниями типа, так что любой путь, разрешенный потоком управления, является правильным по состоянию типа. Штром и Йемини предлагают алгоритм линейного времени , который проверяет заданный текст программы на согласованность состояния типа и вычисляет, куда вставить какую операцию приведения, если таковая имеется.
Для достижения точного и эффективного анализа typestate необходимо решить проблему псевдонимов . Псевдонимы возникают, когда объект имеет более одной ссылки или указателя, указывающего на него. Чтобы анализ был правильным, изменения состояния данного объекта должны быть отражены во всех ссылках, указывающих на этот объект, но в целом отследить все такие ссылки — сложная задача. Это становится особенно сложным, если анализ должен быть модульным, то есть применяться к каждой части большой программы отдельно, не принимая во внимание остальную часть программы.
Еще одна проблема заключается в том, что для некоторых программ метод взятия наибольшей нижней границы при сходящихся путях выполнения и добавления соответствующих операций понижающего приведения оказывается неадекватным. Например, перед return 1
в следующей программе [примечание 3] все компоненты x
, y
и инициализируются, но подход Строма и Йемини не распознает этого, поскольку каждая инициализация компонента структуры в теле цикла должна быть подвергнута понижающему приведению при повторном входе в цикл, чтобы соответствовать состоянию типа самого первого входа в цикл, а именно ⊥. Связанная с этим проблема заключается в том, что этот пример потребует перегрузки переходов состояний типов; например, z
изменяет состояние типа " no component initialized " на " x component initialized ", а также " y component initialized " на " x and y component initialized ".coord
parse_int_attr("x",&coord->x)
int parse_coord ( struct { int x ; int y ; int z ; } * coord ) { int seen = 0 ; /* запоминаем, какие атрибуты были проанализированы */ while ( 1 ) if ( parse_int_attr ( "x" , & coord -> x )) виден |= 1 ; else if ( parse_int_attr ( "y" , & coord -> y )) виден |= 2 ; else if ( parse_int_attr ( "z" , & coord -> z )) виден |= 4 ; else break ; если ( seen != 7 ) /* отсутствует какой-то атрибут, неудача */ return 0 ; ... /* все атрибуты присутствуют, выполняем некоторые вычисления и добиваемся успеха */ return 1 ; }
Существует несколько подходов, направленных на выведение состояний типов из программ (или даже других артефактов, таких как контракты). Многие из них могут выводить состояния типов во время компиляции [6] [7] [8] [9] , а другие извлекают модели динамически. [10] [11] [12] [13] [14] [15]
Typestate — это экспериментальная концепция, которая пока не перешла в основные языки программирования. Однако многие академические проекты активно исследуют, как сделать ее более полезной в качестве повседневной техники программирования. Два примера — языки Plaid и Obsidian, которые разрабатываются группой Джонатана Олдрича в Университете Карнеги-Меллона . [16] [17] Другие примеры включают в себя фреймворк для исследования языка Clara [18] , более ранние версии языка Rust и >>
ключевое слово в ATS . [19]
+=
в C, и стандартные библиотечные процедуры, напримерfopen()
malloc
ed-память очищена free
. В большинстве языков программирования время жизни переменной может закончиться до завершения программы; тогда понятие typestate-correctness должно быть соответствующим образом уточнено.int parse_int_attr(const char *name,int *val)
инициализация происходит *val
всякий раз, когда она успешна