Однопроходный компилятор
Эта статья , возможно, содержит оригинальные исследования . ( январь 2017 г. ) |
В компьютерном программировании однопроходный компилятор — это компилятор , который проходит через части каждой единицы компиляции только один раз, немедленно переводя каждую часть в окончательный машинный код. В этом отличие от многопроходного компилятора , который преобразует программу в одно или несколько промежуточных представлений между исходным кодом и машинным кодом и повторно обрабатывает всю единицу компиляции на каждом последовательном проходе.
Это относится к логическому функционированию компилятора, а не к фактическому однократному чтению исходного файла. Например, исходный файл может быть прочитан один раз во временное хранилище, но затем эту копию можно будет сканировать много раз. Компилятор IBM 1130 Fortran сохранял исходный код в памяти и использовал множество проходов; напротив, ассемблер в системах, в которых отсутствует дисковое запоминающее устройство, требовал, чтобы исходная колода карт была дважды подана к устройству считывания/перфорации карт.
Характеристики
[ редактировать ]Этот раздел нуждается в расширении . Вы можете помочь, добавив к нему . ( февраль 2018 г. ) |
Однопроходные компиляторы меньше и быстрее многопроходных. [1]
Однопроходные компиляторы не могут генерировать столь же эффективные программы, как многопроходные компиляторы, из-за ограниченного объема доступной информации. Многие эффективные оптимизации компилятора требуют нескольких проходов по базовому блоку , циклу (особенно вложенным циклам), подпрограмме или всему модулю. Некоторые требуют прохождения всей программы. Некоторые языки программирования просто не могут быть скомпилированы за один проход из-за их конструкции. Например, PL/I позволяет размещать объявления данных в любом месте программы, в частности, после некоторых ссылок на еще не объявленные элементы, поэтому никакой код не может быть сгенерирован до тех пор, пока не будет просканирована вся программа. Определение языка также включает операторы препроцессора, которые генерируют исходный код для компиляции: гарантировано несколько проходов. Напротив, многие языки программирования были разработаны специально для компиляции с помощью однопроходных компиляторов и включают специальные конструкции , позволяющие осуществлять однопроходную компиляцию.
Трудности
[ редактировать ]Основная проблема заключается в прямых ссылках. Правильная интерпретация символа в какой-то момент исходного файла может зависеть от присутствия или отсутствия других символов далее в исходном файле, и до тех пор, пока они не будут обнаружены, правильный код для текущего символа не может быть создан. Это проблема контекстной зависимости, и диапазон может быть любым: от соседних символов до сколь угодно больших объемов исходного текста.
Местный контекст
[ редактировать ]Предположим, что символ < распознается как символ сравнения «меньше чем», а не, например, «больше чем». Из-за ограничений кодировки символов глиф ≤ может быть недоступен в стандартной кодировке, поэтому допускается составное представление «<=". Несмотря на то, что этот контекст определяется следующим символом, неизвестно, когда встречается «<». Аналогично, символ «=" не всегда означает «=", например, когда он является частью составного символа. Другие составные символы могут включать «.lt». для случая, когда специальный символ «<» недоступен. Еще одна возможность, когда код символа для глифа ¬ («не») недоступен, — это «<>» для «¬=" или «не равно» — в некоторых системах используются ~ или ! for¬ как еще одна вариация. Один из подходов — продолжить сканирование после «<» и, встретив «=", вернуться назад. Это, конечно, означает, что будет два прохода по этой части текста, чего следует избегать. В этом случае исходный файл может быть получен с устройства, не поддерживающего операцию возврата и повторного чтения, например устройства чтения карт. Вместо принятия раннего решения, которое впоследствии, возможно, придется отменить, лексический анализатор может поддерживать множество интерпретаций, подобно понятию квантовой суперпозиции, скатываясь к конкретному выбору только при более позднем наблюдении определяющего символа. Примечательно, что компиляторы COBOL уделяют особое внимание различению точек, появляющихся в десятичных константах, и точек, которые появляются в конце операторов. Такая схема недоступна однопроходному компилятору.
Аналогично и с названиями предметов. Немногие языки ограничиваются односимвольными именами, поэтому символ «x» в односимвольном имени сильно отличается от символа «x» в таких именах, как «текст» — теперь контекст выходит за пределы непосредственно соседних символов. Задача лексического анализатора — разделить элементы последовательного исходного потока на лексемы языка. Это не просто слова, потому что «<» и «<=" тоже являются токенами. Имена обычно начинаются с буквы и продолжаются буквами и цифрами и, возможно, несколькими дополнительными символами, такими как «_». Синтаксис, разрешенный для указания чисел, на удивление сложен, например, +3.14159E+0 может быть допустимым. Обычно допускается произвольное количество пробелов между токенами, а Фортран необычен тем, что разрешает (и игнорирует) пробелы внутри видимых токенов, так что «GO TO» и «GOTO» эквивалентны, как и «<=" и «<» =". Однако в некоторых системах могут потребоваться пробелы для разделения определенных токенов, а другие, например Python, используют начальные пробелы для обозначения объема программных блоков, которые в противном случае могли бы обозначаться Начало ... Конец или подобные маркеры.
Контекст внутри выражений
[ редактировать ]Языки, допускающие арифметические выражения, обычно следуют синтаксису инфиксной записи с правилами приоритета. Это означает, что генерация кода для оценки выражения не происходит гладко, поскольку токены выражения извлекаются из исходного текста. Например, выражение x + y*(u - v) не приводит к эквиваленту нагрузки x, прибавьте y, поскольку x не добавляется к y. Если для арифметики используется стековая схема, код может начинаться с Load x, но код, соответствующий следующему токену +, тогда не следует. Вместо этого генерируется код для (u - v), за которым следует умножение на y, и только после этого добавляется x. Парсер арифметических выражений не перемещается вперед и назад по источнику во время анализа, он использует локальный стек отложенных операций, управляемый правилами приоритета. Этого танца можно избежать, потребовав, чтобы арифметические выражения были представлены в обратной польской нотации или аналогичной; для приведенного выше примера что-то вроде uv - y * x + и которое будет сканироваться строго слева направо.
Оптимизирующий компилятор может анализировать форму арифметического выражения, чтобы выявить и устранить повторения или внести другие потенциальные улучшения. Учитывать
a*sin(x) + b*sin(x)
Некоторые языки допускают присваивание значений внутри арифметического выражения, поэтому программист мог бы написать что-то вроде
a*(t:=sin(x)) + b*t
но если не считать усилий, необходимых для этого, форма результирующего утверждения беспорядочна, и ее уже нелегко сравнивать с математическим выражением, для которого закодировано. Ошибки будут легко допущены. Вместо этого компилятор может представить всю форму выражения (обычно используя древовидную структуру), проанализировать и изменить эту структуру, а затем выдать код для улучшенной формы, возможно, что-то вроде (a + b)*sin(x)
. Было бы очевидно расширение блоков последовательных операторов присваивания. Это не предполагает повторного прохождения исходного текста как такового.
Контекст среднего уровня
[ редактировать ]Хотя лексический анализатор разделил входной поток на поток токенов (и отбросил любые комментарии), интерпретация этих токенов в соответствии с синтаксисом языка все же может зависеть от контекста. Рассмотрим следующие утверждения в псевдокоде Фортрана:
if (expression) = etc. if (expression) label1,label2,label3 if (expression) x = .true. if (expression) then
Первый — это присвоение значения некоторого арифметического выражения ( etc ) элементу одномерного массива, называемому «if». Фортран необычен тем, что не содержит зарезервированных слов, поэтому токен «запись» не обязательно означает, что выполняется оператор записи. Остальные операторы на самом деле являются операторами if. Второй — арифметический if, который проверяет знак результата выражения и на основании того, что он отрицательный, нулевой или положительный, переходит к метке 1, 2 или 3; третий — логическое «если» и требует, чтобы результат его выражения был логическим («логическим» в терминологии Фортрана), что, если оно истинно, означает, что следующая часть оператора будет выполнена (здесь присваивается значение true для x и «истина» «не является зарезервированным словом, особое значение указывается точками, отделяющими его); и четвертый - это начало последовательности if... then... else... end if, которая также требует, чтобы выражение давало логический результат.
Таким образом, правильная интерпретация токена «если», выходящего из лексического анализатора, не может быть сделана до тех пор, пока после сканирования выражения и после закрывающей скобки не появится либо знак равенства, либо цифра (являющаяся текстом label1 : в то время как фортран использует в качестве меток можно использовать только целые числа, метки могут быть названы с помощью оператора ASSIGN, поэтому сканирование должно основываться на поиске двух запятых), начала оператора присваивания (или оператора записи и т. д.) или «то» ( за ним не должен следовать другой текст), и поэтому теперь контекст охватывает произвольный объем исходного текста, поскольку выражение произвольно. Однако во всех четырех случаях компилятор может генерировать код для оценки выражения по мере его сканирования. Таким образом, лексический анализ не всегда может определить значение только что идентифицированных токенов из-за особенностей допустимого синтаксиса, и поэтому синтаксический анализ должен поддерживать суперпозицию возможных состояний, чтобы избежать обратного отслеживания.
Поскольку синтаксический анализ дрейфует в тумане наложенных состояний, в случае возникновения ошибки (то есть, обнаружен токен, который не может быть помещен в какой-либо допустимый синтаксический фрейм), создание полезного сообщения может быть затруднено. Например, компилятор B6700 Algol был известен сообщениями об ошибках, такими как «ожидается точка с запятой», а также списком строки исходного кода и маркером, показывающим место неисправности, часто обозначающим точку с запятой. При отсутствии точки с запятой, если она действительно была размещена так, как указано, при перекомпиляции для нее вполне могло возникнуть сообщение «неожиданная точка с запятой». Часто стоит обратить внимание только на первое сообщение об ошибке компилятора, потому что последующие сообщения пошли не так. Отменить текущую интерпретацию и затем возобновить сканирование в начале следующего оператора сложно, если исходный файл содержит ошибку, и поэтому последующие сообщения бесполезны. Дальнейшее производство кода, конечно, прекращается.
Эту проблему можно решить за счет использования зарезервированных слов, так что, например, «if», «then» и «else» всегда являются частями оператора if и не могут быть именами переменных, но представляют собой удивительно большое количество полезных слов. слова могут стать недоступными. Другой подход — «обрезание», при котором зарезервированные слова выделяются, например, путем помещения их между специальными символами, такими как точки или апострофы, как в некоторых версиях Алгола. Это означает, что 'if'
и if
— это разные токены, последнее — обычное имя, но использование всех этих апострофов вскоре становится утомительным. Во многих языках пробел предоставляет достаточную информацию, хотя это может быть сложно. Часто это не просто пробел (или табуляция и т. д.), а символ, отличный от буквы или цифры, который завершает текст возможного токена. В приведенном выше примере выражение оператора if должно быть заключено в скобки, чтобы «(» определенно заканчивало идентификацию «if» и, аналогично, «)» позволяло идентифицировать «then»; кроме того, другие части составного оператора if должны располагаться на новых строках: «else» и «end if» (или «endif») и «else if». Напротив, в Алголе и других системах скобки не нужны, и все части оператора if могут располагаться в одной строке. В Паскале , если a или b , то и т. д. допустимо , но если a и b являются выражениями, то они должны быть заключены в скобки.
Списки исходных файлов, создаваемые компилятором, можно облегчить чтение, если зарезервированные слова, которые он идентифицирует, будут выделены подчеркнутыми , жирным шрифтом или курсивом , но была критика: «Algol - единственный язык, который различает курсив и обычную точку». . На самом деле это не шутка. В Фортране начало оператора do, например DO 12 I = 1,15
отличается от DO 12 I = 1.15
(присвоение значения 1,15 переменной с именем DO12I
; помните, что пробелы не имеют значения) только из-за разницы между запятой и точкой, а глифы печатного списка могут быть неправильно сформированы.
Пристальное внимание к конструкции языка может способствовать ясности и простоте выражения с целью создания надежного компилятора, поведение которого легко понять. Тем не менее, неправильный выбор является обычным явлением. Например, Matlab обозначает транспонирование матрицы с помощью апострофа, как в A', который не является исключением и точно соответствует математическому использованию. Ну и хорошо, но для разделителей текстовой строки Matlab игнорирует возможность, предоставляемую символом двойной кавычки, для любых целей и также использует для этого апострофы. Хотя Octave использует двойные кавычки для текстовых строк, он также стремится принимать операторы Matlab, поэтому проблема распространяется на другую систему.
Расширения препроцессора
[ редактировать ]Именно на этом этапе применяются опции препроцессора, называемые так потому, что они выполняются до того, как компилятор обработает входящий исходный код. Они повторяют возможности «макрорасширения» ассемблерных систем, надеюсь, с более изящным синтаксисом. Наиболее распространенной является вариация
if condition then this source else other source fi
часто с некоторой договоренностью, позволяющей отличать исходные операторы препроцессора от «обычных» исходных операторов, таких как оператор, начинающийся с символа % в pl/i или # и т. д. Другой простой вариант — это вариант
define this = that
Но необходима осторожность, как в
define SumXY = (x + y) sum:=3*SumXY;
Поскольку без скобок результатом будет sum:=3*x + y; Аналогичным образом, необходима осторожность при определении границ замещающего текста и способа сканирования полученного текста. Учитывать
#define three = 3; #define point = .; #define one = 1; x:=three point one;
Здесь оператор определения завершается точкой с запятой, и сама точка с запятой не является частью замены. Вызов не может быть x:=threepointone;
потому что это другое имя, но three point one
было бы 3 . 1
и последующее сканирование может рассматривать или не рассматривать это как один токен.
Некоторые системы позволяют определять процедуры препроцессора, выходными данными которых является компилируемый исходный текст, и могут даже позволять такому источнику определять дополнительные элементы препроцессора. Умелое использование таких опций позволяет давать константам поясняющие имена, заменять непонятные детали простой мнемоникой, появляться новые формы операторов и генерировать встроенный код для конкретных применений общей процедуры (например, сортировки). , а не разрабатывать реальные процедуры. С увеличением количества параметров и типов параметров количество необходимых комбинаций растет в геометрической прогрессии.
Кроме того, один и тот же синтаксис препроцессора может использоваться для нескольких разных языков, даже естественных языков, как при создании истории из шаблона истории с использованием имени человека, клички, клички домашней собаки и т. д., и возникает искушение разработать программу препроцессора, которая бы принимала исходный файл, выполняла действия препроцессора и выдавала результат, готовый для следующего этапа — компиляции. Но это явно означает как минимум один дополнительный проход через исходный код, и поэтому такое решение будет недоступно для однопроходного компилятора. Таким образом, продвижение фактического входного исходного файла может происходить урывками, но оно по-прежнему является однонаправленным.
Дальний контекст
[ редактировать ]Генерация кода компилятором также сталкивается с проблемой прямой ссылки, особенно в таких случаях, как « Перейти к метке» , где метка назначения находится на неизвестном расстоянии дальше в исходном файле, и, таким образом, инструкция перехода для достижения местоположения этой метки включает в себя неизвестную расстояние по еще не сгенерированному коду. Некоторые языковые конструкции, возможно, под влиянием того, что «GOTO считаются вредными» , не имеют оператора GOTO, но это не решает проблемы, поскольку в программе существует множество неявных эквивалентов GOTO. Учитывать
if condition then code true else code false fi
Как упоминалось ранее, код для оценки условия можно сгенерировать сразу. Но когда встречается токен then , должен быть размещен код операции JumpFalse, адрес назначения которого является началом кода для операторов code false , и аналогичным образом, когда else встречается токен code true , только что завершенный код для операторов за ним должна следовать операция перехода в стиле GOTO, назначением которой является код, следующий за концом оператора if, отмеченный здесь токеном fi . Эти пункты назначения становятся известны только после того, как для еще не отсканированного источника будет сгенерировано произвольное количество кода. Аналогичные проблемы возникают для любого оператора, части которого охватывают произвольное количество источников, например, оператора case .
Компилятор рекурсивного спуска активирует процедуру для каждого типа оператора, такого как оператор if, в свою очередь вызывая соответствующие процедуры для генерации кода для операторов кода true и кодирования ложных частей своего оператора, и аналогично для другие утверждения в соответствии с их синтаксисом. В своем локальном хранилище он будет отслеживать местоположение поля адреса своей незавершенной операции JumpFalse и при обнаружении своего тогдашнего токена поместит теперь известный адрес и аналогичным образом при обнаружении фи- токена для перехода, необходимого после кода. истинный код. Оператор GoTo отличается тем, что код, через который осуществляется переход, не находится внутри его формы оператора, поэтому необходима запись во вспомогательной таблице «исправлений», которая будет использоваться, когда наконец встретится его метка. Это понятие можно расширить. Все переходы в неизвестный пункт назначения могут быть выполнены с помощью записи в таблице переходов (адреса которой позже заполняются по мере обнаружения пунктов назначения), однако необходимый размер этой таблицы неизвестен до конца компиляции.
Одним из решений этой проблемы является то, что компилятор выдает исходный код ассемблера (с сгенерированными компилятором метками в качестве пунктов назначения для переходов и т. д.), а ассемблер определяет фактические адреса. Но это явно требует дополнительного прохождения (версии) исходного файла и поэтому запрещено для однопроходных компиляторов.
Неудачные решения
[ редактировать ]Хотя в приведенном выше описании использовалось представление о том, что код может быть сгенерирован с определенными полями, которые необходимо исправить позже, существовало неявное предположение, что размер таких кодовых последовательностей был стабильным. Возможно, это не так. Во многих компьютерах предусмотрены возможности для операций, занимающих разные объемы памяти, в частности, для относительной адресации: если пункт назначения находится в пределах, скажем, -128 или +127 шагов адресации, то можно использовать восьмибитное поле адреса, в противном случае для достижения цели требуется гораздо большее адресное поле. . Таким образом, если код был сгенерирован с обнадеживающим коротким полем адреса, позже может возникнуть необходимость вернуться и скорректировать код для использования более длинного поля, в результате чего более ранний код, ссылающийся на местоположения после изменения, также придется корректировать. Аналогичным образом, необходимо будет исправить более поздние ссылки, идущие назад по изменению, даже те, которые были на известные адреса. Кроме того, сама информация об исправлении должна быть исправлена правильно. С другой стороны, длинные адреса можно использовать во всех случаях, когда близость не определена, но результирующий код уже не будет идеальным.
Однопроходный последовательный ввод, вывод нерегулярной последовательности
[ редактировать ]Уже упоминалось о некоторых возможностях оптимизации в рамках одного оператора. Оптимизация нескольких операторов потребует, чтобы содержимое таких операторов хранилось в какой-то структуре данных, которую можно было бы анализировать и манипулировать до создания кода. В таком случае создание предварительного кода, даже с учетом разрешенных исправлений, будет помехой. В предельном случае это означает, что компилятор сгенерирует структуру данных, представляющую всю программу во внутренней форме, но можно схватиться за соломинку и сделать заявление, что фактического второго прохода исходного файла от начала до конца не существует. Возможно, в PR-документе, рекламирующем компилятор.
Примечательно, что компилятор не может генерировать свой код в одной непрерывной последовательности, тем более сразу после чтения каждой части исходного кода. Вывод по-прежнему может записываться последовательно, но только в том случае, если вывод раздела отложен до тех пор, пока не будут выполнены все ожидающие исправления для этого раздела.
Декларация перед использованием
[ редактировать ]При создании кода для различных выражений компилятору необходимо знать природу операндов. Например, такой оператор, как A:=B; может создавать совершенно разный код в зависимости от того, являются ли A и B целыми числами или переменными с плавающей запятой (и какого размера: одинарной, двойной или четырехкратной точностью) или комплексными числами, массивами, строками, типами, определяемыми программистом и т. д. В этом случае простым подходом была бы передача подходящего количества слов хранения, но для строк это может быть непригодно, поскольку получатель может быть меньше, чем поставщик, и в любом случае можно использовать только часть строки - возможно, в ней есть место на тысячу символов, но на данный момент содержит десять. Затем существуют более сложные конструкции, предлагаемые COBOL и pl/i, например: A:=B by name;
В этом случае A и B представляют собой агрегаты (или структуры), причем A состоит, например, из частей. A.x
, A.y
и A.other
в то время как B имеет части B.y
, B.c
и B.x
, и именно в таком порядке. Функция «по имени» означает эквивалент A.y:=B.y; A.x:=B.x;
Но потому что B.c
не имеет аналога в A, и A.other
не имеет аналога в B, они не задействованы.
Все это можно решить, потребовав, чтобы элементы объявлялись до их использования. Некоторые языки не требуют явных объявлений и генерируют неявное объявление при первом обнаружении нового имени. Если компилятор Фортрана встретит ранее неизвестное имя, первая буква которого одна из I,J,...,N, то переменная будет целым числом, в противном случае - переменной с плавающей запятой. Таким образом, имя DO12I
будет переменной с плавающей запятой. Это удобно, но после нескольких опытов с ошибочными именами большинство программистов соглашаются, что следует использовать опцию компилятора «неявный none».
Другие системы используют характер первой встречи для определения типа, например, строка, массив и т. д. Интерпретируемые языки могут быть особенно гибкими: решения принимаются во время выполнения, примерно так:
if condition then pi:="3.14" else pi:=3.14 fi; print pi;
Если бы для такого языка существовал компилятор, ему пришлось бы создать сложный объект для представления переменной pi, содержащий указание на то, каков ее текущий тип, и соответствующее хранилище для представления такого типа. Это, конечно, гибко, но может оказаться бесполезным для интенсивных вычислений, например, при решении Ax = b, где A — матрица сотого порядка, и внезапно любой из ее элементов может относиться к другому типу.
Процедуры и функции
[ редактировать ]Объявление перед использованием также является простым требованием для процедур и функций, и это также относится к вложенности процедур внутри процедур. Как и в ALGOL, Pascal, PL/I и многих других, MATLAB и (с 1995 года) Fortran позволяют функции (или процедуре) содержать определение другой функции (или процедуры), видимой только внутри содержащей функции, но эти системы требуют что они будут определены после завершения содержащей процедуры.
Но когда рекурсия разрешена, возникает проблема. Две процедуры, каждая из которых вызывает другую, не могут быть объявлены одновременно перед использованием. Один из них должен быть первым в исходном файле. Это не имеет значения, если, как и в случае встречи с неизвестной переменной, из этой встречи можно сделать достаточные выводы, чтобы компилятор мог сгенерировать подходящий код для вызова неизвестной процедуры, конечно же, с аппаратом «исправления» для возврата. и введите правильный адрес назначения, когда встретите определение процедуры. Например, это относится к процедуре без параметров. Возвращаемый результат вызова функции может иметь тип, различимый при вызове, но это не всегда может быть правильным: функция может возвращать результат с плавающей запятой, но ее значение присваивается целому числу.
Паскаль решает эту проблему, требуя «предварительного объявления». Сначала должно быть указано одно из объявлений процедуры или функции, но вместо тела процедуры или функции ключевое слово «forward» указывается . Затем можно объявить другую процедуру или функцию и определить ее тело. В какой-то момент «прямая» процедура или функция переобъявляется вместе с телом функции.
При вызове процедуры (или функции) с параметрами их тип будет известен (они объявляются перед использованием), но их использование при вызове процедуры может быть не известно. Например, Фортран передает все параметры по ссылке (т. е. по адресу), поэтому не возникает непосредственных трудностей с генерацией кода (как всегда, фактические адреса будут исправлены позже), но Паскаль и другие языки позволяют передавать параметры разными методами. по выбору программиста ( по ссылке , или по значению , или даже, возможно, по «имени» ), и это обозначается только в определении процедуры, которая неизвестна до того, как определение было встречено. Конкретно для Паскаля в спецификации параметров префикс "Var" означает, что его необходимо получить по ссылке, его отсутствие означает по значению. В первом случае компилятор должен сгенерировать код, передающий адрес параметра, а во втором — другой код, передающий копию значения, обычно через стек. Как всегда, для решения этой проблемы можно было бы использовать механизм «исправления», но это было бы очень грязно. Многопроходные компиляторы, конечно, могут сопоставлять всю необходимую информацию при перемещении вперед и назад, но однопроходные компиляторы не могут. Генерация кода может быть приостановлена на время сканирования (а его результаты будут храниться во внутренней памяти) до тех пор, пока не будет обнаружен необходимый объект, и это не может рассматриваться как результат второго прохода через источник, поскольку этап генерации кода будет скоро наверстаем упущенное, оно просто на время остановилось. Но это было бы сложно. Вместо этого вводится специальная конструкция, в которой определение использования параметра в процедуре объявляется «передовым» по сравнению с его более поздним полным определением, чтобы компилятор мог знать его перед использованием, как ему требуется.
Начиная с первого Фортрана (1957 г.), стала возможна отдельная компиляция частей программы, поддерживающая создание библиотек процедур и функций. Процедура в компилируемом исходном файле, которая вызывает функцию из такой внешней коллекции, должна знать тип результата, возвращаемого неизвестной функцией, хотя бы для того, чтобы сгенерировать код, который ищет результат в нужном месте. Первоначально, когда существовали только целые числа и переменные с плавающей запятой, выбор можно было оставить за правилами неявного объявления, но с увеличением размеров, а также типов вызывающей процедуре потребуется объявление типа для функции. Это не что-то особенное: оно имеет ту же форму, что и переменная, объявленная внутри процедуры.
Требование, которое должно быть выполнено, заключается в том, что на текущем этапе однопроходной компиляции необходима информация об объекте, чтобы правильный код для него мог быть создан сейчас, если с исправлениями адреса позже. Независимо от того, встретится ли необходимая информация позже в исходном файле или ее можно найти в каком-то отдельно скомпилированном файле кода, эта информация предоставляется здесь каким-то протоколом.
Проверяются или нет все вызовы процедуры (или функции) на совместимость друг с другом и их определениями — это отдельный вопрос. В языках, созданных на основе Алгола, эта проверка обычно строгая, но другие системы могут быть безразличны. Если оставить в стороне системы, которые позволяют процедуре иметь необязательные параметры, ошибки в количестве и типе параметров обычно приводят к сбою программы. Системы, которые позволяют раздельную компиляцию частей полной программы, которые позже «связываются» вместе, также должны проверять правильный тип и количество параметров и результатов, поскольку ошибки сделать еще легче, но часто этого не происходит. В некоторых языках (например, Algol) есть формальное понятие «обновления», «расширения» или «продвижения», при котором процедура, ожидающая, скажем, параметра двойной точности, может быть вызвана с ним как с переменной одинарной точности, и в этом случае компилятор генерирует код, который сохраняет переменную одинарной точности во временную переменную двойной точности, которая становится фактическим параметром. Однако это меняет механизм передачи параметров на копирование и копирование , что может привести к тонким различиям в поведении. Гораздо менее тонкими являются последствия, когда процедура получает адрес переменной одинарной точности, когда она ожидает параметр двойной точности, или другие варианты размера. Когда внутри процедуры считывается значение параметра, будет прочитано больше памяти, чем у данного параметра, и полученное значение вряд ли будет улучшением. Гораздо хуже, когда процедура меняет значение своего параметра: что-то обязательно будет повреждено. На поиск и исправление этих упущений можно потратить немало терпения.
Пример Паскаля
[ редактировать ]Примером такой конструкции является предварительное объявление в Паскале . Паскаль требует, чтобы процедуры были объявлены или полностью определены перед использованием. Это помогает однопроходному компилятору при проверке типов : вызов процедуры, которая нигде не была объявлена, является явной ошибкой. Упреждающие объявления помогают взаимно рекурсивным процедурам напрямую вызывать друг друга, несмотря на правило объявления перед использованием:
function odd(n : integer) : boolean;
begin
if n = 0 then
odd := false
else if n < 0 then
odd := even(n + 1) { Compiler error: 'even' is not defined }
else
odd := even(n - 1)
end;
function even(n : integer) : boolean;
begin
if n = 0 then
even := true
else if n < 0 then
even := odd(n + 1)
else
even := odd(n - 1)
end;
Добавив предварительное объявление для функции even
перед функцией odd
, однопроходному компилятору сообщают, что будет определение even
далее в программе.
function even(n : integer) : boolean; forward;
function odd(n : integer) : boolean;
{ Et cetera }
При фактическом объявлении тела функции параметры либо опускаются, либо должны быть абсолютно идентичны исходному предварительному объявлению, либо будет отмечена ошибка.
Рекурсия препроцессора
[ редактировать ]При объявлении сложных агрегатов данных может возникнуть возможность использования функций Odd и Even. Возможно, если совокупность данных X имеет размер хранилища, равный нечетному числу байтов, к нему можно добавить однобайтовый элемент под контролем теста на Odd(ByteSize(X)), чтобы получить четное число. Учитывая эквивалентные объявления Odd и Even, как указано выше, «прямое» объявление, вероятно, не понадобится, поскольку использование параметров известно препроцессору, который вряд ли предоставит возможность выбора между ссылкой и значением. Однако вызовы этих функций в исходном коде (вне их определений) невозможны до тех пор, пока они не будут фактически определены, поскольку результат вызова должен быть известен. Если, конечно, препроцессор не выполнил несколько проходов исходного файла.
Форвардные заявления признаны вредными
[ редактировать ]Любой, кто пытался поддерживать согласованность между объявлениями и использованием процедур в большой программе и использованием библиотек подпрограмм, особенно тех, которые претерпевают изменения, будет бороться с использованием прямых или подобных добавленных объявлений для процедур, вызываемых, но не определенных в текущая компиляция. Поддержание синхронизации между удаленными друг от друга локациями, особенно между разными исходными файлами, требует усердия. Эти объявления, использующие зарезервированное слово, легко найти, но если полезные объявления не отличаются от обычных объявлений, задача становится затруднительной. Выгода от предположительно более быстрой компиляции может показаться недостаточной, поскольку простой отказ от цели однопроходной компиляции устранит это навязывание.
См. также
[ редактировать ]Ссылки
[ редактировать ]- ^ «Однопроходные, двухпроходные и многопроходные компиляторы» . Гики для Гиков . 13 марта 2019 г. Проверено 15 мая 2023 г.