Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

73 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

C++ Calculator

Консольный калькулятор на C++17: интерактивный цикл «прочитал строку → вычислил → вывел результат». Поддерживает арифметику с приоритетами и скобками, переменные и константы, тригонометрию с градусами/радианами и пакетное вычисление выражений из текстового файла.

Главная особенность реализации — собственный разбор выражений методом рекурсивного спуска: грамматика описана в виде набора взаимно вызывающих друг друга функций (statement, expression, term, primary), а вычисление происходит прямо во время разбора, без построения синтаксического дерева. Подробно грамматика описана в разделе «Грамматика».


Содержание

  1. Возможности
  2. Сборка и запуск
  3. Структура репозитория
  4. Как устроена обработка ввода
  5. Грамматика
  6. Тесты
  7. Известные особенности реализации

Возможности

  • Операции +, -, *, /, скобки, унарные + и -.
  • Дробные числа (.5, 3.25, запись с экспонентой).
  • Переменные: let имя = выражение.
  • Константы: let const имя = выражение, снятие константности через let unconst.
  • Функции sin, cos, tg и суффиксы единиц угла deg / rad.
  • Несколько выражений в одной строке.
  • Команда calculate "путь" — вычисление выражений из файла.
  • Команды help и quit.
  • Сообщения об ошибках и восстановление после них без аварийного выхода.

Сборка и запуск

Нужен компилятор с поддержкой C++17 (используется std::filesystem) и CMake ≥ 3.10.

cmake -S . -B build
cmake --build build
./build/calculator          # в Windows: build\calculator.exe

После запуска появляется приглашение >. Каждая введённая строка обрабатывается отдельно, выход — командой quit (или концом ввода).


Структура репозитория

.
├── CMakeLists.txt
├── src/
│   ├── main.cpp                 # цикл чтения строк (REPL)
│   ├── calculator.cpp           # верхний уровень: разбор команд, calculate_file
│   ├── grammar.cpp              # рекурсивный спуск: statement / expression / term / primary
│   ├── token.cpp                # конструкторы и сравнение Token
│   ├── token_stream.cpp         # лексер: get(), ignore(), get_file_path()
│   ├── variable.cpp             # таблица переменных и разбор объявления let
│   ├── utility.cpp              # error(), clean_up_mess(), write_input_text(), help()
│   └── internal/
│       ├── calculator.h
│       ├── calculator_context.h # общее состояние сессии
│       ├── grammar.h
│       ├── token.h              # enum Kind и struct Token
│       ├── token_stream.h       # таблицы операторов и ключевых слов, класс Token_stream
│       ├── utility.h
│       └── variable.h           # struct Variable, класс Symbol_table
└── tests/
    ├── integration/             # data/input.txt, data/expected.txt, run_tests.ps1
    └── unit/                    # заготовки test_grammar / test_token / test_variable
Модуль Роль
main.cpp Печатает приглашение, читает строку, передаёт её в calculator(), печатает результат. Завершает работу по quit.
calculator.cpp Верхнеуровневый разбор: пропуск ;, команды quit / help / calculate, вызов statement(), перехват ошибок.
grammar.cpp Синтаксический анализатор и вычислитель в одном лице.
token_stream.cpp Превращает поток символов в токены; хранит буфер на один токен.
variable.cpp Symbol_table (вектор Variable, линейный поиск) и разбор формы let ….
utility.cpp Выброс ошибок, пропуск входа после ошибки, запись «эха» выражения, текст справки.

Как устроена обработка ввода

Общее состояние: Calculator_context

struct Calculator_context {
    Token_stream token_stream;          // источник токенов
    Symbol_table symbol_table;          // переменные и константы
    std::string  text_input;            // нормализованная запись разобранного выражения
    std::stack<std::string> file_stack; // файлы, которые сейчас вычисляются (защита от рекурсии)
};

Контекст передаётся по ссылке во все функции грамматики. Один контекст живёт всю интерактивную сессию, поэтому переменные сохраняются между строками.

Путь одной строки

main.cpp:run_calculator()
   └─ std::getline  →  calculator(строка, context)
          ├─ строка оборачивается в std::istringstream, привязывается к token_stream
          └─ цикл по элементам верхнего уровня:
               • ';'            → пропускается
               • конец строки   → выход из цикла
               • quit           → throw std::string("quit")  → завершение программы
               • help           → добавить текст справки
               • calculate      → прочитать путь в кавычках, вычислить файл
               • иначе          → statement(context) и вывод «эхо = значение»

