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 )
Other articles: