Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolves #6 implement asm translator with documentation #7

Merged
merged 7 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ It includes:

`brainfuck | bf | harv | hw | instr | struct | stream | port | - | - | -`

Also includes Asm translator: [./python/README_asm.md](./python/README_asm.md)

1. [./ocaml/](./ocaml/) -- processor model implemented in OCaml language in functional style.

- machine cli: [./ocaml/machine_cli.ml](./ocaml/machine_cli.ml)
Expand Down
4 changes: 4 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ comment ::= <any symbols except: "><+-.,[]">

Память выделяется статически, при запуске модели. Видимость данных -- глобальная. Поддержка литералов -- отсутствует.

## Язык программирования Asm

Альтернативный вариант, реализующий язык программирования Asm вместо Brainfuck см. в файле [./README_asm.md](./README_asm.md).

## Организация памяти

Модель памяти процессора (приведено списком, так как тривиальна):
Expand Down
77 changes: 77 additions & 0 deletions python/README_asm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Язык программирования Asm

Синтаксис в расширенной БНФ.

- `[ ... ]` -- вхождение 0 или 1 раз
- `{ ... }` -- повторение 0 или несколько раз
- `{ ... }-` -- повторение 1 или несколько раз

``` ebnf
program ::= { line }

line ::= label [ comment ] "\n"
| instr [ comment ] "\n"
| [ comment ] "\n"

label ::= label_name ":"

instr ::= op0
| op1 label_name

op0 ::= "increment"
| "decrement"
| "right"
| "left"
| "print"
| "input"

op1 ::= "jmp"
| "jz"

integer ::= [ "-" ] { <any of "0-9"> }-

label_name ::= <any of "a-z A-Z _"> { <any of "a-z A-Z 0-9 _"> }

comment ::= ";" <any symbols except "\n">
```

Поддерживаются однострочные комментарии, начинающиеся с `;`.

Операции:

- `increment` -- увеличить значение в текущей ячейке на 1
- `decrement` -- уменьшить значение в текущей ячейке на 1
- `right` -- перейти к следующей ячейке
- `left` -- перейти к предыдущей ячейке
- `print` -- напечатать значение из текущей ячейки (символ)
- `input` -- ввести извне значение и сохранить в текущей ячейке (символ)
- `jmp addr` -- безусловный переход по заданному адресу или метке
- `jz addr` -- условный переход по заданному адресу или метке, если значение текущей ячейки ноль

Перед операцией можно определить метку:

``` asm
label: increment
```

И в другом месте (неважно, до или после определения) сослаться на эту метку:

``` asm
jmp label ; --> `jmp 123`, где 123 - номер инструкции после объявления метки
```

Транслятор поставит на место использования метки адрес той инструкции, перед которой она определена.

В программе не может быть дублирующихся меток, определенных в разных местах с одним именем.

Пример кода на Asm см. в файле [./examples/cat.asm](./examples/cat.asm).

## Транслятор

Интерфейс командной строки: `translator_asm.py <input_file> <target_file>`

Реализован в модуле [translator_asm](./translator_asm.py)

Ассемблерные мнемоники один в один транслируются в машинные команды.

Метки из программы исчезают, а на место обращений к ним подставляются конкретные адреса.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need golden tests for the translator.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two paragraphs?

