- Кобелев Роман, P3212
forth | stack | harv | mc -> hw | tick -> instr | struct | stream | port | pstr | prob2
- Базовый вариант
По варианту нужно реализовать FORTH-подобный язык
<program> ::= <definition>*
<definition> ::= <word-definition> | <variable-definition> |<variable-allocation-definition> | <word>
<word-definition> ::= ":" <identifier> <body> ";"
<body> ::= <word>*
<word> ::= <number> | <operand> | <word-name> | <conditional> | <loop> | <comment>
<number> ::= <digit>+
<operand> ::= "+"
| "-"
| "*"
| "/"
| "read"
| "emit"
| "@"
| "!"
| "mod"
| "<"
| ">"
| "="
| "swap"
| "over"
| "dup"
| "drop"
<variable-definition> ::= "VARIABLE" <word-name>
<variable-allocation-definition> ::= "VARIABLE" <word-name> <number> "ALLOCATE"
<conditional> ::= "IF" <body> "THEN" | "IF" <body> "ELSE" <body> "THEN"
<loop> ::= "do" <body> "loop" | "begin" <body> "until"
<identifier> ::= <letter> (<letter> | <digit>)*
<comment> ::= "\" <any symbol except "\n">
nop
- ничего не делает (нигде не используется)
+
- (n1 n2 -- n3)
-
- (n1 n2 -- n3)
*
- (n1 n2 -- n3)
/
- (n1 n2 -- n3)
mod
- (n1 n2 -- n3)
=
- (n1 n2 -- n3) n3 = -1 if n1 == n2 else 0
<
- (n1 n2 -- n3) n3 = -1 if n1 < n2 else 0
>
- (n1 n2 -- n3) n3 = -1 if n1 > n2 else 0
swap
- (n1 n2 -- n2 n1)
drop
- (n1 n2 -- n1)
over
- (n1 n2 -- n1 n2 n1)
read
- (n1 -- n2) - прочитать символ из порта n1
и положить на стек
emit
- (n1 n2 -- ) - вывести ASCII символ с кодом n1
в порт n2
!
- (n1 n2 -- ) - записывает в память по адресу n1
занчение n2
@
- (n1 -- n2) - загружает из памяти значение по адресу n1
Условный оператор if
берёт значение из стек и выполняет код только, если на стеке 0
Оператор цикла do loop
, напоминающий конструкцию for
в большинтсве языков на основе C
. специальное слово i
помещает текущий индекс цикла в стек. Два верхних значения в стеке дают начальное значение (включительно) и конечное значение (исключая) для значения i
. Начальное значение берется с вершины стека.
Оператор цикла begin until
выполняет цикл до того момента, пока в конце итерации в начале стека не окажется 0
Оператор объявления переменной variable
создаёт переменную и сопоставляет ей определённую ячейку памяти. Взаимодействовать с переменной можно, обюратившись к ней по имени переменной и операторов !
и @
. Также с помощью ключевого слова allocate
можно выделить произвольное количество памяти, где обратившись по начальному адресу найдём длину выделенного участка.
Процедура должна задаваться отдельно, начиная с :
, дальше идёт название процедуры, а конец процедуры обозначается ;
. Вызов процедуры происходит, написав название процедуры.
- Память раздельная
- Размер машинного слова - 32 бита.
- Размер машинного слова команд -- 40 бит, первые 8 бит -- тип команды (размер условен, кодирование в виде json)
- Машинная команда может быть с аргументом и без
- Для реализации подпрограмм присутствует стек возврата, как отдельное физическое устройство, в котором хранятся адреса возврата
- Управление процессором осуществляется с помощью микрокода
Instruction memory
+-----------------------------+
| 00 program |
| 01 program |
| ... |
| 15000 program |
+-----------------------------+
Memory
+-----------------------------+
| 00 variables |
| 01 variables |
| ... |
| 10 size of allocation |
| 11 1 object of alloc |
| ... |
+-----------------------------+
Data stack
+-----------------------------+
| 00 value |
| 01 value |
| ... |
| 10 NEXT | +--+
| 11 TOP | <-- |SP|
| ... | +--+
+-----------------------------+
Microprogram memory
+------------------------------+
| 00 Signals |
| 01 Signals |
| ... |
+------------------------------+
В памяти данных хранятся 2 вида переменных:
- Единичные переменные
- Переменные, созданные с помощью ключевого слова
ALLOCATE
. Выделяетсяn+1
ячейка, где в первой хранится размер, а в последующих сами данные
MUX
- управляющиё сигнал на мультиплексореTOP
- входящее значение на стекSP
- выбор значенияSP
RSP
- выбор значенияRSP
RS
- выбор значенияRS
PC
- выбор значенияPC
JMP_TYPE
- тип выбораPC
MPC
- выбор значенияMPC
ALU
- сигнал выбора операции на АЛУLatch
- защёлкнуть регистрTOP
- Top of StackNEXT
- Next to TopPC
- Program CounterMPC
- Micro Program CounterRS
- Top of Return StackIR
- Instruction RegisterSP
- Stack PointerRSP
- Return Stack Pointer
MEMORY
- сигнал на управление памятью данныхWR
- записать значение переменной в памятьRD
- получить значение переменной из памяти
IO
- сигналы работы с портамиIN
- получить значение из нужного портаOUT
- отдать значение по нужному порту
Все сигналы хранится в модуле mc.py
- Адресация - абсолютная.
- Доступ к памяти данных осуществляется по адресу, который хранится в регистре
NEXT
, к памяти инструкций по адресу, который лежит в регистреPC
- Ввод/Вывод происходит с данными, которые лежат в
TOP
иNEXT
- Порты реализованы с помощью буфера чисел
- Поток управления
- Автоинкремент
PC
- Безусловный переход -
JMP
ZJMP
CALL
RET
- Условный переход -
JMP
, если вTOP
не ноль HALT
- остановка программы
- Автоинкремент
- Выборка инструкции (Инкрементирование
PC
, загрузка команды вIR
) - Исполнение команды (Ниже в таблицы представлено все выполняемы команды)
В таблице ниже
приведены все элементы системы команд (isa.py:Opcode). Большинство инструкций соответствуют слову из языка программирования (см. описание в разделе Язык программирования
)
Инструкция | Кол-во тактов | Описание |
---|---|---|
ADD |
1 | NEXT = TOP + NEXT, SP-- |
MUL |
1 | NEXT = TOP * NEXT, SP-- |
DIV |
1 | NEXT = NEXT / TOP, SP-- |
SUB |
1 | NEXT = NEXT - TOP, SP-- |
MOD |
1 | NEXT = NEXT % TOP, SP-- |
HALT |
1 | Остановка программы |
JUMP [addr] |
1 | PC = IM[PC] |
ZJUMP [addr] |
1 | IF Z PC = IM[PC] |
CALL [addr] |
2 | RSP++, RS[RSP] = PC, PC = IM[PC] |
RET |
1 | PC = RS[RSP], RSP-- |
NOP |
1 | Пустышка |
DROP |
1 | SP-- |
GR |
1 | if NEXT > TOP NEXT = -1 else NEXT = 0, SP-- |
LS |
1 | if NEXT < TOP NEXT = -1 else NEXT = 0, SP-- |
EQ |
1 | if NEXT == TOP NEXT = -1 else NEXT = 0, SP-- |
DUP |
2 | RSP++, RS[RSP] = TOP, SP++, TOP = RS[RSP], RSP-- |
OVER |
3 | SP--, RSP++, RS[RSP] = TOP, SP = SP+2, TOP = RS[RSP], RSP-- |
SWAP |
4 | RSP++, RS[RSP] = TOP, SP--, RSP++, RS[RSP] = TOP, SP++, TOP = RS[RSP], RSP--, NEXT = RS[RSP], RSP-- |
POP |
1 | RSP++, RS[RSP] = TOP, SP-- |
RPOP |
1 | SP++, TOP = RS[RSP], RSP-- |
PUSH [value] |
2 | RSP++, TOP = IM[PC] |
POP |
1 | RSP++, RS[RSP] = TOP, SP-- |
READ |
1 | TOP = INPUT |
EMIT |
2 | RSP++, RS[RSP] = TOP, SP--, OUTPUT = TOP, SP--, RSP-- |
LOAD |
1 | TOP = MEM[TOP] |
STORE |
1 | MEM[NEXT] = TOP, SP = SP - 2 |
Память микрокоманд хранится в модуле mc.py
Инструкции и память кодируются в формат JSON и имеют вид:
{
"memory": {
"0": 16,
},
"code": [
{
"index": 0,
"command": "push",
"arg": 8
}
]
}
В блоке memory
храниться, которая будет инициализироваться в начале выполнения. В блоке code
аргумент может быть упущен
Интерфейс командной строки translator.py <input_file> <output_file>
Основные существенные элементы, используемые в процессе трансляции:
- слово - исходное слово в тексте программы (разделён пробелами)
- терм (isa.py:Term) -- слово программы, преобразованное в формат машинной команды
- машинное слово (isa.py:Instruction) -- сама машинная команда
Трансляция происходит в несколько этапов (translator.py (translator.py:translate)):
- Разбиение исходного в текста программы в набор термов
- Валидация полученных термов (переменные, циклы, процедуры, условные операторы)
- Трансляция термов в машинний код
- Линковка
Так как исходный код подаётся нам, как список слов (разделённые пробелами и переносами строки), процесс трансляции слов в токены не нуждается в объяснении (translator.py:split_to_terms)
Пользовательская программа должа удовлетворять формальным условиям (см. описание в разделе Язык программирования
)
- терму
:
должен соответствовать терм;
- терму
do
должен соответствовать термloop
- терму
begin
должен соответствовать термuntil
- терму
if
должен соответствовать термthen
, между ними может бытьelse
- объявление переменной должно иметь вид
variable <name> [allocate <int>]
Таким образом проводиться проверка, пользователь при ошибке будет оповещён с термом и номером слова в тексте
Реализована (translator.py:code_correctness_check)
В нашем случае каждому терму могут быть сопоставлены несколько инструкций, поэтому важно правильно заменить все термы на инструкции. Происходит следующая трансформация:
list[Term] -> list[list[Instruction]] -> list[Instruction]
Хотел бы заострить внимание на трансляции таких термов, как loop
и i
. Они выглядят больше других из-за работы со стеком возврата, где мы храним параметры инициализации цикла.
Правила трансляции определены в функции translator.py:term_to_instruction
Такие термы, как if
, loop
, until
, :
нуждаются в дополняющем терме, к которому будут обращаться. Такая операция также происходит с variable
.
Для преобразования данной структуры в последовательный список необходимо провести линковку. Для этого используется абослютная адресация, которая указывапет на номер конкретного терма.
Реализуется функцией translator.py:fix_addresses
Интерфейс командной строки: machine.py <machine_code_file> <input_file>
DataPath реализован в классе datapath.py:DataPath.
Особенности архитектуры:
Memory
- двунаправленная память данныхPORT
- Устройство, которое хранит в себе В/У ввода/выоводаReturn Stack
- стек возвратаData Stack
- стек данных. Схема с более детальным описанием модуляTOS
:
Выполнение микроинструкции происходит в методе control_unit.py:dispatch_micro_instruction
Control Unit реализован в классе control_unit.py:ControlUnit
Особенности работы модели:
- Цикл осуществляется в функции machine.py:simulation. там выполняется декодирование и выполнение инструкций
- Шаг моделирование соответствует одному тику с выводом состояния в журнал
- Для журнала используется стандартный модуль
logging
- Количество тиков для моделирования лимитировано
- Остановка моделирования возможна при:
- превышении лимита тиков
- исключении
StopIteration
- если выполнена инструкцияHALT
- Тестирование осуществляется при помощи golden test-ов
- Настройка golden тестирования находится в golden_test.py
- Конфигурация golden test-ов лежит в директории golden
Запустить тесты - poetry run pytest . -v
Обновить конфигурацию golden test-ов - poetry run pytest . -v --update-goldens
CI при помощи Github Actions:
name: Check Python
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.12
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
pip3 install poetry
poetry install
- name: Run tests and collect coverage
run: |
poetry run coverage run -m pytest .
poetry run coverage report -m
env:
CI: true
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.12
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
pip3 install poetry
poetry install
- name: Check code formatting with Ruff
run: poetry run ruff format --check .
- name: Run Ruff linters
run: poetry run ruff check .
где:
poetry
- управление зависимостями для языка Pyuthoncoverage
- формирование отчёта об уровне покрытия исходного кодаpytest
- утилита для запуска тестовruff
- утилита для форматирования и проверки стиля кодирования
Пример использования и журнал работы процессора на примере loop
:
py translator.py forth/loop.fth machine/loop.json
source LoC: 1 code instr: 25
py machine.py machine/loop.json
DEBUG:root:[-1: NO_OPCODE] TICK: 0 MPC: 0 IR: None TOP: 0 NEXT: 0 RS: 0 SP: -1 RSP: -1
DEBUG:root:[0: NO_OPCODE] TICK: 1 MPC: 1 IR: None TOP: 0 NEXT: 0 RS: 0 SP: -1 RSP: -1
DEBUG:root:[0: push] TICK: 2 MPC: 2 IR: push const 8 TOP: 0 NEXT: 0 RS: 0 SP: -1 RSP: -1
DEBUG:root:[0: push] TICK: 3 MPC: 22 IR: push const 8 TOP: 0 NEXT: 0 RS: 0 SP: -1 RSP: -1
DEBUG:root:[0: push] TICK: 4 MPC: 23 IR: push const 8 TOP: 0 NEXT: 0 RS: 0 SP: 0 RSP: -1
DEBUG:root:[0: push] TICK: 5 MPC: 0 IR: push const 8 TOP: 8 NEXT: 0 RS: 0 SP: 0 RSP: -1
DEBUG:root:[1: push] TICK: 6 MPC: 1 IR: push const 8 TOP: 8 NEXT: 0 RS: 0 SP: 0 RSP: -1
DEBUG:root:[1: push] TICK: 7 MPC: 2 IR: push const 1 TOP: 8 NEXT: 0 RS: 0 SP: 0 RSP: -1
DEBUG:root:[1: push] TICK: 8 MPC: 22 IR: push const 1 TOP: 8 NEXT: 0 RS: 0 SP: 0 RSP: -1
...
...
...
DEBUG:root:[23: drop] TICK: 676 MPC: 0 IR: drop None TOP: 0 NEXT: 0 RS: 0 SP: -1 RSP: -1
DEBUG:root:[24: drop] TICK: 677 MPC: 1 IR: drop None TOP: 0 NEXT: 0 RS: 0 SP: -1 RSP: -1
DEBUG:root:[24: halt] TICK: 678 MPC: 2 IR: halt None TOP: 0 NEXT: 0 RS: 0 SP: -1 RSP: -1
DEBUG:root:[24: halt] TICK: 679 MPC: 4 IR: halt None TOP: 0 NEXT: 0 RS: 0 SP: -1 RSP: -1
DEBUG:root:OUTPUT: 1234567
instr_counter: 145 ticks: 679
Пример проверки исходного кода:
py -m poetry run pytest . -v
============================================================================================ test session starts =============================================================================================
platform win32 -- Python 3.12.0, pytest-7.4.4, pluggy-1.5.0 -- C:\Users\ronya\AppData\Local\pypoetry\Cache\virtualenvs\forth-model-Opn_VMTx-py3.12\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\ronya\forth_model
configfile: pyproject.toml
plugins: golden-0.2.2
collected 5 items
golden_test.py::test_translator_and_machine[golden/cat.yml] PASSED [ 20%]
golden_test.py::test_translator_and_machine[golden/hello.yml] PASSED [ 40%]
golden_test.py::test_translator_and_machine[golden/hello_user_name.yml] PASSED [ 60%]
golden_test.py::test_translator_and_machine[golden/loop.yml] PASSED [ 80%]
golden_test.py::test_translator_and_machine[golden/prob2.yml] PASSED [100%]
============================================================================================= 5 passed in 1.87s ==============================================================================================
| ФИО | алг | LoC | code инстр. | такт. |
| Кобелев Роман Павлович | loop | 1 | 25 | 679 |
| Кобелев Роман Павлович | cat | 8 | 16 | 1527 |
| Кобелев Роман Павлович | hello | 31 | 91 | 1379 |
| Кобелев Роман Павлович | hello_user_name | 60 | 164 | 2792 |
| Кобелев Роман Павлович | prob2 | 32 | 55 | 5135 |