Info Guide
#12
31 декабря 2017 |
|
Системки - NedoLang: Путь к самокомпиляции (часть 2).
Путь к самокомпиляции Alone Coder Как только я начал реализовывать типы, компилятор стал стремительно пухнуть. Сох─ ранилась первая версия на C++, сконверти─ рованная из Delphi. Это было всего лишь 3 ноября (проект начат 14 сентября), а там уже торчало12 модулей толщиной 109 кило─ байт. Почему C++? Потому что система долж─ на работать под Linux. А почему Delphi? Потому что там проще начинать.Читабельнее. Мой язык тоже был такой весь из себя читабельный - с кучей ключевых слов на все случаи жизни. За счёт этого в поле команды я ждал именно команду, а не что-нибудь иное.Достаточно было проверять 1-2 символа команды, что я и сделал,когда избавился от AnsiString. Некоторое зондирование масс показало, что народ не хочет заново писать кодогене─ ратор с нашего фирменного языка высочайше─ го уровня (его название вам ничего не ска─ жет) на промежуточный язык, который там сейчас Си.В тот же момент на меня снизошло откровение, что без самокомпиляции я этот шедевр вообще не отлажу. Обычно раскрутку новых языков для само─ компиляции делают через написание первого урезанного компилятора на ассемблере. У меня уже был урезанный компилятор на C++. Зачем писать новый? Вот я и не стал писать новый. Я переделал его на C. И смог даже скомпилировать IAR'ом и SDCC. Но на этом мечты о постоянно висящей в48K памяти си─ стеме разбились о реальность - размер ко─ да. Я перевалил на ассемблер так много за─ дач (вплоть до временного хранения метки вызова, пока вычисляются параметры), что требовался вполне серьёзный ассемблер. Мо─ жно было догадаться, что он будет весить не меньше, чем сам компилятор. И в48K они вместе не влезут. Даже неизвестно, влезет ли ассемблер со всеми метками компилятора, когда он будет этот компилятор ассемблиро─ вать. (Я не планировал линкер, вместо него куски кода должен был раскидывать загруз─ чик в ОС - и то, если в 48K памяти будет действительно много программ.) Вообще ассемблер может быть с процеду─ рами, оптимизацией, макросами, локалами, автометками и т.п., или вообще без всего, даже может быть из токенизированного текс─ та с разжёванными выражениями и метками фиксированной длины. А можно было вообще сделать компилятор без ассемблера. Чтобы долго не думать, я достал из кучи выписок проект ассемблера, транслирующего из полупереваренного представления. Это когда-то планировалось на замену ALASM. Должно было работать страшно быстро, обс─ луживая приходящие токены переходом по та─ блице. Правда, быстро оно должно было ра─ ботать,если бы было написано на ассемблере Z80. Я написал это на Си. И токенизатор- детокенизатор в комплекте. Токены были выбраны с учётом следующих ограничений: - токенизатор не знает коды команд; - токенизатор видит только левый кон─ текст, поэтому не различает омонимичные команды (например, есть куча разных команд LD ); - детокенизатор (он же экспортёр) просто подставляет тексты токенов или прямой текст после токена "прямой текст". Это чтобы будущая оболочка ассемблерного реда─ ктора легко отображала токенизированное представление. Чтобы это реализовать, понадобились невидимые токены форматов команд, которые пишутся после тела команды, а ещё форматы выражений, которые пишутся после второго операнда у сложения и т.п. Форматов оказа─ лось так много, что еле хватило 256 кодов. Но в общем к концу года всё заработало. Только в коде ассемблера былswitch по токенам. Это был уже несколько другой уровень "простоты" языка.switch можно заменить на статически инициализированный массив ука─ зателей на функции, но это тоже надо реа─ лизовать, и работать будет медленнее - вы─ зов вместо перехода (перехода по указателю в Си нет). А можно не заменять, тогда надо уже на уровне ассемблера поддержать вычис─ ляемые метки. Ведь ассемблер получил бы на вход только имя текущегоswitch'а и имено─ ванную константу для веткиcase, которую надо вычислить и приклеить к имени. Писать в case прямо числа было бы неподдерживае─ мо. А так у меня уже был файл со всеми то─ кенами из токенизатора. Итак, я должен был написать не просто новый язык,а язык,совместимый с Си,с почти сравнимыми возможностями. Именно тогда - и не раньше - я выхожу на самокомпиляцию. 24.10.2016: - убрано ограничение на чередование слов и знаков в лексере.Теперь синтаксис такой: var int a var byte bb var long LONA var int array[15] module vasya( proc xx local() ( ) func byte byter local() ( var byte byterbb let byterbb=byterbb let LONA$=LONA$ {let bb$=$3a^(bb$^ ?byter({stacked}{byterbb=%1111}) )} let bb$=%111^bb$^byterbb )%101 func long longer local(long L1 long L2){recursive local} ( let a$=(+1); let LONA$=?longer()+$01020304; let bb$=(%1); )LONA$ func int dup local(int r){recursive local} ( var int p1 var uint p2 if(<p1#+ <<< 0)( let p2=p1 &+$ffff; )~() *(@array$+[7*WORDSIZE])(555){write array} let p2=*(+@array$+p1*+[WORDSIZE]); {read from array} )(p1+p1) 25.10.2016: - слово local больше не нужно для нере─ курсивных процедур - команда записи в память теперь начина─ ется со словаpoke (как в Бейсике): poke (@array$+[7*WORDSIZE]),555 - выделен модуль typecodes с кодами типов, он используется в compiler и emitcommands 26.10.2016: - равенство и неравенство пишутся как== и!= (как в Си) - полноценная работа с массивами через a[N] (как в Си) - реализован давно добавленный типBOOL. Была идея хранить его прямо во флаговом регистре (проще всегоCY ), тогда if/while прямо по флагу: AND: jr FALSE,$+2+2 pop af push af pop bc OR: jr TRUE,$+2+2 pop af push af pop bc XOR (только для флага в CY): jr FALSE,$+2+3 pop af ccf push af pop af pushvar (только для флага в CY): push af;зависит от типа выше ld a,(var) rlca popvar (только для флага в CY): rrca ld (var),a pop af;зависит от типа выше LESS (старое меньше нового): ;если leftsaved=FALSE ;ex de,hl ;pop hl or a sbc hl,de MORE (новое меньше старого): ;если leftsaved=FALSE ;ex de,hl ;pop hl ex de,hl or a sbc hl,de MOREEQ (старое >= нового): ;если leftsaved=FALSE ;ex de,hl ;pop hl or a sbc hl,de ccf EQ: ;если leftsaved=FALSE ;ex de,hl ;pop hl or a sbc hl,de jr z,$+2+1;CY=0 scf ccf NOTEQ: ;если leftsaved=FALSE ;ex de,hl ;pop hl or a sbc hl,de jr z,$+2+1;CY=0 scf Но я решил, что проще в аккумуляторе (как байтовый тип) 27.10.2016: ->= и <= (как в Си) -repeat .. until (как в Паскале) - в команду while добавлено слово loop (там, где в Паскале стоитdo, но do мешает совместимости с Си) - в командуif добавлено слово then (как в Паскале) - при вызове рекурсивных функций после имени функции пишется словоrecursive, а stacked не пишется - попытка убрать все повторные обращения к строкам, чтобы можно было их стримить из входного файла (приклеивание областей ви─ димости планировалось возложить на ассемб─ лер) 28.10.2016: - блоки команд выделяются не круглыми, а фигурными скобками (как в Си). Фирменный вложенный комментарий фигурными скобками теперь оформляется как{ ... } -poke теперь выглядит так (совместимо с Си): poke *(a$)=( " as" " ) {write memory} - определение метки для перехода теперь выглядит не как :mylabel , а как LABEL mylabel: (совместимо с Си) - глобальные переменные начинаются с_ (сначала сделал с точки, но передумал из-за несовместимости с Си) - функции в выражениях вызываются через префикс@ (был ? , он оставлен для булевых констант/*?*/_TRUE и /*?*/_FALSE ),а про─ цедуры вызываются черезCALL (совместимо с Си) - в определении функции вместо блока local теперь обычное объявление локальных переменных в теле черезRECURSIVEVAR и фи─ гурную скобку после объявления (ограничи─ вает место,где локальные переменные сохра─ няются в стеке, а потом восстанавливаются) - поддерживаются (и рекомендуются) кома─ нды большими буквами (как в Обероне, плюс обычно в Си дефайны именно большими буква─ ми) - все несовместимые с Си закорючки можно экранировать комментарием/*...*/ ,который для NedoLang'а - не комментарий. Настоящий комментарий -/**...*/ Чтобы понять идею совместимости своего языка с Си, ср. пример из оригинального Bourne Shell: #define BEGIN { #define END } #define SWITCH switch( #define IN ){ #define ENDSW } #define FOR for( #define WHILE while( #define DO ){ #define OD ;} #define REP do{ #define PER }while( #define DONE ); #define LOOP for(;;){ #define POOL } 29.10.2016: - командаreturn перенесена в тело функ─ ции (как в Си) - числовые константы совместимы с Си 31.10.2016: - убрана зависимость от библиотеки SysUtils (функцияIntToStr ) - в рекурсивных процедурах вычисления выражения убрана локальная строковая пере─ менная 01.11.2016: - убраны все локальные строковые пере─ менные, для этого вif и т.п. генерируются специальные метки для ассемблера: @TEMPELSE, @TEMPENDIF... 02.11.2016: - при вызове функции пишется тип функции и параметров - строковые константы работают: компили─ руются с автометкой 03.11.2016 - компилятор перенесён из Delphi в C++ Builder 05.11.2016 - строкиAnsiString заменены на MYSTRING (определено какchar*, но с длиной в первом байте), первая компиляция на IAR (для этого написан свой модуль syscalls со строковыми и файловыми проце─ дурами, последние в виде заглушек) 22.11.2016 - создал проект-песочницу на C++, где экспериментировал, какие обороты вообще может съесть компилятор. Такое он ест: i=+(uint)+(uint)0; j = + +i; k = + -i; А вот фигурные скобки в произвольном месте - нет. А я так хотел ими экраниро─ вать сишный код, который не должен видеть NedoLang. Но компиляторы вообще бывают разные. 23.11.2016 - появились указатели (типыpint и т.п.) - переменные выгружаются в отдельный ассемблерный файл (до этого они выгружа─ лись из памяти в конце компиляции,а у меня всё ещё были надежды избавиться от таблицы меток в компиляторе благодаря ручному про─ писыванию типов в обращениях) 24.11.2016 - паскалевский формат строк заменён на сишный 25.11.2016: - добавлена командаbreak для выхода из циклов, потому что я не смог избавиться от break везде в компиляторе. Лучшее, что я придумал вместо break , - сложный формат цикла с предусловием: //сравнение строк //а) символы не совпали (не годно) //б) символы совпали (продолжаем,если //ненулевые, иначе годно) WHILE(( PRECOND(//precond1 LET c1=s1[i+FIRST], LET c2=s2[i] ),COND(c1==c2) LUCK(//cond1==TRUE //(одинаковые символы) COND(c1!=' ') LUCK(//cond2==TRUE //(not end) LET i=i+1 )FAIL(LET result=TRUE),NODO //cond2==FALSE //(end) )FAIL(LET result=FALSE),NODO //cond1==FALSE //(неодинаковые символы) ))LOOP{ } Всё понятно? Нет? Ладно,дам подсказку:) #define PRECOND /**/ #define COND /**/ #define LUCK ? #define FAIL :( #define NODO FALSE) К сожалению, фигурные скобки нельзя ис─ пользовать в тернарном операторе. -if теперь заканчивается endif , а else не обязательно 30.11.2016 - написаны токенизатор и де─ токенизатор (не отдельно, а в составе той же программы) 01.12.2016 - написан ассемблер (тоже в составе) 02.12.2016 - написан мануал 05.12.2016: - убраны@TEMPELSE,@TEMPENDIF ...,вместо них нумерованные метки - добавлен комментарий// до конца стро─ ки - добавлена командаasm 12.12.2016 - багфиксы в токенизаторе 13.12.2016 - написал в отчёте примерно следующее: "Написание полноценного компилятора C или C++ - огромная задача, требующая про─ фессиональных программистов. Помимо боль─ шого объёма стандарта языка проблема ещё в том, что язык содержит множество тонкостей в разборе текста и работе с типами, дово─ льно сложных в реализации. Но поскольку мы используем только небольшое подмножество языка Си, мы можем реализовать компилятор только для этого подмножества, а разбор текста упростить с помощью расстановки в тексте подсказок компилятору. На данный момент у нас есть прототип компилятора из языка, похожего на Си, в ассемблерный текст. ... Также имеются зачатки самописного ассе─ мблера, пока только под систему команд 8-битного процессора Z80. Поддержка других архитектур затронет только специально вы─ деленную часть компилятора и ассемблера (менее 40% объёма исходного текста, причём довольно однообразная часть) и может быть добавлена сравнительно легко. Проблема будет в реализации полноценной системы команд (мы заранее не знаем, что может потребоваться, но можем примерно оценить это с помощью самокомпиляции, то есть когда добьёмся, чтобы компилятор ком─ пилировал собственный исходный текст) и полной отладке с документированием, кото─ рая может занять времени значительно боль─ ше, чем сама разработка." Что нужно сделать для самокомпиляции: # константные массивы для размеров типов и названий регистров (можно заменить на функции или на заполнение массивов) #forwardопределения функций или объяв─ ление функции также при использовании - если писать вызов /*@*/(type).name(/*par1=*/n1,/*par2=*/n2) то Си увидит(type).name(n1,n2) - если писать вызов /*@type.*/name( /*type par1=*/n1,/*type par2=*/n2) то Си увидитname(n1,n2) - вместо/*@*/можно писатьCALL, но не в выражении (т.к. может быть идентификатор CALL ) - при вызове из выражения ожидается иде─ нтификатор или дефайн, как и в случае true/false #include #define(коды префиксов и регистров) # таблица меток должна быть определена на массиве! или вообще её убрать (жёстко писать типы везде и чередовать csegи dseg )
Другие статьи номера:
Похожие статьи:
В этот день... 21 ноября