11 changes: 11 additions & 0 deletions python/examples/cat.asm
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
; Данный пример генерирует идентичный машинный код, что и программа на brainfuck:
;
; ,[.,]
;
; Каждый символ brainfuck соответствует одной инструкции на Asm.
input ; ,
loop: jz break ; [
print ; .
input ; ,
jmp loop ; ]
break: halt ; конец файла
48 changes: 48 additions & 0 deletions python/golden/cat_asm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
lang: asm
in_source: |-
; Данный пример генерирует идентичный машинный код, что и программа на brainfuck:
;
; ,[.,]
;
; Каждый символ brainfuck соответствует одной инструкции на Asm.
input ; ,
loop: jz break ; [
print ; .
input ; ,
jmp loop ; ]
break: halt ; конец файла
in_stdin: |-
foo
out_code: |-
[{"index": 0, "opcode": "input", "term": [6, 9, "input"]},
{"index": 1, "opcode": "jz", "arg": 5, "term": [7, 9, "jz break"]},
{"index": 2, "opcode": "print", "term": [8, 9, "print"]},
{"index": 3, "opcode": "input", "term": [9, 9, "input"]},
{"index": 4, "opcode": "jmp", "arg": 1, "term": [10, 9, "jmp loop"]},
{"index": 5, "opcode": "halt", "term": [11, 9, "halt"]}]
out_stdout: |
source LoC: 11 code instr: 6
============================================================
foo
instr_counter: 11 ticks: 21
out_log: |
DEBUG machine:simulation TICK: 0 PC: 0 ADDR: 0 MEM_OUT: 0 ACC: 0 input ('input'@6:9)
DEBUG machine:signal_wr input: 'f'
DEBUG machine:simulation TICK: 2 PC: 1 ADDR: 0 MEM_OUT: 102 ACC: 0 jz 5 ('jz break'@7:9)
DEBUG machine:simulation TICK: 4 PC: 2 ADDR: 0 MEM_OUT: 102 ACC: 102 print ('print'@8:9)
DEBUG machine:signal_output output: '' << 'f'
DEBUG machine:simulation TICK: 6 PC: 3 ADDR: 0 MEM_OUT: 102 ACC: 102 input ('input'@9:9)
DEBUG machine:signal_wr input: 'o'
DEBUG machine:simulation TICK: 8 PC: 4 ADDR: 0 MEM_OUT: 111 ACC: 102 jmp 1 ('jmp loop'@10:9)
DEBUG machine:simulation TICK: 9 PC: 1 ADDR: 0 MEM_OUT: 111 ACC: 102 jz 5 ('jz break'@7:9)
DEBUG machine:simulation TICK: 11 PC: 2 ADDR: 0 MEM_OUT: 111 ACC: 111 print ('print'@8:9)
DEBUG machine:signal_output output: 'f' << 'o'
DEBUG machine:simulation TICK: 13 PC: 3 ADDR: 0 MEM_OUT: 111 ACC: 111 input ('input'@9:9)
DEBUG machine:signal_wr input: 'o'
DEBUG machine:simulation TICK: 15 PC: 4 ADDR: 0 MEM_OUT: 111 ACC: 111 jmp 1 ('jmp loop'@10:9)
DEBUG machine:simulation TICK: 16 PC: 1 ADDR: 0 MEM_OUT: 111 ACC: 111 jz 5 ('jz break'@7:9)
DEBUG machine:simulation TICK: 18 PC: 2 ADDR: 0 MEM_OUT: 111 ACC: 111 print ('print'@8:9)
DEBUG machine:signal_output output: 'fo' << 'o'
DEBUG machine:simulation TICK: 20 PC: 3 ADDR: 0 MEM_OUT: 111 ACC: 111 input ('input'@9:9)
WARNING machine:simulation Input buffer is empty!
INFO machine:simulation output_buffer: 'foo'
13 changes: 11 additions & 2 deletions python/integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import machine
import pytest
import translator
import translator_asm


@pytest.mark.golden_test("golden/*.yml")
Expand All @@ -48,6 +49,7 @@ def test_translator_and_machine(golden, caplog):

- `in_source` -- исходный код
- `in_stdin` -- данные на ввод процессора для симуляции
- `lang` -- язык: "bf" (по умолчанию) или "asm"

Выход:

Expand All @@ -60,8 +62,12 @@ def test_translator_and_machine(golden, caplog):

# Создаём временную папку для тестирования приложения.
with tempfile.TemporaryDirectory() as tmpdirname:
# Определяем язык golden теста. По умолчанию предполагается язык Brainfuck
lang = golden.get("lang", "bf")
assert lang == "bf" or lang == "asm"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be a little bit more simple. Just pass: in_source xor in_source_asm.

If both or none -- just fail the test.

Copy link
Contributor Author

@k1b24 k1b24 Feb 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this code will be a little bigger, if I get you right

lang = None
source = None
if "in_source_asm" in golden:
  lang = "asm"
  source = golden["in_source_asm"]
if "in_source" in golden:
  assert lang is None, "both in_source and in_source_asm are present"
  lang = "bf"
  source = golden["in_source"]
assert lang is not None, "either in_source xor in_source_asm must be set"

And in this case we also need to add some logic about referencing the in_source param on this line below:
file.write(golden["in_source"])

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just an idea

Comment on lines +66 to +67
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
lang = golden.get("lang", "bf")
assert lang == "bf" or lang == "asm"
assert "in_source" in golden != "in_source_asm" in golden


