Еще один простой компилятор для PDP-11. Альтернативная история
Довольно давно я сделал версию компилятора языка TinyContext
для миникомпьютера PDP-11. Это практически дословный перевод версии для MS-DOS
и он может быть скомпилирован с ее помощью. Для тестирования использовался эмулятор
simh и основной тест - компиляция собственного
исходного кода был выполнен.
Но это совсем не равно созданию компилятора на реальной машине, тем более с нуля. Если есть
машина без систем программирования и даже без операционной системы, то начать придется с ручной
компиляции компилятора и ручной же пробивки перфолент или перфокарт с кодом компилятора
и его исходным текстом. При этом неизбежно будут допущены ошибки и их надо будет как-то искать
и исправлять. И второй вопрос - как сделать перфоленты, особенно перфоленту с кодом?
PDP-11 - не первый в истории компьютер, до него уже много чего было, но допустим, что он был первым.
Это машина с простым и удобным набором команд, написать для нее небольшую программу прямо в машинных
кодах относительно просто, но написание большой программы - совсем непростая задача.
Я никогда не работал на PDP-11, разве что когда-то проходил мимо машины СМ-4, поэтому прехде чем
что-то делать спросил у искуственного интеллекта, как задачу можно решить. Ответ был следующий:
Машина имеет панель управления с переключателями и линейками индикаторов, показывающих
текущий адрес и слово данных. Ее можно использовать для ввода программы и начинать необходимо
с этого, то такой способ ввода сложный и медленный.
Также есть телепринтер (телеnайп?) Model 33 ASR, который помимо своих основных функций ввода
и вывода текста позволяет пробить перфоленту, причем подключение к машине для этого не требуется.
Но пробить можно не любые символы и из-за этого ввод произвольных кодов невозможен. Это ограничение
можно обойти если вместо кодов вводить их текстовые представления. Длина ленты увеличится почти в шесть раз,
но ее можно сделать и ее легче читать. Программа загрузки должна преобразовать последовательности
из одной-шести восьмеричных цифр в шестнадцатиразрядные коды. Телепринтер также может использоваться
для ввода перфоленты в машину, но работает он медленно - за секунду он может прочитать всего 10 символов.
На загрузку кода компилятора потребуется примерно 40 минут.
Есть более быстродействующее устройство чтения/пробивки перфолент PC-05, за секунду оно может прочитать
300 символов в непрерывном режиме и 20 символов при чтении по одному символу. На эмуляторе simh этого
не видно - чтение происходит очень быстро.
Оценки затрат времени на кодирование и ввод следующие:
Перевод одной ассемблерной команды в код - 30-60 секунд, примерно 50 часов на весь код.
Ввод - 4-6 часов или неделю(!) если использовать переключатели на передней панели.
Делать это придется дваджы и затем сравнивать результаты. Для сравнения лент можно использовать машину.
Скорее всего нужна возможность разбивки и кода и исходного текста на несколько лент.
Я попробовал перевести в код самую первую функцию компилятора (mul), длина кода 83 слова,
это заняло 100 минут. Сравнение со сгенерированным кодом заняло еще 11 минут. Если экстраполировать,
то получится примерно 5000 минут на все - больше восемьдесяти часов. Но может быть, дальше будет быстрее?
Перевести еще одну-две функции вполне возможно, переводить весь текст я не готов.
Начал я с ошибки - при кодировании первой же строки M:=0; пропустил команду записи
переменной M (010037/117226). При кодировании перехода в начало вложенного цикла
я это заметил, сделал вставку двух команд, написанные на бумаге адреса решил не менять. Поскольку
все переходы относительные, неверные адреса не должны были привести к ошибкам, но привели:
При кодировании перехода в начало внешнего цикла я зачем-то пересчитал адреса
и получил 000640 вместо 000642. В десятичной системе
это было бы правильно (636 + 4 = 642), но здесь то система восьмеричная.
В результате длина перехода 224 вместо 226 и, соответственно,
неверое смещение 177554 = 2000000 - 224 вместо 177552 = 200000 - 226.
Неправильно дополнил код нулями. В исходном варианте их не было, здесь же я решил в конце
каждой функции резервировать минимум восемь слов для возможных исправлений.
При сравнении со сгенерированным кодом я пропустил еще одну ошибку - неправильный код инструкции копирования
содержимого регистра R0 в R1. На бумаге был написан код 0100001 вместо 10001.
Обнаружилось после ввода инструкций в файл и сравнения файлов с помощью diff.
К слову, код функции значительно больше, чем он мог бы быть. При ручном кодировании можно уложиться в 22 слова.
Длина кода функции init() 710 слов, ее я не переводил целиком, но чтобы оценить минимально необходимый размер
достаточно перевести в код три команды. Получится 271 слово.
Примитивный распределитель регистров позволяет уменьшить длину кода init() до 541 слова, но смысла в нем нет.
Он сокращает код компилятора меньше чем на 5 процентов, а возможность сделать ошибку наверняка увеличивает.
Самый первый вариант компилятора содержал одну незначительную оптимизацию (исключение выдачи повторных команд
возврата из функции) и она была сделана неправильно.
В принципе можно выполнять оптимальную компиляцию вручную, это сократит затраты на ввод кода, но не факт,
что сократит затраты на компиляцию.
Пробивка ленты занимает меньше времени, на первую попытку ввода 85 инструкций (83 слова функции mul
и 2 слова перехода к главной функции) я потратил 7 минут и понял, что запутался. Вторая попытка тоже
оказалась неудачной. Четыре ошибки были замечены сразу при вводе и исправлены (BACKSPACE+RUBOUT),
один код введен неверно из-за ошибки при компиляции (не при вводе!). И все это было впустую, поскольку
в начале я забыл пробить стартовый адрес.
Третья попытка выполнялась на модифицированном эмуляторе телепринтера
github.com/progs-n-things/asr33emu. Модификация состояла
в замене кодов символов для клавиш 'закрывающая квадратная скобка', 'обратный слеш' и 'кавычка'
на 'перевод строки', 'возврат каретки' и 'удаление' (DELETE/RUBOUT) соответственно. Это сделало клавиатуру ПК
похожей на клавиатуру телепринтера и дало естественный способ ввода символа 'перевод строки' (при нажатии
клавиши Enter вводится 'возврат каретки', чтобы ввести символ 'перевод строки' есть комбинация клавиш Ctrl-J,
но это путь к лишним ошибкам). При отключенной печати на бумаге (т.е. только при пробикве прфоленты)
на ввод тех же 85 инструкций я потратил 17 минут. Наверное, отверстия на перфоленте читаются хуже,
чем символы на бумаге (и наверное, на реальном устройстве они читаются еще хуже). Я сделал пять ошибок,
которые заметил и исправил. В итоге одна ошибка - в самом конце кода ввел 1777<DELETE/RUBOUT>17755213700
(нет возрата каретки между 177552 и 13700).
Общие затраты на пробивку лент примерно 850 минут - чуть больше четырнадцати часов. Тоже не быстро.
Стало ясно, что процесс компиляции и ввода должен быть изменен:
При компиляции функции на бумаге надо писать стартовый адрес.
Возможно, нужен способ указать адрес в любом месте ленты, а не только в начале.
Возможно, имеет смысл делать на ленте отметки адресов, например, кратных 108 или 208. Можно, например, использовать код RUBOUT.
Перед кодированием функции необходимо было вычислить адреса глобальных переменных, на что
ушло 19 минут, 11 минут собственно на вычисления и 8 минут на проверку. Но это разовые затраты,
при переводе каждой следующей функции нужно вычислять адреса только ее переременных.
Перед этим я сделал еще две ошибки:
Вычислил адреса в десятичной системе (видиимо, думая потом их преобразовать в восьмеричную),
при этом не учел, что переменная Ofs - это массив и неправильно назначил адреса
всем переменным начиная с nName. Времени потратил 14 минут.
При повторении вычисления уже в восьмеричной системе неправильно вычислил адрес
переменной nCode. Вместо 104406 получил 050406,
ошибка произошла из-за неправильного перевода длины массива Code в восьмеричную
систему и я прибавил 4000 вместо 40000. Адреса всех следующих переменных
тоже получились неправильными. Времени потратил 23 минуты, но часть его ушла на то чтобы научиться
считать в восьмеричной системе. Если бы константы в исходном тексте были записаны в восьмеричной
системе, было бы проще. Кодов команд это тоже касается, но комментарии с восьмеричными кодами всех
команд все были написаны.
Обшие затраты на компиляцию и ввод примерно 98 часов. И для выявления ошибок сделать это придется дважды.
И не факт, что все заработает сразу.
Для компиляции в среде MS-DOS (теперь для этого нужен DOSBox/86Box или какой-нибудь другой эмулятор)
нужно заменить все функции ввода-вывода от open() до halt() их аналогами,
увеличить размер массива Text c 2048 символов до 16384 символов (ЭТО ВАЖНО!) и получить работающий
в той же среде MS-DOS кросс-компилятор. С его помощью получить образ перфоленты с компилятором,
работающим на PDP-11 без операционной системы.
Итак, нужно добавить возможность смены ленты с исходным текстом и вывод текстовых представлений кодов.
Исходный текст компилятора, пока не окончательный. Результат компляции выводится на телепринтер:
char Text [ 2048];
word pText;
word nText;
word nLine;
word Code [ 8192];
word nCode;
word hFile;
char Heap [ 2048];
word nHeap;
word Name [ 256];
word Cls [ 256];
word Sub [ 256];
word Type [ 256];
word Size [ 256];
word Ofs [ 256];
word nName;
word nData;
word Stk [ 128];
word pStk;
char Buff [ 128];
word mul (word A, word B) is
word M:=0;
while B >0 do
word T :=A;
word I :=1;
while B-I>=I do
T:=T+T;
I:=I+I;
end
M:=M+T;
B:=B-I;
end
return M;
end
word div (word A, word B) is
word D:=0;
while A>=B do
word T :=B;
word I :=1;
while A-T>=T do
T:=T+T;
I:=I+I;
end
A:=A-T;
D:=D+I;
end
return D;
end
word mod (word A, word B) is
return A-mul(div(A,B),B);
end
word open() is
return 0;
end
word create() is
return 0;
end
word getc() is
inline 0x15C1, 0xFF68; // 012701 177550 // mov #TRS, R1
inline 0x0A89; // 005211 // inc (R1)
inline 0x35C9, 0x8080; // 032711 100200 // bit #100200, (R1)
inline 0x8104; // 100404 // bmi
inline 0x03FC; // 001774 // beq
inline 0x9C40, 0x0002; // 116100 000002 // movb 2(R1), R0
inline 0x0102; // 000402 // br
inline 0x15C0, 0xFF00; // 012700 177400 // mov #ERR, R0
end
word read() is
word P:=0;
word W:=getc();
while W!=0xFF00 do
Text[P]:=char(W);
P :=P+1;
if P>=2048 then
return P;
end
W:=getc();
end
return P;
end
char punch(word B) is
word B1:=B;
inline 0x15C1, 0xFF6C; // 012701 177554 // mov #TPS, R1
inline 0x9031, 0x0002; // 110061 000002 // movb R0, 2(R1)
inline 0x15C0, 0x0000; // 012700 000000 // mov #0, R0
inline 0x35C9, 0x8080; // 032711 100200 // bit #100200, (R1)
inline 0x8102; // 100402 // bmi
inline 0x03FC; // 001774 // beq
inline 0x0102; // 000402 // br
inline 0x15C0, 0x0001; // 012700 000001 // mov #1, R0
end
word write() is
word pCode:=0;
while pCode=nText then
pText :=0;
nText :=read();
if nText=0 then
wait();
nText :=read();
end
//if pText>=nText then
if nText=0 then
//return char(0);
Stop();
end
end
Ch:=Text[pText];
//if Ch=char(255) then
if Ch=DEL then
pText:=pText+1;
end
end
//return Text[pText];
return Ch;
end
char Read() is
char Ch:=Look();
if Ch =char(10) then
nLine :=nLine+1;
end
pText :=pText+1;
putc (Ch);
return Ch;
end
word isalnum() is
if 'A'<=Look() then
if Look()<='Z' then
return 0;
end
end
if 'a'<=Look() then
if Look()<='z' then
return 0;
end
end
if '0'<=Look() then
if Look()<='9' then
return 0;
end
end
return 1;
end
word Digraph(char C1, char C2) is
if Buff[0]=C1 then
if Look()=C2 then
Buff[1]:=Read();
Buff[2]:=char(0);
end
end
end
char Scan() is
word pBuff:=0;
while pBuff =0 do
word sFlag:=0;
while sFlag =0 do
if Look()!=char( 0) then
if Look()!=char( 9) then
if Look()!=char(10) then
if Look()!=char(13) then
if Look()!=char(32) then
sFlag:=1;
end
end
end
end
end
if sFlag=0 then
Read();
end
end
while isalnum()=0 do
Buff[pBuff]:= Read();
pBuff :=pBuff+1;
end
if pBuff=0 then
Buff[pBuff]:= Read();
pBuff :=pBuff+1;
end
Buff[pBuff] :=char(0);
Digraph('<', '=');
Digraph('!', '=');
Digraph('>', '=');
Digraph(':', '=');
if Buff[0]='/' then
if Look()='/' then
while Look()!=char(10) do
if Read()=char(0) then
Stop();
end
end
pBuff:=0;
end
end
end
end
word Comp(word pHeap) is
word pBuff:=0;
while Buff[pBuff]=Heap[pHeap] do
if Buff[pBuff]=char(0) then
return 0;
end
pHeap:=pHeap+1;
pBuff:=pBuff+1;
end
return 1;
end
word Find(word fFlag) is
word pName:=0;
while pName< nName do
if Comp(Name[pName])=0 then
return pName;
end
pName:=pName+1;
end
if fFlag=0 then
Stop();
end
return pName;
end
word Emi2(word W) is
Code[nCode]:=W;
nCode:=nCode+1;
end
word Assign(word I) is
if Size[I]>1 then
Emi2(0x1581); // 012601 // mov (SP)+, R1
if Size[Type[I]]=1 then
Emi2(0x9031); // 110061 // movb R0, Adr(R1)
end
if Size[Type[I]]=2 then
Emi2(0x1031); // 010061 // mov R0, Adr(R1)
end
end
if Size[I]=1 then
if Size[Type[I]]=1 then
Emi2(0x901F); // 110037 // movb R0, @#Adr
end
if Size[Type[I]]=2 then
Emi2(0x101F); // 010037 // mov R0, @#Adr
end
end
Emi2(Ofs[I]);
end
word Expr() is
word eFlag:=0;
//if eFlag =0 then
if '0'<=Buff[0] then
if Buff[0]<='9' then
Emi2(0x15C0); // 012700 // mov #Val, R0
Emi2(val());
eFlag:=1;
end
end
if Buff[0]=''' then
//TODO 128..255
Emi2(0x15C0); // 012700 // mov #Val, R0
Emi2(word(Read()));
Read();
eFlag:=1;
end
if Buff[0]='(' then
Scan();
Expr();
eFlag:=1;
end
//end
if eFlag =0 then
word I:=Find(0);
if Cls[I]=1 then
Push(I);
Scan(); // (
Scan();
Expr();
I:=Pop();
end
if Cls[I]=2 then
if Size[I]>1 then
Push(I);
Scan(); // [
Scan();
Expr();
I:=Pop();
if Size[Type[I]]=1 then
//TODO Use CLR/BISB
Emi2(0x9C00); // 116000 // movb Adr(R0), R0
end
if Size[Type[I]]=2 then
Emi2(0x0CC0); // 006300 // asl R0
Emi2(0x1C00); // 016000 // mov Adr(R0), R0
end
end
if Size[I]=1 then
if Size[Type[I]]=1 then
//TODO Use CLR/BISB
Emi2(0x97C0); // 113700 // movb @#Adr, R0
end
if Size[Type[I]]=2 then
Emi2(0x17C0); // 013700 // mov @#Adr, R0
end
end
Emi2(Ofs[I]);
//INFO Added 22.03.2026
if Size[Type[I]]=1 then
Emi2(0x45C0); // 042700 // bic #177400, R0
Emi2(0xFF00);
end
end
if Cls[I]=3 then
Scan(); // (
Push(I);
Sub[nName]:= 0;
word J:=I+1;
while Sub[J]=1 do
Push(J);
Scan();
Expr();
J:=Pop();
Assign(J);
J:=J+1;
end
I:=Pop();
if J=I+1 then
Scan(); // )
end
word W:=(0xFFFF-(((nCode+nCode)+4)-Ofs[I]))+1;
Emi2(0x09F7); // 004767 // jsr Ofs
Emi2(W);
end
end
Scan();
word I:=0;
if Buff[0]='+' then
I:=1;
end
if Buff[0]='-' then
I:=2;
end
if I!=0 then
Emi2(0x1026); // 010046 // mov R0, -(SP)
Push(I);
Scan();
Expr();
I:=Pop();
if I=1 then
Emi2(0x1581); // 012601 // mov (SP)+, R1
Emi2(0x6040); // 060100 // add R0, R1
end
if I=2 then
Emi2(0x1001); // 010001 // mov R0, R1
Emi2(0x1580); // 012600 // mov (SP)+, R0
Emi2(0xE040); // 160100 // sub R0, R1
end
end
end
word Cond() is
Scan();
Expr();
//TODO
word jCode:=0;
if Buff[0]='<' then
jCode:=0x8700; // 1034XX // blo Ofs
if Buff[1]='=' then
jCode:=0x8300; // 1014XX // blos Ofs
end
end
if Buff[0]='=' then
jCode:=0x0300; // 0014XX // beq Ofs
end
if Buff[0]='!' then
jCode:=0x0200; // 0010XX // bne Ofs
end
if Buff[0]='>' then
jCode:=0x8200; // 1010XX // bhi Ofs
if Buff[1]='=' then
jCode:=0x8600; // 1030XX // bhis Ofs
end
end
if jCode=0 then
Stop();
end
Emi2(0x1026); // 010046 // mov R0, -(SP)
Scan();
Expr();
Emi2(0x1581); // 012601 // mov (SP)+, R1
Emi2(0xE001); // 160001 // sub R1, R0
Emi2(jCode+2); // bxx 2
Push(nCode);
Emi2(0x0077); // 000167 // jmp ?
//nCode:=nCode+1;
Emi2(0);
end
word Obj (word T) is
if Cls[T]!=1 then
Stop();
end
Name[nName]:=nHeap;
Type[nName]:= T;
Scan();
if Find(1)1 then
Scan(); // [
Scan();
Expr();
if Size[Type[I]]=2 then
Emi2(0x0CC0); // 006300 // asl R0
end
Emi2(0x1026); // 010046 // mov R0, -(SP)
end
Scan(); // :=
Scan();
Expr();
Assign(I);
end
if Cls[I]=3 then
Expr();
end
//rFlag:=1; // 14.05.2006
end
Scan();
end
word Func(word Rts) is
Scan();
Ctrl();
while Comp(60)!=0 do // !end
Ctrl();
end
//if rFlag!=0 then // 14.05.2006 // if rFlag=0 then
//Emi2(0x0087); // 000207 // rts
Emi2(Rts);
//end
end
word Temp[8];
word oct(word W) is
word pTemp:=0;
word X :=1;
while X!=0 do
Temp[pTemp]:=mod(W,8);
pTemp :=pTemp+1;
W :=div(W,8);
X :=W;
end
while pTemp>0 do
pTemp:=pTemp-1;
putc(char(Temp[pTemp]+48));
end
end
word dump() is
putc(char(10));
oct (256);
putc(' ');
word pCode:=0;
while pCode