math-engine 0.6.4__tar.gz → 0.6.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {math_engine-0.6.4 → math_engine-0.6.6}/PKG-INFO +4 -3
- {math_engine-0.6.4 → math_engine-0.6.6}/README.md +1 -1
- {math_engine-0.6.4 → math_engine-0.6.6}/math_engine/__init__.py +158 -3
- math_engine-0.6.6/math_engine/calculator/AST_Node_Types.py +364 -0
- {math_engine-0.6.4 → math_engine-0.6.6}/math_engine/calculator/ScientificEngine.py +80 -36
- math_engine-0.6.6/math_engine/calculator/__init__.py +10 -0
- {math_engine-0.6.4 → math_engine-0.6.6}/math_engine/calculator/calculator.py +449 -74
- {math_engine-0.6.4 → math_engine-0.6.6}/math_engine/calculator/translator.py +208 -38
- math_engine-0.6.6/math_engine/cli/__init__.py +29 -0
- {math_engine-0.6.4 → math_engine-0.6.6}/math_engine/cli/cli.py +163 -8
- math_engine-0.6.6/math_engine/plugins/__init__.py +10 -0
- math_engine-0.6.6/math_engine/plugins/my_plugin.py +46 -0
- math_engine-0.6.6/math_engine/utility/__init__.py +11 -0
- {math_engine-0.6.4 → math_engine-0.6.6}/math_engine/utility/config_manager.py +91 -7
- math_engine-0.6.6/math_engine/utility/error.py +280 -0
- math_engine-0.6.6/math_engine/utility/non_decimal_utility.py +493 -0
- {math_engine-0.6.4 → math_engine-0.6.6}/math_engine/utility/plugin_manager.py +124 -8
- math_engine-0.6.6/math_engine/utility/utility.py +194 -0
- {math_engine-0.6.4 → math_engine-0.6.6}/math_engine.egg-info/PKG-INFO +4 -3
- {math_engine-0.6.4 → math_engine-0.6.6}/pyproject.toml +5 -2
- math_engine-0.6.4/math_engine/calculator/AST_Node_Types.py +0 -162
- math_engine-0.6.4/math_engine/calculator/__init__.py +0 -1
- math_engine-0.6.4/math_engine/cli/__init__.py +0 -16
- math_engine-0.6.4/math_engine/plugins/__init__.py +0 -3
- math_engine-0.6.4/math_engine/plugins/my_plugin.py +0 -24
- math_engine-0.6.4/math_engine/utility/__init__.py +0 -0
- math_engine-0.6.4/math_engine/utility/error.py +0 -181
- math_engine-0.6.4/math_engine/utility/non_decimal_utility.py +0 -240
- math_engine-0.6.4/math_engine/utility/utility.py +0 -97
- {math_engine-0.6.4 → math_engine-0.6.6}/LICENSE +0 -0
- {math_engine-0.6.4 → math_engine-0.6.6}/math_engine/config.json +0 -0
- {math_engine-0.6.4 → math_engine-0.6.6}/math_engine.egg-info/SOURCES.txt +0 -0
- {math_engine-0.6.4 → math_engine-0.6.6}/math_engine.egg-info/dependency_links.txt +0 -0
- {math_engine-0.6.4 → math_engine-0.6.6}/math_engine.egg-info/entry_points.txt +0 -0
- {math_engine-0.6.4 → math_engine-0.6.6}/math_engine.egg-info/requires.txt +0 -0
- {math_engine-0.6.4 → math_engine-0.6.6}/math_engine.egg-info/top_level.txt +0 -0
- {math_engine-0.6.4 → math_engine-0.6.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: math-engine
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.6
|
|
4
4
|
Summary: A fast and secure mathematical expression evaluator.
|
|
5
5
|
Author-email: Jan Teske <jan.teske.06@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/JanTeske06/math_engine
|
|
@@ -17,9 +17,10 @@ Requires-Dist: prompt_toolkit
|
|
|
17
17
|
Provides-Extra: test
|
|
18
18
|
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
19
19
|
Requires-Dist: pytest-cov; extra == "test"
|
|
20
|
+
Dynamic: license-file
|
|
20
21
|
|
|
21
22
|
|
|
22
|
-
# Math Engine v0.6.
|
|
23
|
+
# Math Engine v0.6.6
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
[](https://badge.fury.io/py/math-engine)
|
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
math_engine — A fast, safe, and configurable mathematical expression evaluator.
|
|
3
|
+
|
|
4
|
+
This module is the public API surface for the math_engine package. It exposes
|
|
5
|
+
high-level functions for evaluating expressions, managing settings, and working
|
|
6
|
+
with an in-memory variable store.
|
|
7
|
+
|
|
8
|
+
Pipeline overview:
|
|
9
|
+
1. Tokenizer — converts raw input strings into token lists
|
|
10
|
+
2. Parser — builds an Abstract Syntax Tree (recursive descent)
|
|
11
|
+
3. Evaluator — numeric evaluation or linear equation solving
|
|
12
|
+
4. Formatter — renders results in the requested output type
|
|
13
|
+
|
|
14
|
+
Usage::
|
|
15
|
+
|
|
16
|
+
import math_engine
|
|
17
|
+
|
|
18
|
+
math_engine.evaluate("2 + 3") # Decimal('5')
|
|
19
|
+
math_engine.evaluate("hex: 255") # '0xff'
|
|
20
|
+
math_engine.evaluate("x + 3 = 10") # Decimal('7')
|
|
21
|
+
math_engine.evaluate("x + 1", x=4) # Decimal('5')
|
|
22
|
+
"""
|
|
23
|
+
|
|
1
24
|
from . import calculator
|
|
2
25
|
from .calculator import ScientificEngine
|
|
3
26
|
from .utility import config_manager as config_manager
|
|
@@ -7,14 +30,39 @@ from .utility import config_manager as config_manager
|
|
|
7
30
|
from typing import Optional
|
|
8
31
|
from typing import Union
|
|
9
32
|
from typing import Any, Mapping
|
|
10
|
-
|
|
33
|
+
|
|
34
|
+
__version__ = "0.6.6"
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# In-memory variable store
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Variables stored here are automatically merged into every ``evaluate()``
|
|
40
|
+
# call. Keyword arguments passed directly to ``evaluate()`` take precedence
|
|
41
|
+
# over memory entries with the same key.
|
|
11
42
|
memory = {}
|
|
12
43
|
|
|
13
44
|
def set_memory(key_value: str, value:str):
|
|
45
|
+
"""Store a key-value pair in the global in-memory variable store.
|
|
46
|
+
|
|
47
|
+
Memory variables are automatically included in the evaluation context
|
|
48
|
+
for all subsequent ``evaluate()`` calls.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
key_value: The variable name to store (used as identifier in expressions).
|
|
52
|
+
value: The value to associate with the key.
|
|
53
|
+
"""
|
|
14
54
|
global memory
|
|
15
55
|
memory[key_value] = value
|
|
16
56
|
|
|
17
57
|
def delete_memory(key_value: str):
|
|
58
|
+
"""Remove a variable from the in-memory store.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
key_value: The key to remove. Pass ``"all"`` to clear the entire store.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
E.SyntaxError: If the key does not exist in memory (code ``4000``).
|
|
65
|
+
"""
|
|
18
66
|
global memory
|
|
19
67
|
try:
|
|
20
68
|
if key_value == "all":
|
|
@@ -25,10 +73,27 @@ def delete_memory(key_value: str):
|
|
|
25
73
|
raise E.SyntaxError(f"Entry {key_value} does not exist.", code = "4000")
|
|
26
74
|
|
|
27
75
|
def show_memory():
|
|
76
|
+
"""Return the current contents of the in-memory variable store.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
dict: A dictionary mapping variable names to their stored values.
|
|
80
|
+
"""
|
|
28
81
|
return memory
|
|
29
82
|
|
|
30
83
|
|
|
31
84
|
def change_setting(setting: str, new_value: Union[int, bool]):
|
|
85
|
+
"""Modify a single configuration setting and persist it to disk.
|
|
86
|
+
|
|
87
|
+
The new value must match the type of the existing setting (with special
|
|
88
|
+
handling for bool/int interoperability).
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
setting: The setting key name (e.g., ``"decimal_places"``).
|
|
92
|
+
new_value: The new value to assign.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
int: ``1`` on success, ``-1`` on failure.
|
|
96
|
+
"""
|
|
32
97
|
saved_settings = config_manager.save_setting(setting, new_value)
|
|
33
98
|
|
|
34
99
|
if saved_settings != -1:
|
|
@@ -37,6 +102,21 @@ def change_setting(setting: str, new_value: Union[int, bool]):
|
|
|
37
102
|
return -1
|
|
38
103
|
|
|
39
104
|
def load_preset(settings: dict):
|
|
105
|
+
"""Replace all settings at once with a complete settings dictionary.
|
|
106
|
+
|
|
107
|
+
The dictionary must contain exactly the same keys as the current
|
|
108
|
+
configuration — no more, no fewer.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
settings: A complete settings dictionary (all keys required).
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
int: ``1`` on success.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
E.SyntaxError: If the dictionary contains unknown keys (code ``5003``)
|
|
118
|
+
or is missing required keys (code ``5004``).
|
|
119
|
+
"""
|
|
40
120
|
current = config_manager.load_setting_value("all")
|
|
41
121
|
unknown = [k for k in settings.keys() if k not in current]
|
|
42
122
|
if unknown:
|
|
@@ -51,10 +131,23 @@ def load_preset(settings: dict):
|
|
|
51
131
|
|
|
52
132
|
|
|
53
133
|
def load_all_settings():
|
|
134
|
+
"""Return the complete current settings dictionary.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
dict: All configuration key-value pairs.
|
|
138
|
+
"""
|
|
54
139
|
settings = config_manager.load_setting_value("all")
|
|
55
140
|
return settings
|
|
56
141
|
|
|
57
142
|
def load_one_setting(setting):
|
|
143
|
+
"""Return the value of a single configuration setting.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
setting: The setting key name.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
The setting value, or ``0`` if the key does not exist.
|
|
150
|
+
"""
|
|
58
151
|
settings = config_manager.load_setting_value(setting)
|
|
59
152
|
return settings
|
|
60
153
|
|
|
@@ -62,7 +155,40 @@ def evaluate(expr: str,
|
|
|
62
155
|
variables: Optional[Mapping[str, Any]] = None,
|
|
63
156
|
is_cli: bool = False,
|
|
64
157
|
**kwvars: Any) -> Any:
|
|
158
|
+
"""Evaluate a mathematical expression and return the typed result.
|
|
159
|
+
|
|
160
|
+
This is the primary entry point for the library. The expression is
|
|
161
|
+
tokenized, parsed into an AST, evaluated (or solved if it is a linear
|
|
162
|
+
equation), and the result is formatted according to the active settings
|
|
163
|
+
and any output prefix present in the expression.
|
|
164
|
+
|
|
165
|
+
Variables can be supplied either as a mapping or as keyword arguments.
|
|
166
|
+
Keyword arguments take precedence over the mapping, and both take
|
|
167
|
+
precedence over values stored in the global memory.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
expr: The expression string (e.g., ``"2 + 3"``, ``"hex: 255"``).
|
|
171
|
+
variables: Optional variable mapping (e.g., ``{"x": 5}``).
|
|
172
|
+
is_cli: When ``True``, error diagnostics use CLI-style formatting
|
|
173
|
+
(position pointer above the expression).
|
|
174
|
+
**kwvars: Additional variables as keyword arguments.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The evaluation result. The concrete type depends on the expression
|
|
178
|
+
and the active output prefix:
|
|
65
179
|
|
|
180
|
+
- ``Decimal`` — default numeric results
|
|
181
|
+
- ``int`` — with ``int:`` prefix
|
|
182
|
+
- ``float`` — with ``float:`` prefix
|
|
183
|
+
- ``bool`` — with ``bool:`` prefix or equality expressions
|
|
184
|
+
- ``str`` — with ``str:``, ``hex:``, ``bin:``, ``oct:`` prefixes
|
|
185
|
+
- ``None`` — when ``readable_error=True`` and an error occurred
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
E.MathError: (or a subclass) when ``readable_error=False`` and the
|
|
189
|
+
expression is invalid or cannot be evaluated.
|
|
190
|
+
"""
|
|
191
|
+
# Merge variable sources: memory < variables dict < keyword args
|
|
66
192
|
if variables is None:
|
|
67
193
|
merged = dict(kwvars)
|
|
68
194
|
else:
|
|
@@ -72,12 +198,15 @@ def evaluate(expr: str,
|
|
|
72
198
|
merged = dict(list(memory.items()) + list(merged.items()))
|
|
73
199
|
settings = load_all_settings()
|
|
74
200
|
|
|
75
|
-
|
|
201
|
+
# --- Path 1: Exception mode (readable_error=False) ---
|
|
202
|
+
# Exceptions propagate directly to the caller.
|
|
76
203
|
if settings["readable_error"] == False:
|
|
77
204
|
result = calculate(expr, merged,1) # 0 = Validate, 1 = Calculate
|
|
78
205
|
return result
|
|
79
206
|
|
|
80
|
-
|
|
207
|
+
# --- Path 2: Visual diagnostics mode (readable_error=True) ---
|
|
208
|
+
# Errors are caught, a human-readable diagnostic is printed to stdout,
|
|
209
|
+
# and the function returns None.
|
|
81
210
|
elif settings["readable_error"]== True:
|
|
82
211
|
result = -1
|
|
83
212
|
try:
|
|
@@ -87,12 +216,14 @@ def evaluate(expr: str,
|
|
|
87
216
|
|
|
88
217
|
return result
|
|
89
218
|
except E.MathError as e:
|
|
219
|
+
# Labels for the diagnostic output
|
|
90
220
|
Errormessage = "Errormessage: "
|
|
91
221
|
code = "Code: "
|
|
92
222
|
Equation = "Equation: "
|
|
93
223
|
positon_start = e.position_start
|
|
94
224
|
positon_end = e.position_end
|
|
95
225
|
|
|
226
|
+
# --- CLI-style output: pointer appears above the expression ---
|
|
96
227
|
if is_cli:
|
|
97
228
|
if positon_start != -1:
|
|
98
229
|
if positon_end == -1:
|
|
@@ -111,6 +242,8 @@ def evaluate(expr: str,
|
|
|
111
242
|
print(Errormessage + str(e.message))
|
|
112
243
|
print(Equation + str(e.equation))
|
|
113
244
|
print(" ")
|
|
245
|
+
|
|
246
|
+
# --- Library-style output: underlined error segment in equation ---
|
|
114
247
|
if is_cli == False:
|
|
115
248
|
print(Errormessage + str(e.message))
|
|
116
249
|
print(code + str(e.code))
|
|
@@ -118,6 +251,7 @@ def evaluate(expr: str,
|
|
|
118
251
|
if positon_end == -1:
|
|
119
252
|
positon_end = positon_start
|
|
120
253
|
calc_equation = str(e.equation)
|
|
254
|
+
# Underline the problematic segment using ANSI escape codes
|
|
121
255
|
print(
|
|
122
256
|
Equation + calc_equation[:positon_start] + "\033[4m" + calc_equation[
|
|
123
257
|
positon_start:positon_end + 1] + "\033[0m" + calc_equation[
|
|
@@ -138,6 +272,23 @@ def evaluate(expr: str,
|
|
|
138
272
|
def validate(expr: str,
|
|
139
273
|
variables: Optional[Mapping[str, Any]] = None,
|
|
140
274
|
**kwvars: Any) -> Any:
|
|
275
|
+
"""Parse and validate an expression without performing full evaluation.
|
|
276
|
+
|
|
277
|
+
Internally calls the calculator with ``validate=0``, which builds the
|
|
278
|
+
AST but skips the final numeric evaluation for pure expressions. This
|
|
279
|
+
is useful for checking whether an expression is syntactically valid.
|
|
280
|
+
|
|
281
|
+
On error, a visual diagnostic is always printed to stdout (regardless
|
|
282
|
+
of the ``readable_error`` setting).
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
expr: The expression string to validate.
|
|
286
|
+
variables: Optional variable mapping.
|
|
287
|
+
**kwvars: Additional variables as keyword arguments.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
The AST tree on success, or ``None`` if an error was caught.
|
|
291
|
+
"""
|
|
141
292
|
explanation = False
|
|
142
293
|
if variables is None:
|
|
143
294
|
merged = dict(kwvars)
|
|
@@ -182,6 +333,10 @@ def validate(expr: str,
|
|
|
182
333
|
|
|
183
334
|
|
|
184
335
|
def reset_settings():
|
|
336
|
+
"""Reset all settings to their factory defaults.
|
|
337
|
+
|
|
338
|
+
Overwrites the ``config.json`` file with the hardcoded default values.
|
|
339
|
+
"""
|
|
185
340
|
config_manager.reset_settings()
|
|
186
341
|
|
|
187
342
|
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Abstract Syntax Tree (AST) node types for the math_engine expression parser.
|
|
3
|
+
|
|
4
|
+
The parser (:func:`calculator.calculator.ast`) builds a tree from three node
|
|
5
|
+
types:
|
|
6
|
+
|
|
7
|
+
- :class:`Number` — numeric literals (backed by ``decimal.Decimal``)
|
|
8
|
+
- :class:`Variable` — symbolic variable placeholders (e.g., ``"var0"``)
|
|
9
|
+
- :class:`BinOp` — binary operations with a left subtree, operator, and
|
|
10
|
+
right subtree
|
|
11
|
+
|
|
12
|
+
Each node implements:
|
|
13
|
+
|
|
14
|
+
- ``evaluate()`` — recursively compute the numeric value
|
|
15
|
+
- ``collect_term(var)`` — decompose the subtree into
|
|
16
|
+
``(factor_of_var, constant)`` for the linear equation solver
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from decimal import Decimal
|
|
20
|
+
from ..utility import error as E
|
|
21
|
+
|
|
22
|
+
class Number:
|
|
23
|
+
"""AST node representing a numeric literal, backed by ``decimal.Decimal``.
|
|
24
|
+
|
|
25
|
+
A ``Number`` is always a leaf node in the AST. It stores a single
|
|
26
|
+
``Decimal`` value and carries optional source-position information so
|
|
27
|
+
that error messages can point back to the original input string.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
value (Decimal): The numeric value of the literal.
|
|
31
|
+
position_start (int): Character index where this literal begins
|
|
32
|
+
in the source string (``-1`` when unknown).
|
|
33
|
+
position_end (int): Character index where this literal ends
|
|
34
|
+
in the source string (``-1`` when unknown).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, value, position_start=-1, position_end=-1):
|
|
38
|
+
"""Create a Number node.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
value: Any numeric type. Non-Decimal values are first
|
|
42
|
+
converted to ``str`` before being passed to the ``Decimal``
|
|
43
|
+
constructor so that floating-point artifacts (e.g.
|
|
44
|
+
``Decimal(0.1)`` producing ``0.1000000000000000055...``)
|
|
45
|
+
are avoided.
|
|
46
|
+
position_start: Start index in the source string.
|
|
47
|
+
position_end: End index in the source string.
|
|
48
|
+
"""
|
|
49
|
+
# Always normalize input to Decimal via string to avoid float artifacts
|
|
50
|
+
if not isinstance(value, Decimal):
|
|
51
|
+
value = str(value)
|
|
52
|
+
self.value = Decimal(value)
|
|
53
|
+
self.position_start = position_start
|
|
54
|
+
self.position_end = position_end
|
|
55
|
+
|
|
56
|
+
def evaluate(self):
|
|
57
|
+
"""Return the stored ``Decimal`` value.
|
|
58
|
+
|
|
59
|
+
Because a ``Number`` is a leaf, no recursion is required.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Decimal: The numeric value of this literal.
|
|
63
|
+
"""
|
|
64
|
+
return self.value
|
|
65
|
+
|
|
66
|
+
def collect_term(self, var_name):
|
|
67
|
+
"""Decompose this node for the linear equation solver.
|
|
68
|
+
|
|
69
|
+
A numeric literal contains no variable term, so the factor is
|
|
70
|
+
always ``0`` and the constant is the literal's value.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
var_name (str): The variable name being collected (unused
|
|
74
|
+
for ``Number`` nodes, but required by the interface).
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
tuple[int, Decimal]: ``(0, self.value)`` -- zero coefficient
|
|
78
|
+
for the variable and the literal as the constant part.
|
|
79
|
+
"""
|
|
80
|
+
# A plain number contributes nothing to the variable factor
|
|
81
|
+
# and its full value to the constant term.
|
|
82
|
+
return (0, self.value)
|
|
83
|
+
|
|
84
|
+
def __repr__(self):
|
|
85
|
+
"""Return a human-readable representation, e.g. ``Number(3.14)``."""
|
|
86
|
+
try:
|
|
87
|
+
display_value = self.value.to_normal_string()
|
|
88
|
+
except AttributeError:
|
|
89
|
+
display_value = str(self.value)
|
|
90
|
+
return f"Number({display_value})"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Variable:
|
|
94
|
+
"""AST node representing a single symbolic variable (e.g. ``"var0"``).
|
|
95
|
+
|
|
96
|
+
A ``Variable`` is a leaf node that acts as a placeholder for an
|
|
97
|
+
unknown value. The tokenizer assigns internal names like ``"var0"``,
|
|
98
|
+
``"var1"``, etc. -- these are the names stored in :attr:`name`.
|
|
99
|
+
|
|
100
|
+
Attributes:
|
|
101
|
+
name (str): Internal variable identifier assigned by the tokenizer
|
|
102
|
+
(e.g. ``"var0"``).
|
|
103
|
+
position_start (int): Character index where this variable begins
|
|
104
|
+
in the source string (``-1`` when unknown).
|
|
105
|
+
position_end (int): Character index where this variable ends
|
|
106
|
+
in the source string (``-1`` when unknown).
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(self, name, position_start=-1, position_end=-1):
|
|
110
|
+
"""Create a Variable node.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
name: Internal variable identifier (e.g. ``"var0"``).
|
|
114
|
+
position_start: Start index in the source string.
|
|
115
|
+
position_end: End index in the source string.
|
|
116
|
+
"""
|
|
117
|
+
self.name = name
|
|
118
|
+
self.position_start = position_start
|
|
119
|
+
self.position_end = position_end
|
|
120
|
+
|
|
121
|
+
def evaluate(self):
|
|
122
|
+
"""Attempt to numerically evaluate this variable.
|
|
123
|
+
|
|
124
|
+
Variables have no numeric value on their own -- they must be
|
|
125
|
+
solved for via the equation solver. Calling ``evaluate()`` on a
|
|
126
|
+
``Variable`` therefore always raises ``SolverError``.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
E.SolverError: Always raised (code ``3005``), indicating
|
|
130
|
+
that a numeric evaluation path encountered an unresolved
|
|
131
|
+
variable.
|
|
132
|
+
"""
|
|
133
|
+
raise E.SolverError(f"Non linear problem.", code="3005", position_start=self.position_start)
|
|
134
|
+
|
|
135
|
+
def collect_term(self, var_name):
|
|
136
|
+
"""Decompose this variable for the linear equation solver.
|
|
137
|
+
|
|
138
|
+
If this variable matches the one being solved for, it
|
|
139
|
+
contributes a coefficient of ``1`` and a constant of ``0``.
|
|
140
|
+
If it does *not* match, the expression contains multiple
|
|
141
|
+
distinct unknowns, which the linear solver cannot handle.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
var_name (str): The internal variable name being collected
|
|
145
|
+
(e.g. ``"var0"``).
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
tuple[int, int]: ``(1, 0)`` when ``self.name == var_name``.
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
E.SolverError: When ``self.name != var_name`` (code ``3002``),
|
|
152
|
+
meaning the expression has more than one distinct variable.
|
|
153
|
+
"""
|
|
154
|
+
if self.name == var_name:
|
|
155
|
+
# This is the variable we are solving for: coefficient = 1, constant = 0
|
|
156
|
+
return (1, 0)
|
|
157
|
+
else:
|
|
158
|
+
# A second, different variable was encountered -- not solvable linearly
|
|
159
|
+
raise E.SolverError(f"Multiple variables found: {self.name}", code="3002", position_start=self.position_start)
|
|
160
|
+
|
|
161
|
+
def __repr__(self):
|
|
162
|
+
"""Return a human-readable representation, e.g. ``Variable('var0')``."""
|
|
163
|
+
return f"Variable('{self.name}')"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class BinOp:
|
|
167
|
+
"""AST node for a binary operation: ``left <operator> right``.
|
|
168
|
+
|
|
169
|
+
Attributes:
|
|
170
|
+
left: Left subtree (Number, Variable, or BinOp).
|
|
171
|
+
operator: Operator string (``"+"``, ``"-"``, ``"*"``, ``"/"``,
|
|
172
|
+
``"**"``, ``"="``, ``"&"``, ``"|"``, ``"^"``,
|
|
173
|
+
``"<<"``, ``">>"``)
|
|
174
|
+
right: Right subtree.
|
|
175
|
+
position_start: Character index of the operator in the source string.
|
|
176
|
+
position_end: End index of the operator in the source string.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def __init__(self, left, operator, right, position_start=-1, position_end=-1):
|
|
180
|
+
"""Create a BinOp node.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
left: Left-hand subtree (``Number``, ``Variable``, or ``BinOp``).
|
|
184
|
+
operator (str): The operator string (e.g. ``"+"``, ``"*"``).
|
|
185
|
+
right: Right-hand subtree (``Number``, ``Variable``, or ``BinOp``).
|
|
186
|
+
position_start (int): Start index of the operator in the source.
|
|
187
|
+
position_end (int): End index of the operator in the source.
|
|
188
|
+
"""
|
|
189
|
+
self.left = left
|
|
190
|
+
self.operator = operator
|
|
191
|
+
self.right = right
|
|
192
|
+
self.position_start = position_start
|
|
193
|
+
self.position_end = position_end
|
|
194
|
+
|
|
195
|
+
def evaluate(self):
|
|
196
|
+
"""Recursively evaluate both subtrees and apply the binary operator.
|
|
197
|
+
|
|
198
|
+
Arithmetic operators (``+``, ``-``, ``*``, ``/``, ``**``) work on
|
|
199
|
+
``Decimal`` values. Bitwise operators (``&``, ``|``, ``^``, ``<<``,
|
|
200
|
+
``>>``) require integer operands and return ``Decimal`` results.
|
|
201
|
+
The equality operator (``=``) returns a Python ``bool``.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Decimal or bool: The computed result.
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
E.CalculationError: Division by zero (code ``3003``), non-integer
|
|
208
|
+
operands for bitwise ops (code ``3042``), or unknown operator
|
|
209
|
+
(code ``3004``).
|
|
210
|
+
"""
|
|
211
|
+
# Recursively evaluate both child subtrees first (post-order traversal)
|
|
212
|
+
left_value = self.left.evaluate()
|
|
213
|
+
right_value = self.right.evaluate()
|
|
214
|
+
|
|
215
|
+
def check_int(val_l, val_r):
|
|
216
|
+
"""Guard: bitwise operators require both operands to be integers."""
|
|
217
|
+
if val_l % 1 != 0 or val_r % 1 != 0:
|
|
218
|
+
raise E.CalculationError(f"Operator '{self.operator}' requires integers.", code="3042", position_start=self.position_start)
|
|
219
|
+
|
|
220
|
+
# --- Arithmetic operators (Decimal -> Decimal) ---
|
|
221
|
+
if self.operator == '+':
|
|
222
|
+
return left_value + right_value
|
|
223
|
+
|
|
224
|
+
elif self.operator == '-':
|
|
225
|
+
return left_value - right_value
|
|
226
|
+
|
|
227
|
+
# --- Bitwise operators (int -> Decimal) ---
|
|
228
|
+
elif self.operator == '&':
|
|
229
|
+
check_int(left_value, right_value)
|
|
230
|
+
return Decimal(int(left_value) & int(right_value))
|
|
231
|
+
|
|
232
|
+
elif self.operator == '|':
|
|
233
|
+
check_int(left_value, right_value)
|
|
234
|
+
return Decimal(int(left_value) | int(right_value))
|
|
235
|
+
|
|
236
|
+
elif self.operator == '^':
|
|
237
|
+
check_int(left_value, right_value)
|
|
238
|
+
return Decimal(int(left_value) ^ int(right_value))
|
|
239
|
+
|
|
240
|
+
elif self.operator == '<<':
|
|
241
|
+
check_int(left_value, right_value)
|
|
242
|
+
return Decimal(int(left_value) << int(right_value))
|
|
243
|
+
|
|
244
|
+
elif self.operator == '>>':
|
|
245
|
+
check_int(left_value, right_value)
|
|
246
|
+
return Decimal(int(left_value) >> int(right_value))
|
|
247
|
+
|
|
248
|
+
# --- Multiplicative / power operators ---
|
|
249
|
+
elif self.operator == '*':
|
|
250
|
+
return left_value * right_value
|
|
251
|
+
|
|
252
|
+
elif self.operator == '**':
|
|
253
|
+
return left_value ** right_value
|
|
254
|
+
|
|
255
|
+
elif self.operator == '/':
|
|
256
|
+
if right_value == 0:
|
|
257
|
+
raise E.CalculationError("Division by zero", code="3003", position_start=self.position_start)
|
|
258
|
+
return left_value / right_value
|
|
259
|
+
|
|
260
|
+
# --- Equality (returns bool, not Decimal) ---
|
|
261
|
+
elif self.operator == '=':
|
|
262
|
+
return left_value == right_value
|
|
263
|
+
else:
|
|
264
|
+
raise E.CalculationError(f"Unknown operator: {self.operator}", code="3004", position_start=self.position_start)
|
|
265
|
+
|
|
266
|
+
def collect_term(self, var_name):
|
|
267
|
+
"""Collect linear terms on this subtree into ``(factor_of_var, constant)``.
|
|
268
|
+
|
|
269
|
+
Used by the linear equation solver. The subtree is decomposed into
|
|
270
|
+
the form ``factor * var_name + constant``.
|
|
271
|
+
|
|
272
|
+
Supported operators:
|
|
273
|
+
- ``+``, ``-``: factors and constants are added/subtracted
|
|
274
|
+
- ``*``: only ``constant * linear`` is allowed (not ``linear * linear``)
|
|
275
|
+
- ``/``: divisor must be constant (no division by variable)
|
|
276
|
+
- ``**``: always raises (non-linear)
|
|
277
|
+
- ``=``: should never appear inside a subtree
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
var_name: The internal variable name to collect (e.g., ``"var0"``).
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
tuple: ``(factor, constant)`` where ``factor`` is the coefficient
|
|
284
|
+
of *var_name* and ``constant`` is the numeric remainder.
|
|
285
|
+
|
|
286
|
+
Raises:
|
|
287
|
+
E.SyntaxError: Non-linear multiplication (code ``3005``).
|
|
288
|
+
E.SolverError: Division by variable (``3006``), division by
|
|
289
|
+
zero (``3003``), power (``3007``), or ``=``
|
|
290
|
+
inside subtree (``3720``).
|
|
291
|
+
"""
|
|
292
|
+
# ---- Linear decomposition core ----
|
|
293
|
+
# Each subtree is expressed as: factor * var_name + constant
|
|
294
|
+
# where "factor" is the coefficient of the unknown variable and
|
|
295
|
+
# "constant" is the purely numeric part.
|
|
296
|
+
#
|
|
297
|
+
# Example: the subtree (2*x + 3) yields (factor=2, constant=3).
|
|
298
|
+
(left_factor, left_constant) = self.left.collect_term(var_name)
|
|
299
|
+
(right_factor, right_constant) = self.right.collect_term(var_name)
|
|
300
|
+
|
|
301
|
+
# --- Addition: (a*x + b) + (c*x + d) = (a+c)*x + (b+d) ---
|
|
302
|
+
if self.operator == '+':
|
|
303
|
+
return (left_factor + right_factor, left_constant + right_constant)
|
|
304
|
+
|
|
305
|
+
# --- Subtraction: (a*x + b) - (c*x + d) = (a-c)*x + (b-d) ---
|
|
306
|
+
elif self.operator == '-':
|
|
307
|
+
return (left_factor - right_factor, left_constant - right_constant)
|
|
308
|
+
|
|
309
|
+
# --- Multiplication ---
|
|
310
|
+
# (a*x + b) * (c*x + d) is linear ONLY when at most one side
|
|
311
|
+
# contains the variable. If both sides have a non-zero factor
|
|
312
|
+
# the product introduces an x^2 term, which is non-linear.
|
|
313
|
+
elif self.operator == '*':
|
|
314
|
+
if left_factor != 0 and right_factor != 0:
|
|
315
|
+
# Both sides depend on x => x * x = x^2 => non-linear
|
|
316
|
+
raise E.SyntaxError("x^x Error (Non-linear).", code="3005", position_start=self.position_start)
|
|
317
|
+
|
|
318
|
+
elif left_factor == 0:
|
|
319
|
+
# Left side is a pure constant k.
|
|
320
|
+
# k * (c*x + d) = (k*c)*x + (k*d)
|
|
321
|
+
return (left_constant * right_factor, left_constant * right_constant)
|
|
322
|
+
|
|
323
|
+
elif right_factor == 0:
|
|
324
|
+
# Right side is a pure constant k.
|
|
325
|
+
# (a*x + b) * k = (k*a)*x + (k*b)
|
|
326
|
+
return (right_constant * left_factor, right_constant * left_constant)
|
|
327
|
+
|
|
328
|
+
elif left_factor == 0 and right_factor == 0:
|
|
329
|
+
# Both sides are pure constants (no variable at all).
|
|
330
|
+
# 0*x + b * 0*x + d = 0*x + b*d
|
|
331
|
+
return (0, right_constant * left_constant)
|
|
332
|
+
|
|
333
|
+
# --- Division ---
|
|
334
|
+
# (a*x + b) / (c*x + d) is linear only when the divisor is a
|
|
335
|
+
# pure constant (c == 0). Division BY the variable would make
|
|
336
|
+
# the expression non-linear (1/x term).
|
|
337
|
+
elif self.operator == '/':
|
|
338
|
+
if right_factor != 0:
|
|
339
|
+
# Divisor contains the variable => non-linear (e.g. 1/x)
|
|
340
|
+
raise E.SolverError("Non-linear equation (Division by variable).", code="3006", position_start=self.position_start)
|
|
341
|
+
elif right_constant == 0:
|
|
342
|
+
# Division by zero in the constant divisor
|
|
343
|
+
raise E.SolverError("Solver: Division by zero", code="3003", position_start=self.position_start)
|
|
344
|
+
else:
|
|
345
|
+
# Divisor is a non-zero constant k.
|
|
346
|
+
# (a*x + b) / k = (a/k)*x + (b/k)
|
|
347
|
+
return (left_factor / right_constant, left_constant / right_constant)
|
|
348
|
+
|
|
349
|
+
# --- Exponentiation: always non-linear for the solver ---
|
|
350
|
+
elif self.operator == '**':
|
|
351
|
+
raise E.SolverError("Powers are not supported by the linear solver.", code="3007", position_start=self.position_start)
|
|
352
|
+
|
|
353
|
+
# --- Equality inside a subtree should never occur ---
|
|
354
|
+
# The '=' is handled at the top level by the solver, not
|
|
355
|
+
# inside a recursive collect_term call.
|
|
356
|
+
elif self.operator == '=':
|
|
357
|
+
raise E.SolverError("Should not happen: '=' inside collect_terms", code="3720", position_start=self.position_start)
|
|
358
|
+
|
|
359
|
+
else:
|
|
360
|
+
raise E.CalculationError(f"Unknown operator: {self.operator}", code="3004", position_start=self.position_start)
|
|
361
|
+
|
|
362
|
+
def __repr__(self):
|
|
363
|
+
"""Return a human-readable representation of the BinOp tree."""
|
|
364
|
+
return f"BinOp({self.operator!r}, left={self.left}, right={self.right})"
|