# Готовим имена файлов для входных и выходных данных.
source = os.path.join(tmpdirname, "source.bf")
source = os.path.join(tmpdirname, "source." + lang)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
source = os.path.join(tmpdirname, "source." + lang)
source = os.path.join(tmpdirname, "source")

No body see this files...

input_stream = os.path.join(tmpdirname, "input.txt")
target = os.path.join(tmpdirname, "target.o")

Expand All @@ -74,7 +80,10 @@ def test_translator_and_machine(golden, caplog):
# Запускаем транслятор и собираем весь стандартный вывод в переменную
# stdout
with contextlib.redirect_stdout(io.StringIO()) as stdout:
translator.main(source, target)
if lang == "bf":
translator.main(source, target)
elif lang == "asm":
translator_asm.main(source, target)
Comment on lines +83 to +86
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if lang == "bf":
translator.main(source, target)
elif lang == "asm":
translator_asm.main(source, target)
if "in_source" in golden:
with open(source, "w", encoding="utf-8") as file:
file.write(golden["in_source"])
translator.main(source, target)
elif ...

print("============================================================")
machine.main(target, input_stream)

Expand Down
122 changes: 122 additions & 0 deletions python/translator_asm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/usr/bin/python3
"""Транслятор Asm в машинный код.
"""

import re
import sys

from isa import Opcode, Term, write_code


def translate(text):
"""Трансляция текста программы на Asm в машинный код.

Выполняется в два этапа:

1. Разбор текста на метки и инструкции.

2. Подстановка адресов меток в инструкции.
"""
labels, code = parse(text)

for instr in code:
if "arg" in instr:
# у инструкции есть аргумент, и этот аргумент - строка (название метки)
label = instr["arg"]
assert label in labels, "Undefined label: {}".format(label)
# подставляем адрес
instr["arg"] = labels[label]

return code


def parse(text):
"""Разбор текста на словарь с определением меток и список машинных инструкций."""
pc = 0
labels = {}
code = []

for line_num, raw_line in enumerate(text.splitlines(), 1):
line = remove_comment(raw_line)

line, label = parse_label(line)
if label is not None:
assert label not in labels, "Redefinition of label: {}".format(label)
# адрес метки - адрес следующей инструкции
labels[label] = pc

if not line:
# на строке нет инструкции
continue

# вычисляем номер столбца, с которого начинается инструкция
col = raw_line.find(line) + 1
term = Term(line_num, col, line)

instr = parse_instr(line, pc, term)
code.append(instr)
pc += 1

return labels, code


def remove_comment(line):
"""Удаляет комментарий, начинающийся с `;` из строки."""
return line.split(";", 1)[0].strip()


def is_valid_label_name(name):
"""Проверяет название метки на корректность синтаксиса."""
return re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", name)


def parse_label(line):
"""Достает из строки метку, если она присутствует, и проверяет ее на корректность.

Возвращает остаток строки и название метки (или `None`, если метки нет)"""
parts = line.split(":", 1)
if len(parts) == 1:
return line, None

label = parts[0].strip()
rest = parts[1].strip()

assert is_valid_label_name(label), "Invalid label name: {}".format(label)

return rest, label


def parse_instr(line, index, term):
"""Разбирает инструкцию из строки. Конвертирует мнемонику в опкод, а аргумент - в число."""
parts = line.split(None)

mnemonic = parts[0]
opcode = Opcode(mnemonic)

arg = parts[1] if len(parts) > 1 else None
if arg is not None:
assert opcode == Opcode.JZ or opcode == Opcode.JMP, "Only `jz` and `jnz` instructions take an argument"
assert len(parts) == 2, "Trailing characters"
assert is_valid_label_name(arg), "Invalid label name: {}".format(arg)

# порядок полей такой же, как в трансляторе brainfuck
return {"index": index, "opcode": opcode, "arg": arg, "term": term}

return {"index": index, "opcode": opcode, "term": term}


def main(source, target):
"""Функция запуска транслятора. Параметры -- исходный и целевой файлы."""
with open(source, encoding="utf-8") as f:
source = f.read()

code = translate(source)

write_code(target, code)
print("source LoC:", len(source.split("\n")), "code instr:", len(code))


if __name__ == "__main__":
assert len(sys.argv) == 3, "Wrong arguments: translator_asm.py <input_file> <target_file>"
_, source, target = sys.argv
main(source, target)