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/_sealed.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: mathspec Contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""The seal on what the language hands out — a mapping nothing can write to, which pickles."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from typing import TYPE_CHECKING, override
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Iterator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Sealed[K, V](Mapping[K, V]):
|
|
17
|
+
"""A mapping nothing can write to, which pickles.
|
|
18
|
+
|
|
19
|
+
What every group a program hands out is held behind, and a node's
|
|
20
|
+
keyword arguments with them: a consumer cannot rewrite what another
|
|
21
|
+
consumer reads, and the whole crosses a process — which is the one thing
|
|
22
|
+
:class:`types.MappingProxyType` cannot do. Equal to any mapping with the
|
|
23
|
+
same items, and hashable over them where they are, so a dataclass may
|
|
24
|
+
hold one as a default.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
__slots__ = ('_items',)
|
|
28
|
+
|
|
29
|
+
def __init__(self, items: Mapping[K, V]) -> None:
|
|
30
|
+
self._items = dict(items)
|
|
31
|
+
|
|
32
|
+
@override
|
|
33
|
+
def __getitem__(self, key: K) -> V:
|
|
34
|
+
return self._items[key]
|
|
35
|
+
|
|
36
|
+
@override
|
|
37
|
+
def __iter__(self) -> Iterator[K]:
|
|
38
|
+
return iter(self._items)
|
|
39
|
+
|
|
40
|
+
@override
|
|
41
|
+
def __len__(self) -> int:
|
|
42
|
+
return len(self._items)
|
|
43
|
+
|
|
44
|
+
@override
|
|
45
|
+
def __hash__(self) -> int:
|
|
46
|
+
return hash(frozenset(self._items.items()))
|
|
47
|
+
|
|
48
|
+
def __or__(self, other: Mapping[K, V]) -> dict[K, V]:
|
|
49
|
+
"""A plain dict of both, the other's entries winning — what the proxy answered ``|`` with."""
|
|
50
|
+
return {**self._items, **other}
|
|
51
|
+
|
|
52
|
+
def __ror__(self, other: Mapping[K, V]) -> dict[K, V]:
|
|
53
|
+
return {**other, **self._items}
|
|
54
|
+
|
|
55
|
+
def __repr__(self) -> str:
|
|
56
|
+
return f'{type(self).__name__}({self._items!r})'
|
|
57
|
+
|
|
58
|
+
def __getstate__(self) -> dict[K, V]:
|
|
59
|
+
return self._items
|
|
60
|
+
|
|
61
|
+
def __setstate__(self, items: dict[K, V]) -> None:
|
|
62
|
+
self._items = items
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: mathspec Contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""The where-string grammar and the ``Unresolved*`` nodes it emits, package-private.
|
|
6
|
+
|
|
7
|
+
A where string is a boolean algebra over comparisons, and a comparison's
|
|
8
|
+
sides are the expression grammar's own arithmetic. What a side *is* — a
|
|
9
|
+
parameter, a dimension, a relation column, a ``position()`` — only the schema
|
|
10
|
+
knows, so the grammar hands both sides over bare and
|
|
11
|
+
:mod:`mathspec.resolution` reads them. The resolved vocabulary lives in
|
|
12
|
+
:mod:`mathspec.program`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from functools import lru_cache
|
|
19
|
+
from typing import TYPE_CHECKING, cast, get_args
|
|
20
|
+
|
|
21
|
+
import pyparsing as pp
|
|
22
|
+
|
|
23
|
+
from mathspec._expression_parser import (
|
|
24
|
+
ARITHMETIC,
|
|
25
|
+
NAME,
|
|
26
|
+
ArithmeticNode,
|
|
27
|
+
KeywordNode,
|
|
28
|
+
NameNode,
|
|
29
|
+
children,
|
|
30
|
+
keywords,
|
|
31
|
+
parse_text,
|
|
32
|
+
)
|
|
33
|
+
from mathspec._sealed import Sealed
|
|
34
|
+
from mathspec.program import (
|
|
35
|
+
And,
|
|
36
|
+
BooleanLiteral,
|
|
37
|
+
Connective,
|
|
38
|
+
Not,
|
|
39
|
+
Or,
|
|
40
|
+
Predicate,
|
|
41
|
+
PredicateOperator,
|
|
42
|
+
where_children,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from collections.abc import Callable, Mapping
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# AST nodes
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class ColumnNode:
|
|
55
|
+
"""``relation.column`` on a side of a comparison — the one place the language names a column."""
|
|
56
|
+
|
|
57
|
+
relation: str
|
|
58
|
+
column: str
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def shown(self) -> str:
|
|
62
|
+
"""The column as the file wrote it, for an error message."""
|
|
63
|
+
return f'{self.relation}.{self.column}'
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class UnresolvedPredicateCallNode:
|
|
68
|
+
"""``<name>(<predicate>[, <kwarg>…])`` — an operator reading a predicate rather than arithmetic.
|
|
69
|
+
|
|
70
|
+
The expression grammar cannot carry this shape: its call rule takes
|
|
71
|
+
arithmetic arguments, and a predicate is not arithmetic. So the where
|
|
72
|
+
grammar reads it, and resolution decides which operator the name is and
|
|
73
|
+
what the kwargs mean. ``kwargs`` is held and hashed as
|
|
74
|
+
:class:`~mathspec._expression_parser.FunctionCallNode` holds its own.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
name: str
|
|
78
|
+
operand: Predicate | UnresolvedWhereNode
|
|
79
|
+
kwargs: Mapping[str, ArithmeticNode] = field(default_factory=dict, hash=False)
|
|
80
|
+
|
|
81
|
+
def __post_init__(self) -> None:
|
|
82
|
+
object.__setattr__(self, 'kwargs', Sealed(self.kwargs))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class UnresolvedCountNode:
|
|
87
|
+
"""``count(<predicate>, over=<dim>) <op> <number>`` — a count against a literal.
|
|
88
|
+
|
|
89
|
+
Its own node rather than an :class:`UnresolvedComparisonNode` with a call
|
|
90
|
+
on the left: every other comparison has arithmetic on both sides, and
|
|
91
|
+
widening that one to carry a predicate would widen every reader of a side
|
|
92
|
+
with it.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
call: UnresolvedPredicateCallNode
|
|
96
|
+
op: PredicateOperator
|
|
97
|
+
value: ArithmeticNode
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class UnresolvedComparisonNode:
|
|
102
|
+
"""``side <op> side`` before the sides are read; ``_where_resolver.py`` decides what each is.
|
|
103
|
+
|
|
104
|
+
A side is the expression grammar's arithmetic, so a name, a number and a
|
|
105
|
+
``position(...)`` call all arrive as the nodes an expression would carry
|
|
106
|
+
them in, a quoted label as the :class:`KeywordNode` a quoted kwarg is, and a
|
|
107
|
+
relation column in a node of its own.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
left: ArithmeticNode | ColumnNode
|
|
111
|
+
op: PredicateOperator
|
|
112
|
+
right: ArithmeticNode | ColumnNode | KeywordNode
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
#: What resolution rewrites away on the where side — the nodes whose leaves
|
|
116
|
+
#: are still names the schema has not been asked about.
|
|
117
|
+
UnresolvedWhereNode = NameNode | UnresolvedComparisonNode | UnresolvedPredicateCallNode | UnresolvedCountNode
|
|
118
|
+
|
|
119
|
+
#: Every node a parsed where string is built of: the connectives and literals,
|
|
120
|
+
#: the unresolved leaves, and the arithmetic and the two side nodes under a
|
|
121
|
+
#: comparison. What the depth measurement walks.
|
|
122
|
+
ParsedWhere = Predicate | UnresolvedWhereNode | ArithmeticNode | ColumnNode | KeywordNode
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# Grammar
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _predicate_call(tokens: pp.ParseResults) -> UnresolvedPredicateCallNode:
|
|
131
|
+
"""The call node, with a keyword given twice refused as the arithmetic grammar refuses it."""
|
|
132
|
+
name, operand, *pairs = tokens
|
|
133
|
+
return UnresolvedPredicateCallNode(name, operand, keywords(name, pairs))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _build_where_grammar() -> pp.ParserElement:
|
|
137
|
+
"""Build the pyparsing grammar for where strings.
|
|
138
|
+
|
|
139
|
+
Both quote characters are accepted because YAML already owns one of them.
|
|
140
|
+
``NOT`` binds tightest, then ``AND``, then ``OR``. A comparison is tried
|
|
141
|
+
before a bare name, since its left side begins with one.
|
|
142
|
+
"""
|
|
143
|
+
where_expr = pp.Forward()
|
|
144
|
+
|
|
145
|
+
true_lit = pp.CaselessKeyword('True').set_parse_action(lambda: BooleanLiteral(True))
|
|
146
|
+
false_lit = pp.CaselessKeyword('False').set_parse_action(lambda: BooleanLiteral(False))
|
|
147
|
+
|
|
148
|
+
name = pp.Regex(NAME)
|
|
149
|
+
# pyrefly: ignore[implicit-any-lambda]
|
|
150
|
+
column = pp.Regex(rf'({NAME})\.({NAME})').set_parse_action(lambda t: ColumnNode(*t[0].split('.')))
|
|
151
|
+
quoted = (pp.QuotedString("'", esc_char='\\') | pp.QuotedString('"', esc_char='\\')).set_parse_action(
|
|
152
|
+
# pyrefly: ignore[implicit-any-lambda]
|
|
153
|
+
lambda t: KeywordNode(t[0])
|
|
154
|
+
)
|
|
155
|
+
comparator = pp.one_of(list(get_args(PredicateOperator)))
|
|
156
|
+
|
|
157
|
+
kwarg = (name + pp.Suppress('=') + (quoted | ARITHMETIC)).set_parse_action(lambda t: (t[0], t[1]))
|
|
158
|
+
|
|
159
|
+
def _call(head: pp.ParserElement) -> pp.ParserElement:
|
|
160
|
+
"""``<head>(<predicate>[, <kwarg>…])`` — the one shape whose operand is a predicate.
|
|
161
|
+
|
|
162
|
+
``count`` is spelled in the grammar rather than left to resolution, as
|
|
163
|
+
``position`` is, because only the grammar can decide to read its
|
|
164
|
+
argument as a predicate. Every other predicate-taking call stands where
|
|
165
|
+
arithmetic cannot, so the comparison above it has already been tried
|
|
166
|
+
and the name is free.
|
|
167
|
+
"""
|
|
168
|
+
return (
|
|
169
|
+
head + pp.Suppress('(') + where_expr + pp.ZeroOrMore(pp.Suppress(',') + kwarg) + pp.Suppress(')')
|
|
170
|
+
).set_parse_action(_predicate_call)
|
|
171
|
+
|
|
172
|
+
count_call = _call(pp.CaselessKeyword('count'))
|
|
173
|
+
predicate_call = _call(name.copy())
|
|
174
|
+
|
|
175
|
+
count_comparison = (count_call + comparator + ARITHMETIC).set_parse_action(
|
|
176
|
+
# pyrefly: ignore[implicit-any-lambda]
|
|
177
|
+
lambda t: UnresolvedCountNode(t[0], t[1], t[2])
|
|
178
|
+
)
|
|
179
|
+
comparison = (
|
|
180
|
+
(column | ARITHMETIC) + comparator + (quoted | column | ARITHMETIC)
|
|
181
|
+
# pyrefly: ignore[implicit-any-lambda]
|
|
182
|
+
).set_parse_action(lambda t: UnresolvedComparisonNode(t[0], t[1], t[2]))
|
|
183
|
+
# pyrefly: ignore[implicit-any-lambda]
|
|
184
|
+
existence = name.copy().set_parse_action(lambda t: NameNode(t[0]))
|
|
185
|
+
|
|
186
|
+
atom = (
|
|
187
|
+
true_lit
|
|
188
|
+
| false_lit
|
|
189
|
+
| count_comparison
|
|
190
|
+
| comparison
|
|
191
|
+
| predicate_call
|
|
192
|
+
| existence
|
|
193
|
+
| (pp.Suppress('(') + where_expr + pp.Suppress(')'))
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
NOT = pp.CaselessKeyword('NOT').suppress()
|
|
197
|
+
# pyrefly: ignore[implicit-any-lambda]
|
|
198
|
+
not_expr = (NOT + atom).set_parse_action(lambda t: Not(t[0])) | atom
|
|
199
|
+
|
|
200
|
+
AND = pp.CaselessKeyword('AND').suppress()
|
|
201
|
+
and_expr = not_expr + pp.ZeroOrMore(AND + not_expr)
|
|
202
|
+
and_expr.set_parse_action(_folder(And))
|
|
203
|
+
|
|
204
|
+
OR = pp.CaselessKeyword('OR').suppress()
|
|
205
|
+
or_expr = and_expr + pp.ZeroOrMore(OR + and_expr)
|
|
206
|
+
or_expr.set_parse_action(_folder(Or))
|
|
207
|
+
|
|
208
|
+
where_expr <<= or_expr
|
|
209
|
+
return where_expr
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _folder(node_type: type[And] | type[Or]) -> Callable[[pp.ParseResults], Predicate | UnresolvedWhereNode]:
|
|
213
|
+
"""A parse action left-folding a flat operator chain into *node_type*."""
|
|
214
|
+
|
|
215
|
+
def fold(tokens: pp.ParseResults) -> Predicate | UnresolvedWhereNode:
|
|
216
|
+
items: list[Predicate | UnresolvedWhereNode] = list(tokens)
|
|
217
|
+
result = items[0]
|
|
218
|
+
for item in items[1:]:
|
|
219
|
+
result = node_type(cast('Predicate', result), cast('Predicate', item))
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
return fold
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
_WHERE_GRAMMAR = _build_where_grammar()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _named_rewrite(text: str, loc: int) -> str | None:
|
|
229
|
+
"""The rewrite for a connective habit of pandas or C at the token where the grammar gave up, or ``None``.
|
|
230
|
+
|
|
231
|
+
``!=``, ``<`` and ``>`` are legal here, so only the tokens no predicate
|
|
232
|
+
admits are diagnosed.
|
|
233
|
+
"""
|
|
234
|
+
rest = text[loc:].lstrip()
|
|
235
|
+
if rest.startswith('&'):
|
|
236
|
+
return "'&' is not the conjunction — both predicates at once is written AND."
|
|
237
|
+
if rest.startswith('|'):
|
|
238
|
+
return "'|' is not the disjunction — either predicate is written OR."
|
|
239
|
+
if rest.startswith(('~', '!')) and not rest.startswith('!='):
|
|
240
|
+
return f"'{rest[0]}' is not the negation — it is written NOT, before the predicate."
|
|
241
|
+
if rest.startswith('=') and not rest.startswith('=='):
|
|
242
|
+
return "'=' compares nothing — equality is written ==."
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
#: The rewrite an over-deep where string is given. A long chain of predicates
|
|
247
|
+
#: is a test the file could carry as data instead, which is the language's own
|
|
248
|
+
#: answer before the general one.
|
|
249
|
+
_DEEP_REWRITE = (
|
|
250
|
+
'Declare a parameter or relation carrying part of the test and name that here, or split the '
|
|
251
|
+
'declaration into two, each masked by one half.'
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def nested(node: ParsedWhere) -> tuple[ParsedWhere, ...]:
|
|
256
|
+
"""What a where string nests through: a connective's operands, a comparison's sides, and a call's predicate."""
|
|
257
|
+
if isinstance(node, UnresolvedCountNode):
|
|
258
|
+
return (node.call, node.value)
|
|
259
|
+
if isinstance(node, UnresolvedPredicateCallNode):
|
|
260
|
+
return (node.operand, *node.kwargs.values())
|
|
261
|
+
if isinstance(node, UnresolvedComparisonNode):
|
|
262
|
+
return (node.left, node.right)
|
|
263
|
+
if isinstance(node, ArithmeticNode):
|
|
264
|
+
return children(node)
|
|
265
|
+
if isinstance(node, Connective):
|
|
266
|
+
return where_children(node)
|
|
267
|
+
return ()
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@lru_cache(maxsize=4096)
|
|
271
|
+
def parse_where(text: str) -> Predicate | UnresolvedWhereNode:
|
|
272
|
+
"""Parse a where string into an AST, its leaves still unresolved.
|
|
273
|
+
|
|
274
|
+
The connectives and literals are the resolved vocabulary's own; the leaves
|
|
275
|
+
naming declarations are a bare :class:`NameNode` or an ``Unresolved*``
|
|
276
|
+
node, which only :func:`~mathspec.resolution.resolve_where` takes.
|
|
277
|
+
|
|
278
|
+
Raises:
|
|
279
|
+
SchemaError: If *text* is not a where string of the language. A
|
|
280
|
+
predictable mistake — ``&``/``|``/``~``/``!`` for a connective, a
|
|
281
|
+
lone ``=`` — is named with its rewrite beside the grammar's own
|
|
282
|
+
complaint. A side nesting past what an expression may is refused
|
|
283
|
+
as an expression is.
|
|
284
|
+
"""
|
|
285
|
+
return cast(
|
|
286
|
+
'Predicate | UnresolvedWhereNode',
|
|
287
|
+
parse_text(_WHERE_GRAMMAR, text, 'where string', _named_rewrite, nested, _DEEP_REWRITE),
|
|
288
|
+
)
|