Info Guide
#12
31 декабря 2017 |
|
Системки - NedoLang: Начало - самый простой процедурный язык (часть 1).
NedoLang: Начало Alone Coder В сентябре 2016 года на работе встала задача - найти отечественный компилятор промежуточного языка под процессоры ARM, который можно применить в военной аппара─ туре. Мы разрабатываем новую систему, и загружаемые программы на языке высокого уровня нам бы очень помогли. Всякие Phyton'ы мы поставлять не могли - они работают только под Windows. Был в теории вариант компилятора под ARM в сос─ таве версии Astra Linux под ARM же (на ка─ ком компьютере его запускать?). А "прог─ раммы из интернета" в военной аппаратуре использовать нельзя, всё должно быть сдано с децимальным номером в соответствии с ГОСТами. Можно, конечно, было распечатать миллионы строк исходника GCC и сдать как свою программу... со всеми закладками :) В прошлом номере Info Guide как раз об─ суждались компиляторы. Возможно,я читал те статьи подробнее, чем вы,- я же их перево─ дил :) В голове после этого была картина, которой не было 10 лет назад,когда я хотел писать ОС и начинал собирать идеи по сис─ темам программирования в одну папочку. Не было особой надежды, что на ZX поя─ вятся нативные версии Oberon и ZX Like Pascal, полного исходника Hitech C не наш─ лось,а под компиляторы z88dk и SDCC просто не хватит памяти. Хотя, впрочем,был деком─ пилированный исходник Turbo Pascal, но он написан на ассемблере,такое развивать тру─ дно. Даже авторы iS-DOS не сделали норма─ льный язык высокого уровня для разработки под свою систему. Почему бы не написать продукт, полезный и тут, и там? * * * Как раз в это время я в общих чертах закончил 3D движок в стиле Total Eclipse (я его показывал на ближайшей тусовке NedoPC ), и голова несколько освободилась. В общем, ради эксперимента я сел и на─ писал самый простой процедурный язык, ка─ кой пришёл в голову. Он компилировался в ассемблерный текст и переваливал на ассем─ блер всю работу с метками. Сначала у него даже не было типов. Самое сложное там было - вычисление выражений. И то несложное. А работало всё в формочке Delphi и выдавало примерно такой код: ;PUSHNUM 1 PUSH HL LD HL,0+1 ;POPVAR _nemain_v1 LD [_nemain_v1],HL POP HL ;PUSHNUM 2 PUSH HL LD HL,0+2 ;PUSHNUM 2 PUSH HL LD HL,0+2 ;OPERATION + POP DE ADD HL,DE ;POPVAR _nemain_v2 LD [_nemain_v2],HL POP HL ;CALL tnemain CALL tnemain ;PUSHNUM 2 PUSH HL LD HL,0+2 ;NEG XOR A SUB L LD L,A SBC A,H SUB L LD H,A ;POPVAR _main_b LD [_main_b],HL POP HL У каждого регистра была своя роль. Это что-то вроде кодогенератора в C Warp. Важно отметить, что это уже изначально был именно язык высокого уровня, а не ас─ семблер с процедурным синтаксисом типа "Sphinx C--" by Peter Cellik, "Трамплина" С. Веремеенко, "PLM" Александра Корюшкина или "K65" уKK для Atari 2600. Планирова─ лись только подсказки компилятору для бо─ лее эффективной кодогенерации. У меня были давние тёрки с языком Си, поэтому первые версии компилятора имели очень далёкий от него синтаксис. Для начала, уже на уровне лексера исхо─ дник воспринимался как чередование слов и односимвольных знаков. Это чрезвычайно ле─ гко разбирать. Была только пара исключений типа скобок, но они не создавали больших затруднений - просто игнорировалось "пус─ тое" слово. Параметры функций принципиально не от─ личались от локальных меток, в параметрах стоял оператор присваивания, а когда поя─ вились типы - и тип. И хранились они (и пока до сих пор хранятся) именно как лока─ льные переменные.Рекурсия делалась и дела─ ется путём сохранения на стеке старых па─ раметров (во время вызова) и старых лока─ льных переменных (во время входа-выхода). Когда я набросал интерфейс вызова функ─ ций (с именованными параметрами), я пока─ зал компилятор паре спектрумистов. Разуме─ ется, отзывов особых не было - ведь реаль─ но в этом компиляторе ничего нельзя было собрать. Я тогда даже не проверял, что ре─ зультирующий ассемблерный текст ассембли─ руется. А что он работает...Как это вообще проверить? Я мог только проверить,что каж─ дый оборот языка транслируется в конкрет─ ный оборот ассемблерного кода.Но это нель─ зя зафиксировать навечно,иначе не получишь хороший выходной код! Потом, подумав в сторону крупных проек─ тов, я внедрил концепцию вложенных прост─ ранств имён, так что внутри функции другая функция выглядела как... ну, представьте себе, как вы обращаетесь к файлу в другой директории: ../dir2/filename Вот так же и с функциями, только вместо слешей тоже использовались точки: .func2.variable Шикарно? Сам придумал:) А использование _variable для обозначения глобальных пере─ менных подсказал сосед Гриша,до этого гло─ бальные переменные помечались знаком$ или # справа. На самом деле странно, что в Си и UNIX, которые вроде как писали одни и те же лю─ ди,совершенно разное понимание пространств имён (а заодно и форматов вызова). Но сразу пришлось отказаться от .func2.variable в параметрах вызова - сли─ шком длинно для реальной работы. Но снача─ ла просто не было возможности сделать по-другому - ведь даже если запомнить имя (точнее, полный путь) вызываемой функции и приклеивать к нему variable, то внутри па─ раметра может быть ещё вызов функции, и мы тут же забудем текущее имя! Да-да, в ком─ пиляторе была рекурсия и строки в локаль─ ных переменных. А как это будет самокомпи─ лироваться, когда станет продуктом? Строки сначала лежали вAnsiString. Это же Delphi. Но долго они там лежать не мог─ ли.Я не планировал реализовывать объектно- ориентированное программирование. (Вообще много чего не планировал, но потом появля─ лась возможность. Может, приду и к ООП.) Логично было предположить, что строки в конце концов будут лежать в статически выделенном массиве длиной как максимальный идентификатор. Но сохранять такой массив на стеке... Стек не резиновый. Так что я решил убрать массивы из локальных перемен─ ных, а грязную работу по склейке меток пе─ ревалить на ассемблер. На первом этапе язык удерживался в как можно малых размерах. Исходник состоял из парсера (порядка1000 строк) и кодогенера─ тора (порядка500 ).Система на ZX Spectrum предполагалась как постоянно висящая в па─ мяти вместе с текстовым редактором и одним текущим исходником. Я даже предполагал, что 48K хватит на такой сервис. Почему 48K? А это чтобы не поддерживать банкинг в компиляторе. А теперь подробно: 14-16.09.2016 - начат проект. В дневни─ ке написано "писал компилятор". Судя по логам #mhm, первая версия от 14 числа весила всего200 строк. Она компилировала исходный текст из одного поля окна в псев─ докод в другом поле того же окна.Но от неё сохранились только файлы*.res и .dpr. 22.09.2016 - отправил первый релиз на оценку спектрумистам: ┌────────────────────────────────────────┐ Здравствуйте, товарищи! Представляю вашему вниманию новый язык программирования :) Язык оптимизирован под удобство компиляции (переобероним Оберон:) следующим образом: 1) после слова всегда ожидается символ, он сразу считывается, никаких "подглядыва─ ний на символ вперёд". 2) все символы однобайтовые.Поэтому при разборе какого-нибудь"<" не надо смотреть в следующий символ. Все редкие операции кодируются через escape-коды (> для >=, < для<=,= для !=,* для <<,/ для >> ), в разбиральщик они уже поступают односим─ вольными. 3) нет ни одного зарезервированного слова. В поле команды ожидается только команда. Поэтому присваивание делается командой=var(expression) (для переменных) или командой*value(expression) (для ячеек памяти). 4) отсутствует таблица меток. Это воз─ лагается на ассемблер. Точнее, в текущей реализации есть список меток, но только для того, чтобы их красиво сгенерить в ко─ нце исходника (но можно так же успешно ге─ нерить их в середине с помощью org'ов с переназначаемыми метками). Из-за этого ло─ кальные и глобальные метки различаются с помощью постфикса#. 5) отсутствует C-like вызов функций.Па─ раметры передаются по имени,прямо в ячейку памяти. 6) рекурсия описывается явно - с помо─ щью программных скобок var:int(parameter)command (так описываются все локальные переменные, которые затрагиваются рекурсией). После рекурсивного вызова локалы считаются испо─ рченными, их можно использовать только вне этих программных скобок. 7) константы в выражениях описываются явно - через=(constexpr) Фичи: - комментарии вложенные. - нет точек с запятой. Косяки: - цикл толькоwhile (поправимо). - метки для переходов только локальные (поправимо). - нет функций с возвращаемым значением. - нет типов. Сейчас все переменные типа u16. Нет ли идей, как реализовать функции и типы, не сильно усложняя компилятор? Вот как выглядит исходный текст: int(a) proc(nemain) ( par:int(v1) ( {recursive param} par:int(v2) ( {recursive param} =a# (=(1>5)+2*5) =a# (*(22/#33/-v1)--6*7+-a#) *a# ("as") ) ) ) proc(main)( int(b) int(c) call(nemain,v1(1),v2(2+2)) =b (-2) {=a# (0)} while ((a#<5)|b&#ff)( =a# (a#+%1) goto(mylabel) while (c=0) =c(1) :mylabel: =b (b+a#) ) ) {proc} Вот как выглядит результат: ORG #8000 begin: ;.CSEG tnemain: ;PUSHVAR _nemain_nemain_v1 PUSH HL LD HL,[_nemain_nemain_v1] ;PUSHVAR _nemain_nemain_v2 PUSH HL LD HL,[_nemain_nemain_v2] ;PUSHNUM (1>5) PUSH HL LD HL,0+(1>5) ;PUSHNUM 2 PUSH HL LD HL,0+2 ;PUSHNUM 5 PUSH HL LD HL,0+5 ;OPERATION * LD B,L POP HL ADD HL,HL DJNZ $-1 ;OPERATION + POP DE ADD HL,DE ;POPVAR __a LD [__a],HL POP HL ;PUSHNUM 22 PUSH HL LD HL,0+22 ;PUSHNUM #33 PUSH HL LD HL,0+#33 ;OPERATION / POP DE CALL DIV_DE_HL ;PUSHVAR _nemain_v1 PUSH HL LD HL,[_nemain_v1] ;NEG XOR A SUB L LD L,A SBC A,H SUB L LD H,A ;OPERATION / POP DE CALL DIV_DE_HL ;PEEK LD A,[HL] INC HL LD H,[HL] LD L,A ;PUSHNUM 6 PUSH HL LD HL,0+6 ;NEG XOR A SUB L LD L,A SBC A,H SUB L LD H,A ;PUSHNUM 7 PUSH HL LD HL,0+7 ;OPERATION * POP BC CALL MUL_BC_HL ;OPERATION - EX DE,HL POP HL OR A SBC HL,DE ;PUSHVAR __a PUSH HL LD HL,[__a] ;NEG XOR A SUB L LD L,A SBC A,H SUB L LD H,A ;OPERATION + POP DE ADD HL,DE ;POPVAR __a LD [__a],HL POP HL ;PUSHVAR __a PUSH HL LD HL,[__a] ;PUSHNUM "as" PUSH HL LD HL,0+"as" ;POKE EX DE,HL POP HL LD [HL],E INC HL LD [HL],D POP HL ;POPVAR _nemain_nemain_v2 LD [_nemain_nemain_v2],HL POP HL ;POPVAR _nemain_nemain_v1 LD [_nemain_nemain_v1],HL POP HL RET ... ;.DSEG __a: DW 0 _nemain_v1: DW 0 _nemain_v2: DW 0 _main_b: DW 0 _main_c: DW 0 end: └────────────────────────────────────────┘ К письму прилагался исходник компилято─ ра. Судьба сложилась так, что эта первая релизная версия так и сохранилась только в письме. Зато все следующие версии склади─ ровались в архив. 23.09.2016: - добавил экспорт комментов и номеров строк в тело выходного ассемблерного файла - добавил массивы какint(A[15]). Доступ к ячейкам пока только через вычисление с использованием константы[WORDSIZE] и раз─ адресации@var - сменил=(constexpr) на [constexpr] - добавилif()then()else() 05.10.2016 - добавлена поддержка имено─ ванных пространств имён (командаmodule ): module(vasya)( int(a2) int(array[15]) proc(dup)( int(p1) int(p2) if(p1#0)then(=p2(p1))else() *(@array$+[7*WORDSIZE])(555) =p2(*(@array$+p1*[WORDSIZE])) return(p1+p1) ) ... ) {module} ) {end} Из этого выдавался код в таком стиле: ;PUSHNUM 0 PUSH HL LD HL,0+0 ;OPERATION = POP DE OR A SBC HL,DE JR Z,$+5 LD HL,#FFFF ;JNZ l.vasya.dup.0 LD A,L OR H POP HL ;END COUNT JP NZ,l.vasya.dup.0 ;line 8 ;BEGIN COUNT ;PUSHVAR _.vasya.dup.p1 PUSH HL LD HL,[_.vasya.dup.p1] ;POPVAR _.vasya.dup.p2 LD [_.vasya.dup.p2],HL POP HL ;END COUNT l.vasya.dup.0: ;JUMP l.vasya.dup.1 JP l.vasya.dup.1 l.vasya.dup.1: 11.10.2016: - появились типы(INT и UINT) - отличаю─ тся наличием знака у констант.Неудобно,что проверка int/uint стоит в двух местах - в compile_command и вcompile_var. Надо бы символ-префикс, который обрабатывать как команду? Тогда не будет и пересечения по первой букве. (Потом просто выделил чтение типа в отдельную процедуру,а много позже - внутрь чтения идентификатора, так что имя типа лежит в таблице меток.) - команда вызоваCALL заменена на ? -ELSE заменено на ~ , ибо было обязате─ льным (позже я откатил это изменение) 12.10.2016: - добавлены типы BOOL, CHAR, STRING (но пока не поддерживаются) - появилась проверка типа по таблице ме─ ток. До этого были надежды вообще убрать таблицу меток: контроль типов можно возложить на ассе─ мблер,если к каждому слову приписывать тип но для этого надо откуда-то знать типы переменных, и при присваивании, и при чте─ нии! случаи: 1. параметр функции - добавить тип легко (внутри /**/) 2. чтение глобала 3. запись глобала 4. чтение локала 5. запись неменяющегося локала (можно совместить с определением) 6. запись меняюшегося локала 7. вызов функции - добавить тип легко (внутри /**/) можно _xxxxxxx в контексте терма заре─ зервировать не под глобалы,а под подсказки компилятору (в Си там будет пусто) или весь набор типов продублировать с подчёркиванием? (как тогда расширять сис─ тему типов?) или кодировать тип в имени переменной (как тогда расширять систему типов?) ivar uvar - нет в венгерской нотации? lvar cvar - нет в венгерской нотации bvar - в венг. нотации это bool или byte fvar - хотелось бы для flag (бывает иногда в венгерской нотации) pvar - в венгерской нотации не пишется тип указателя? Это всё - плохой стиль венгерской нота─ ции (хороший - это класс значения или его размерность) - переменные и параметры объявляются ко─ мандой+ ,а локальные переменные рекурсив─ ных функций оформляются какstacked (при вызове тоже): func(nemain)stacked(uint(z1)int(z2)) ( +uint(v1) {parameter} +int(v2) {parameter} ( =a$(?dup(p1(+15)p2(11))){call function} *a$("as"){write memory} ) ) - в компиляторе отдельные процедуры вы─ вода комментариев(emitcomment) и отладоч─ ной информации(emithint) - в кодогенераторе проверяется занятость регистров (до этого одинаковые команды всегда компилировались одинаково),но прак─ тически это не использовано,текущее значе─ ние всегда вhl или de - добавлены операции сдвига влево и вправо на 1 бит (префиксные< , > ) - добавлен типBYTE для переменных 13.10.2016: - типы у функций - типBYTE поддержан и в параметрах фун─ кций, ветвление по типу перенесено из ком─ пилятора в кодогенератор - в кодогенераторе проверяется глубина стека - выделены модули emits (вывод сообще─ ний) иemitdirectives (вывод директив ас─ семблера, которые предполагались машинно─ независимыми) - кодогенератор разделён на процедуры, не проверяющие занятость регистров (досту─ пные компилятору), и проверяющие их и генерирующие непосредственно код (нижний уровень - недоступные компилятору) 14.10.2016: - добавлен типLONG для переменных.Лонги сделаны равными по размеру двум интам. Был вопрос, в каком порядке хранить половинки в стеке, я выбрал младшую часть на вершине (так можно складывать-вычитать, имея всего три регистра) - лексер выделен в модульreads, компи─ лятор выделен в модульcompile (в главном модуле остался GUI) 17.10.2016: - типLONG поддержан у функций - в кодогенераторе процедуры занятия регистров emitgethl, emitgetde, emitgetbc заменены наemitgetreg с параметром, доба─ влен байтовый контекст с другим порядком использования регистров. Все возможные со─ стояния регистров выглядели так: - hl hl,de a c,a hl,a hl,c,a bc,hl,de bc,hl 18.10.2016 - в кодогенераторе в отдельные процедуры передаётся номер регистра, который мы по─ лучаем черезgetnewreg (вершина стека) или getoldreg (предыдущее значение) - теперь сравнения "больше" и "меньше" делаются одинаково - для сравнений сделана отдельная проце─ дура вычитания,которая выдаёт только флаги - часть кодогенератора, доступная компи─ лятору (машиннонезависимая), выделена в модульemitcommands - в заголовке рекурсивных функций слово stacked с перечнем локальных переменных заменено на local (всё равно я не смог придумать код, который одинаково обрабаты─ вает оба словаstacked - здесь и при вызо─ ве) 19.10.2016: - добавлена перенумерация регистров (мо─ дульregs ), теперь с точки зрения кодоге─ нератора все регистры равноправны. Но в самом модуле regs пока что ограничены воз─ можные состояния регистров - в 8-битном контексте возможные состо─ яния регистров теперь (решил сэкономить с помощьюpush bc...pop af ): - b a b,a 20.10.2016: - регистры выделяются не по списку воз─ можных состояний,а по приоритетам (hl,de, bc для 16-битного контекста,a, h для бай─ тового контекста) - изучал систему команд ARM Thumb и был в шоке, что нет простого способа использо─ вать константу. Отложил ARM на потом,когда отлажу Z80. В комплекте компилятора появи─ лась памятка по командам ARM. Возник вопрос - как компилировать под несколько процессоров? а) ветки в каждой процедуре emit... (другие варианты,другие коды команд,другие таблицы названий регистров) число регистров не должно отличаться б) отдельный кодогенератор как программа неудобно будет отладить передачу данных от компилятора к кодогенератору и будет тормозить в) объектыemitasmz80,emitasmarm,насле─ дованные от одного классаemitasm неудобно будет переписывать компилятор на свой язык г) структура с адресами всех процедур emit... заполняется вinitz80иinitarm(вызываем одну из двух) д) код для каждого таргета в отдельных файлах, генерируется отдельный экзешник как собирать из разных файлов? через копи─ рование? или собрать все машиннозависимые инклюды в одном главном модуле? Выбрал последний вариант. 21.10.2016 - багфиксы (особенно приме─ чательно былоSLA вместо SRL )
Другие статьи номера:
Похожие статьи:
В этот день... 21 ноября