Конструкция программы
Александр Шкотин,
ashkotin@acm.org
Абстракт
Обычно программа представляется как цепь слов. Именно цепь слов появляется на выходе лексического анализатора и подвергается синтаксическому анализу. В работе показывается, что синтаксически программа может быть представлена как ориентированное дерево слов - синтаксическое дерево программы. Причём слова программы располагаются как в узлах так и на стрелках дерева. Основное свойство дерева — стрелки исходящие из каждого узла помечены разными словами (включая пустое слово). В таком случае семантику можно задавать прямо на этом дереве — как требованиями так и дополнительными связями, а пополнение некоторых узлов дерева инструкциями даёт возможность задать выполнение программы, т.е. описать конструкцию программы "до конца".
Раздел 1 содержит краткое изложение подхода и пример текста программы и её синтаксического дерева. Для задания семейства синтаксических деревьев используется синтаксическая схема, описанная в разделе 2. В результате язык оказывается семейством деревьев, заданных схемой. К синтаксическому дереву программы предъявляются дополнительные требования, отражающие семантику языка программирования. Кроме того на синтаксическом дереве программы проводятся дополнительные стрелки — "семантические связи". Семантика зависит от языка программирования и будет рассмотрена на примере языка Turingol [SoCFL] в разделе 3. Конструкция внешних данных, существующих независимо от программы, требует отдельного рассмотрения. В разделе 4 данные описаны только для Turingol, программа которого работает с лентой. Программа в виде синтаксического дерева с семантическими связями должна быть инициализирована. Например к ней должны быть подсоединены внешние данные с которыми она будет работать. Кроме того в узлы соответствующие выполняемым операторам заносятся инструкции для Исполнителя. Инициализация описана в разделе 5, а исполнение в разделе 6.
В Приложении 1 описано как привести авторскую грамматику Turingol к форме дающей схему, а в Приложении 2 собраны необходимые средства работы с конечными помеченными графами.
На протяжении всей статьи мы будем рассматривать в качестве примера Turingol [SoCFL]. Это простой язык программирования для которого с самого его рождения была описана, причём автором, формальная семантика. Правда, при ближайшем рассмотрении оказывается, что описана трансляция программ Turingol'а в программы машин Тьюринга. Трансляция при которой проверяется правильность Turingol программы. О самом же языке автор говорит, что он и так понятен [SoCFL, p.138, lines 1-3.]. Таким образом требования к Turingol программе "скрыты" в трансляции. Впрочем пара требований упомянута явно [SoCFL, p.139]:
"...programs are malformed if the same identifier is used twice as a label or if a go to statement specifies an identifier which is not a statement label. "
Мы не будем рассматривать программы машин Тьюринга, а сосредоточимся на самих Turingol программах. Предполагается, что некий Исполнитель выполняет саму Turingol программу, работающую с лентой. Соответственно будет описана конструкция ленты.
Программа представляет собой граф слов, описание его свойств и правил использования и есть основная цель статьи. В основе графа программы лежит ориентированное дерево слов языка — синтаксическое дерево программы (или её части). Для задания семейства деревьев вводится "синтаксическая" схема, в определённом смысле аналогичная синтаксическим диаграммам Wirth [Wirth]. Но в то время как синтаксические диаграммы задают правила построения цепочки слов, схема задаёт правила построения деревьев, помеченных словами. Способов создания под-деревьев всего два: цепь узлов (связанных стрелками) и узел из которого выходит заданный схемой набор стрелок. То что конкретная схема, порождает деревья исходящие стрелки которых помечены по разному надо доказывать, что и будет сделано для Turingol.
То что программа сама по себе представляет дерево позволяет по новому взглянуть на описание её семантики. Конечно узлам дерева можно приписывать различные атрибуты, в том числе для компиляции [SoCFL].
Полученное дерево должно удовлетворять некоторым требованиям, которые дадут "well-formed Turingol program" [SoCFL,p.138]. Для простоты мы будем предполагать, что существует отдельная фаза проверки этих требований. Хотя безусловно все проверки могут выполняться по мере построения дерева.
Если дерево "хорошее", то между некоторыми его узлами будут проведены дополнительные стрелки выражающие синтаксические и семантические отношения, облегчающие работу Исполнителя. Также для простоты, достройка дерева (до графа) выделена в отдельную фазу.
Чтобы до конца понять программу надо определить способ выполнения. Для этого аналогично [PostM] предполагается наличие абстрактного Исполнителя перемещающегося по графу программы и выполняющего инструкции в узлах соответствующих операторам. Этот подход соответствуют тому как мыслит свою программу программист. Размещение инструкций в узлах отнесено к фазе инициализации программы. Также к ней отнесено соединение программы с внешними данными, что в случае Turingol состоит в присоединении к ней ленты.
Исполнитель выполняет программу перемещаясь по некоторым её узлам и выполняя указанные в них инструкции (в том числе действия с лентой). Это именно то что имел в виду программист создавая программу. И тезис состоит в том, что программист в принципе может выполнить свою программу и сам, не обращаясь к машине (например машине Тьюринга). Более того, именно это понимание программистом выполнения "исходного текста" программы неким Исполнителем обязывает этот Исполнитель (aka debugger) взаимодействовать с программистом как-будто он (Исполнитель) исполняет "исходный текст" программы.
Рассмотрим программу 4.1 [SoCFL] p.137. Для простоты некоторые словосочетания записаны через тире что делает их лексически одним сложным словом. Соответственно тире нужно добавить в алфавит языка.
tape-alphabet is blank, one, zero, point;
print "point";
go to carry;
test: if the-tape-symbol is "one" then
{print "zero"; carry: move left one-square; go to test};
print "one";
realign: move right one square;
if the-tape-symbol is "zero" then go to realign.
Каждое слово программы (включая специальные слова такие как ';', ',') попадает в одну конкретную метку (узла или стрелки). Метка стрелки, а в общем случае и метка узла может быть пустым словом.
Можно проверить, что стрелки исходящие из каждого узла помечены по разному.
Отметим цепи узлов с разделителем, размещённым на стрелках:
- в первом ряду: 'blank' ',' 'one' ',' 'zero' ',' 'point'
- в пятом ряду: 'print' ';' 'move' ';' 'go'
- вертикально: 'print' ';' 'go' ';' 'if' ';' 'print' ';' 'move' ';' 'if'.
При этом первая цепь являются однородной — её члены имеют одинаковое строение. В данном случае это просто слово. Вторая и третья цепь являются разнородными, т.к. составляющие их операторы имеют различное строение дерева.
Заметим также, что дерево программы не является упорядоченным, т.е. не содержит порядка дочерних узлов. Порядка, который мог бы использоваться чтобы задать рисование (расположение на плоскости или линии) дерева.
Мы увидим, что в таком дереве достаточно информации для всей семантики программы. Соответственно, можно предположить, что правила рисования используются лишь для удобства распознавания частей дерева.
Синтаксическая схема используется для задания семейств помеченных деревьев.
Для задания допустимого значения слова в узле или на стрелке дерева используются регулярные выражения (РВ). Впрочем обычно допустимо лишь какое-то конкретное слово. Для Turingol нам понадобятся всего лишь два РВ: left|right и [a-z]+, последнее для задания слова над алфавитом малых английских букв.
Далее в тексте определений в квадратных скобках пишется поясняющий комментарий, формально в определение не входящий.
Пусть PLA и MLA два не пересекающихся алфавита.
PLA – алфавит языка программирования, MLA – алфавит "метазыка".
Определения
Синтаксическое дерево (сокращённо - сид) есть ориентированное помеченное дерево, такое что метка узла и стрелки это слово из PLA*.
Умеченное дерево есть сид у которого стрелки исходящие из каждого узла помечены различно.
[Термин "умеченное" выбран по аналогии с термином "упорядоченное дерево", у которого стрелки исходящие из каждого узла упорядочены.]
Сентенциальное дерево (сед) это ориентированное помеченное дерево, такое что:
- Метка узла это слово из PLA* или из MLA+.
- Метка стрелки это слово из PLA*.
Узлы помеченные словом из MLA+ называются вспомогательные и играют роль аналогичную нетерминалам в КСГ.
Синтаксическая схема есть ориентированный помеченный граф с 2-мя видами стрелок (и-стрелка и или-стрелка) при этом и-стрелки делятся на два подвида (обязательная, необязательная).
Метки это РВ над алфавитом PLA. [В том числе слова из PLA*.]
Узел из которого нет исходящих стрелок называется атомарный (а-узел).
Узел из которого исходят только или-стрелки называется или-узел.
Узел из которого исходят только и-стрелки называется и-узел.
Каждому узлу схемы можно приписать уникальное имя (схемное имя) - слово из MLA+. Такая схема называется полностью поименованная схема.
конец определений
В качестве примера рассмотрим полностью поименованную схему Turingol:
Узлы это прямоугольники. Овалы содержат схемные имена. Или-стрелки обозначены жирной линией. Необязательные стрелки обозначены прерывистой линией.
PLA - строчные латинские буквы, а также: |.,;:-{}"|, где "|" буква не встречающаяся в PLA.
MLA - прописные латинские буквы.
Большинство узлов и стрелок помечены конкретным словом PLA. Узлы DL, LD, I помечены РВ [a-z]+. Стрелка из SM в OS помечена РВ left|right. Узлы L, S, SE помечены пустым словом. Также пустым словом помечены стрелки P-DOT, SI-A, SP-STR.
Теперь опишем как синтаксическая схема задаёт семейство сид.
Построение сентенциального дерева от узла схемы: Пусть схема как-то полностью поименована и указан некоторый узел У0 схемы. Тогда, чтобы создать сентенциальное дерево от узла У0 надо:
Создать изолированный узел У1 (копию узла У0).
Провести из него копии всех обязательных исходящих и-стрелок и какие-то из необязательных узла У0, пометить копии стрелок словами допустимыми РВ их образцов в схеме. На конце каждой стрелки создать узел со схемным именем соответствующего узла схемы.
Если у узла У0 есть или-стрелки, то взять имя узла на конце одной из них и записать в У1 иначе пометить У1 словом заданным РВ узла У0.
Для или-узла инструкция существенно упрощается: Создать изолированный узел У1. Взять имя узла на конце одной из исходящих или-стрелок узла У0 и записать в У1.
Также проста инструкция и для атомарного узла: Создать изолированный узел У1. Пометить У1 словом заданным РВ узла У0.
Таким образом схема даёт возможность сопоставить каждому схемному имени узла совокупность сентенциальных деревьев. Фактически мы имеем в компактной форме КСГ деревьев, когда в левой части правила — изолированный узел помеченный каким-то схемным именем, а в правой части — сед или сид.
Например, от L порождаются два сед (из-за необязательной стрелки): одно - просто S, второе S со стрелкой ';' в L; от S - 12 сед: шесть изолированных узлов (SG, SI...) и шесть этих узлов со стрелкой ':' в LD.
Или-стрелки (в том числе одна как у L) дают возможность задавать сед у которых в корне — нетерминал.
Подстановка же дерева в дерево выполняется заменой узла одного дерева на корень другого дерева.
Схема удобна тем, что представляя собой связный граф даёт возможность некоторые свойства графа сопоставить свойствам семейства порождаемых деревьев.
Построение сид по схеме: Пусть схема полностью поименована. Чтобы получить начальный сед достаточно взять любой узел схемы и создать изолированный узел помеченный схемным именем этого узла схемы.
Пусть дан сед С1 в котором есть узел У1 помеченный каким-то схемным именем. Тогда можно создать сед С2 от узла схемы со схемным именем равным метке У1 и подставить С2 на место У1 в С1.
Если через несколько подстановок в С1 не найдётся узла помеченного словом из MLA+, то мы получили сид.
Свойства схемы: Легко видеть что:
- метка или-стрелки не используется;
- параллельные или-стрелки избыточны;
- метка узла из которого есть или-стрелка не используется.
Таким образом без ограничения общности можно считать, что:
- метка или-стрелки, а также узла из которого есть или-стрелка есть пустое слово;
- на схеме нет параллельных или-стрелок.
Важную роль играют одиночные и-петли (т.е., когда петля у узла единственная), т.к. они задают цепи. Параллельные и-петли задают дерево, конструкцию, которая в языках программирования, кажется, не встречается. Заметим, что если и-стрелка петли обязательна, то процесс построения никогда не закончится и для конечных деревьев эта часть схемы является бесполезной. Таким образом можно считать, что все и-петли необязательны.
Итак, схема задаёт семейство деревьев. То что деревья семейства только умеченные можно попробовать доказать анализируя схему. Следующее свойство схемы необходимо для умеченности семейства.
И-условие: регулярные множества РВ и-стрелок исходящих из каждого узла не пересекаются между собой.
И-условие легко проверить на схеме. Но его не достаточно, т.к. если из узла У1 есть и и-стрелки и или-стрелки, то к и-стрелкам У1 могут присоединяться и-стрелки из узлов на окончаниях или-стрелок и т.д. Например, по схеме Turingol и-стрелка ":" из S ("метка оператора") может распространиться по всем или-стрелкам на все операторы.
Важнейшим свойством "интересных" схем является наличие на графе циклов. Цикл можно характеризовать типами узлов в него входящими.
Или-петля обладает следующим свойством: в случае если она в или-узле, то она избыточна, а иначе приводит к не умеченному сид, т.к. по построению из узла будут нарисованы и-стрелки, а в узле останется тот же "нетерминал", а значить из него опять могут быть нарисованы те же стрелки. Также и или-цикл (цикл или-стрелок на графе схемы) даёт не умеченные деревья. Поэтому, нас будут интересовать схемы у которых выполнено условие И-цикла: в каждом цикле есть и-узел. Что заодно означает, что в схеме нет или-петель.
В случае выполнения условия И-цикла срабатывает простой алгоритм распространения меток и-стрелок по схеме: образуем для каждой и-стрелки пару <схемное имя узла, метка стрелки> и поместим её в узел начала и-стрелки. Каждая такая пара продвигается вдоль всех или-стрелок и "оседает" на и-, а-узлах.
Достаточное условие: Если после "продвижения" совокупный состав пар накопившихся в и-,а-узлах (включая имеющиеся изначально) имеет попарно не пересекающиеся регулярные множества (не конфликтуют), то схема порождает только умеченные деревья.
Схема Turingol удовлетворяет И-условию и условию И-цикла, т.к. на схеме есть только два цикла с или-стрелками: SC-L-S-SC, SI-S-SI и каждый содержит И-узел (SC, SI соответственно).
Алгоритм распространения даёт: Пары <L,';'>, <S,':'> прибудут в узлы на концах или-стрелок из S, но конфликтов не будет.
Таким образом достаточное условие выполнено и схема задаёт семейство умеченных деревьев.
Ясно, что произвольная схема может порождать экзотические деревья, в том числе бесконечные. В Приложении 1 рассматривается соотношение между схемой и КСГ. В том числе показывается к какому виду надо привести КСГ Turingol, чтобы получить схему.
Следуя формулировке exercise 2.4.28, 2.4 Context-free languages [AU-1] имеем: в [Greibach-65] показано, что каждый КС язык порождается грамматикой, все правила которой имеют вид A:aBbC, A:aBb, A:aB, A:a. Если пустая строка принадлежит языку, то допускается правило S:e. Где заглавные буквы обозначают какие-то нетерминалы, строчные буквы (кроме "e") – какие-то терминалы, а "e" – пустую строку.
Легко видеть, что каждому виду правой части можно сопоставить сентенциальное дерево. Записывая граф тройками по количеству стрелок: <начало-стрелки метка-стрелки окончание-стрелки>, получим:
- для первого вида правила: <a '' B>, <a b C>. т.е. "a" – корень и из него идёт стрелка помеченная пустой строкой в "B" и стрелка помеченная "b" в "C".
- для второго: <a b- B>. Здесь "-" после "b" означает, что при линеаризации дерева b должно идти после линеаризации B. Без знака "-" ситуация как раз обратная.
- для третьего: a '' B.
- четвёртое и пятое правила дают атомарные узлы.
Приведённая выше форма КСГ называется стандартной операторной формой (СОФ). Таким образом если грамматику привести к СОФ, то можно будет "механически" перейти к умеченным деревьям. Можно сказать, что теоретически для каждого ЯП существует представление его программ умеченными деревьями. Ясно что таких представлений несколько и некоторые из них наверно будут наглядными, естественными. Лучше всего если о представлении в виде деревьев позаботится ещё Автор языка.
К синтаксическому дереву программы существуют требования, описанные далее (ср. WFC в XML).
На хорошем (удовлетворяющем требованиям) синтаксическом дереве программы достраиваются:
- семантическая связь — is-declared-at;
- семантические связи управления — стрелки: next, yes, no;
превращающие сид в граф.
Обозначения: требования поименованы. Требование несоблюдение которого не критично для выполнения программы имеет в своём имени букву W (warning).
В Приложении 2 собраны некоторые способы работы с графами, которые нам понадобятся по ходу дела.
Определение: узел объявления слова ленты (w-declaration-point) есть узел в цепи с головой 'tape-alphabet'+is.
Здесь 'tape-alphabet'+is есть "формула пути" (см. Приложение 2) и означает узел на конце стрелки помеченной "is" и исходящей из узла помеченного "tape-alphabet", т.е. предлагается пройти от узла помеченного 'tape-alphabet' по ходу (о чём говорит "+") стрелки помеченной "is".
Определение: узел использования слова ленты (w-usage-point) есть каждый узел print+" а также каждый узел if+''+is+".
Здесь print+" и if+''+is+" есть "формулы пути" (см. Приложение 2).
То что эти пути имеют смысл ("проходимы") легко видеть на схеме.
Требования AW1, AW2, AW3
(AW1) метки во всех w-declaration-point должны быть различны.
Содержательно: не следует объявлять слово ленты многократно. Это скорее всего говорит о небрежности или опечатке.
Алгоритмика. Вар-1 - классический. Проще всего иметь в первом узле накопительный атрибут - множество, заполнять проходом цепи и сообщать о том что очередной уже есть. Вар-2 - по простому. Попав в текущий узел проверять - нет ли его дальше по цепи! Тут ничего копить не надо, но конечно не эффективно... Вар-3 - многопроцессорный. Каждый узел высылает остальным своё имя. Каждое пришедшее имя сравнивает со своим и если найдёт совпадение - выдаёт сообщение.
(AW2) значение метки в каждой w-usage-point должно быть равно метке какой-то w-declaration-point.
Содержательно: каждое используемое программой слово ленты должно быть в ней объявлено.
(AW3) метка каждой w-declaration-point должна быть использована в какой-то w-usage-point.
Содержательно: не следует объявлять слово ленты и не использовать его.
Замечание-1. В примере 4.1 это не так и может быть поводом для warning.
Замечание-2. Требование AW3 порождает вопрос - что мы декларируем в фразе tape-alphabet: состав допустимых слов ленты или состав слов ленты используемых в программе.
Выполнение требования (AW2) даёт возможность провести стрелку "is-declared-at" от w-usage-point к w-declaration-point.
Замечание: эти стрелки не нужны Исполнителю.
В других языках они нужны для дальнейшей обработки.
Определение: место задания метки (l-target-point) есть любой узел, в который входит стрелка ':'.
Определение: место использования метки (l-usage-point) есть любой узел, в который входит стрелка 'to'.
Требования L1, L2, LW1
(L1) - все метки в l-target-points должны быть различны.
Содержательно: метка должна однозначно идентифицировать свой узел.
(L2) - каждый l-usage-point должен иметь l-target-point с той же меткой.
Содержательно: должна быть возможность перейти к узлу с указанной меткой.
(LW1) - каждая l-target-point должна иметь l-usage-point.
Содержательно: каждая метка должна быть использована. Иначе она бесполезна.
Пусть дан сид.
Слово go, может оказаться ещё и меткой и словом ленты. Введём на сид простейшую классификацию:
Узел данных: это узел поддерева начинающегося с узла 'tape-alphabet'+is, а также узел на конце ":",to,'"'-стрелок.
S-узел: это не узел данных помеченный словом: go, if, print, move, "", "{".
Член L-цепи: в него входит стрелка ";".
Введём также простую классификацию S-узлов: операторы управления это: if, "{", go; остальные назовём - обычные. Это print, move, "".
Выполнив инструкцию в очередном S-узле Исполнитель должен знать куда идти дальше и так до тех пор пока не выполнит директиву Стоп. Для большинства узлов по которым перемещается Исполнитель следующий исполняемый узел один и только один. Из каждого мы проведём семантическую стрелку "next" в следующий исполняемый узел. Исключение составляют только if-узлы: из них будет проведено две стрелки управления: yes, no - аналогично ситуации описанной у Поста [PostM]:
"(B) Perform operation (e) and according as the answer is yes or no correspondingly
follow direction ji' or ji",".
Исполнитель начинает в узле alphabet-tape и синтаксически должен уйти по стрелке ";" к первому оператору. Это частая ситуация, когда стрелка управления (next, yes, no) параллельна синтаксической стрелке.
Построение: проведём стрелку next параллельно ";"-стрелке исходящей из узла "alphabet-tape".
Останов программы есть специальное действие, которое в таком языке как Turingol лишь подразумевается, т.е. не имеет явного оператора. Для упрощения строения программы и следуя идеям Поста [PostM], введём специальный дополнительный узел, который будет называться Stop, и в котором на этапе инициализации программы (см. далее) будет размещена директива Стоп.
Построение: создать узел и пометить его словом "stop".
Узлы "tape-alphabet" и "{" имеют в подчинении цепь операторов; узел "if" – один оператор. Для обычного оператора если он последний в L-цепи или подчинён if, то какой оператор выполнять следующим указывается в подчиняющем. При условии, что он сам не является подчинённым. Тогда надо идти к его подчиняющему. Чтобы отразить отношение подчинения проведём вспомогательную стрелку "back".
Построение back-стрелок: В каждой L-цепи из последнего члена проводится стрелка "back" в узел подчиняющий цепь (т.е. в узел "tape-alphabet" или "{"). Из каждого узла подвешенного к "if" по стрелке "then" проводится стрелка "back" в этот "if".
Таким образом, по построению, для каждого S-узла выполнена одна и только одна из ситуаций:
- ситуация back: из него есть стрелка back;
- ситуация ";": из него есть стрелка ";", т.е. оператор есть не последний в L-цепи.
Построение: Перенаправим в узел Stop стрелку back входящую в узел alphabet-tape.
Ниже на рисунке представлен результат построения back-стрелок для примера 4.1.
Построение стрелок управления не зависящих от ситуации back or ";":
Для каждого if-узла параллельно then рисуется стрелка yes.
Для каждого "{"-узла параллельно "}" рисуется стрелка next.
Для каждого S-узла помеченного "go", используя его l-usage-point, рисуется стрелка next к последнему узлу до которого можно подняться по ':'-стрелкам от l-target-point с тем же значением метки что и метка l-usage-point.
Возможность последнего построения обеспечивается выполнением требований L1, L2.
Построение стрелок управления для ситуации ";": т.е. не последнего в L-цепи:
Для обычных операторов ("", print, move) создаётся стрелка next параллельная стрелке ";".
Для оператора if создаётся стрелка "no" параллельная стрелке ";".
Таким образом в ситуации ";" синтаксическая стрелка ";" не используется для построения стрелок next для узлов "go" и "{".
Построение управления для узлов имеющих исходящую стрелку back:
1. пометить все back-стрелки как необработанные.
2. если нет необработанных back-стрелок, то стоп.
3. выделить целиком (на всю длину) необработанную back-цепь. Она либо переходит в ";" (назовём это ситуация next и назовём эту ";"-стрелку - С1) либо завершается в узле Stop (назовём это ситуация Стоп).
Из каждого обычного узла back-цепи, имеющего исходящую стрелку back, рисуется стрелка next, а из узла if рисуется стрелка no. Эта стрелка:
если ситуация next, то рисуется туда же куда входит и С1;
если ситуация Стоп, то рисуется в узел Stop.
Все стрелки back-цепи помечаются как обработанные. Идти на 2.
Мы получили граф управления. Именно по нему перемещается Исполнитель, выполняя программу.
Граф управления построен на S-узлах, узле "alphabet-tape", который является стартовым узлом и узле Stop. Тем самым он построен на "синтаксических узлах" (плюс узел Stop) и по исходящим стрелкам устроен просто:
- из каждого S-узла (кроме if) и из узла "alphabet-tape" выходит одна и только одна стрелка next.
- из каждого if-узла выходит одна и только одна стрелка "yes" и одна и только одна стрелка "no".
На рисунке приведённом ниже, и соответствующем примеру 4.1, стрелки управления параллельные синтаксическим стрелкам не нарисованы, но соответствующие синтаксические стрелки нарисованы жирными. Метку соответствующей стрелки управления легко восстановить. Так стрелке ";" из "if" в 4-ой строке конечно соответствует "no".
Требования CW1, C2
Идея управления порождает естественное требование:
(CW1) достижимость из "alphabet-tape" по графу управления любого S-узла программы.
Например: Если внутри L-цепи находится узел go, то узел на окончании исходящей из него ";"-стрелки должен иметь метку, иначе он недостижим. См., например, в 4.1 ";"-стрелку из go в третьей строке.
Проблема останова программы на данных (в том числе —останова на любых данных) чрезвычайна важна, т.к. говорит о применимости программы. Решать её приходится конкретно для каждой программы или класса программ. Но есть и очевидные отрицательные результаты требующие лишь понимания идеи управления.
Требование (C2): next стрелки не должны образовывать цикл.
В том числе goto не должен указывать на самого себя. Заметим, что C2 не является следствием CW1, так в узел может входить несколько стрелок управления.
Наличие конструкции в виде дерева даёт возможность точно и естественно выразить требования предъявляемые к программе, а также задать правила построения дополнительных связей.
Связи is-declared-at, back, next, yes, no, а также узел Stop нужны в большинстве языков программирования.
Нужны ли ещё какие-то связи есть предмет текущего исследования, в котором рассматривается Standard Pascal [SP]. Одна из кандидатов - связь "has-type" для языков с типами данных.
Граф управления проще "странслировать" в машину Поста чем машину Тьюринга.
Лента есть конечная цепь узлов помеченных словами. Стрелки помечены пустым словом.
Конечность ленты приводит к необходимости её достройки, если понадобится сделать шаг влево или вправо к узлу которого нет. Это делается по мере необходимости. Узел и стрелка при создании метятся пустым словом.
Замечание: конечно эта лента не является лентой машины Тьюринга, т.к.:
- содержит в ячейках слова, а не буквы;
- является конечной (см. [PostM] p.105, о возможных "улучшениях" ленты.).
Являясь "синтаксической" конструкцией семейство лент может быть описано формально в дополнение к грамматике языка:
T::=I| I T
где I - нетерминал для идентификатора (см. Приложение 1).
Фактически при описании языка должно быть два стартовых символа: для программы и для внешних данных.
Исполнитель будет:
- сравнивать метки некоторых узлов программы и узлов ленты;
- заносить метки некоторых узлов программы на узлы ленты;
- наращивать ленту.
Наращивание ленты основано на следующих ситуациях и действиях.
Пусть У1 именует корень ленты.
Тогда в результате выполнения директивы "Создать узел и стрелку из него в узел У1." (см. "Приложение 2") мы опять получим ленту. С новым корнем и стрелкой из него в старый корень.
Пусть У2 именует последний узел ленты ленты.
Тогда в результате выполнения директивы "Создать узел и стрелку в него из узла У1." (см. "Приложение 2") мы опять получим ленту. С новым последним узлом.
Лента с которой работает программа Turingol является прообразом файла ЯП и техника представления в виде цепи может быть применена и к файлу.
На этапе инициализации:
- создаётся стрелка "tape" из узла "tape-alphabet" в некоторый узел ленты, с которой должна начать работать программа. Узел ленты в который входит стрелка tape называется текущим узлом ленты.
Обычно при написании программы предполагается, что "после открытия" текущий узел — первый узел ленты. Но, например, для программы примера 4.1 начальным текущим должен быть последний узел ленты.
Кроме того в некоторые узлы программы заносятся инструкции для Исполнителя. Расположение инструкций в узлах аналогично подходу Поста [PostM], правда у него программа состоит из нумерованных строк и есть переход по номеру, а у нас – из узлов и есть переход по стрелке.
Замечание: Эти инструкции можно приписывать узлам схемы как атрибут и копировать в сид ещё на стадии построения.
Далее даются инструкции для S-узлов с той или иной меткой, для начального узла "tape-alphabet" и узла Stop. У остальных узлов инструкций нет.
Для узлов с меткой "move" существуют два варианта инструкции.
Важно подчеркнуть, что по характеру операций выполняемых в инструкциях мы работаем с помеченными графами, меняя метки узлов, создавая узлы и стрелки, переназначая окончание стрелки tape. Если терминология не ясна интуитивно, она более точно описана в Приложении 2.
Узел "tape-alphabet"
Перейти по стрелке "next".
Узел "stop"
Стоп.
Узлы "go"
Перейти по стрелке "next".
Узлы "{"
Перейти по стрелке "next".
Узлы "if"
Если метка узла 'tape-alphabet'+tape равна метке узла +''+is+'"', то перейти по стрелке "yes" иначе перейти по стрелке "no".
Узлы ""
Перейти по стрелке "next".
Узлы "print"
Пометить узел 'tape-alphabet'+tape меткой узла +''+'"'.
Перейти по стрелке "next".
Узлы "move"
Вариант инструкции для move-узла из которого существует стрелка "left":
Если не существует стрелка "" в узел 'tape-alphabet'+tape, то создать узел и стрелку из него в узел 'tape-alphabet'+tape.
Переназначить стрелку tape на узел 'tape-alphabet'+tape-''.
Перейти по стрелке "next".
Вариант инструкции для move-узла из которого существует стрелка "right":
Если не существует стрелка "" из узла 'tape-alphabet'+tape, то создать узел и стрелку в него из узла 'tape-alphabet'+tape.
Переназначить стрелку tape на узел 'tape-alphabet'+tape+''.
Перейти по стрелке "next".
Замечание: Единственное место где нам понадобился проход против хода стрелки и был употреблён оператор "-" это формула пути 'tape-alphabet'+tape-'', т.е. перейти от узла, помеченного "tape-alphabet" по ходу стрелки "tape" к узлу на её окончании (это так называемый текущий узел ленты), а затем перейти от текущего узла против хода стрелки помеченной пустым словом к узлу в её начале. Этот последний узел можно назвать предыдущий.
"оптимизация кода": для узлов move существует два комплекта инструкций. Выбирать надо посмотрев окрестности. Продолжая эту тему можно в узле "if" взять метку узла на конце пути +''+is+'"' и поместить на соответствующее место в инструкцию. Аналогично можно поступить с узлами "print".
Инструкции обладают свойствами, которые в совокупности обеспечивают безаварийность выполнения любой программы на любой ленте.
Укажем некоторые основные свойства.
В целом:
- строение графа меняется только инструкциями в узлах move и состоит в наращивании ленты. При этом структура ленты как цепи не нарушается.
- разметка графа меняется только инструкциями в узлах print и касается только узлов ленты.
кроме того:
- Каждая инструкция уводит Исполнителя из узла, т.е. Исполнитель не может попасть в ситуацию "исчерпаны директивы в узле.".
- Каждая инструкция в S-узле уводит Исполнителя в S-узел или узел Stop. Тем самым Исполнитель всегда попадает в узел в котором есть инструкция и не может попасть в ситуацию "в узле нет инструкции.".
- Все формулы путей в инструкциях проходимы. Некоторые - проходимы всегда, по построению, как например путь +''+'"' от узла "print" или путь +''+is+'"' от узла "if". Пути в которых участвует стрелка tape по мере выполнения программы указывают на разные узлы ленты, но всегда остаются проходимыми в момент употребления.
Исполнитель выполняет программу начиная с некоторого стартового узла и выполняет инструкции приписанные очередному узлу. В общем случае выполнение может завершиться аварийно. Для Turingol высказывается гипотеза, что можно доказать безаварийность.
Начальное состояние конструкции программа плюс лента:
- к узлу "tape-alphabet" по стрелке "tape" подсоединен некоторый узел ленты.
- в узлы занесены инструкции.
Исполнитель начинает в узле "tape-alphabet".
Исполнитель перемещается по стрелкам из узла в узел. Придя в узел Исполнитель выполняет содержащуюся в нём инструкцию.
Если в узле нет инструкции, то Исполнитель завершает выполнение аварийно: ситуация "нет инструкций". Аналогично если "директивы в узле кончились".
Каждая директива имеет своё необходимое и достаточное условие выполнимости (НДУВ). В общем случае Исполнитель обязан действовать осторожно и проверять НДУВ прежде чем выполнить директиву. Это даст ему возможность сообщить об аварии.
Правда, для Turingol имеет место следующая гипотеза безаварийности:
Пусть сид С1 построен по схеме, удовлетворяет требованиям, пополнен связями и инструкциями.
Пусть Л1 - лента.
Если из узла "tape-alphabet" провести стрелку в любой узел ленты Л1 и в узел "tape-alphabet" запустить Исполнителя, то процесс никогда не завершится аварийно.
Так ситуация "инструкции в узле кончились" не может возникнуть, т.к. каждая инструкция имеет последней директивой "перейти по стрелке".
Таким образом не смотря на то, что элементы инструкций (включая формулы пути) в произвольной ситуации аварийны, вычислительные системы (программа + данные) Turingol обладают необычным для ЯП свойством — безаварийностью.
Одно из интересных направлений — разработать для действий аксиоматику Hoare [Hoare] и доказать гипотезу безаварийности.
Получившаяся конструкция программы кажется довольно реалистичной: ведь создавая программу Программист имел в виду такие связи как next и is-declared-at, а записывая тот или иной оператор он имел в виду именно то действие, которое позже найдёт в узле Исполнитель.
В описанном подходе Исполнителем является человек, т.к. инструкции написаны на ограниченном естественном языке. Исполнитель должен уметь работать с помеченными графами и конечно строками букв. При этом сама конструкция вычислительной системы - граф программы заполненный инструкциями и подсоединённый к данным, приемлема до тех пор пока мы можем доказывать её свойства.
Интересно, что умеченного дерева достаточно для семантики, т.е. нам не понадобилось упорядоченное дерево. Скорее всего (как всегда с графами) некие соглашения о рисовании нужны для узнавания по рисунку, диаграмме элементов графа.
Ясно, что для ЯП существует несколько вариантов сидов. Тут было бы лучше всего чтобы автор языка дал сразу свой вариант.
То что лента оказалась графом является началом направления исследований в котором и внешние и внутренние данные программы предполагается представить помеченными графами — обычно деревьями.
Оптимизация и машина Поста: безусловно граф программы может быть оптимизирован по управлению — многие пути могут быть сокращены. В результате мы придём к конструкции изоморфной машине Поста.
Атрибуты Кнута (компиляция): на схеме для узлов можно указать атрибуты, а сид с атрибутами позволяет использовать технику Кнута для вычисления их значений, например для компиляции.
Хотя при определении схемы упомянуты РВ, на практике в ЯП применяются всего несколько РВ:
- конкретные слова языка (включая пустое слово) или их конечные |-комбинации;
- идентификаторы;
- числа (целые, действительные).
Конечный помеченный граф являясь математической конструкцией даёт возможность давать определения, алгоритмы и проводить доказательства. Всё это "хозяйство", если надо может быть формализовано, но в данной работе такой цели не ставилось.
Автор языка мог бы сразу описывать строение деревьев. Хотя это требует дополнительных усилий, на дереве автор языка мог бы точно задать "семантические" требования к программе. Точное описание семантики языка должно привести к качественным и даже "умным" компиляторам.
Программисту при изучении фразы языка возможно стоит знать не только порядок членов, но и какой из терминалов главный, а какой пойдёт на стрелку. Впрочем подходящий редактор деревьев может скрыть от программиста строение дерева.
Если создать редактор деревьев, то построение и хранение программы в виде дерева сделает ненужным лексический и синтаксический анализ.
Кратко же можно сказать, что если обычно считается что программа это цепь слов, то на самом деле оказывается, что программа это умеченное дерево слов.
[SoCFL] Donald E. Knuth, Semantics of Context-Free Languages, Mathematical Systems Theory, Vol. 2, No. 2, p.127-145.
[Wirth] Niklaus Wirth, The Programming Language Pascal. (July 1973)
[XML] Extensible Markup Language (XML) 1.0 (Fifth Edition) W3C Recommendation 26 November 2008, Tim Bray, Jean Paoli, C. M. Sperberg-McQueen, Eve Maler, François Yergeau eds.
[AU-1] Aho, A.V and Ullman, J. D. (1972) "The Theory of Parsing, Translation, and Compiling, Volume 1: Parsing." Englewood Cliffs, N.J.: Prentice-Hall.
[Greibach-65] S. A. Greibach, A new normal-form theorem for context-free, phrase-structure grammars, JACM 12 (1965), 42-52.
[PostM] 1936, "Finite Combinatory Processes - Formulation 1" Journal of Symbolic Logic 1: 103-105.
[Hoare] C. A. R. Hoare. "An axiomatic basis for computer programming". Communications of the ACM, 12(10):576–580,583 October 1969.
[SP] ISO 7185:1990, Information technology - Programming languages – Pascal.
Чтобы представить сид в виде цепи слов его надо упорядочить. Для этого на схеме вводится нумерация узлов и исходящих стрелок: узел и исходящие из него и-стрелки нумеруются числами начиная с 1. Или-стрелки не нумеруются.
Если слово и-стрелки должно идти после цепи слов порождённой узлом на окончании стрелки, то после номера ставится буква "-".
Алгоритм построения правила по узлу схемы:
- левая часть: схемное имя узла.
- правая часть: РВ на и-стрелках и схемные имена узлов на их окончаниях выписываются в порядке номеров на стрелках. При этом если номер стрелки содержит "-", то РВ на стрелке пишется после схемного имени. Если стрелка необязательная то "парочке" приписывается мета-оператор необязательности.
На месте под номером узла ставится: если нет или-стрелок — РВ-узла, иначе - |-выражение от схемных имён узлов на окончаниях или-стрелок.
Примеры:
Пусть нумерация для L: в L-узле — 1, на ";"-стрелке — 2.
получим:
L:S(';' L)?
Пусть нумерация для S: S-узел — 2, ":"-стрелка — 1-, имеем
S:LD ':' (SG|SI|SP|SM|SE|SC)
Чтобы получить синтаксическую схему мы начнём с авторской версии грамматики, преобразуем её к нужной форме и покажем как представить грамматику в виде схемы, порождающей синтаксические деревья программ Turingol.
Для записи КСГ используется нотация EBNF из [XML].
Авторская КСГ Turingol (см. таблицу 1 в [SoCFL]).
A::= [a-z]
I::= A | I A
D::= 'tape alphabet is' I | D ',' I
O::= 'left' | 'right'
S::= 'print' '"' I '"' | 'move' O 'one square' | 'go to' I
| '' /*empty string*/
| 'if the tape symbol is' '"' I '"' 'then' S | I ':' S
| '{' L '}'
L::= S | L ';' S
P::= D ';' L '.'
Некоторые несущественные изменения в лексике языка:
- сложные термины пишутся через дефис (например, tape-alphabet).
Так что нам понадобится дополнительная буква "-".
Алфавит: PLA ::= [-a-z,:;{}.]
Эквивалентное преобразование грамматики: Нам нужны правила у которых правая часть обладает определёнными свойствами, грубо говоря нам надо чтобы нетерминалы перемежались терминалами.
Преобразование:
1. нетерминалы D, O подставлены в свои места употребления.
2. для нетерминала L имеем
L::= S(';' L)?
Введём DL::= [a-z]+ (',' DL)?
Мы получаем:
I ::= [a-z]+
S::= I ':' S | 'print' '"' I '"' | 'move' ('left' | 'right') 'one-square' | 'go' 'to' I | ''
| 'if' 'the-tape-symbol' 'is' '"' I '"' 'then' S
| '{' L '}'
P::= 'tape-alphabet' 'is' DL ';' L '.'
3. Введём LD::=[a-z]+ (':' LD)? тогда S::= I ':' S | Q, можно записать как S::= (LD ':')? Q
что даёт
S::= (LD ':')? ( 'print' '"' I '"' | 'move' ('left' | 'right') 'one-square' | 'go' 'to' I | ''
| 'if' 'the-tape-symbol' 'is' '"' I '"' 'then' S
| '{' L '}'
)
4. Bведём дополнительные нетерминалы:
OS::= 'one-square'
DOT::= '.'
STR::= '"' I '"'
A::= 'the-tape-symbol' 'is' STR
SG::= 'go' 'to' I
SI::= 'if' A 'then' S
SP::= 'print' STR
SM::= 'move' ('left' | 'right') OS
SE::= ''
SC::= '{' L '}'
5. Получаем окончательно:
I::= [a-z]+
OS::= 'one-square'
DOT::='.'
LD::= [a-z]+ (':' LD)?
DL::= [a-z]+ (',' DL)?
STR::='"' I '"'
A::= 'the-tape-symbol' 'is' STR
SG::= 'go' 'to' I
SI::= 'if' A 'then' S
SP::= 'print' STR
SM::= 'move' ('left' | 'right') OS
SE::= ''
SC::= '{' L '}'
S::= (LD ':')? (SP | SM | SG | SE | SI | SC)
L::= S(';' L)?
P::= 'tape-alphabet' 'is' DL ';' L DOT
Приведённым правилам соответствует полностью поименованная схема для Turingol.
Для работы с конечными ориентированными помеченными графами нам понадобятся некоторые элементарные высказывания.
Например нам понадобится элементарное высказывание:
"существует единственная стрелка 'З1'", где З1 — какое-то слово. Это высказывание истинно если на всём графе существует одна и только одна стрелка, имеющая меткой слово З1.
Кроме того нам понадобятся действия, в том числе меняющие граф.
Условие выполнимости (УВ) есть свойство графа необходимое чтобы выполнилось действие или высказывание. При этом некоторая совокупность УВ задаёт необходимое и достаточное условие выполнимости.
Замечание: У Поста существуют ситуации когда Исполнитель может завершить выполнение аварийно, правда Пост говорит о неприменимости (assuming [PostM]). Но аварии — важный элемент мышления программиста: они случаются и он должен думать о безаварийности.
У Поста всего две "аварийных" ситуации:
- попытка записать в ячейку уже содержащую значение;
- попытка стереть значение из ячейки, где его нет.
Для указания узла на графе введём формулу пути:
Пусть wordn — обозначает метку которую имеет единственный узел графа (обозначим его У1), а word — какое-то слово. Тогда,
wordn+word означает узел на окончании стрелки исходящей из У1 и помеченной словом word;
wordn-word означает узел на начале стрелки входящей в У1 и помеченной словом word.
Т.е. В первом случае мы идём по ходу стрелки, а во втором — против хода. Если стрелок для перехода несколько или нет ни одной, то формула считается неприменимой, а при исполнении алгоритма попытка использовать такую формулу приведёт к аварийной ситуации.
Начальный узел должен быть единственным на графе. Например, если формула пути начинается с узла помеченного "tape-alphabet", то возможна авария - указанных узлов больше одного или нет.
Если мы находимся в некотором узле, который считается текущим, то +word означает узел расположенный от текущего узла по ходу стрелки, а -word — против хода стрелки.
Если word содержит букву "-" или является пустым словом, то оно заключается в апострофы, т.е. пустое слово в формуле пути будет обозначаться ''.
Проходимость пути: Пусть П1 формула пути. Нам понадобится высказывание вида «Путь П1 проходим.» от текущего или заданного узла.
Далее приведены некоторые элементарные высказывания, использующие формулы пути. Приведены также необходимые и достаточные условия выполнимости этих высказываний.
Пусть З1 — обозначает какое-то слово, П1, П2 — какие-то формулы пути.
Действия: далее перечислены некоторые действия для работы с ориентированными графами и их необходимые и достаточные условия выполнимости.
Пусть З1 — обозначает какое-то слово, П1, П2 — какие-то формулы пути.
Директива: Любая фраза действия оформленная как предложение есть директива.
Пусть В1 — высказывание, Д1 — действие. Тогда,
"Если В1, то Д1." есть директива.
Замечание:
У Поста direction (директива) это нумерованное предложение на английском языке.
У него есть три формы директив[PostM,p.103-104].
"
The worker is assumed to be capable of performing the following primitive
acts:
(a) Marking the box he is in (assumed empty),
(b) Erasing the mark in the box he is in (assumed marked),
(c) Moving to the box on his right,
(d) Moving to the box on his left,
(e) Determining whether the box he is in, is or is not marked.
The set of directions which, be it noted, is the same for all specific problems
and thus corresponds to the general problem, is to be of the following form. It is
to be headed:
Start at the starting point and follow direction 1.
It is then to consist of a finite number of directions to be numbered 1, 2,
3, . . . n. The i-th direction is then to have one of the following forms:
(A) Perform operation Oi [Oi =(a), (b), (c), or (d) ] and then follow direction ji,
(B) Perform operation (e) and according as the answer is yes or no correspondingly
follow direction ji' or ji",
(C) stop.
Clearly but one direction need be of type C. Note also that the state of the symbol
space directly affects the process only through directions of type B.
"
Примечание о "starting point".
У Поста одномерное пространство ящиков бесконечно в обе стороны, а один из ящиков выделен как начальный — в нём начинает работник.
Инструкция это последовательность из одной или более директив.
Замечание: директивы Поста формы (A) и (B) можно представить как:
(A) Perform operation Oi. Follow direction ji.
(B) If (e) then follow direction ji1. Follow direction ji2.
Тогда они будут состоять из двух директив попроще.
Ясно что инструкция является частным случаем алгоритма.
2