Часть 4: QFTASM и Cogol
Обзор архитектуры
Короче говоря, наш компьютер имеет 16-битную асинхронную архитектуру RISC Harvard. При создании процессора вручную архитектура RISC ( компьютер с сокращенным набором команд ) практически обязательна. В нашем случае это означает, что количество кодов операций невелико и, что гораздо важнее, что все инструкции обрабатываются очень похожим образом.
Для справки, компьютер Wireworld использовал триггерную архитектуру , в которой MOV
выполнялась единственная инструкция, а вычисления выполнялись путем записи / чтения специальных регистров. Несмотря на то, что эта парадигма приводит к очень простой в реализации архитектуре, результат также не может быть использован на границе: все арифметические / логические / условные операции требуют трех инструкций. Нам было ясно, что мы хотим создать гораздо менее эзотерическую архитектуру.
Чтобы сделать наш процессор простым и повысить удобство использования, мы приняли несколько важных проектных решений:
- Нет регистров. Каждый адрес в ОЗУ обрабатывается одинаково и может использоваться в качестве любого аргумента для любой операции. В некотором смысле это означает, что вся оперативная память может рассматриваться как регистры. Это означает, что нет специальных инструкций по загрузке / хранению.
- В том же духе, отображение памяти. Все, что может быть записано или прочитано, использует единую схему адресации. Это означает, что программный счетчик (ПК) имеет адрес 0, и единственное различие между обычными инструкциями и инструкциями потока управления состоит в том, что инструкции потока управления используют адрес 0.
- Данные последовательны в передаче, параллельны в памяти. Благодаря «электронному» характеру нашего компьютера, сложение и вычитание значительно легче реализовать, когда данные передаются в последовательной форме с прямым порядком байтов (младший значащий бит вначале). Более того, последовательные данные устраняют необходимость в громоздких шинах данных, которые действительно широки и громоздки для правильного времени (чтобы данные оставались вместе, все «полосы» шины должны испытывать одинаковую задержку движения).
- Гарвардская архитектура, означающая разделение между программной памятью (ПЗУ) и памятью данных (ОЗУ). Хотя это снижает гибкость процессора, это помогает оптимизировать размер: длина программы намного больше, чем объем ОЗУ, который нам понадобится, поэтому мы можем разбить программу на ПЗУ и затем сосредоточиться на сжатии ПЗУ , что намного проще, когда это только для чтения.
- 16-битная ширина данных. Это наименьшая сила двоих, которая шире стандартной доски Tetris (10 блоков). Это дает нам диапазон данных от -32768 до +32767 и максимальную длину программы 65536 инструкций. (2 ^ 8 = 256 инструкций достаточно для самых простых вещей, которые мы могли бы сделать для игрушечного процессора, но не для тетриса.)
- Асинхронный дизайн. Вместо того, чтобы иметь центральные часы (или, что то же самое, несколько часов), определяющие синхронизацию компьютера, все данные сопровождаются «тактовым сигналом», который передается параллельно с данными, проходящими вокруг компьютера. Некоторые пути могут быть короче, чем другие, и, хотя это может создать трудности для централизованно-тактовой схемы, асинхронная схема может легко справляться с операциями с переменным временем.
- Все инструкции имеют одинаковый размер. Мы чувствовали, что архитектура, в которой каждая инструкция имеет 1 код операции с 3 операндами (значение-значение-назначение), была наиболее гибкой опцией. Это включает в себя операции с двоичными данными, а также условные перемещения.
- Простая система адресации. Наличие множества режимов адресации очень полезно для поддержки таких вещей, как массивы или рекурсия. Нам удалось реализовать несколько важных режимов адресации с помощью относительно простой системы.
Иллюстрация нашей архитектуры содержится в обзорном посте.
Функциональность и операции ALU
Отсюда вопрос определения функциональности нашего процессора. Особое внимание было уделено простоте реализации, а также универсальности каждой команды.
Условные ходы
Условные перемещения очень важны и служат как мелким, так и крупномасштабным потоком управления. «Маломасштабный» относится к его способности контролировать выполнение конкретного перемещения данных, в то время как «крупномасштабный» относится к его использованию в качестве операции условного перехода для передачи потока управления любому произвольному фрагменту кода. Выделенных операций перехода не существует, поскольку из-за сопоставления памяти условное перемещение может как копировать данные в обычную оперативную память, так и копировать адрес назначения на ПК. Мы также решили отказаться от как безусловных ходов, так и безусловных переходов по той же причине: оба могут быть реализованы как условный ход с условием, жестко заданным как TRUE.
Мы выбрали два разных типа условных перемещений: «двигаться, если не ноль» ( MNZ
) и «двигаться, если ноль меньше» ( MLZ
). Функционально MNZ
означает проверку того, является ли какой-либо бит в данных единицей, а равно MLZ
проверке, имеет ли бит знака 1. Они полезны для равенств и сравнений соответственно. Причина, по которой мы выбрали эти два, вместо других, таких как «переместить, если ноль» ( MEZ
) или «переместить, если больше нуля» ( MGZ
), заключалась в том, что MEZ
это потребовало бы создания ИСТИННОГО сигнала из пустого сигнала, в то время MGZ
как это более сложная проверка, требующая знаковый бит будет 0, в то время как по крайней мере еще один бит будет 1.
арифметика
Следующими наиболее важными инструкциями, с точки зрения руководства конструкцией процессора, являются основные арифметические операции. Как я упоминал ранее, мы используем последовательные данные с прямым порядком байтов, причем выбор порядка байтов определяется простотой операций сложения / вычитания. Получив первым младший бит, арифметические единицы могут легко отслеживать бит переноса.
Мы решили использовать представление дополнения 2 для отрицательных чисел, поскольку это делает сложение и вычитание более согласованными. Стоит отметить, что компьютер Wireworld использовал 1 дополнение.
Сложение и вычитание являются степенью естественной арифметической поддержки нашего компьютера (кроме сдвигов битов, которые будут обсуждаться позже). Другие операции, такие как умножение, слишком сложны для нашей архитектуры и должны быть реализованы в программном обеспечении.
Побитовые операции
Наш процессор имеет AND
, OR
и XOR
инструкции, которые делают то, что вы ожидаете. Вместо того, чтобы иметь NOT
инструкцию, мы решили использовать инструкцию "а не" ( ANT
). Сложность NOT
инструкции заключается в том, что она должна создавать сигнал из-за отсутствия сигнала, что сложно с клеточными автоматами. ANT
Инструкция возвращает 1 , только если первый аргумент бит равен 1 , а второй аргумент бит равен 0. Таким образом, NOT x
эквивалентно ANT -1 x
(а также XOR -1 x
). Кроме того, ANT
он универсален и имеет основное преимущество в маскировании: в случае программы Tetris мы используем его для стирания тетромино.
Сдвиг бит
Операции сдвига битов являются наиболее сложными операциями, выполняемыми АЛУ. Они принимают два ввода данных: значение для сдвига и значение для его сдвига. Несмотря на их сложность (из-за разной степени смещения), эти операции имеют решающее значение для многих важных задач, включая множество «графических» операций, связанных с тетрисом. Сдвиги битов также послужат основой для эффективных алгоритмов умножения / деления.
Наш процессор имеет три операции сдвига битов: «сдвиг влево» ( SL
), «сдвиг вправо» ( SRL
) и «сдвиг вправо» ( SRA
). Первые два битовых сдвига ( SL
и SRL
) заполняют новые биты всеми нулями (это означает, что отрицательное число, сдвинутое вправо, больше не будет отрицательным). Если второй аргумент сдвига находится вне диапазона от 0 до 15, результат, как и следовало ожидать, будет иметь все нули. Для последнего сдвига бит сдвиг SRA
бит сохраняет знак ввода и, следовательно, действует как истинное деление на два.
Инструкция по конвейерной обработке
Сейчас самое время поговорить о некоторых мельчайших деталях архитектуры. Каждый цикл ЦП состоит из следующих пяти шагов:
1. Получить текущую инструкцию из ПЗУ
Текущее значение ПК используется для извлечения соответствующей инструкции из ПЗУ. Каждая инструкция имеет один код операции и три операнда. Каждый операнд состоит из одного слова данных и одного режима адресации. Эти части отделены друг от друга, поскольку они читаются из ПЗУ.
Код операции состоит из 4 битов для поддержки 16 уникальных кодов операций, из которых 11 назначены:
0000 MNZ Move if Not Zero
0001 MLZ Move if Less than Zero
0010 ADD ADDition
0011 SUB SUBtraction
0100 AND bitwise AND
0101 OR bitwise OR
0110 XOR bitwise eXclusive OR
0111 ANT bitwise And-NoT
1000 SL Shift Left
1001 SRL Shift Right Logical
1010 SRA Shift Right Arithmetic
1011 unassigned
1100 unassigned
1101 unassigned
1110 unassigned
1111 unassigned
2. Записать результат (при необходимости) предыдущей инструкции в ОЗУ
В зависимости от условия предыдущей инструкции (например, значения первого аргумента для условного перемещения) выполняется запись. Адрес записи определяется третьим операндом предыдущей инструкции.
Важно отметить, что запись происходит после извлечения инструкций. Это приводит к созданию интервала задержки ветвления, в котором инструкция сразу после инструкции ветвления (любая операция, которая записывает в ПК) выполняется вместо первой инструкции в целевом объекте ветвления.
В некоторых случаях (например, безусловные переходы) интервал задержки перехода может быть оптимизирован. В других случаях это невозможно, и инструкция после ветки должна оставаться пустой. Кроме того, этот тип интервала задержки означает, что ветви должны использовать цель перехода, которая на 1 адрес меньше, чем фактическая целевая команда, для учета происходящего приращения ПК.
Короче говоря, поскольку вывод предыдущей инструкции записывается в ОЗУ после извлечения следующей инструкции, условные переходы должны иметь пустую инструкцию после них, иначе ПК не будет обновляться должным образом для перехода.
3. Считайте данные для аргументов текущей инструкции из RAM
Как упоминалось ранее, каждый из трех операндов состоит из слова данных и режима адресации. Слово данных составляет 16 бит, такой же ширины, как ОЗУ. Режим адресации - 2 бита.
Режимы адресации могут быть источником значительной сложности для процессора, подобного этому, поскольку многие реальные режимы адресации требуют многоэтапных вычислений (например, добавление смещений). В то же время, универсальные режимы адресации играют важную роль в удобстве использования процессора.
Мы стремились объединить концепции использования жестко закодированных чисел в качестве операндов и использования адресов данных в качестве операндов. Это привело к созданию режимов адресации на основе счетчика: режим адресации операнда - это просто число, представляющее, сколько раз данные должны передаваться по циклу чтения ОЗУ. Это включает немедленную, прямую, косвенную и двойную косвенную адресацию.
00 Immediate: A hard-coded value. (no RAM reads)
01 Direct: Read data from this RAM address. (one RAM read)
10 Indirect: Read data from the address given at this address. (two RAM reads)
11 Double-indirect: Read data from the address given at the address given by this address. (three RAM reads)
После выполнения разыменования три операнда инструкции играют разные роли. Первый операнд обычно является первым аргументом для бинарного оператора, но также служит условием, когда текущая инструкция является условным перемещением. Второй операнд служит вторым аргументом для бинарного оператора. Третий операнд служит адресом назначения для результата инструкции.
Поскольку первые две инструкции служат данными, а третьи служат адресом, режимы адресации имеют несколько разные интерпретации в зависимости от того, в какой позиции они используются. Например, прямой режим используется для чтения данных с фиксированного адреса ОЗУ (так как требуется одно чтение из ОЗУ), но непосредственный режим используется для записи данных на фиксированный адрес ОЗУ (поскольку не требуется чтение из ОЗУ).
4. Подсчитать результат
Код операции и первые два операнда отправляются в АЛУ для выполнения двоичной операции. Для арифметических, побитовых и сдвиговых операций это означает выполнение соответствующей операции. Для условных ходов это означает просто возврат второго операнда.
Код операции и первый операнд используются для вычисления условия, которое определяет, записывать или нет результат в память. В случае условных перемещений это означает либо определение того, является ли какой-либо бит в операнде 1 (для MNZ
), либо определение, является ли бит знака 1 (для MLZ
). Если код операции не является условным перемещением, тогда запись всегда выполняется (условие всегда выполняется).
5. Увеличьте счетчик программы
Наконец, счетчик программы читается, увеличивается и записывается.
Из-за позиции приращения ПК между считанной инструкцией и записью инструкции, это означает, что инструкция, которая увеличивает ПК на 1, является недопустимой. Инструкция, которая копирует ПК на себя, вызывает выполнение следующей инструкции дважды подряд. Но, имейте в виду, несколько инструкций для ПК подряд могут вызывать сложные эффекты, включая бесконечный цикл, если вы не обращаете внимание на конвейер инструкций.
Квест для Тетрис Ассамблеи
Мы создали новый язык ассемблера QFTASM для нашего процессора. Этот язык ассемблера соответствует 1-к-1 машинному коду в ПЗУ компьютера.
Любая программа QFTASM написана в виде серии инструкций, по одной на строку. Каждая строка отформатирована так:
[line numbering] [opcode] [arg1] [arg2] [arg3]; [optional comment]
Список кодов операций
Как обсуждалось ранее, компьютер поддерживает одиннадцать кодов операций, каждый из которых имеет три операнда:
MNZ [test] [value] [dest] – Move if Not Zero; sets [dest] to [value] if [test] is not zero.
MLZ [test] [value] [dest] – Move if Less than Zero; sets [dest] to [value] if [test] is less than zero.
ADD [val1] [val2] [dest] – ADDition; store [val1] + [val2] in [dest].
SUB [val1] [val2] [dest] – SUBtraction; store [val1] - [val2] in [dest].
AND [val1] [val2] [dest] – bitwise AND; store [val1] & [val2] in [dest].
OR [val1] [val2] [dest] – bitwise OR; store [val1] | [val2] in [dest].
XOR [val1] [val2] [dest] – bitwise XOR; store [val1] ^ [val2] in [dest].
ANT [val1] [val2] [dest] – bitwise And-NoT; store [val1] & (![val2]) in [dest].
SL [val1] [val2] [dest] – Shift Left; store [val1] << [val2] in [dest].
SRL [val1] [val2] [dest] – Shift Right Logical; store [val1] >>> [val2] in [dest]. Doesn't preserve sign.
SRA [val1] [val2] [dest] – Shift Right Arithmetic; store [val1] >> [val2] in [dest], while preserving sign.
Режимы адресации
Каждый из операндов содержит как значение данных, так и ход адресации. Значение данных описывается десятичным числом в диапазоне от -32768 до 32767. Режим адресации описывается однобуквенным префиксом к значению данных.
mode name prefix
0 immediate (none)
1 direct A
2 indirect B
3 double-indirect C
Пример кода
Последовательность Фибоначчи в пяти строках:
0. MLZ -1 1 1; initial value
1. MLZ -1 A2 3; start loop, shift data
2. MLZ -1 A1 2; shift data
3. MLZ -1 0 0; end loop
4. ADD A2 A3 1; branch delay slot, compute next term
Этот код вычисляет последовательность Фибоначчи с адресом ОЗУ 1, содержащим текущий термин. Это быстро переполняется после 28657.
Серый код:
0. MLZ -1 5 1; initial value for RAM address to write to
1. SUB A1 5 2; start loop, determine what binary number to covert to Gray code
2. SRL A2 1 3; shift right by 1
3. XOR A2 A3 A1; XOR and store Gray code in destination address
4. SUB B1 42 4; take the Gray code and subtract 42 (101010)
5. MNZ A4 0 0; if the result is not zero (Gray code != 101010) repeat loop
6. ADD A1 1 1; branch delay slot, increment destination address
Эта программа вычисляет код Грея и сохраняет код в последовательных адресах, начиная с адреса 5. Эта программа использует несколько важных функций, таких как косвенная адресация и условный переход. Он останавливается, как только 101010
получится код Грея , что происходит для входа 51 по адресу 56.
Онлайн переводчик
El'endia Starman создала очень полезного онлайн-переводчика здесь . Вы можете пошагово выполнять код, устанавливать точки останова, выполнять ручную запись в ОЗУ и визуализировать ОЗУ как дисплей.
Cogol
После того как архитектура и язык ассемблера были определены, следующим шагом в «программной» части проекта стало создание языка более высокого уровня, подходящего для тетриса. Таким образом я создал Cogol . Название - это и каламбур на «COBOL», и аббревиатура от «C of Game of Life», хотя стоит отметить, что Cogol является для C тем же, что и наш компьютер для реального компьютера.
Cogol существует на уровне чуть выше ассемблера. Как правило, большинство строк в программе Cogol каждая соответствует одной строке сборки, но есть некоторые важные особенности языка:
- Основные функции включают именованные переменные с присваиваниями и операторы, которые имеют более читаемый синтаксис. Например,
ADD A1 A2 3
становится z = x + y;
с компилятором, отображающим переменные на адреса.
- Циклические конструкции, такие как
if(){}
, while(){}
и do{}while();
поэтому компилятор обрабатывает ветвления.
- Одномерные массивы (с арифметикой указателей), которые используются для платы тетриса.
- Подпрограммы и стек вызовов. Они полезны для предотвращения дублирования больших кусков кода и для поддержки рекурсии.
Компилятор (который я написал с нуля) очень простой / наивный, но я попытался вручную оптимизировать некоторые языковые конструкции для достижения короткой длины скомпилированной программы.
Вот несколько кратких обзоров того, как работают различные языковые функции:
лексемизацию
Исходный код токенизируется линейно (однопроходно) с использованием простых правил о том, какие символы могут быть смежными внутри токена. Когда встречается символ, который не может быть смежным с последним символом текущего токена, текущий токен считается завершенным, и новый персонаж начинает новый токен. Некоторые символы (такие как {
или ,
) не могут быть смежными с любыми другими символами и поэтому являются их собственным токеном. Другие (как >
и =
) разрешается только быть рядом с другими персонажами в рамках своего класса, и таким образом могут образовывать маркеры , такие как >>>
, ==
или >=
, но не нравится =2
. Пробельные символы устанавливают границу между токенами, но сами не включаются в результат. Самый сложный символ для токенизации-
потому что он может представлять как вычитание, так и унарное отрицание, и, следовательно, требует некоторого специального случая.
анализ
Разбор также выполняется за один проход. Компилятор имеет методы для обработки каждой из различных языковых конструкций, и токены извлекаются из глобального списка токенов, поскольку они используются различными методами компилятора. Если компилятор когда-либо видит токен, которого он не ожидает, он вызывает синтаксическую ошибку.
Глобальное распределение памяти
Компилятор присваивает каждой глобальной переменной (слову или массиву) свой собственный адрес (а) ОЗУ. Необходимо объявить все переменные, используя ключевое слово, my
чтобы компилятор знал, как выделить для него место. Гораздо круче, чем именованные глобальные переменные, это управление памятью с нуля. Многие инструкции (особенно условные и многие обращения к массиву) требуют временных «чистых» адресов для хранения промежуточных вычислений. Во время процесса компиляции компилятор распределяет и отменяет выделение пустых адресов по мере необходимости. Если компилятору нужно больше чистых адресов, он выделит больше оперативной памяти в качестве чистых адресов. Я полагаю, что для программы типично требовать только несколько чистых адресов, хотя каждый чистый адрес будет использоваться много раз.
IF-ELSE
Заявления
Синтаксис для if-else
операторов является стандартной формой C:
other code
if (cond) {
first body
} else {
second body
}
other code
При преобразовании в QFTASM код выглядит следующим образом:
other code
condition test
conditional jump
first body
unconditional jump
second body (conditional jump target)
other code (unconditional jump target)
Если первое тело выполнено, второе тело пропускается. Если первое тело пропущено, второе тело выполняется.
В сборке условный тест обычно представляет собой просто вычитание, и знак результата определяет, следует ли выполнить переход или выполнить тело. MLZ
Инструкция используется для обработки неравенства , такие как >
или <=
. Для обработки MNZ
используется инструкция ==
, поскольку она перепрыгивает через тело, когда разница не равна нулю (и, следовательно, когда аргументы не равны). Условные выражения с несколькими выражениями в настоящее время не поддерживаются.
Если else
оператор опущен, безусловный переход также пропущен, и код QFTASM выглядит следующим образом:
other code
condition test
conditional jump
body
other code (conditional jump target)
WHILE
Заявления
Синтаксис для while
операторов также является стандартной формой C:
other code
while (cond) {
body
}
other code
При преобразовании в QFTASM код выглядит следующим образом:
other code
unconditional jump
body (conditional jump target)
condition test (unconditional jump target)
conditional jump
other code
Условное тестирование и условный переход находятся в конце блока, что означает, что они выполняются повторно после каждого выполнения блока. Когда условие возвращает false, тело не повторяется, и цикл заканчивается. В начале выполнения цикла поток управления перепрыгивает через тело цикла к коду условия, поэтому тело никогда не выполняется, если условие ложно в первый раз.
MLZ
Инструкция используется для обработки неравенства , такие как >
или <=
. В отличие от if
операторов MNZ
while, для обработки используется инструкция !=
, поскольку она переходит к телу, когда разница не равна нулю (и, следовательно, когда аргументы не равны).
DO-WHILE
Заявления
Единственная разница между while
и do-while
заключается в том, что do-while
тело цикла изначально не пропускается, поэтому оно всегда выполняется хотя бы один раз. Обычно я использую do-while
операторы для сохранения пары строк кода сборки, когда я знаю, что цикл никогда не нужно будет пропускать полностью.
Массивы
Одномерные массивы реализованы в виде смежных блоков памяти. Все массивы имеют фиксированную длину в зависимости от их объявления. Массивы объявлены так:
my alpha[3]; # empty array
my beta[11] = {3,2,7,8}; # first four elements are pre-loaded with those values
Для массива это возможное отображение ОЗУ, показывающее, как адреса 15-18 зарезервированы для массива:
15: alpha
16: alpha[0]
17: alpha[1]
18: alpha[2]
Адрес маркированы alpha
заполняется указателем на местоположение alpha[0]
, поэтому в Thie адрес случае 15 содержит значение 16. alpha
Переменная может использоваться внутри кода Cogol, возможно , в качестве указателя стека , если вы хотите использовать этот массив в качестве стека ,
Доступ к элементам массива осуществляется в стандартной array[index]
записи. Если значение index
является константой, эта ссылка автоматически заполняется абсолютным адресом этого элемента. В противном случае он выполняет некоторую арифметику указателя (просто сложение), чтобы найти нужный абсолютный адрес. Также возможно вложенное индексирование, например alpha[beta[1]]
.
Подпрограммы и вызов
Подпрограммы - это блоки кода, которые можно вызывать из нескольких контекстов, предотвращая дублирование кода и позволяя создавать рекурсивные программы. Вот программа с рекурсивной подпрограммой для генерации чисел Фибоначчи (в основном самый медленный алгоритм):
# recursively calculate the 10th Fibonacci number
call display = fib(10).sum;
sub fib(cur,sum) {
if (cur <= 2) {
sum = 1;
return;
}
cur--;
call sum = fib(cur).sum;
cur--;
call sum += fib(cur).sum;
}
Подпрограмма объявляется с ключевым словом sub
, и подпрограмма может быть размещена в любом месте внутри программы. Каждая подпрограмма может иметь несколько локальных переменных, которые объявлены как часть списка аргументов. Этим аргументам также могут быть заданы значения по умолчанию.
Для обработки рекурсивных вызовов локальные переменные подпрограммы хранятся в стеке. Последняя статическая переменная в ОЗУ - это указатель стека вызовов, а вся память после этого служит стеком вызовов. Когда вызывается подпрограмма, она создает новый кадр в стеке вызовов, который включает все локальные переменные, а также адрес возврата (ПЗУ). Каждой подпрограмме в программе присваивается один статический адрес ОЗУ, который служит указателем. Этот указатель указывает местоположение «текущего» вызова подпрограммы в стеке вызовов. Ссылка на локальную переменную выполняется с использованием значения этого статического указателя плюс смещение, чтобы дать адрес этой конкретной локальной переменной. Также в стеке вызовов содержится предыдущее значение статического указателя. Вот'
RAM map:
0: pc
1: display
2: scratch0
3: fib
4: scratch1
5: scratch2
6: scratch3
7: call
fib map:
0: return
1: previous_call
2: cur
3: sum
Одной вещью, которая интересна в подпрограммах, является то, что они не возвращают никакого конкретного значения. Скорее, все локальные переменные подпрограммы могут быть прочитаны после выполнения подпрограммы, поэтому из вызова подпрограммы могут быть извлечены различные данные. Это достигается путем сохранения указателя для этого конкретного вызова подпрограммы, который затем может быть использован для восстановления любой из локальных переменных из (недавно освобожденного) стекового фрейма.
Существует несколько способов вызова подпрограммы, все из которых используют call
ключевое слово:
call fib(10); # subroutine is executed, no return vaue is stored
call pointer = fib(10); # execute subroutine and return a pointer
display = pointer.sum; # access a local variable and assign it to a global variable
call display = fib(10).sum; # immediately store a return value
call display += fib(10).sum; # other types of assignment operators can also be used with a return value
В качестве аргументов для вызова подпрограммы может быть указано любое количество значений. Любой не предоставленный аргумент будет заполнен значением по умолчанию, если оно есть. Аргумент, который не предоставлен и не имеет значения по умолчанию, не очищается (чтобы сохранить инструкции / время), поэтому потенциально может принимать любое значение в начале подпрограммы.
Указатели - это способ доступа к нескольким локальным переменным подпрограммы, хотя важно отметить, что указатель является только временным: данные, на которые указывает указатель, будут уничтожены при выполнении другого вызова подпрограммы.
Метки отладки
Любому {...}
блоку кода в программе Cogol может предшествовать описательная метка из нескольких слов. Эта метка прикрепляется как комментарий в скомпилированном коде сборки и может быть очень полезна для отладки, поскольку она облегчает поиск определенных фрагментов кода.
Оптимизация слотов задержки филиала
Чтобы повысить скорость скомпилированного кода, компилятор Cogol выполняет некоторую действительно базовую оптимизацию слота задержки в качестве заключительного прохода над кодом QFTASM. Для любого безусловного перехода с пустым интервалом задержки ветвления интервал задержки может быть заполнен первой инструкцией в месте назначения перехода, и место назначения перехода увеличивается на единицу, чтобы указывать на следующую инструкцию. Это обычно сохраняет один цикл каждый раз, когда выполняется безусловный переход.
Написание кода тетриса в Cogol
Финальная программа Tetris была написана на Cogol, а исходный код доступен здесь . Скомпилированный код QFTASM доступен здесь . Для удобства здесь приведена постоянная ссылка: Tetris в QFTASM . Поскольку цель состояла в том, чтобы ввести в действие код сборки (а не код Cogol), полученный код Cogol является громоздким. Многие части программы обычно находятся в подпрограммах, но эти подпрограммы на самом деле были достаточно короткими, чтобы дублирование кода сохраняло инструкции надcall
заявления. В конечном коде есть только одна подпрограмма в дополнение к основному коду. Кроме того, многие массивы были удалены и заменены либо эквивалентно длинным списком отдельных переменных, либо множеством жестко закодированных чисел в программе. Окончательный скомпилированный код QFTASM содержит менее 300 инструкций, хотя он лишь немного длиннее, чем сам исходный код Cogol.