Я написав SLIC (Система мов впровадження компіляторів) у себе. Потім рука склала його на збірку. У SLIC є багато, оскільки це був єдиний компілятор з п'яти підмов:
- Мова програмування SYNTAX Parser PPL
- Генерація мови на основі PSEUDO-коду на основі дерев, що повзають по деревах
- ISO в послідовності, код 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) => дія> пар ...
<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;
Зокрема, вищевказаний unprse expr_gen відповідає двому гілці ADD. У межах тестового шаблону з цим гілкою буде викликатися генератор одного аргументу, розміщений у гілці дерева. Список аргументів, хоча це локальні змінні, присвоєні поверненим об'єктам. Вище розпарювання вказує дві гілки - розбирання дерева 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, а правої гілки - у. Відповідний вектор дії, виконаний на x і y, повернувся. Останній непарний => пар дій відповідає числовим і символьним об'єктам.
Символ та ознаки символу
Символи, можливо, назвали атрибути. val: (x) доступ до атрибуту val об'єкта символів, що міститься у x. Узагальнений стек таблиць символів є частиною SLIC. Таблиця SYMBOL може бути натиснута та висунута, надаючи локальні символи для функцій. Новостворений символ каталогізується у верхній таблиці символів. Пошук символів шукає стек таблиці символів з верхньої таблиці спочатку назад вниз по стеку.
Створення незалежного машинного коду
Мова генератора SLIC виробляє об'єкти інструкцій PSEUDO, додаючи їх до списку кодів розділів. A .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-бітний опдд, 4-бітний регістр, непрямий біт та 4-розрядний індексний регістр поєднуються та друкуються так, ніби єдине 18-бітове поле. 18-бітова адреса / 36 або негайне значення виводиться і друкується у вісімці. Приклад MOVEI роздруковується з r1 = 1 і r2 = 2:
400020 201082 000005 MOVEI r1,5(r2)
За допомогою параметра збирання компілятора ви отримуєте згенерований код складання у списку компіляції.
Зв’яжіть це разом
Лінкер SLIC постачається у вигляді бібліотеки, яка обробляє роздільну здатність зв'язку та символів. Форматування файлу вихідного завантаження для цільового, хоча має бути записане для цільових машин і пов'язане з бібліотекою бібліотек лінкерів.
Мова генератора здатна записувати дерева у файл та читати їх, що дозволяє реалізовувати компілятор багатопрохідного режиму.
Короткі літні генерації коду та їх походження
Я спершу пережив генерацію коду, щоб переконатися, що зрозумів, що SLIC був справжнім компілятором. SLIC був натхненний CWIC (Компілятор для написання та реалізації компіляторів), розроблений корпорацією System Development в кінці 1960-х. У CWIC були лише мови SYNTAX і GENERATOR, що створюють числовий байт-код з мови GENERATOR. Байт-код був розміщений або посаджений (термін, що використовується в документації на CWIC) в буфери пам'яті, пов'язані з названими розділами і виписаний оператором .FLUSH. Папір ACM на CWIC доступний в архівах ACM.
Успішно реалізується основна мова програмування
Наприкінці 1970-х SLIC був використаний для написання перехресного компілятора COBOL. Виконується приблизно за 3 місяці здебільшого одним програмістом. Я трохи працював з програмістом у міру необхідності. Інший програміст написав бібліотеку виконання та MACHOPs для цільового міні-комп'ютера TI-990. Цей компілятор 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.
Підсумок: Клас символів - це список альтернативних варіантів, який може бути лише символьною константою, порядковим символом або раніше визначеним класом символів. Як я реалізував класи символів: Формулі класу присвоюється бітова маска класу. (Показано в коментарях вище) Будь-яка формула класу, яка має будь-який буквальний чи порядковий символ, призводить до виділення біта класу. Маска робиться шляхом упорядкування масок (класів) включених класів (класів) разом з виділеним бітом (якщо такий є). З класів символів створюється таблиця класів. Запис, індексований порядковим порядком символу, містить біти, що вказують на членство персонажа в класі. Тестування класу проводиться в режимі inline. Приклад коду IA-86 з порядковим символом символу в eax ілюструє тестування класу:
test byte ptr [eax+_classmap],dgt
Після цього:
jne <success>
або
je <failure>
Приклади кодів інструкцій IA-86 використовуються, тому що я думаю, що інструкції IA-86 сьогодні широко відомі. Ім'я класу, що оцінює його маску класу, не руйнує AND з таблицею класів, індексованою порядковими символами (в eax). Ненульовий результат вказує на приналежність до класу. (EAX нульовий, за винятком al (низьких 8 біт EAX), що містить символ).
Токени були дещо іншими в цих старих компіляторах. Ключові слова не були пояснені як лексеми. Вони просто узгоджувались цитованими рядковими константами мовою розбору. Котировані рядки зазвичай не зберігаються. Можливо використовувати модифікатори. A + підтримує відповідність рядку. (тобто + '-' відповідає а - символу, що зберігає символ при успішному виконанні). Операція (тобто "E") вставляє рядок у маркер. Білий пробіл обробляється формулою лексеми, пропускаючи провідні символи SKIP_CLASS, поки не буде зроблено перший збіг. Зауважте, що явна відповідність символу skip_class зупинить пропуск, що дозволить маркеру починатись із символу skip_class. Формула лексеми рядка пропускає провідні символи skip_class, що відповідають одному запитуваному символу цитати або подвійному котируваному рядку. Цікавим є відповідність "символу в" цитованому рядку:
string .. (''' .ANY ''' | '"' $(-"""" .ANY | """""","""") '"') MAKSTR[];
Перша альтернатива відповідає будь-якій символіці, що цитується окремо. Правильна альтернатива відповідає рядку з цитатами подвійної лапки, яка може включати символи подвійної цитати, використовуючи два "символи разом, щоб представити один" символ. Ця формула визначає рядки, використані у власному визначенні. Внутрішня права альтернатива '"' $ (-" "" ".ANY |" "" "" "" "" ")" "" відповідає подвійному котируваному рядку. Ми можемо використовувати один "котируваний символ", щоб відповідати подвійній цитаті "символ. Однак у двомісному" цитувальному рядку, якщо ми хочемо використовувати "символ, ми повинні використовувати два" символи, щоб отримати один. Наприклад, у внутрішній лівій альтернативній відповідності будь-якому символу, крім цитати:
-"""" .ANY
негативний погляд вперед - "" "" використовується, що при успішному (не відповідає "символу), то він відповідає .ANY символу (який не може бути" символом, тому що "" "" усунув цю можливість). Правильною альтернативою є прийняття - "" "" відповідність "символу і невдача були правильною альтернативою:
"""""",""""
намагається співставити два "символи, замінюючи їх одним подвійним" символом "," "", щоб вставити thw єдиний "символ. Обидва внутрішні альтернативи, у разі відмови символу цитати, що завершується, збігаються, і 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" замінювати його на "E".
Можливо, ви помітили послідовність класів символів та формули лексеми. Формула розбору продовжує додавання альтернатив зворотного відтворення та операторів побудови дерев. Альтернативні оператори зворотного відстеження та неприйняття не можуть змішуватися в межах рівня вираження. Можливо, у вас (a | b \ c) не змішується неповторний трек | з альтернативою зворотного відстеження. (a \ b \ c), (a | b | c) та ((a | b) \ c) є дійсними. Альтернатива зворотного відстеження зберігає стан розбору перед спробою лівої альтернативи, а після відмови відновлює стан розбору перед спробою правої альтернативи. У послідовності альтернатив перша успішна альтернатива задовольняє групу. Подальші альтернативи не намагаються. Факторинг та групування передбачають безперервний просунутий синтаксичний аналіз. Альтернатива зворотного треку створює збережений стан розбору, перш ніж спробувати його ліву альтернативу. Зворотний трек потрібен, коли синтаксичний аналіз може отримати часткове збіг, а потім не вдасться:
(a b | c d)\ e
У вищесказаному, якщо повертається збій, робиться спробу альтернативного CD. Якщо потім c повертає помилку, буде здійснена спроба альтернативи зворотного треку. Якщо це вдасться і не вдасться, синтаксис розбору буде відкликаний і здійснено спробу. Аналогічно, невдалий c успішний і b не виконаний синтаксичний аналіз аналізується, і застосовується альтернатива e. Зворотний трек не обмежується лише формулою. Якщо будь-яка формула синтаксичного аналізу в будь-який час робить часткову відповідність, а потім не відповідає, синтаксичний розбір повертається до верхнього зворотного треку та приймається його альтернатива. Помилка компіляції може статися, якщо код виведений у сенсі, що створено зворотній трек. Перед запуском компіляції встановлюється зворотний трек. Повернення відмови або зворотного відстеження до нього - це збій компілятора. Зворотні композиції складаються з накопиченням. Ми можемо використовувати негативні - і позитивні? зазирнути / дивитись вперед операторів для тестування без просування синтаксичного аналізу. тест рядка - це заглянути вперед, потребує збереження та скидання стану введення. Погляд вперед був би синтаксичним синтаксисом, який дає часткове збіг, перш ніж провалитися. Погляд вперед реалізується за допомогою зворотного відстеження.
Мова аналізатора не є ні 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.
// Зауважте, як ідентифікатор факту вимикається та пізніше поєднується під час створення дерева.
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, тут мова самої куб. Для цього ще немає компілятора. Але я можу вручну компілювати його в код складання, голий asm c або c ++ функції.