Это калькулятор арифметических выражений, написанный на языке 64-битного варианта Microsoft Assembler (MASM64) под ОС Windows.
Этот проект был написан мной из чистого интереса в качестве первой хоть сколько-то масштабной программы на ассемблере, в основном в целях практики. Интересно, что калькулятор такого рода на языках высокого уровня я до этого никогда не писал.
Этот калькулятор умеет вычислять арифметические выражения, записанные в традиционной инфиксной нотации.
Поддерживаются:
- числа, целые или дробные; десятичный разделитель — точка
- четыре арифметических действия (
+,-,*,/), в том числе унарный минус - вложенные выражения в скобках
(), произвольной глубины
Во-первых, очевидно, что код не кроссплатформенный. Программа динамически линкуется с kernel32.dll и задействует функции API ядра Windows, в числе которых: GetStdHandle, WriteConsole, ReadConsole, HeapAlloc, ExitProcess.
Во-вторых, как следствие упрощения кода, калькулятор не валидирует синтаксис выражений, за исключением обработки неизвестных токенов, поэтому некорректные выражения, например, с несбалансированными скобками или нарушенной нотацией операций, могут как частично вычислиться, так и привести к краху программы.
Программа работает в цикле. Каждое введённое выражение проходит через три стадии:
-
Лексинг. Символы и числа превращаются в отдельные токены. Это нужно как для удобства обработки в дальнейшем, так и для распознавания записанных чисел сразу. Отдельно для этого пришлось написать процедуру для перевода числа из его строкового представления. Программа работает с числами в формате с плавающей запятой двойной точности, однако моя упрощённая реализация ограничивает точность 9 знаками до и после запятой.
-
Парсинг. Цепочка токенов переводится в формат, пригодный для вычисления в дальнейшем. В результате получается прямая (префиксная) запись — это оказалось удобнее, хотя обычно и применяется обратная (постфиксная) запись.
Алгоритм я придумал сам, не изучив предварительно имеющиеся подходы. Потом оказалось, что получилось что-то вроде алгоритма Дейкстры, но с рекурсией. Мой алгоритм наверняка не оптимален, но показался мне самым простым и очевидным. -
Вычисление. Поскольку легче оказалось создать прямую запись, а для вычислений пригодней обратная, приходится обходить результат парсинга с конца до начала, в обратном порядке. Числа помещаются на стек, а операции производятся над числами на верхушке стека: бинарные над парами, унарные над одиночными. Результат помещается обратно на стек. В конце должно остаться одно число, которое и будет значением выражения.
После этого результат выводится на экран, для чего пришлось также написать процедуру перевода числа в его строковое представление, работающую с той же точностью, что и её распознающая пара.
- Покрытие тестами для упрощения разработки в будущем
- Написание эквивалентного псевдокода для облегчения восприятия
- Рефакторинг кода, в частности — токенизации
- Более полезное и точное покрытие комментариями
- Написание версии под Linux, из чистого интереса
- Валидация синтаксиса выражений
- Добавление математических функций (идентификаторы со скобками)
- Добавление возведения в степень (+ 1 уровень приоритета!)
- Освобождение выделенной памяти после каждой итерации цикла
- Более аккуратное, то есть точное, выделение памяти
- Возвращение в цикл после ошибки вместо завершения программы
- Вывод более информативных сообщений об ошибках
Требуется наличие Microsoft Visual Studio Build Tools, поэтому процесс сборки тоже ограничен ОС Windows. После установки компонента для нативной разработки нужно запустить x64 Native Tools Command Prompt и перейти в корень репозитория, после чего надо исполнить скрипт build.cmd. В результате появится исполняемый файл app.exe.
Expression calculator.
> 2+2
= 4.0
> 1 - 2 - 3
= -4.0
> 2 + 2*2
= 6.0
> 1 + 23.8 * (-31 + 445 * 53) - 67
= 560519.2
> 1 + 2 * 3 / 4 + 5 - 6 - (-7)*8 + 9
= 66.5
> 1 + 23 * (2 * 31 + 2 * 45/53) - 67 / 2
= 1432.556603774
> 43252 * 0.011 + 0.1 / 2 - (2 + 2 * 1.2) + 2 * (1 + (-1))
= 471.422
> 5/8*3
= 1.875
> 1/2/3/4
= 0.041666667
> 1/2-3/4
= -0.25
> 1 - (1 - (1 - (1 - (1 - 1))))
= 0.0
>