Я написал SLIC (Систему языков для реализации компиляторов) сам по себе. Затем рука скомпилировала его в сборку. SLIC имеет много общего, так как это был один компилятор из пяти подъязыков:
- SYNTAX Parser Язык программирования PPL
- Язык генерации кода PSEUDO для сканирования дерева на основе GENERATOR LISP 2
- ISO in Sequence, код PSEUDO, язык оптимизации
- PSEUDO Макро-подобный язык сборки ассемблерного кода.
- MACHOP Сборка-машинная инструкция, определяющая язык.
SLIC был вдохновлен CWIC (Компилятор для написания и реализации компиляторов). В отличие от большинства пакетов разработки компиляторов, SLIC и CWIC предназначены для генерации кода со специализированными, специфичными для домена языками. SLIC расширяет генерацию кода CWIC, добавляя подязыки ISO, PSEUDO и MACHOP, отделяя специфику целевой машины от языка генератора обхода дерева.
LISP 2 деревья и списки
Система динамического управления памятью на основе языка генератора LISP 2 является ключевым компонентом. Списки выражаются на языке, заключенном в квадратные скобки, компоненты которого разделены запятыми, то есть списком из трех элементов [a, b, c].
Деревья:
ADD
/ \
MPY 3
/ \
5 x
представлены списками, первая запись которых является объектом узла:
[ADD,[MPY,5,x],3]
Деревья обычно отображаются с отдельным узлом, предшествующим ветвям:
ADD[MPY[5,x],3]
Разбор с использованием функций генератора на основе LISP 2
Функция генератора - это именованный набор (unparse) => action> пар ...
<NAME>(<unparse>)=><action>;
(<unparse>)=><action>;
...
(<unparse>)=><action>;
Разреженные выражения - это тесты, которые соответствуют шаблонам дерева и / или типам объектов, разбивая их на части и назначая эти части локальной переменной для обработки ее процедурным действием. Вроде как перегруженная функция, принимающая разные типы аргументов. За исключением () => ... тесты предпринимаются в кодированном порядке. Первым успешно разобрано выполнение соответствующего ему действия. Разреженные выражения - это разборки тестов. ADD [x, y] соответствует дереву ADD с двумя ветвями, присваивая его ветви локальным переменным x и y. Действие может быть простым выражением или блоком ограниченного кода .BEGIN ... .END. Я бы использовал блоки c style {...} сегодня. Соответствие дерева, [], разбор правил может вызывать генераторы, передающие возвращенные результаты в действие:
expr_gen(ADD[expr_gen(x),expr_gen(y)])=> x+y;
В частности, приведенное выше выражение expr_gen unparse соответствует дереву ADD с двумя ветвями. Внутри тестового шаблона будет вызываться генератор одного аргумента, помещенный в ветвь дерева с этой ветвью. Его список аргументов - это локальные переменные, которым назначены возвращаемые объекты Выше unparse указывает две ветки для разборки дерева ADD, рекурсивное нажатие каждой ветви на expr_gen. Левая ветвь возврата помещена в локальные переменные x. Аналогично, правая ветвь передается expr_gen с y возвращаемым объектом. Выше может быть частью числового выражения оценки. Существовали функции ярлыков, называемые векторами, которые были выше, вместо строки узлов вектор узлов мог использоваться с вектором соответствующих действий:
expr_gen(#node[expr_gen(x),expr_gen(y)])=> #action;
node: ADD, SUB, MPY, DIV;
action: x+y, x-y, x*y, x/y;
(NUMBER(x))=> x;
(SYMBOL(x))=> val:(x);
Вышеупомянутый более полный вычислитель выражений, присваивающий возврат из левой ветви expr_gen к x и правой ветви к y. Соответствующий вектор действия, выполненный для x и y, вернулся. Последние пары действий unparse => соответствуют объектам чисел и символов.
Символ и атрибуты символа
Символы могут иметь именованные атрибуты. val: (x) получить доступ к атрибуту val объекта символа, содержащегося в x. Обобщенный стек таблиц символов является частью SLIC. Таблицу SYMBOL можно нажимать и выдвигать, предоставляя локальные символы для функций. Вновь созданные символы заносятся в верхнюю таблицу символов. Поиск символов просматривает стек таблицы символов сначала из верхней таблицы вниз по стеку.
Генерация машинно-независимого кода
Язык генератора SLIC создает объекты инструкций PSEUDO, добавляя их в список кодов секций. .FLUSH вызывает запуск своего списка кодов PSEUDO, удаляя каждую инструкцию PSEUDO из списка и вызывая ее. После выполнения память объектов PSEUDO освобождается. Процедурные органы действий PSEUDO и GENERATOR в основном являются одним и тем же языком, за исключением их результатов. PSEUDO предназначены для работы в качестве макросов сборки, обеспечивающих машинно-независимую секвенизацию кода. Они обеспечивают отделение конкретной целевой машины от языка генератора обхода дерева. PSEUDO вызывают функции MACHOP для вывода машинного кода. MACHOP используются для определения псевдоопераций сборки (таких как dc, определение константы и т. Д.) И машинных инструкций или семейства похожих форматированных инструкций с использованием векторной записи. Они просто преобразуют свои параметры в последовательность битовых полей, составляющих инструкцию. Вызовы MACHOP должны выглядеть как сборка и обеспечивать форматирование печати полей, когда сборка отображается в списке компиляции. В коде примера я использую комментирование в стиле c, которое можно легко добавить, но не на оригинальных языках. MACHOP производят код в немного адресуемой памяти. Линкер SLIC обрабатывает вывод компилятора. MACHOP для инструкций пользовательского режима DEC-10 с использованием векторной записи: MACHOP производят код в немного адресуемой памяти. Линкер SLIC обрабатывает вывод компилятора. MACHOP для инструкций пользовательского режима DEC-10 с использованием векторной записи: MACHOP производят код в немного адресуемой памяти. Линкер SLIC обрабатывает вывод компилятора. MACHOP для инструкций пользовательского режима DEC-10 с использованием векторной записи:
.MACHOP #opnm register,@indirect offset (index): // Instruction's parameters.
.MORG 36, O(18): $/36; // Align to 36 bit boundary print format: 18 bit octal $/36
O(9): #opcd; // Op code 9 bit octal print out
(4): register; // 4 bit register field appended print
(1): indirect; // 1 bit appended print
(4): index; // 4 bit index register appended print
O(18): if (#opcd&&3==1) offset // immediate mode use value else
else offset/36; // memory address divide by 36
// to get word address.
// Vectored entry opcode table:
#opnm := MOVE, MOVEI, MOVEM, MOVES, MOVS, MOVSI, MOVSM, MOVSS,
MOVN, MOVNI, MOVNM, MOVNS, MOVM, MOVMI, MOVMM, MOVMS,
IMUL, IMULI, IMULM, IMULB, MUL, MULI, MULM, MULB,
...
TDO, TSO, TDOE, TSOE, TDOA, TSOA, TDON, TSON;
// corresponding opcode value:
#opcd := 0O200, 0O201, 0O202, 0O203, 0O204, 0O205, 0O206, 0O207,
0O210, 0O211, 0O212, 0O213, 0O214, 0O215, 0O216, 0O217,
0O220, 0O221, 0O222, 0O223, 0O224, 0O225, 0O226, 0O227,
...
0O670, 0O671, 0O672, 0O673, 0O674, 0O675, 0O676, 0O677;
.MORG 36, O (18): $ / 36; выравнивает местоположение по 36-битной границе, печатая адрес $ / 36 слова из 18 бит в восьмеричном виде. 9-битный регистр opcd, 4-битный регистр, косвенный бит и 4-битный индексный регистр объединяются и печатаются, как будто одно 18-битное поле. 18-битный адрес / 36 или непосредственное значение выводится и печатается в восьмеричном виде. Пример MOVEI распечатать с r1 = 1 и r2 = 2:
400020 201082 000005 MOVEI r1,5(r2)
С опцией сборки компилятора вы получите сгенерированный код сборки в списке компиляции.
Свяжите это вместе
Компоновщик SLIC поставляется в виде библиотеки, которая обрабатывает связывание и разрешение символов. Целевое форматирование выходного файла загрузки должно быть записано для целевых машин и связано с библиотекой библиотеки компоновщика.
Язык генератора способен записывать деревья в файл и читать их, что позволяет реализовать многопроходный компилятор.
Краткое резюме генерации кода и происхождения
Сначала я рассмотрел генерацию кода, чтобы убедиться, что он понимает, что SLIC был настоящим компилятором компилятора. SLIC был вдохновлен CWIC (компилятором для написания и реализации компиляторов), разработанным в корпорации Systems Development в конце 1960-х годов. В CWIC были только языки SYNTAX и GENERATOR, производящие числовой байтовый код из языка GENERATOR. Байт-код был помещен или помещен (термин, используемый в документации CWIC) в буферы памяти, связанные с именованными разделами и записанные с помощью оператора .FLUSH. Документ ACM о CWIC доступен из архивов ACM.
Успешная реализация основного языка программирования
В конце 1970-х SLIC использовался для написания кросс-компилятора COBOL. Завершено примерно за 3 месяца в основном одним программистом. Я работал немного с программистом по мере необходимости. Другой программист написал библиотеку времени выполнения и MACHOP для целевого мини-компьютера TI-990. Этот компилятор COBOL компилировал значительно больше строк в секунду, чем собственный компилятор COBOL DEC-10, написанный на ассемблере.
Больше компилятору тогда обычно говорили о
Большая часть написания компилятора с нуля - это библиотека времени выполнения. Вам нужна таблица символов. Вам нужен ввод и вывод. Динамическое управление памятью и т. Д. Написание библиотеки времени выполнения для компилятора может оказаться более трудоемким, чем написание компилятора. Но с SLIC эта библиотека времени выполнения является общей для всех компиляторов, разрабатываемых в SLIC. Обратите внимание, что есть две библиотеки времени выполнения. Один для целевой машины языка (например, COBOL). Другая - это библиотека времени выполнения компиляторов.
Я думаю, я установил, что это не генераторы парсеров. Так что теперь с небольшим пониманием серверной части я могу объяснить язык программирования анализатора.
Парсер языка программирования
Парсер написан с использованием формулы, написанной в виде простых уравнений.
<name> <formula type operator> <expression> ;
Элементом языка на самом низком уровне является символ. Токены формируются из подмножеств символов языка. Классы символов используются для именования и определения этих подмножеств символов. Определяющим классом символов является символ двоеточия (:). Символы, которые являются членами класса, закодированы в правой части определения. Печатные символы заключены в простые простые строки. Непечатаемые и специальные символы могут быть представлены их числовым порядковым номером. Члены класса разделены альтернативой | оператор. Формула класса заканчивается точкой с запятой. Классы символов могут включать ранее определенные классы:
/* Character Class Formula class_mask */
bin: '0'|'1'; // 0b00000010
oct: bin|'2'|'3'|'4'|'5'|'6'|'7'; // 0b00000110
dgt: oct|'8'|'9'; // 0b00001110
hex: dgt|'A'|'B'|'C'|'D'|'E'|'F'|'a'|'b'|'c'|'d'|'e'|'f'; // 0b00011110
upr: 'A'|'B'|'C'|'D'|'E'|'F'|'G'|'H'|'I'|'J'|'K'|'L'|'M'|
'N'|'O'|'P'|'Q'|'R'|'S'|'T'|'U'|'V'|'W'|'X'|'Y'|'Z'; // 0b00100000
lwr: 'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i'|'j'|'k'|'l'|'m'|
'n'|'o'|'p'|'q'|'r'|'s'|'t'|'u'|'v'|'w'|'x'|'y'|'z'; // 0b01000000
alpha: upr|lwr; // 0b01100000
alphanum: alpha|dgt; // 0b01101110
Skip_class 0b00000001 предопределен, но может быть слишком длинным, определяя skip_class.
Итак, класс символов - это список альтернатив, который может быть только символьной константой, порядковым номером символа или ранее определенным классом символов. Как я реализовал классы символов: формуле класса назначается битовая маска класса. (Показано в комментариях выше). Любая формула класса, имеющая какой-либо символьный литерал или порядковый номер, вызывает выделение бита класса. Маска создается путем направления маски (ей) классов включенного класса (классов) вместе с выделенным битом (если есть). Таблица классов создается из классов символов. Запись, индексированная по порядковому номеру символа, содержит биты, указывающие членство класса в классе. Классовое тестирование проводится в линию. Пример кода IA-86 с порядковым номером символа в eax иллюстрирует тестирование класса:
test byte ptr [eax+_classmap],dgt
Вслед за:
jne <success>
или
je <failure>
Примеры кода инструкции IA-86 используются, потому что я думаю, что инструкции IA-86 более широко известны сегодня. Имя класса, вычисляемое по его маске класса, неразрушающим образом и с таблицей классов, индексированной по порядковым номерам символов (в eax). Ненулевой результат указывает на членство в классе. (EAX обнуляется, за исключением al (младшие 8 битов EAX), который содержит символ).
Токены были немного другими в этих старых компиляторах. Ключевые слова не были объяснены как токены. Они просто были сопоставлены строковыми константами в кавычках на языке синтаксического анализатора. Строки в кавычках обычно не сохраняются. Модификаторы могут быть использованы. A + поддерживает соответствие строки. (т. е. + '-' соответствует символу -, сохраняя символ в случае успеха) Операция, (т. е. 'E') вставляет строку в токен. Пробел обрабатывается формулой токена, пропускающей первые символы SKIP_CLASS, пока не будет найдено первое совпадение. Обратите внимание, что явное сопоставление символов skip_class остановит пропуск, позволяя токену начинаться с символа skip_class. В формуле строкового токена пропускаются первые символы skip_class, соответствующие одинарным кавычкам или двойным кавычкам. Интерес представляет сопоставление «символа в строке» в кавычках:
string .. (''' .ANY ''' | '"' $(-"""" .ANY | """""","""") '"') MAKSTR[];
Первый вариант соответствует любому символу в кавычках. Правильная альтернатива соответствует строке в двойных кавычках, которая может включать символы в двойных кавычках, использующие два символа «вместе» для представления одного символа. Эта формула определяет строки, используемые в ее собственном определении. Внутренняя правая альтернатива '"' $ (-" "". .ANY | "" "" "", "" "") '"' соответствует строке в двойных кавычках. Мы можем использовать один символ «в кавычках» для соответствия символу «двойной кавычки». Однако в строке «двойные» в кавычках, если мы хотим использовать «символ», мы должны использовать два символа, чтобы получить один. Например, во внутренней левой альтернативе, совпадающей с любым символом, кроме кавычки:
-"""" .ANY
отрицательный заглядывание вперед - "" "" используется, если в случае успеха (не совпадает с "символ"), то соответствует .ANY символ (который не может быть "символом, потому что -" "" "исключил эту возможность). Правильная альтернатива берет на себя - "" "" сопоставление "символа и неудача были правильной альтернативой:
"""""",""""
пытается сопоставить два "символа, заменив их одним двойным" используя "," "" для вставки одного символа ". Обе внутренние альтернативы, не прошедшие закрывающий символ кавычки строки, сопоставляются, и MAKSTR [] вызывается для создания строкового объекта. последовательность, цикл при успешном выполнении, оператор используется при сопоставлении последовательности. Формула токена пропускает первые символы класса пропуска (пробел). Как только первое совпадение выполнено, пропуск skip_class отключен. Мы можем вызывать функции, запрограммированные на других языках, используя []. MAKSTR [], MAKBIN [], MAKOCT [], MAKHEX [], MAKFLOAT [] и MAKINT [] являются поставляемой библиотечной функцией, которая преобразует совпавшую строку токена в типизированный объект. Приведенная ниже формула числа иллюстрирует довольно сложное распознавание токена:
number .. "0B" bin $bin MAKBIN[] // binary integer
|"0O" oct $oct MAKOCT[] // octal integer
|("0H"|"0X") hex $hex MAKHEX[] // hexadecimal integer
// look for decimal number determining if integer or floating point.
| ('+'|+'-'|--) // only - matters
dgt $dgt // integer part
( +'.' $dgt // fractional part?
((+'E'|'e','E') // exponent part
('+'|+'-'|--) // Only negative matters
dgt(dgt(dgt|--)|--)|--) // 1 2 or 3 digit exponent
MAKFLOAT[] ) // floating point
MAKINT[]; // decimal integer
Приведенная выше формула числового токена распознает целые числа и числа с плавающей запятой. Альтернативы всегда успешны. Числовые объекты могут быть использованы в расчетах. Объекты-токены помещаются в стек анализа при успешном выполнении формулы. Показатель степени в (+ 'E' | 'e', 'E') интересен. Мы хотим всегда иметь заглавную E для MAKEFLOAT []. Но мы разрешаем заменять его строчными буквами «е», используя «E».
Возможно, вы заметили соответствие класса символов и формулы токена. Формула синтаксического анализа продолжается, добавляя альтернативы обратного отслеживания и операторы построения дерева. Альтернативные операторы с возвратом и без возврата могут не смешиваться в пределах уровня выражения. У вас может не быть (a | b \ c) смешивания без возврата | с альтернативой возврата. (a \ b \ c), (a | b | c) и ((a | b) \ c) действительны. Альтернатива \ backtracking сохраняет состояние анализа перед попыткой его левой альтернативы, а при сбое восстанавливает состояние анализа перед попыткой правильной альтернативы. В последовательности альтернатив первая удачная альтернатива удовлетворяет группе. Дальнейшие альтернативы не предпринимаются. Факторинг и группировка обеспечивают непрерывный анализ. Альтернатива backtrack создает сохраненное состояние синтаксического анализа, прежде чем попытается использовать левую альтернативу. Возврат требуется, когда анализ может выполнить частичное совпадение, а затем потерпеть неудачу:
(a b | c d)\ e
В приведенном выше примере, если возвращается ошибка, предпринимается попытка альтернативного компакт-диска. Если тогда c возвращает ошибку, будет предпринята попытка возврата. Если a завершился успешно, а b потерпел неудачу, анализ будет возвращен и попытка e. Аналогично, неудачный c успешен и b терпит неудачу, анализ возвращается, и альтернатива принимается. Возврат не ограничен формулой. Если какая-либо формула синтаксического анализа делает частичное совпадение в любое время, а затем терпит неудачу, анализ сбрасывается на верхний обратный ход и берется его альтернатива. Ошибка компиляции может возникнуть, если код был выведен в том смысле, что был создан возврат. Возврат устанавливается перед началом компиляции. Возврат ошибки или возврат к ней - ошибка компилятора. Возвраты сложены. Мы можем использовать отрицательный - и положительный? операторы «заглянуть / посмотреть вперед» для тестирования без ускорения разбора. проверка строки - это шаг вперед, требующий сохранения и сброса входного состояния. Забегая вперед, можно было бы проанализировать выражение, которое до частичного сбоя выполнит частичное совпадение. Взгляд в будущее реализован с использованием возврата.
Язык парсера не является ни парсером LL, ни LR. Но язык программирования для написания рекурсивного приличного парсера, в котором вы программируете построение дерева:
:<node name> creates a node object and pushes it onto the node stack.
.. Token formula create token objects and push them onto
the parse stack.
!<number> pops the top node object and top <number> of parstack
entries into a list representation of the tree. The
tree then pushed onto the parse stack.
+[ ... ]+ creates a list of the parse stack entries created
between them:
'(' +[argument $(',' argument]+ ')'
could parse an argument list. into a list.
Обычно используемый пример синтаксического анализа - это арифметическое выражение:
Exp = Term $(('+':ADD|'-':SUB) Term!2);
Term = Factor $(('*':MPY|'/':DIV) Factor!2);
Factor = ( number
| id ( '(' +[Exp $(',' Exp)]+ ')' :FUN!2
| --)
| '(' Exp ')" )
(^' Factor:XPO!2 |--);
Использование Exp и Term с помощью цикла создает левостороннее дерево. Фактор, использующий правильную рекурсию, создает правостороннее дерево:
d^(x+5)^3-a+b*c => ADD[SUB[EXP[EXP[d,ADD[x,5]],3],a],MPY[b,c]]
ADD
/ \
SUB MPY
/ \ / \
EXP a b c
/ \
d EXP
/ \
ADD 3
/ \
x 5
Вот немного компилятора cc, обновленная версия SLIC с комментариями в стиле c. Типы функций (грамматика, токен, класс символов, генератор, PSEUDO или MACHOP определяются их начальным синтаксисом, следующим за их идентификатором. С этими анализаторами сверху вниз вы начинаете с программы, определяющей формулу:
program = $((declaration // A program is a sequence of
// declarations terminated by
|.EOF .STOP) // End Of File finish & stop compile
\ // Backtrack: .EOF failed or
// declaration long-failed.
(ERRORX["?Error?"] // report unknown error
// flagging furthest parse point.
$(-';' (.ANY // find a ';'. skiping .ANY
| .STOP)) // character: .ANY fails on end of file
// so .STOP ends the compile.
// (-';') failing breaks loop.
';')); // Match ';' and continue
declaration = "#" directive // Compiler directive.
| comment // skips comment text
| global DECLAR[*1] // Global linkage
|(id // functions starting with an id:
( formula PARSER[*1] // Parsing formula
| sequencer GENERATOR[*1] // Code generator
| optimizer ISO[*1] // Optimizer
| pseudo_op PRODUCTION[*1] // Pseudo instruction
| emitor_op MACHOP[*1] // Machine instruction
) // All the above start with an identifier
\ (ERRORX["Syntax error."]
garbol); // skip over error.
// Обратите внимание на то, как id отключается и позже объединяется при создании дерева.
formula = ("==" syntax :BCKTRAK // backtrack grammar formula
|'=' syntax :SYNTAX // grammar formula.
|':' chclass :CLASS // character class define
|".." token :TOKEN // token formula
)';' !2 // Combine node name with id
// parsed in calling declaration
// formula and tree produced
// by the called syntax, token
// or character class formula.
$(-(.NL |"/*") (.ANY|.STOP)); Comment ; to line separator?
chclass = +[ letter $('|' letter) ]+;// a simple list of character codes
// except
letter = char | number | id; // when including another class
syntax = seq ('|' alt1|'\' alt2 |--);
alt1 = seq:ALT!2 ('|' alt1|--); Non-backtrack alternative sequence.
alt2 = seq:BKTK!2 ('\' alt2|--); backtrack alternative sequence
seq = +[oper $oper]+;
oper = test | action | '(' syntax ')' | comment;
test = string | id ('[' (arg_list| ,NILL) ']':GENCALL!2|.EMPTY);
action = ':' id:NODE!1
| '!' number:MAKTREE!1
| "+[" seq "]+" :MAKLST!1;
// C style comments
comment = "//" $(-.NL .ANY)
| "/*" $(-"*/" .ANY) "*/";
Следует отметить, как язык синтаксического анализатора обрабатывает комментирование и исправление ошибок.
Я думаю, что я ответил на вопрос. Написав большую часть преемников SLIC, язык cc сам по себе здесь. Для него пока нет компилятора. Но я могу вручную скомпилировать его в ассемблерный код, голые функции asm c или c ++.