mathspec 0.1.0__py3-none-any.whl
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.
- mathspec/__init__.py +59 -0
- mathspec/__main__.py +87 -0
- mathspec/_expression_parser.py +436 -0
- mathspec/_expression_resolver.py +739 -0
- mathspec/_sealed.py +62 -0
- mathspec/_where_parser.py +288 -0
- mathspec/_where_resolver.py +745 -0
- mathspec/_yaml.py +141 -0
- mathspec/advice.py +79 -0
- mathspec/boundedness.py +168 -0
- mathspec/degree.py +209 -0
- mathspec/dimensions.py +377 -0
- mathspec/errors.py +105 -0
- mathspec/exclusivity.py +587 -0
- mathspec/expansion.py +124 -0
- mathspec/lowering.py +302 -0
- mathspec/model.py +905 -0
- mathspec/operators.py +235 -0
- mathspec/piecewise.py +505 -0
- mathspec/program.py +1514 -0
- mathspec/py.typed +0 -0
- mathspec/resolution.py +475 -0
- mathspec/separability.py +185 -0
- mathspec/sos.py +159 -0
- mathspec/typesetting/README.md +110 -0
- mathspec/typesetting/__init__.py +223 -0
- mathspec/typesetting/format.py +273 -0
- mathspec/typesetting/latex.py +139 -0
- mathspec/typesetting/legend.py +286 -0
- mathspec/typesetting/markdown.py +105 -0
- mathspec/typesetting/symbols.py +310 -0
- mathspec/typesetting/typst.py +147 -0
- mathspec/typesetting/walk.py +941 -0
- mathspec/validation.py +338 -0
- mathspec-0.1.0.dist-info/METADATA +247 -0
- mathspec-0.1.0.dist-info/RECORD +38 -0
- mathspec-0.1.0.dist-info/WHEEL +4 -0
- mathspec-0.1.0.dist-info/licenses/LICENSE +21 -0
mathspec/operators.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: mathspec Contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""The closed set of built-in operators and their call shapes.
|
|
6
|
+
|
|
7
|
+
One home for each signature: a composition is a macro, and the set is closed
|
|
8
|
+
to a file.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import TYPE_CHECKING, Literal, NamedTuple
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Iterable
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class Builtin:
|
|
23
|
+
"""The call shape of one built-in operator.
|
|
24
|
+
|
|
25
|
+
Keyword arguments come in four kinds, and the kind decides what resolution
|
|
26
|
+
turns the value into: ``dimension_kwargs`` name a dimension
|
|
27
|
+
(``sum(x, over=generator)``); ``relation_kwargs`` name a relation, which
|
|
28
|
+
carries its own dimensions, so it needs no sibling kwarg;
|
|
29
|
+
``edge_kwargs`` take a closed keyword or a number;
|
|
30
|
+
``required_value_kwargs`` are ordinary values that must be present — a
|
|
31
|
+
number, never a name to resolve (``shift(..., offset=1)``).
|
|
32
|
+
|
|
33
|
+
Every operator takes one positional argument, the expression; every
|
|
34
|
+
dimension or relation it names arrives in a kwarg *value*, which is what
|
|
35
|
+
lets a macro pass one as a formal. ``usage`` is the wording every refusal
|
|
36
|
+
quotes back.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
usage: str
|
|
40
|
+
dimension_kwargs: tuple[str, ...] = ()
|
|
41
|
+
relation_kwargs: tuple[str, ...] = ()
|
|
42
|
+
#: Kwargs naming a column of the relation ``by=`` names — ``over=`` and
|
|
43
|
+
#: ``into=`` — which resolution folds into the direction it is read in.
|
|
44
|
+
role_kwargs: tuple[str, ...] = ()
|
|
45
|
+
#: Kwargs naming a dimension on their own and a column of the relation where
|
|
46
|
+
#: ``by=`` names one. ``sum(x, over=generator)`` reduces the dimension
|
|
47
|
+
#: away; ``sum(x, by=l, over=c)`` names the column the call consumes.
|
|
48
|
+
#: One meaning — what leaves the frame — read in the namespace ``by=``
|
|
49
|
+
#: decides.
|
|
50
|
+
dimension_or_role_kwargs: tuple[str, ...] = ()
|
|
51
|
+
edge_kwargs: tuple[str, ...] = ()
|
|
52
|
+
required_value_kwargs: tuple[str, ...] = ()
|
|
53
|
+
#: Kwargs the call may omit. Their *kind* still comes from the tuples
|
|
54
|
+
#: above — this says only that the operator has an answer without them.
|
|
55
|
+
optional_kwargs: tuple[str, ...] = ()
|
|
56
|
+
#: Kwargs required exactly when the call addresses a relation. A call
|
|
57
|
+
#: names both of its ends and a partition names the columns it groups by,
|
|
58
|
+
#: so that adding a value column to the relation cannot change what an
|
|
59
|
+
#: existing call means.
|
|
60
|
+
with_relation: tuple[str, ...] = ()
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def required(self) -> frozenset[str]:
|
|
64
|
+
"""Every keyword the call must carry."""
|
|
65
|
+
return (
|
|
66
|
+
frozenset(self.dimension_kwargs) | frozenset(self.relation_kwargs) | frozenset(self.required_value_kwargs)
|
|
67
|
+
) - frozenset(self.optional_kwargs)
|
|
68
|
+
|
|
69
|
+
def kind_of(
|
|
70
|
+
self, kwarg: str, *, with_relation: bool = False
|
|
71
|
+
) -> Literal['dimension', 'relation', 'role', 'edge', 'value'] | None:
|
|
72
|
+
"""What resolution turns the value of *kwarg* into, or ``None`` where the operator does not declare it.
|
|
73
|
+
|
|
74
|
+
A dimension, a relation, a column of it, an edge policy, or a plain value.
|
|
75
|
+
*with_relation* says whether the call carries a ``by=``, which is what
|
|
76
|
+
decides the kind of a :attr:`dimension_or_role_kwargs` member.
|
|
77
|
+
"""
|
|
78
|
+
if kwarg in self.dimension_or_role_kwargs:
|
|
79
|
+
return 'role' if with_relation else 'dimension'
|
|
80
|
+
if kwarg in self.dimension_kwargs:
|
|
81
|
+
return 'dimension'
|
|
82
|
+
if kwarg in self.relation_kwargs:
|
|
83
|
+
return 'relation'
|
|
84
|
+
if kwarg in self.role_kwargs:
|
|
85
|
+
return 'role'
|
|
86
|
+
if kwarg in self.edge_kwargs:
|
|
87
|
+
return 'edge'
|
|
88
|
+
if kwarg in self.required_value_kwargs:
|
|
89
|
+
return 'value'
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
#: The closed operator set. ``by=`` is the one keyword that addresses a relation,
|
|
94
|
+
#: and a relation carries its own dimensions, so no sibling kwarg restates them.
|
|
95
|
+
#: On ``shift`` and ``sum_back`` it partitions the axis the operator steps along: it
|
|
96
|
+
#: says which rows are neighbours, not which group a term lands in, and
|
|
97
|
+
#: ``within=`` names the value columns the group is made of, on every call
|
|
98
|
+
#: that names a ``by=``.
|
|
99
|
+
BUILTINS: dict[str, Builtin] = {
|
|
100
|
+
'sum': Builtin(
|
|
101
|
+
'sum(<expr>), sum(<expr>, over=<dim>) or sum(<expr>, by=<relation>, over=<column>, into=<column>)',
|
|
102
|
+
relation_kwargs=('by',),
|
|
103
|
+
role_kwargs=('into',),
|
|
104
|
+
dimension_or_role_kwargs=('over',),
|
|
105
|
+
optional_kwargs=('by',),
|
|
106
|
+
with_relation=('over', 'into'),
|
|
107
|
+
),
|
|
108
|
+
'at': Builtin(
|
|
109
|
+
'at(<expr>, by=<relation>, over=<column>, into=<column>)',
|
|
110
|
+
relation_kwargs=('by',),
|
|
111
|
+
role_kwargs=('over', 'into'),
|
|
112
|
+
with_relation=('over', 'into'),
|
|
113
|
+
),
|
|
114
|
+
'sum_back': Builtin(
|
|
115
|
+
"sum_back(<expr>, along=<dim>, window=<n|parameter>[, edge='wrap'][, by=<relation>, within=<column>])",
|
|
116
|
+
dimension_kwargs=('along',),
|
|
117
|
+
relation_kwargs=('by',),
|
|
118
|
+
role_kwargs=('within',),
|
|
119
|
+
required_value_kwargs=('window',),
|
|
120
|
+
edge_kwargs=('edge',),
|
|
121
|
+
optional_kwargs=('by',),
|
|
122
|
+
with_relation=('within',),
|
|
123
|
+
),
|
|
124
|
+
'shift': Builtin(
|
|
125
|
+
"shift(<expr>, along=<dim>, offset=<n>[, edge='wrap'|<number>][, by=<relation>, within=<column>])",
|
|
126
|
+
dimension_kwargs=('along',),
|
|
127
|
+
relation_kwargs=('by',),
|
|
128
|
+
role_kwargs=('within',),
|
|
129
|
+
required_value_kwargs=('offset',),
|
|
130
|
+
edge_kwargs=('edge',),
|
|
131
|
+
optional_kwargs=('by',),
|
|
132
|
+
with_relation=('within',),
|
|
133
|
+
),
|
|
134
|
+
'dual': Builtin('dual(<constraint>)'),
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
BUILTIN_NAMES = frozenset(BUILTINS)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Amount(NamedTuple):
|
|
141
|
+
"""What the errors of an operator that steps along an axis say about the amount it takes."""
|
|
142
|
+
|
|
143
|
+
#: The word for the amount.
|
|
144
|
+
noun: str
|
|
145
|
+
#: Why negating a named one at the call site is not what the caller means.
|
|
146
|
+
negated: str
|
|
147
|
+
#: What a named one that varies over the axis it steps along becomes.
|
|
148
|
+
varies: str
|
|
149
|
+
#: The least whole number a literal may be.
|
|
150
|
+
minimum: float
|
|
151
|
+
#: What a literal must be written as, after ``operator(kwarg=...)``.
|
|
152
|
+
form: str
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
#: The amount each operator that steps along an axis takes, by operator name.
|
|
156
|
+
AMOUNTS: dict[str, Amount] = {
|
|
157
|
+
'shift': Amount(
|
|
158
|
+
'offset',
|
|
159
|
+
'A named offset carries its sign in its values, so that one row pointing backwards says '
|
|
160
|
+
'so where the data is read — negate the column instead.',
|
|
161
|
+
'a permutation rather than a lag',
|
|
162
|
+
-math.inf,
|
|
163
|
+
'must be a whole number, or the name of an integer parameter when the offset differs per '
|
|
164
|
+
'entity — a lead time, a transit time, a minimum up time.',
|
|
165
|
+
),
|
|
166
|
+
'sum_back': Amount(
|
|
167
|
+
'width',
|
|
168
|
+
'A width counts positions and so has no direction; which way a window reaches is the '
|
|
169
|
+
"operator's own name rather than the sign of its width.",
|
|
170
|
+
'a different window at every position, which is no longer "the last n"',
|
|
171
|
+
1,
|
|
172
|
+
'needs a whole number of positions of at least 1, or the name of an integer parameter when '
|
|
173
|
+
'the window differs per entity. A width of 1 is the operand itself.',
|
|
174
|
+
),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#: The one closed keyword an ``edge=`` accepts. Everything else in that
|
|
178
|
+
#: position is a number: the value the vacated positions contribute.
|
|
179
|
+
EDGE_WRAP = 'wrap'
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def edge_error(name: str, given: str) -> str:
|
|
183
|
+
"""Why an ``edge=`` value is not one the language has."""
|
|
184
|
+
return (
|
|
185
|
+
f'{name}(edge={given}) is not an edge policy.\n'
|
|
186
|
+
f"Write edge='{EDGE_WRAP}' for a cyclic translation, a number for the "
|
|
187
|
+
f'value the vacated positions contribute, or omit it and they are '
|
|
188
|
+
f'absent — which drops the row.'
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def call_shape_error(name: str, positional: int, kwargs: Iterable[str]) -> str | None:
|
|
193
|
+
"""Why a call to *name* does not fit its signature; ``None`` if it fits."""
|
|
194
|
+
builtin = BUILTINS[name]
|
|
195
|
+
keys = set(kwargs)
|
|
196
|
+
optional = {*builtin.edge_kwargs, *builtin.optional_kwargs}
|
|
197
|
+
reads = bool(keys & set(builtin.relation_kwargs))
|
|
198
|
+
required = builtin.required | frozenset(builtin.with_relation) if reads else builtin.required
|
|
199
|
+
optional |= set() if reads else set(builtin.with_relation)
|
|
200
|
+
if reads and (unsaid := sorted(frozenset(builtin.with_relation) - keys)):
|
|
201
|
+
return unsaid_ends_error(name, unsaid)
|
|
202
|
+
fits = positional == 1 and keys - optional == required
|
|
203
|
+
return None if fits else f'{name}() expects {builtin.usage}'
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
#: Why a partition writes ``within=`` whenever it writes ``by=``; ``position()``
|
|
207
|
+
#: in a where string says the same, so the sentence has one home.
|
|
208
|
+
PARTITION_NAMES_ITS_GROUP = (
|
|
209
|
+
'A partition names the value columns it groups by, so that a relation may gain a value '
|
|
210
|
+
'column without changing what this call means.'
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def unsaid_ends_error(name: str, unsaid: list[str]) -> str:
|
|
215
|
+
"""Why a call through a relation has to write every column it reads: both ends of a read, the group of a partition."""
|
|
216
|
+
reason = (
|
|
217
|
+
PARTITION_NAMES_ITS_GROUP
|
|
218
|
+
if 'within' in BUILTINS[name].with_relation
|
|
219
|
+
else 'A call names both of its ends, so that a relation may gain a value column without changing what this call means.'
|
|
220
|
+
)
|
|
221
|
+
return (
|
|
222
|
+
f'{name}() through a relation leaves {", ".join(f"{k}=" for k in unsaid)} unsaid.\n'
|
|
223
|
+
f'{reason}\n'
|
|
224
|
+
f'Write: {BUILTINS[name].usage}'
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def unknown_operator_message(name: str) -> str:
|
|
229
|
+
"""The one wording for "that is not an operator"."""
|
|
230
|
+
return (
|
|
231
|
+
f"Unknown operator '{name}'.\n"
|
|
232
|
+
f'Available: {sorted(BUILTIN_NAMES)}\n'
|
|
233
|
+
f"Define '{name}' as a macro under 'macros:' if it composes built-ins. "
|
|
234
|
+
f'A file cannot add an operator: see docs/about/limits.md.'
|
|
235
|
+
)
|