Выполнил Русинов Дмитрий Станиславович P33302
Вариант
lisp | acc | neum | hw | tick | struct | stream | port | pstr | prob1 | 8bit
Без усложнения
Язык 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- выводит строку, равную значению выражения, возвращает выведенную строку.progn- последовательное выполнение выражений, возвращает результат последнего выражения.- вызов функции - возвращает результат функции (последнего выражения в теле функции).
- литералы - возвращают сами себя.
- идентификаторы - возвращают значение переменной, к которой они привязаны. Использование идентификатора функции без ее вызова недопустимо.
Фон Неймановская архитектура.
Память представляет из себя четыре секции:
+-----------------+
| JMP_TO_CODE |
+-----------------+
| STATIC_DATA |
| |
| |
| |
+-----------------+
| CODE |
| |
| |
| |
+-----------------+
| STACK |
| |
| |
+-----------------+
-
JMP_TO_CODE- единственная инструкция, которая по своей сути является адресной командой JMP, которая переходит на начало секцииCODE. -
STATIC_DATA- секция, в которой хранятся строки (строковые литералы, а также строки, введенные пользователем) Строки расположены в том порядке, в котором они встречаются в AST программы (в этом контексте объявление буффера для ввода пользователя - тоже строка) -
CODE- секция, в которой хранятся инструкции.
После вычисления любого выражения, его результат кладется в аккумулятор. При вычислении выражения с бинарным оператором, второй операнд вычисляется, кладется на стек, после чего вычисляется перывый и проводится операция над ними с помощью адресации относительно стека.
Функции хранятся в секции CODE в виде списка инструкций, которые выполняются последовательно. Перед
телом функции идет инструкция JMP, которая позоляет перепрыгнуть через ее тело.
Все переменные аллоцируются на стеке. Если при этом переменная была объявлена внутри функции, область ее видимости ограничивается телом функции.
Числовые литералы загружаются с помощью непосредственной адресации, считается, что они всегда помещаются в машинное слово.
Так как у процессора аккумуляторная архитектура, то в аккумуляторе всегда хранится лишь результат последнего вычисленного выражения, дополнительных регистров для хранения исключительно переменных нет.
- Машинное слово - 64 бита, знаковое.
- Так как архитектура аккумуляторная, все команды имеют всего один аргумент (или ни одного), а регистр общего назначения всего один.
- Ввод-вывод осущетсвляется как поток токенов, port-mapped.
- Поток управления:
- Поддерживаются условные и безусловные переходы.
- В случае, если инструкция не касается переходов, то после нее инкрементится IP (Instruction Pointer).
Поддерживаются 4 вида адресации:
- Непосредственная
- Относительно стека
- Косвенная (относительно значения по переданному адресу)
- Адресная
Также команда может быть безадресной.
На выполнение каждой инструкции есть 4 цикла:
- Цикл выборки инструкции. (2 такта)
- Цикл выборки адреса (для адресации относительно стека и косвенной). (1 такт)
- Цикл выборки операнда (для всех видов адресации, кроме непосредственной). (1 такт)
- Цикл исполнения.
| Инструкция | ард/безадр | Количество тактов в цикле исполнения | Описание |
|---|---|---|---|
LD |
ард | 1 | AC <- MEM(ARD) |
ST |
ард | 1 | MEM(ARD) <- AC |
ADD |
ард | 1 | AC <- AC + MEM(ARD) |
SUB |
ард | 1 | AC <- AC - MEM(ARD) |
MUL |
ард | 1 | AC <- AC * MEM(ARD) |
DIV |
ард | 1 | AC <- AC / MEM(ARD) |
MOD |
ард | 1 | AC <- AC % MEM(ARD) |
EQ |
ард | 1 | if AC == MEM(ARD) then AC <- 1 else AC <- 0 |
GT |
ард | 1 | if AC > MEM(ARD) then AC <- 1 else AC <- 0 |
LT |
ард | 1 | if AC < MEM(ARD) then AC <- 1 else AC <- 0 |
JZ |
ард | 1 | if AC == 0 then IP <- ARD |
JNZ |
ард | 1 | if AC != 0 then IP <- ARD |
JMP |
ард | 1 | IP <- ARD |
PUSH |
безадр | 2 | SP <- SP - 1; MEM(SP) <- AC |
POP |
безадр | 1 | SP <- SP + 1 |
IN |
безадр | 1 | AC <- next_token |
OUT |
безадр | 1 | print AC |
CALL |
ард | 3 | SP <- SP - 1; MEM(SP) <- IP; IP <- ARD |
RET |
безадр | 2 | IP <- MEM(SP); SP <- SP + 1 |
HLT |
безадр | 1 | завершение работы программы |
Транслятор состоит из двух частей:
- Лексер, реализован в tokenizer
- Модуль, преобразующий токены в программу, реализован в expression_translator
Также транслятор поддерживает базовый препроцессинг (поддерживается директива import, которая инлайнит весь код по указанному файлу на место самого выражения, содержащего директиву). Это позволяет, например, выделить функцию print_int в отдельную библиотеку здесь
На вход принимает два файла:
- Файл с программой на языке высокого уровня.
- Путь к файлу, в который будет записана программа в машинных словах (в виде JSON)
Модель процессора реализована в 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))))
После трансляции она будет выглядеть вот так:
{
"memory": [
{
"opcode": "JMP",
"address": 0,
"arg": {
"value": 512,
"type": "ADDRESS"
},
"comment": "Skip static memory"
},
{
"opcode": "PUSH",
"address": 512,
"comment": "Push var a"
},
{
"opcode": "IN",
"address": 513
},
{
"opcode": "ST",
"address": 514,
"arg": {
"value": 0,
"type": "STACK_OFFSET"
}
},
{
"opcode": "LD",
"address": 515,
"arg": {
"value": 0,
"type": "DIRECT"
}
},
{
"opcode": "PUSH",
"address": 516,
"comment": "Push var #binop result"
},
{
"opcode": "LD",
"address": 517,
"arg": {
"value": 1,
"type": "STACK_OFFSET"
}
},
{
"opcode": "GT",
"address": 518,
"arg": {
"value": 0,
"type": "STACK_OFFSET"
}
},
{
"opcode": "POP",
"address": 519
},
{
"opcode": "JZ",
"address": 520,
"arg": {
"value": 526,
"type": "ADDRESS"
}
},
{
"opcode": "LD",
"address": 521,
"arg": {
"value": 0,
"type": "STACK_OFFSET"
}
},
{
"opcode": "OUT",
"address": 522
},
{
"opcode": "IN",
"address": 523
},
{
"opcode": "ST",
"address": 524,
"arg": {
"value": 0,
"type": "STACK_OFFSET"
}
},
{
"opcode": "JMP",
"address": 525,
"arg": {
"value": 515,
"type": "ADDRESS"
}
},
{
"opcode": "HLT",
"address": 526
}
]
}В начале происходит пропуск статической памяти, иницилизируется перменная a посредством пуша ее на стек.
Далее она считывается впервые и начинается цикл.
В начале цикла мы делаем JNZ на случай, если из ввода нам пришел 0, что будет означать, что в буффере не осталось
символов.
Далее с помощью вызовов OUT и IN мы выводим пришедший нам символ и считываем его снова.
В конце тела цикла мы делаем JMP в его начало, чтобы вновь свериться с условием продолжения.
Лог модели процессора (его начало) выглядит вот так:
DEBUG machine:simulation TICK: 0, IP: 0, DR: 0, AR: 0, AC: 0, Z: True, INSTR: None, SP: 2048, Stack:
DEBUG machine:simulation TICK: 1, IP: 0, DR: 512, AR: 0, AC: 0, Z: True, INSTR: Instr(JMP arg[512 (ADDRESS)] (Skip static memory)), SP: 2048, Stack:
DEBUG machine:simulation TICK: 2, IP: 1, DR: 512, AR: 0, AC: 0, Z: True, INSTR: Instr(JMP arg[512 (ADDRESS)] (Skip static memory)), SP: 2048, Stack:
DEBUG machine:simulation TICK: 3, IP: 512, DR: 512, AR: 0, AC: 0, Z: True, INSTR: Instr(JMP arg[512 (ADDRESS)] (Skip static memory)), SP: 2048, Stack:
DEBUG machine:simulation TICK: 4, IP: 512, DR: 512, AR: 0, AC: 0, Z: True, INSTR: Instr(PUSH (Push var a)), SP: 2048, Stack:
DEBUG machine:simulation TICK: 5, IP: 513, DR: 512, AR: 0, AC: 0, Z: True, INSTR: Instr(PUSH (Push var a)), SP: 2048, Stack:
DEBUG machine:simulation TICK: 6, IP: 513, DR: 512, AR: 0, AC: 0, Z: True, INSTR: Instr(PUSH (Push var a)), SP: 2047, Stack: 0
DEBUG machine:simulation TICK: 7, IP: 513, DR: 512, AR: 0, AC: 0, Z: True, INSTR: Instr(PUSH (Push var a)), SP: 2047, Stack: 0
DEBUG machine:simulation TICK: 8, IP: 513, DR: 512, AR: 0, AC: 0, Z: True, INSTR: Instr(IN), SP: 2047, Stack: 0
DEBUG machine:simulation TICK: 9, IP: 514, DR: 512, AR: 0, AC: 0, Z: True, INSTR: Instr(IN), SP: 2047, Stack: 0
DEBUG hardwire:latch_ac IN: 102 - "f"
DEBUG machine:simulation TICK: 10, IP: 514, DR: 512, AR: 0, AC: 102, Z: True, INSTR: Instr(IN), SP: 2047, Stack: 0
DEBUG machine:simulation TICK: 11, IP: 514, DR: 0, AR: 0, AC: 102, Z: True, INSTR: Instr(ST arg[0 (STACK_OFFSET)] ), SP: 2047, Stack: 0
DEBUG machine:simulation TICK: 12, IP: 515, DR: 0, AR: 0, AC: 102, Z: True, INSTR: Instr(ST arg[0 (STACK_OFFSET)] ), SP: 2047, Stack: 0
DEBUG machine:simulation TICK: 13, IP: 515, DR: 2047, AR: 0, AC: 102, Z: False, INSTR: Instr(ST arg[0 (STACK_OFFSET)] ), SP: 2047, Stack: 0
DEBUG machine:simulation TICK: 14, IP: 515, DR: 2047, AR: 0, AC: 102, Z: False, INSTR: Instr(ST arg[0 (STACK_OFFSET)] ), SP: 2047, Stack: 102
DEBUG machine:simulation TICK: 15, IP: 515, DR: 0, AR: 0, AC: 102, Z: False, INSTR: Instr(LD arg[0 (DIRECT)] ), SP: 2047, Stack: 102
DEBUG machine:simulation TICK: 16, IP: 516, DR: 0, AR: 0, AC: 102, Z: False, INSTR: Instr(LD arg[0 (DIRECT)] ), SP: 2047, Stack: 102
DEBUG machine:simulation TICK: 17, IP: 516, DR: 0, AR: 0, AC: 0, Z: False, INSTR: Instr(LD arg[0 (DIRECT)] ), SP: 2047, Stack: 102
DEBUG machine:simulation TICK: 18, IP: 516, DR: 0, AR: 0, AC: 0, Z: False, INSTR: Instr(PUSH (Push var #binop result)), SP: 2047, Stack: 102
DEBUG machine:simulation TICK: 19, IP: 517, DR: 0, AR: 0, AC: 0, Z: False, INSTR: Instr(PUSH (Push var #binop result)), SP: 2047, Stack: 102
DEBUG machine:simulation TICK: 20, IP: 517, DR: 0, AR: 0, AC: 0, Z: False, INSTR: Instr(PUSH (Push var #binop result)), SP: 2046, Stack: 0 102
DEBUG machine:simulation TICK: 21, IP: 517, DR: 0, AR: 0, AC: 0, Z: False, INSTR: Instr(PUSH (Push var #binop result)), SP: 2046, Stack: 0 102
DEBUG machine:simulation TICK: 22, IP: 517, DR: 1, AR: 0, AC: 0, Z: False, INSTR: Instr(LD arg[1 (STACK_OFFSET)] ), SP: 2046, Stack: 0 102
DEBUG machine:simulation TICK: 23, IP: 518, DR: 1, AR: 0, AC: 0, Z: False, INSTR: Instr(LD arg[1 (STACK_OFFSET)] ), SP: 2046, Stack: 0 102
DEBUG machine:simulation TICK: 24, IP: 518, DR: 2047, AR: 0, AC: 0, Z: False, INSTR: Instr(LD arg[1 (STACK_OFFSET)] ), SP: 2046, Stack: 0 102
DEBUG machine:simulation TICK: 25, IP: 518, DR: 102, AR: 0, AC: 0, Z: False, INSTR: Instr(LD arg[1 (STACK_OFFSET)] ), SP: 2046, Stack: 0 102
DEBUG machine:simulation 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 machine:simulation TICK: 27, IP: 518, DR: 0, AR: 0, AC: 102, Z: False, INSTR: Instr(GT arg[0 (STACK_OFFSET)] ), SP: 2046, Stack: 0 102
DEBUG machine:simulation TICK: 28, IP: 519, DR: 0, AR: 0, AC: 102, Z: False, INSTR: Instr(GT arg[0 (STACK_OFFSET)] ), SP: 2046, Stack: 0 102
DEBUG machine:simulation TICK: 29, IP: 519, DR: 2046, AR: 0, AC: 102, Z: False, INSTR: Instr(GT arg[0 (STACK_OFFSET)] ), SP: 2046, Stack: 0 102
DEBUG machine:simulation TICK: 30, IP: 519, DR: 0, AR: 0, AC: 102, Z: False, INSTR: Instr(GT arg[0 (STACK_OFFSET)] ), SP: 2046, Stack: 0 102
DEBUG machine:simulation 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 machine:simulation TICK: 32, IP: 519, DR: 0, AR: 0, AC: 1, Z: False, INSTR: Instr(POP), SP: 2046, Stack: 0 102
DEBUG machine:simulation TICK: 33, IP: 520, DR: 0, AR: 0, AC: 1, Z: False, INSTR: Instr(POP), SP: 2046, Stack: 0 102
DEBUG machine:simulation TICK: 34, IP: 520, DR: 0, AR: 0, AC: 1, Z: False, INSTR: Instr(POP), SP: 2047, Stack: 102
DEBUG machine:simulation TICK: 35, IP: 520, DR: 526, AR: 0, AC: 1, Z: False, INSTR: Instr(JZ arg[526 (ADDRESS)] ), SP: 2047, Stack: 102
DEBUG machine:simulation TICK: 36, IP: 521, DR: 526, AR: 0, AC: 1, Z: False, INSTR: Instr(JZ arg[526 (ADDRESS)] ), SP: 2047, Stack: 102
DEBUG machine:simulation TICK: 37, IP: 521, DR: 526, AR: 0, AC: 1, Z: False, INSTR: Instr(JZ arg[526 (ADDRESS)] ), SP: 2047, Stack: 102
DEBUG machine:simulation TICK: 38, IP: 521, DR: 0, AR: 0, AC: 1, Z: False, INSTR: Instr(LD arg[0 (STACK_OFFSET)] ), SP: 2047, Stack: 102
DEBUG machine:simulation TICK: 39, IP: 522, DR: 0, AR: 0, AC: 1, Z: False, INSTR: Instr(LD arg[0 (STACK_OFFSET)] ), SP: 2047, Stack: 102
DEBUG machine:simulation TICK: 40, IP: 522, DR: 2047, AR: 0, AC: 1, Z: False, INSTR: Instr(LD arg[0 (STACK_OFFSET)] ), SP: 2047, Stack: 102
DEBUG machine:simulation TICK: 41, IP: 522, DR: 102, AR: 0, AC: 1, Z: False, INSTR: Instr(LD arg[0 (STACK_OFFSET)] ), SP: 2047, Stack: 102
DEBUG machine:simulation TICK: 42, IP: 522, DR: 102, AR: 0, AC: 102, Z: False, INSTR: Instr(LD arg[0 (STACK_OFFSET)] ), SP: 2047, Stack: 102
DEBUG machine:simulation TICK: 43, IP: 522, DR: 102, AR: 0, AC: 102, Z: False, INSTR: Instr(OUT), SP: 2047, Stack: 102
DEBUG machine:simulation TICK: 44, IP: 523, DR: 102, AR: 0, AC: 102, Z: False, INSTR: Instr(OUT), SP: 2047, Stack: 102
Здесь показаны значения регистров, флага zero, раскодированной инструкции и стека (сверху вниз) в конце каждого из тактов.
| Русинов Дмитрий Станиславович | hello | 3 | - | 41 | 96 | 368 | lisp | acc | neum | hw | tick | struct | stream | port | pstr | prob1 | 8bit |
| Русинов Дмитрий Станиславович | cat | 6 | - | 16 | 44 | 163 | lisp | acc | neum | hw | tick | struct | stream | port | pstr | prob1 | 8bit |
| Русинов Дмитрий Станиславович | prob1 | 12 | - | 164 | 39251 | 151928 | lisp | acc | neum | hw | tick | struct | stream | port | pstr | prob1 | 8bit |

