Выполнил Соколов Анатолий Владимирович P3212
Вариант
lisp | acc | neum | hw | instr | binary | stream | port | pstr | prob2
Без усложнения
Язык lisp-подобный.
Любое выражение в скобках (s-expression) возвращает значение. Поддерживаются числовые и строковые литералы. Типизация динамическая, поддерживаются два типа: целые числа и строки.
Причем функции get_char, print_char работают именно с целыми числами.
Для while и if можно использовать целые числа, где 0 - это false, а любое другое число - true.
program = s_expression
s_expression = "(" atom ")" | expression | "("s_expression")"
atomic_symbol = identifier | string_literal | number
expression = defun_expr
| if_expr
| while_expr
| setq_exp
| print_char_exp
| print_string_exp
| user_defined_function_call_exp
| progn_exp
| import_expr
defun_expr = "(" "defun" identifier "(" identifiers ")" s_expression ")"
import_expr = "(" "import" *path-to-file ")
identifiers = identifier | identifier identifiers
if_expr = "(" "if" s_expression s_expression s_expression ")"
while_expr = "(" "while" s_expression s_expression ")"
setq_exp = "(" "setq" identifier s_expression ")"
print_char_exp = "(" "print_char" s_expression ")"
print_string_exp = "(" "print_string" s_expression ")"
user_defined_function_call_exp = "(" identifier s_expressions ")"
progn_exp = "(" "progn" s_expressions ")"
s_expressions = s_expression | s_expression s_expressions
identifier = idenitfier_symbol | identifier_symbol identifier
idenitfier_symbol = letter | "_"
string_literal = "\"" *any symbol* "\""defun- определение функции, возвращает 0. Рекурсивные функции не поддерживаются, так как не требовались.if- условный оператор, возвращает значение второго выражения, если первое не равно 0, иначе третье. Обязательно должно быть три выражения - условие, ветка иначе, ветка истины.while- цикл с предусловием, возвращает результат последнего выражения в теле цикла в последней итерации.setq- присваивание, возвращает значение присвоенной переменной.print_char- выводит символ с кодом, равным значению выражения, возвращает код символа.print_string- выводит строку, равную значению выражения, возвращает выведенную строку.read_char- вводит (читает) символ, возвращает его.progn- последовательное выполнение выражений, возвращает результат последнего выражения.- вызов функции - возвращает результат функции (последнего выражения в теле функции).
- литералы - возвращают сами себя.
- идентификаторы - возвращают значение переменной, к которой они привязаны. Использование идентификатора функции без ее вызова недопустимо.
Фон Неймановская архитектура.
Память представляет из себя четыре секции:
-
JMP_TO_CODE- единственная инструкция, которая по своей сути является адресной командой JMP, которая переходит на начало секцииCODE. -
STATIC_DATA- секция, в которой хранятся строки (строковые литералы, а также строки, введенные пользователем) Строки расположены в том порядке, в котором они встречаются в AST программы (в этом контексте объявление буффера для ввода пользователя - тоже строка) -
CODE- секция, в которой хранятся инструкции.
После вычисления любого выражения, его результат кладется в аккумулятор. При вычислении выражения с бинарным оператором, второй операнд вычисляется, кладется на стек, после чего вычисляется перывый и проводится операция над ними с помощью адресации относительно стека.
Функции хранятся в секции CODE в виде списка инструкций, которые выполняются последовательно. Перед
телом функции идет инструкция JMP, которая позоляет перепрыгнуть через ее тело.
Все переменные аллоцируются на стеке. Если при этом переменная была объявлена внутри функции, область ее видимости ограничивается телом функции.
Числовые литералы загружаются с помощью прямой адресации, считается, что они всегда помещаются в машинное слово.
Так как у процессора аккумуляторная архитектура, то в аккумуляторе всегда хранится лишь результат последнего вычисленного выражения, дополнительных регистров для хранения исключительно переменных нет.
- Машинное слово - 64 бита, знаковое.
- Так как архитектура аккумуляторная, все команды имеют всего один аргумент (или ни одного), а регистр общего назначения всего один.
- Ввод-вывод осущетсвляется как поток токенов, port-mapped.
- Поток управления:
- Поддерживаются условные и безусловные переходы.
- В случае, если инструкция не касается переходов, то после нее инкрементится IP (Instruction Pointer).
Поддерживаются 4 вида адресации:
- Прямая
- Относительно стека
- Косвенная (относительно значения по переданному адресу)
- Адресная
Также команда может быть прямой.
На выполнение каждой инструкции есть 4 цикла:
- Цикл выборки инструкции. (2 такта)
- Цикл выборки адреса (для адресации относительно стека и косвенной). (1 такт)
- Цикл выборки операнда (для всех видов адресации, кроме прямой). (1 такт)
- Цикл исполнения.
| Инструкция | addr/direct | Количество тактов в цикле исполнения | Описание |
|---|---|---|---|
LD |
addr | 1 | AC <- MEM(ADDR) |
ST |
addr | 1 | MEM(ADDR) <- AC |
ADD |
addr | 1 | AC <- AC + MEM(ADDR) |
SUB |
addr | 1 | AC <- AC - MEM(ADDR) |
MUL |
addr | 1 | AC <- AC * MEM(ADDR) |
DIV |
addr | 1 | AC <- AC / MEM(ADDR) |
MOD |
addr | 1 | AC <- AC % MEM(ADDR) |
EQ |
addr | 1 | if AC == MEM(ADDR) then AC <- 1 else AC <- 0 |
GT |
addr | 1 | if AC > MEM(ADDR) then AC <- 1 else AC <- 0 |
LT |
addr | 1 | if AC < MEM(ADDR) then AC <- 1 else AC <- 0 |
JZ |
addr | 1 | if AC == 0 then IP <- ADDR |
JNZ |
addr | 1 | if AC != 0 then IP <- ADDR |
JMP |
addr | 1 | IP <- ADDR |
PUSH |
direct | 2 | SP <- SP - 1; MEM(SP) <- AC |
POP |
direct | 1 | SP <- SP + 1 |
IN |
direct | 1 | AC <- next_token |
OUT |
direct | 1 | print AC |
CALL |
addr | 3 | SP <- SP - 1; MEM(SP) <- IP; IP <- ADDR |
RET |
direct | 2 | IP <- MEM(SP); SP <- SP + 1 |
HLT |
direct | 1 | завершение работы программы |
Транслятор состоит из двух частей:
- Лексер, реализован в tokenizer
- Модуль, преобразующий токены в программу, реализован в expression_translator
Также транслятор поддерживает базовый препроцессинг (поддерживается директива import, которая инлайнит весь код по указанному файлу на место самого выражения, содержащего директиву). Это позволяет, например, выделить функцию print_int в отдельную библиотеку здесь
На вход принимает два файла:
- Файл с программой на языке высокого уровня.
- Путь к файлу, в который будет записана программа в машинных словах (в виде binary файла)
Модель процессора реализована в machine
Реализован в классе DataPath
Элементы:
Z- Флаг zeroDR- Data RegisterIP- Instruction PointerSP- Stack PointerAC- AccumulatorALU- Arithmetic Logic Unit
Реализован в классе ControlUnit
Основная работа с данными происходит на уровне DataPath, а ControlUnit с помощью сигналов работает с этими данными. ControlUnit реализован как hardwired.
Для CI использовался пайплайн из примера, но модифицированный под гитхаб:
name: Python CI
on:
push:
branches:
- master
jobs:
computer-simulator:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: 3.12
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install poetry
poetry install
- name: Run tests and coverage
run: |
poetry run pytest --verbose
poetry run coverage run -m pytest
poetry run coverage report
- name: Run mypy checks
run: poetry run mypy .
- name: Check code formatting
run: poetry run ruff format --check .
- name: Run code linting
run: |
poetry run ruff check .В качестве линтеров используются ruff, mypy. Тесты с помощью pytest.
Реализованы unit тесты для лексера (test_tokenizer) Также реализованы golden тесты согласно примеру (test_golden):
Также реализованы некоторые дополнительные алгоритмы:
Возьмем программу (cat):
(progn
(read_char a)
(while (> a 0)
(progn
(print_char a)
(read_char a))))После трансляции она будет выглядеть вот так:
00000000 0c 00 00 00 00 00 02 00 0e 00 00 00 00 00 00 00 |................|
00000010 11 00 00 00 00 00 00 00 01 c0 00 00 00 00 00 00 |................|
00000020 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 00 00 |................|
00000030 00 c0 00 00 00 00 00 01 05 c0 00 00 00 00 00 00 |................|
00000040 0d 00 00 00 00 00 00 00 0a 40 00 00 00 00 02 0e |.........@......|
00000050 00 c0 00 00 00 00 00 00 12 00 00 00 00 00 00 00 |................|
00000060 11 00 00 00 00 00 00 00 01 c0 00 00 00 00 00 00 |................|
00000070 0c 40 00 00 00 00 02 03 13 00 00 00 00 00 00 00 |.@..............|
00000080
Если не умеете читать hexdump'ы: при вызове транслятора создается файл sys.argv[2].debug.txt
0000 - 0C0000000000000200 - JMP 512 (ADDRESS)
0200 - 0E0000000000000000 - PUSH
0201 - 110000000000000000 - IN
0202 - 010000000000000000 - ST 0 (STACK_OFFSET)
0203 - 000000000000000000 - LD 0 (DIRECT)
0204 - 0E0000000000000000 - PUSH
0205 - 000000000000000001 - LD 1 (STACK_OFFSET)
0206 - 050000000000000000 - GT 0 (STACK_OFFSET)
0207 - 0D0000000000000000 - POP
0208 - 0A000000000000020E - JZ 526 (ADDRESS)
0209 - 000000000000000000 - LD 0 (STACK_OFFSET)
020A - 120000000000000000 - OUT
020B - 110000000000000000 - IN
020C - 010000000000000000 - ST 0 (STACK_OFFSET)
020D - 0C0000000000000203 - JMP 515 (ADDRESS)
020E - 130000000000000000 - HLT
В начале происходит пропуск статической памяти, иницилизируется перменная a посредством пуша ее на стек.
Далее она считывается впервые и начинается цикл.
В начале цикла мы делаем JNZ на случай, если из ввода нам пришел 0, что будет означать, что в буффере не осталось
символов.
Далее с помощью вызовов OUT и IN мы выводим пришедший нам символ и считываем его снова.
В конце тела цикла мы делаем JMP в его начало, чтобы вновь свериться с условием продолжения.
Лог модели процессора (его начало) выглядит вот так:
DEBUG:root:INSTR_N: 0, TICK: 0, IP: 0, DR: 0, AR: 0, AC: 0, Z: True, INSTR: None, SP: 2048, Stack:
DEBUG:root:INSTR_N: 1, TICK: 3, IP: 512, DR: 512, AR: 0, AC: 0, Z: True, INSTR: Instr(JMP arg[512 (ADDRESS)] ), SP: 2048, Stack:
DEBUG:root:INSTR_N: 2, TICK: 6, IP: 513, DR: 512, AR: 0, AC: 0, Z: True, INSTR: Instr(PUSH), SP: 2047, Stack: 0
DEBUG:root:INSTR_N: 2, TICK: 7, IP: 513, DR: 512, AR: 0, AC: 0, Z: True, INSTR: Instr(PUSH), SP: 2047, Stack: 0
DEBUG:root:IN: 102 - "f"
DEBUG:root:INSTR_N: 3, TICK: 10, IP: 514, DR: 512, AR: 0, AC: 102, Z: True, INSTR: Instr(IN), SP: 2047, Stack: 0
DEBUG:root:INSTR_N: 4, TICK: 14, IP: 515, DR: 2047, AR: 0, AC: 102, Z: False, INSTR: Instr(ST arg[0 (STACK_OFFSET)] ), SP: 2047, Stack: 102
DEBUG:root:INSTR_N: 5, TICK: 17, IP: 516, DR: 0, AR: 0, AC: 0, Z: False, INSTR: Instr(LD arg[0 (DIRECT)] ), SP: 2047, Stack: 102
DEBUG:root:INSTR_N: 6, TICK: 20, IP: 517, DR: 0, AR: 0, AC: 0, Z: False, INSTR: Instr(PUSH), SP: 2046, Stack: 0 102
DEBUG:root:INSTR_N: 6, TICK: 21, IP: 517, DR: 0, AR: 0, AC: 0, Z: False, INSTR: Instr(PUSH), SP: 2046, Stack: 0 102
DEBUG:root:INSTR_N: 7, TICK: 26, IP: 518, DR: 102, AR: 0, AC: 102, Z: False, INSTR: Instr(LD arg[1 (STACK_OFFSET)] ), SP: 2046, Stack: 0 102
DEBUG:root:INSTR_N: 8, TICK: 31, IP: 519, DR: 0, AR: 0, AC: 1, Z: False, INSTR: Instr(GT arg[0 (STACK_OFFSET)] ), SP: 2046, Stack: 0 102
DEBUG:root:INSTR_N: 9, TICK: 34, IP: 520, DR: 0, AR: 0, AC: 1, Z: False, INSTR: Instr(POP), SP: 2047, Stack: 102
DEBUG:root:INSTR_N: 10, TICK: 37, IP: 521, DR: 526, AR: 0, AC: 1, Z: False, INSTR: Instr(JZ arg[526 (ADDRESS)] ), SP: 2047, Stack: 102
DEBUG:root:INSTR_N: 11, TICK: 42, IP: 522, DR: 102, AR: 0, AC: 102, Z: False, INSTR: Instr(LD arg[0 (STACK_OFFSET)] ), SP: 2047, Stack: 102
DEBUG:root:OUT: 102 - "f"
DEBUG:root:INSTR_N: 12, TICK: 45, IP: 523, DR: 102, AR: 0, AC: 102, Z: False, INSTR: Instr(OUT), SP: 2047, Stack: 102
DEBUG:root:IN: 111 - "o"
DEBUG:root:INSTR_N: 13, TICK: 48, IP: 524, DR: 102, AR: 0, AC: 111, Z: False, INSTR: Instr(IN), SP: 2047, Stack: 102
DEBUG:root:INSTR_N: 14, TICK: 52, IP: 525, DR: 2047, AR: 0, AC: 111, Z: False, INSTR: Instr(ST arg[0 (STACK_OFFSET)] ), SP: 2047, Stack: 111
DEBUG:root:INSTR_N: 15, TICK: 55, IP: 515, DR: 515, AR: 0, AC: 111, Z: False, INSTR: Instr(JMP arg[515 (ADDRESS)] ), SP: 2047, Stack: 111
DEBUG:root:INSTR_N: 16, TICK: 58, IP: 516, DR: 0, AR: 0, AC: 0, Z: False, INSTR: Instr(LD arg[0 (DIRECT)] ), SP: 2047, Stack: 111
DEBUG:root:INSTR_N: 17, TICK: 61, IP: 517, DR: 0, AR: 0, AC: 0, Z: False, INSTR: Instr(PUSH), SP: 2046, Stack: 0 111
DEBUG:root:INSTR_N: 17, TICK: 62, IP: 517, DR: 0, AR: 0, AC: 0, Z: False, INSTR: Instr(PUSH), SP: 2046, Stack: 0 111
DEBUG:root:INSTR_N: 18, TICK: 67, IP: 518, DR: 111, AR: 0, AC: 111, Z: False, INSTR: Instr(LD arg[1 (STACK_OFFSET)] ), SP: 2046, Stack: 0 111
DEBUG:root:INSTR_N: 19, TICK: 72, IP: 519, DR: 0, AR: 0, AC: 1, Z: False, INSTR: Instr(GT arg[0 (STACK_OFFSET)] ), SP: 2046, Stack: 0 111
DEBUG:root:INSTR_N: 20, TICK: 75, IP: 520, DR: 0, AR: 0, AC: 1, Z: False, INSTR: Instr(POP), SP: 2047, Stack: 111
DEBUG:root:INSTR_N: 21, TICK: 78, IP: 521, DR: 526, AR: 0, AC: 1, Z: False, INSTR: Instr(JZ arg[526 (ADDRESS)] ), SP: 2047, Stack: 111
DEBUG:root:INSTR_N: 22, TICK: 83, IP: 522, DR: 111, AR: 0, AC: 111, Z: False, INSTR: Instr(LD arg[0 (STACK_OFFSET)] ), SP: 2047, Stack: 111
DEBUG:root:OUT: 111 - "o"
DEBUG:root:INSTR_N: 23, TICK: 86, IP: 523, DR: 111, AR: 0, AC: 111, Z: False, INSTR: Instr(OUT), SP: 2047, Stack: 111
DEBUG:root:IN: 111 - "o"
DEBUG:root:INSTR_N: 24, TICK: 89, IP: 524, DR: 111, AR: 0, AC: 111, Z: False, INSTR: Instr(IN), SP: 2047, Stack: 111
DEBUG:root:INSTR_N: 25, TICK: 93, IP: 525, DR: 2047, AR: 0, AC: 111, Z: False, INSTR: Instr(ST arg[0 (STACK_OFFSET)] ), SP: 2047, Stack: 111
DEBUG:root:INSTR_N: 26, TICK: 96, IP: 515, DR: 515, AR: 0, AC: 111, Z: False, INSTR: Instr(JMP arg[515 (ADDRESS)] ), SP: 2047, Stack: 111
DEBUG:root:INSTR_N: 27, TICK: 99, IP: 516, DR: 0, AR: 0, AC: 0, Z: False, INSTR: Instr(LD arg[0 (DIRECT)] ), SP: 2047, Stack: 111
DEBUG:root:INSTR_N: 28, TICK: 102, IP: 517, DR: 0, AR: 0, AC: 0, Z: False, INSTR: Instr(PUSH), SP: 2046, Stack: 0 111
DEBUG:root:INSTR_N: 28, TICK: 103, IP: 517, DR: 0, AR: 0, AC: 0, Z: False, INSTR: Instr(PUSH), SP: 2046, Stack: 0 111
DEBUG:root:INSTR_N: 29, TICK: 108, IP: 518, DR: 111, AR: 0, AC: 111, Z: False, INSTR: Instr(LD arg[1 (STACK_OFFSET)] ), SP: 2046, Stack: 0 111
DEBUG:root:INSTR_N: 30, TICK: 113, IP: 519, DR: 0, AR: 0, AC: 1, Z: False, INSTR: Instr(GT arg[0 (STACK_OFFSET)] ), SP: 2046, Stack: 0 111
DEBUG:root:INSTR_N: 31, TICK: 116, IP: 520, DR: 0, AR: 0, AC: 1, Z: False, INSTR: Instr(POP), SP: 2047, Stack: 111
DEBUG:root:INSTR_N: 32, TICK: 119, IP: 521, DR: 526, AR: 0, AC: 1, Z: False, INSTR: Instr(JZ arg[526 (ADDRESS)] ), SP: 2047, Stack: 111
DEBUG:root:INSTR_N: 33, TICK: 124, IP: 522, DR: 111, AR: 0, AC: 111, Z: False, INSTR: Instr(LD arg[0 (STACK_OFFSET)] ), SP: 2047, Stack: 111
DEBUG:root:OUT: 111 - "o"
DEBUG:root:INSTR_N: 34, TICK: 127, IP: 523, DR: 111, AR: 0, AC: 111, Z: False, INSTR: Instr(OUT), SP: 2047, Stack: 111
...
Здесь показаны значения регистров, флага zero, раскодированной инструкции и стека (сверху вниз) в конце каждого из тактов.
| ФИО | алг. | LoC | code байт | code инстр. | инстр. | такт. | вариант |
|---|---|---|---|---|---|---|---|
| Соколов Анатолий Владимирович | hello | 3 | 328 | 41 | 96 | 368 | lisp | acc | neum | hw | instr | binary | stream | port | pstr | prob2 | - | |
| Соколов Анатолий Владимирович | cat | 6 | 128 | 16 | 44 | 163 | lisp | acc | neum | hw | instr | binary | stream | port | pstr | prob2 | - | |
| Соколов Анатолий Владимирович | prob2 | 19 | 1.3k | 157 | 1269 | 5058 | lisp | acc | neum | hw | instr | binary | stream | port | pstr | prob2 | - | |