Важные следствия:

  • Строка — единица ввода. istringstream создаётся на каждую строку, поэтому одно выражение не может продолжаться на следующей строке.
  • Разбор и вычисление совмещены. Функции грамматики возвращают double сразу, дерева нет.
  • Один токен предпросмотра. Token_stream::unget() хранит ровно один токен; этого достаточно для всей грамматики.

Грамматика

1. Принцип построения

Грамматика реализована рекурсивным спуском: каждому нетерминалу соответствует функция, которая читает токены и возвращает значение.

Нетерминал Функция Файл
statement statement() grammar.cpp
declaration declaration() → Symbol_table::define_name() grammar.cpp, variable.cpp
expression expression() grammar.cpp
term term() grammar.cpp
primary primary() grammar.cpp
элемент верхнего уровня (;, команды) calculator() calculator.cpp

Грамматика типа LL(1): решение о том, какое правило применять, принимается по одному очередному токену. Единственное «заглядывание» с возвратом — проверка ( после sin / cos / tg.

2. Лексика (токены)

Лексер — метод Token_stream::get(). Он пропускает пробельные символы и по первому символу выбирает тип токена.

2.1. Односимвольные токены

Символ Kind Используется грамматикой
( left_parenthesis да
) right_parenthesis да
+ plus да
- minus да
* multiply да
/ divide да
= assign да (только в let)
; semicolon да (разделитель, граница восстановления после ошибки)
% remainder нет — токен существует, но правила для него нет

2.2. Числа

  • Токен начинается с символа из набора .0123456789.
  • Дальше значение читается стандартным operator>>(double), поэтому допустимы дробная часть и, по правилам C++, запись с экспонентой.
  • Знак не входит в число: - и + всегда отдельные токены, унарность определяется грамматикой.
  • Если разобрать число не удалось (например, одиночная точка) — ошибка Invalid number.

2.3. Идентификаторы

identifier = буква { буква | цифра | "_" }
  • Начинается только с буквы (не с цифры и не с _).
  • Регистр учитывается.
  • Если лексема совпала с ключевым словом, токен становится ключевым словом, иначе — identifier.

2.4. Ключевые слова

Слово Kind Назначение
let let начало объявления / изменения переменной
const constant модификатор в let
unconst unconst модификатор в let
sin, cos, tg sine, cosine, tangent функции
deg, rad degrees, radians суффиксы единицы угла
help help команда
quit quit команда
calculate calculate команда
global, local global, local зарезервированы, не используются

Ключевые слова нельзя использовать как имена переменных.

2.5. Остальное

  • Любой другой символ (^, ,, !, " вне calculate и т. д.) → ошибка Bad token.
  • Конец строки → токен end_of_input.

3. Синтаксис (EBNF)

Ниже грамматика, восстановленная из исходного кода. Обозначения: [ … ] — необязательно, { … } — повторение ноль и более раз, | — альтернатива, "…" — токен.

(* --- верхний уровень: calculator.cpp --- *)
input        = { ";" | command | statement } ;
command      = "help"
             | "quit"
             | "calculate" path ;
path         = '"' символ { символ } '"' ;      (* любые символы, кроме '"'; не пусто *)

(* --- инструкции: grammar.cpp, variable.cpp --- *)
statement    = "let" declaration
             | expression ;
declaration  = [ "const" | "unconst" ] identifier [ "=" expression ] ;

(* --- выражения: grammar.cpp --- *)
expression   = term    { ( "+" | "-" ) term } ;
term         = primary { ( "*" | "/" ) primary } ;
primary      = number [ "deg" | "rad" ]
             | identifier
             | "(" expression ")"
             | sign { sign } primary
             | function "(" expression ")" ;
sign         = "+" | "-" ;
function     = "sin" | "cos" | "tg" ;

Замечания к записи:

  • command распознаётся только как первый токен элемента верхнего уровня. Внутри выражения help, quit и calculate недопустимы.
  • ; в input — необязательный разделитель; об этом подробнее в п. 7.
  • statement → "let" declaration — это единственное место, где допустимо let. Правая часть объявления — обычное expression, вложенные let невозможны.

4. Как из грамматики получаются приоритет и ассоциативность

Это прямое следствие структуры функций, поэтому достаточно разобрать общий случай.

Приоритет задаётся вложенностью. Операнды + и - — это term, а не primary. Пока term() разбирает цепочку primary { ("*" | "/") primary }, он «забирает» все подряд идущие * и /. Управление возвращается в expression() только тогда, когда следующий токен не * и не /. Поэтому * и / всегда связываются крепче + и -, а primary (скобки, функции, унарные знаки, литералы) — крепче всех.

Левая ассоциативность задаётся циклом. Правило вида

A = B { op B }

реализуется так:

double left = B();
for (;;) {
    Token t = get();
    if (t — это op) left = left op B();
    else { unget(t); return left; }
}

На каждом шаге накопленное значение становится левым операндом следующей операции. Для цепочки b1 op b2 op b3 … это даёт порядок ((b1 op b2) op b3) … — то есть вычисление слева направо. Это относится и к -, и к /.

Итоговая таблица (от высшего приоритета к низшему):

Уровень Конструкции Ассоциативность Функция
1 литералы, идентификаторы, ( … ), sin/cos/tg( … ), унарные + - — primary()
2 *, / левая term()
3 +, - левая expression()

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

5. Особенности правила primary

5.1. Унарные знаки

Ветка sign { sign } primary читает подряд идущие + и -, меняет знак при каждом - и затем применяет итоговый знак к следующему primary. Из этого следует:

  • любое количество знаков подряд допустимо (- - x, + - x);
  • унарный знак стоит на уровне primary, то есть связывается крепче * и /: запись -a * b читается как (-a) * b;
  • знак допустим справа от бинарного оператора, потому что правый операнд — тоже primary: a / -b, a + -b;
  • знак перед скобками и перед функцией работает так же: -(…), -sin(…).

5.2. Функции sin, cos, tg

function "(" expression ")"

После имени функции обязательно должна идти (, иначе ошибка '(' expected after sin (соответственно cos, tg). Аргумент — полное выражение в скобках. Результат функции — обычный primary, поэтому к нему применимы *, /, унарные знаки и т. д.

  • Аргумент трактуется как угол в радианах.
  • tg дополнительно проверяет, что |cos(аргумент)| ≥ 1e-12; иначе ошибка tg is undefined for this value.

5.3. Суффиксы deg и rad

number [ "deg" | "rad" ]
  • <число> deg переводит градусы в радианы: значение умножается на π и делится на 180.
  • <число> rad оставляет значение без изменений (радианы — единица по умолчанию).
  • Суффикс допустим только сразу после числового литерала. После идентификатора, скобки или функции он не разбирается: x deg и (…) deg не являются корректными записями (токен deg останется необработанным и вызовет ошибку primary expected на следующем шаге).

5.4. Идентификатор

Подставляется текущее значение переменной. Если переменной нет — ошибка get: undefined name <имя>. Предопределённых констант (pi, e и т. п.) нет.

6. Объявление переменных: правило declaration

declaration = [ "const" | "unconst" ] identifier [ "=" expression ]

Разбор выполняет Symbol_table::define_name(). Семантика всех форм:

Форма Действие
let x = e Создаёт переменную или присваивает новое значение. Если x — константа, ошибка x is a constant.
let x Выводит текущее значение. Если x не объявлена, ошибка find: undefined name x.
let const x = e Создаёт или присваивает, затем делает x константой. Если x уже константа, присваивание запрещено.
let const x Делает существующую переменную константой.
let unconst x = e Снимает константность, присваивает, переменная остаётся неконстантной. Это способ изменить константу.
let unconst x Снимает константность с существующей переменной.

Дополнительно:

  • Присваивание возможно только через let. Запись x = e без let разбирается как выражение x и завершается ошибкой, так как = не является оператором выражения.
  • Значение инструкции let … — присвоенное (или текущее) значение переменной, оно выводится как результат.
  • Таблица переменных — std::vector<Variable> с линейным поиском; в режиме calculate у файла собственная таблица (см. ниже).

7. Разделение инструкций

Верхний уровень (input) устроен как цикл «взять элемент → выполнить», а ; — всего лишь элемент, который пропускается. Конец expression определяется не ;, а тем, что следующий токен не +, -, * и /. Из этого следуют два важных свойства:

  1. ; необязательна. Разделитель нужен только для читаемости и как граница восстановления после ошибки.
  2. Пропущенный оператор не является синтаксической ошибкой. Если после завершённого выражения идёт токен, который не может продолжить выражение, он начинает новую инструкцию. Поэтому a b — это две инструкции a и b, 2a — число и затем идентификатор, а let x = e1 let y = e2 — два объявления подряд.

Побочный эффект: лишняя закрывающая скобка или % в конце выражения не обрывают разбор в момент встречи, а начинают новую инструкцию, которая завершается ошибкой primary expected.

8. Команды верхнего уровня

Команда Поведение
help Добавляет в вывод текст справки.
quit Завершает программу. Работает и внутри вычисляемого файла — завершается вся программа.
calculate "путь" Вычисляет файл построчно.

Формат пути — в двойных кавычках, пробелы внутри пути допустимы. Завершающая ; не требуется (если она есть, будет пропущена как обычный разделитель). Ошибки пути: file path expected (нет открывающей кавычки), Closing quotation mark expected, File path cannot be empty, Cannot open file: ….

Особенности calculate:

  • Выводится строка Called file: <полный путь>, далее результаты построчно.
  • Файл вычисляется в новом контексте: переменные вызывающей сессии ему недоступны, а переменные, объявленные в файле, после его завершения не сохраняются.
  • Ошибки внутри файла не прерывают вычисление: они печатаются и обработка продолжается со следующей инструкции.
  • Рекурсивный вызов (файл прямо или косвенно вызывает сам себя) обнаруживается через file_stack и приводит к ошибке Recursive file calculation: <путь>.
  • Как и в консоли, каждая строка файла разбирается независимо.

9. Формат вывода

Для каждой успешно разобранной инструкции печатается:

<нормализованная запись> = <значение>

Нормализованная запись («эхо») собирается самой грамматикой через write_input_text() — каждый разобранный токен дописывается в context.text_input и отделяется пробелом:

Токен Что попадает в эхо
число число, до 15 значащих цифр
идентификатор имя
+ - * / ( ) = сам символ
sin, cos, tg sin, cos, tg
deg, rad deg, rad
let, const, unconst не выводятся

Значение печатается стандартным operator<< для double, то есть с точностью по умолчанию — 6 значащих цифр.

Поскольку форма let x = e тоже печатает эхо, её вывод имеет вид x = <эхо e> = <значение>.

10. Ошибки и восстановление

Все ошибки — исключения std::runtime_error, выбрасываемые через error(). Верхний уровень перехватывает их, добавляет в вывод Error: <сообщение> и вызывает clean_up_mess(), которая пропускает остаток строки до ближайшей ; (либо до конца строки, если ; нет). Незавершённое эхо при этом отбрасывается.

Сообщение Причина
Bad token недопустимый символ
Invalid number число не удалось разобрать
primary expected на месте операнда стоит токен, не начинающий primary
')' expected не закрыта скобка
'(' expected after sin / cos / tg у функции нет скобки
tg is undefined for this value |cos| < 1e-12
divide by zero делитель в точности равен нулю
name expected in declaration после let (и const / unconst) нет идентификатора
get: undefined name X использование необъявленной переменной
find: undefined name X let X или let const X без = для необъявленной X
X is a constant попытка присвоить константе
file path expected, Closing quotation mark expected, File path cannot be empty, Cannot open file: …, Recursive file calculation: … ошибки команды calculate

11. Зарезервировано, но не реализовано

  • Оператор % распознаётся лексером (Kind::remainder), но в term() для него нет ветки, поэтому его появление приводит к ошибке primary expected.
  • Ключевые слова global и local распознаются (и поэтому недоступны как имена), но в грамматике не используются.

Тесты

Интеграционные (tests/integration/):

  • data/input.txt подаётся на стандартный вход программы: арифметика, приоритеты, скобки, унарные знаки, дробные числа, переменные, константы, несколько инструкций в строке и т. д.
  • data/expected.txt содержит ожидаемые значения (по одному на строку).
  • run_tests.ps1 (PowerShell, Windows) запускает build\calculator.exe, из каждой строки вывода берёт значение после последнего =, сравнивает с ожидаемым и записывает results/actual.txt и, при расхождениях, results/diff.txt.

Модульные (tests/unit/): файлы test_grammar.cpp, test_token.cpp, test_variable.cpp и CMakeLists.txt пока пусты; корневой CMakeLists.txt подкаталог tests не подключает.


About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages