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/expansion.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: mathspec Contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""Macro calls, expanded into the syntax tree before resolution reads it.
|
|
6
|
+
|
|
7
|
+
A named expression is resolution's: it resolves the entry once and puts that
|
|
8
|
+
node where the name stood.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING, overload
|
|
14
|
+
|
|
15
|
+
from mathspec._expression_parser import (
|
|
16
|
+
ArithmeticNode,
|
|
17
|
+
ComparisonNode,
|
|
18
|
+
FunctionCallNode,
|
|
19
|
+
NameNode,
|
|
20
|
+
ParsedNode,
|
|
21
|
+
parse_expression,
|
|
22
|
+
with_children,
|
|
23
|
+
)
|
|
24
|
+
from mathspec.errors import SchemaError
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from mathspec.model import MacroBlock
|
|
28
|
+
from mathspec.resolution import Namespace
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_and_expand(text: str, ns: Namespace, context: str) -> ParsedNode:
|
|
32
|
+
"""Parse *text* and expand every macro call in it.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
text: The expression as the file wrote it.
|
|
36
|
+
ns: Where the macros are declared.
|
|
37
|
+
context: What an error names.
|
|
38
|
+
"""
|
|
39
|
+
return expand(parse_expression(text), ns, context)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@overload
|
|
43
|
+
def expand(node: ArithmeticNode, ns: Namespace, context: str) -> ArithmeticNode: ...
|
|
44
|
+
@overload
|
|
45
|
+
def expand(node: ComparisonNode, ns: Namespace, context: str) -> ComparisonNode: ...
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def expand(node: ParsedNode, ns: Namespace, context: str) -> ParsedNode:
|
|
49
|
+
"""Expand every macro call under *node*; a comparison stays a comparison and arithmetic stays arithmetic.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
node: The parsed expression.
|
|
53
|
+
ns: Where the macros are declared.
|
|
54
|
+
context: What an error names.
|
|
55
|
+
"""
|
|
56
|
+
if isinstance(node, ComparisonNode):
|
|
57
|
+
return ComparisonNode(node.op, _expand(node.left, ns, context, ()), _expand(node.right, ns, context, ()))
|
|
58
|
+
return _expand(node, ns, context, ())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def macro_signature(name: str, macro: MacroBlock) -> str:
|
|
62
|
+
"""Human-readable call signature, for error messages."""
|
|
63
|
+
parts = [*macro.args, *(f'{k}=...' for k in macro.kwargs)]
|
|
64
|
+
return f'{name}({", ".join(parts)})'
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def parse_template(name: str, macro: MacroBlock, context: str) -> ArithmeticNode:
|
|
68
|
+
"""Parse a macro template, rejecting comparisons."""
|
|
69
|
+
body = parse_expression(macro.template)
|
|
70
|
+
if isinstance(body, ComparisonNode):
|
|
71
|
+
msg = f"{context}: macro '{name}' template must not contain a comparison operator. Got: {macro.template!r}"
|
|
72
|
+
raise SchemaError(msg)
|
|
73
|
+
return body
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _expand(node: ArithmeticNode, ns: Namespace, context: str, stack: tuple[str, ...]) -> ArithmeticNode:
|
|
77
|
+
"""*node* with every macro call under it substituted; *stack* is the macros this walk is inside.
|
|
78
|
+
|
|
79
|
+
A name a template reads is checked against the entries being resolved
|
|
80
|
+
here, where the macros it came through are known, so a cycle closed
|
|
81
|
+
through a macro is reported with the macros in its chain.
|
|
82
|
+
"""
|
|
83
|
+
if isinstance(node, NameNode) and stack and (refusal := ns.cycle(node.name, context, stack)) is not None:
|
|
84
|
+
raise SchemaError(refusal)
|
|
85
|
+
if isinstance(node, FunctionCallNode) and node.name in ns.schema.macros:
|
|
86
|
+
if node.name in stack:
|
|
87
|
+
msg = f'{context}: circular macro reference: {" -> ".join([*stack, node.name])}'
|
|
88
|
+
raise SchemaError(msg)
|
|
89
|
+
return _expand_macro(node, ns, context, stack)
|
|
90
|
+
return with_children(node, lambda child: _expand(child, ns, context, stack))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _expand_macro(call: FunctionCallNode, ns: Namespace, context: str, stack: tuple[str, ...]) -> ArithmeticNode:
|
|
94
|
+
"""Call-by-value: arguments are expanded before substitution, and the substituted body is expanded again."""
|
|
95
|
+
macro = ns.schema.macros[call.name]
|
|
96
|
+
signature = macro_signature(call.name, macro)
|
|
97
|
+
if len(call.args) != len(macro.args):
|
|
98
|
+
msg = (
|
|
99
|
+
f"{context}: macro '{call.name}' expects {len(macro.args)} "
|
|
100
|
+
f'positional argument(s), got {len(call.args)}. Signature: {signature}'
|
|
101
|
+
)
|
|
102
|
+
raise SchemaError(msg)
|
|
103
|
+
if set(call.kwargs) != set(macro.kwargs):
|
|
104
|
+
msg = (
|
|
105
|
+
f"{context}: macro '{call.name}' expects keyword argument(s) "
|
|
106
|
+
f'{sorted(macro.kwargs)}, got {sorted(call.kwargs)}. '
|
|
107
|
+
f'Signature: {signature}'
|
|
108
|
+
)
|
|
109
|
+
raise SchemaError(msg)
|
|
110
|
+
|
|
111
|
+
bindings = {
|
|
112
|
+
**{formal: _expand(arg, ns, context, stack) for formal, arg in zip(macro.args, call.args, strict=True)},
|
|
113
|
+
**{formal: _expand(call.kwargs[formal], ns, context, stack) for formal in macro.kwargs},
|
|
114
|
+
}
|
|
115
|
+
body = parse_template(call.name, macro, context)
|
|
116
|
+
substituted = _substitute(body, bindings)
|
|
117
|
+
return _expand(substituted, ns, context, (*stack, call.name))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _substitute(node: ArithmeticNode, bindings: dict[str, ArithmeticNode]) -> ArithmeticNode:
|
|
121
|
+
"""Replace formal-name NameNodes in *node* with their bound subtrees."""
|
|
122
|
+
if isinstance(node, NameNode) and node.name in bindings:
|
|
123
|
+
return bindings[node.name]
|
|
124
|
+
return with_children(node, lambda child: _substitute(child, bindings))
|
mathspec/lowering.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: mathspec Contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""Lower a model to a :class:`~mathspec.program.Program` — the pass that decides every expression.
|
|
6
|
+
|
|
7
|
+
One lowering, on the language side, run when a :class:`~mathspec.model.Spec`
|
|
8
|
+
loads: it reads every expression and where string into the program's own
|
|
9
|
+
nodes, checks every rule decidable without data, and packages the
|
|
10
|
+
declarations, section for section. The program mirrors the model it was
|
|
11
|
+
lowered from: a ``piecewise:`` block the model still declares is a curve on
|
|
12
|
+
the program, and :meth:`~mathspec.model.Spec.expand` is what writes it out
|
|
13
|
+
as rows.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from mathspec.dimensions import check_schema, dims_of
|
|
21
|
+
from mathspec.errors import SchemaError, prefixed
|
|
22
|
+
from mathspec.expansion import expand, parse_template
|
|
23
|
+
from mathspec.piecewise import assumptions_of, curve_frame, lp_domain_refusal, resolve_links
|
|
24
|
+
from mathspec.program import (
|
|
25
|
+
Assumption,
|
|
26
|
+
BooleanLiteral,
|
|
27
|
+
Cases,
|
|
28
|
+
Constant,
|
|
29
|
+
ConstraintDeclaration,
|
|
30
|
+
DimensionDeclaration,
|
|
31
|
+
ExpressionDeclaration,
|
|
32
|
+
Link,
|
|
33
|
+
Mask,
|
|
34
|
+
Named,
|
|
35
|
+
ObjectiveDeclaration,
|
|
36
|
+
Parameter,
|
|
37
|
+
ParameterDeclaration,
|
|
38
|
+
PiecewiseDeclaration,
|
|
39
|
+
Program,
|
|
40
|
+
SosDeclaration,
|
|
41
|
+
VariableDeclaration,
|
|
42
|
+
VariableDefined,
|
|
43
|
+
walk,
|
|
44
|
+
)
|
|
45
|
+
from mathspec.resolution import (
|
|
46
|
+
Namespace,
|
|
47
|
+
mask_of,
|
|
48
|
+
resolve_constraint_text,
|
|
49
|
+
resolve_expression,
|
|
50
|
+
resolve_expression_text,
|
|
51
|
+
resolve_where_text,
|
|
52
|
+
)
|
|
53
|
+
from mathspec.validation import emitted_name_errors, reference_errors
|
|
54
|
+
|
|
55
|
+
if TYPE_CHECKING:
|
|
56
|
+
from mathspec.model import AssumptionBlock, Spec
|
|
57
|
+
from mathspec.program import Expression
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def lower(schema: Spec) -> Program:
|
|
61
|
+
"""Lower *schema*'s own declarations, checking every rule decidable without data.
|
|
62
|
+
|
|
63
|
+
What is checked:
|
|
64
|
+
|
|
65
|
+
- every rule one declaration is held to against the others
|
|
66
|
+
(:func:`~mathspec.validation.reference_errors`), before any expression
|
|
67
|
+
is read, since resolution assumes each of them;
|
|
68
|
+
- the expression parses, and constraints hold exactly one comparison where
|
|
69
|
+
objectives hold none;
|
|
70
|
+
- every referenced name resolves, and every operator is a built-in whose
|
|
71
|
+
dimension arguments name declared dimensions;
|
|
72
|
+
- where strings parse *and* resolve — an unknown name there is an error,
|
|
73
|
+
not a silently-empty mask;
|
|
74
|
+
- macro formals may shadow model names but not a declared dimension, since
|
|
75
|
+
``over=snapshot`` under a formal ``snapshot`` cannot say which it means;
|
|
76
|
+
- no name a set or curve writes out is one the file declares
|
|
77
|
+
(:func:`~mathspec.validation.emitted_name_errors`), read off the
|
|
78
|
+
curve as lowered;
|
|
79
|
+
- every dim rule (``dimensions.check_schema``), once names resolve.
|
|
80
|
+
|
|
81
|
+
A ``piecewise:`` block's links are resolved and its frame checked here, on
|
|
82
|
+
the link the file wrote, so the expansion writes rows the language has
|
|
83
|
+
already held to every rule; what its method assumes of the breakpoints
|
|
84
|
+
stands under the program's assumptions with the file's own, so a model
|
|
85
|
+
states what it assumes whether or not its curves are written out.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The program of what *schema* declares, section for section.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
SchemaError: Listing every problem found, one per line. A name a set
|
|
92
|
+
or curve writes that the file declares is listed once every other
|
|
93
|
+
problem is gone, since it is read off the curve as lowered.
|
|
94
|
+
DimensionError: The first dim rule a declaration breaks, once every
|
|
95
|
+
name resolves.
|
|
96
|
+
"""
|
|
97
|
+
errors = reference_errors(schema)
|
|
98
|
+
if errors:
|
|
99
|
+
raise SchemaError('\n'.join(errors))
|
|
100
|
+
|
|
101
|
+
ns = Namespace(schema)
|
|
102
|
+
for mname, macro in schema.macros.items():
|
|
103
|
+
context = f"Macro '{mname}'"
|
|
104
|
+
formals = frozenset((*macro.args, *macro.kwargs))
|
|
105
|
+
try:
|
|
106
|
+
body_ast = expand(parse_template(mname, macro, context), ns, context)
|
|
107
|
+
except ValueError as e:
|
|
108
|
+
errors.append(prefixed(context, e))
|
|
109
|
+
continue
|
|
110
|
+
errors.extend(
|
|
111
|
+
f"{context}: formal '{f}' collides with declared dimension '{f}'. "
|
|
112
|
+
f'Rename the formal — a dimension name inside a template is '
|
|
113
|
+
f'ambiguous with the dimension itself.'
|
|
114
|
+
for f in sorted(formals & ns.dimensions)
|
|
115
|
+
)
|
|
116
|
+
resolve_expression(body_ast, ns, context, errors, formals=formals)
|
|
117
|
+
|
|
118
|
+
entries: dict[str, Named] = {}
|
|
119
|
+
for ename in schema.expressions:
|
|
120
|
+
node, refusals = ns.named_entry(ename)
|
|
121
|
+
errors.extend(refusals)
|
|
122
|
+
if node is not None:
|
|
123
|
+
entries[ename] = node
|
|
124
|
+
|
|
125
|
+
variables = {}
|
|
126
|
+
for vname, vdef in schema.variables.items():
|
|
127
|
+
where = resolve_where_text(vdef.where, ns, f"Variable '{vname}'", errors, self_variable=vname)
|
|
128
|
+
if vdef.domain == 'binary':
|
|
129
|
+
lower_bound, upper_bound = Constant(0.0), Constant(1.0)
|
|
130
|
+
else:
|
|
131
|
+
lower_bound, upper_bound = _bound(vdef.bounds.lower), _bound(vdef.bounds.upper)
|
|
132
|
+
variables[vname] = VariableDeclaration(
|
|
133
|
+
tuple(vdef.dims),
|
|
134
|
+
where=mask_of(where),
|
|
135
|
+
lower=lower_bound,
|
|
136
|
+
upper=upper_bound,
|
|
137
|
+
domain=vdef.domain,
|
|
138
|
+
absence=vdef.absence,
|
|
139
|
+
description=vdef.description,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
constraints: dict[str, ConstraintDeclaration] = {}
|
|
143
|
+
for cname, cdef in schema.constraints.items():
|
|
144
|
+
context = f"Constraint '{cname}'"
|
|
145
|
+
where = resolve_where_text(cdef.where, ns, context, errors)
|
|
146
|
+
if (sides := resolve_constraint_text(cdef.expression, ns, context, errors)) is not None:
|
|
147
|
+
lhs, sense, rhs = sides
|
|
148
|
+
constraints[cname] = ConstraintDeclaration(
|
|
149
|
+
tuple(cdef.dims), lhs, sense, rhs, mask_of(where), description=cdef.description
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
objective = None
|
|
153
|
+
if schema.objective is not None:
|
|
154
|
+
expression = resolve_expression_text(schema.objective.expression, ns, 'The objective', errors, ceiling=2)
|
|
155
|
+
if expression is not None:
|
|
156
|
+
objective = ObjectiveDeclaration(schema.objective.sense, expression, schema.objective.description)
|
|
157
|
+
|
|
158
|
+
assumptions: dict[str, Assumption] = {}
|
|
159
|
+
for aname, adef in schema.assumptions.items():
|
|
160
|
+
if (assumption := _assumption(aname, adef, ns, errors)) is not None:
|
|
161
|
+
assumptions[aname] = assumption
|
|
162
|
+
|
|
163
|
+
curves: dict[str, tuple[Expression, ...]] = {}
|
|
164
|
+
for pname, pdef in schema.piecewise.items():
|
|
165
|
+
links = resolve_links(pname, pdef, ns, errors)
|
|
166
|
+
if links is None:
|
|
167
|
+
continue
|
|
168
|
+
if pdef.method == 'lp' and (refusal := lp_domain_refusal(pname, pdef, links)) is not None:
|
|
169
|
+
errors.append(refusal)
|
|
170
|
+
curves[pname] = links
|
|
171
|
+
|
|
172
|
+
if errors:
|
|
173
|
+
raise SchemaError('\n'.join(errors))
|
|
174
|
+
|
|
175
|
+
roots = [side for c in constraints.values() for side in (c.lhs, c.rhs)]
|
|
176
|
+
if objective is not None:
|
|
177
|
+
roots.append(objective.expression)
|
|
178
|
+
roots.extend(link for links in curves.values() for link in links)
|
|
179
|
+
in_math = frozenset(node.name for node in walk(*roots) if isinstance(node, Named))
|
|
180
|
+
|
|
181
|
+
piecewise = {}
|
|
182
|
+
for pname, links in curves.items():
|
|
183
|
+
pdef = schema.piecewise[pname]
|
|
184
|
+
piecewise[pname] = PiecewiseDeclaration(
|
|
185
|
+
over=pdef.over,
|
|
186
|
+
links=tuple(Link(node, link.values, link.sign) for node, link in zip(links, pdef.links, strict=True)),
|
|
187
|
+
method=pdef.method,
|
|
188
|
+
frame=curve_frame(schema, pname, pdef, links),
|
|
189
|
+
activity=pdef.activity,
|
|
190
|
+
points=pdef.points,
|
|
191
|
+
description=pdef.description,
|
|
192
|
+
)
|
|
193
|
+
for aname, assumed in assumptions_of(pname, piecewise[pname]).items():
|
|
194
|
+
assumption = _assumption(aname, assumed, ns, errors)
|
|
195
|
+
assert assumption is not None and not errors, 'what a method assumes is stated in the language'
|
|
196
|
+
assumptions[aname] = assumption
|
|
197
|
+
|
|
198
|
+
program = Program(
|
|
199
|
+
parameters={
|
|
200
|
+
name: ParameterDeclaration(tuple(pdef.dims), pdef.dtype, pdef.description)
|
|
201
|
+
for name, pdef in schema.parameters.items()
|
|
202
|
+
},
|
|
203
|
+
variables=variables,
|
|
204
|
+
constraints=constraints,
|
|
205
|
+
objective=objective,
|
|
206
|
+
dimensions={
|
|
207
|
+
name: DimensionDeclaration(ddef.dtype, ddef.description) for name, ddef in schema.dimensions.items()
|
|
208
|
+
},
|
|
209
|
+
relations=ns.relations,
|
|
210
|
+
sos={
|
|
211
|
+
name: SosDeclaration(sdef.variable, sdef.along, sos_type=sdef.type, description=sdef.description)
|
|
212
|
+
for name, sdef in schema.sos.items()
|
|
213
|
+
},
|
|
214
|
+
piecewise=piecewise,
|
|
215
|
+
assumptions=assumptions,
|
|
216
|
+
expressions={
|
|
217
|
+
name: ExpressionDeclaration(
|
|
218
|
+
entry.body,
|
|
219
|
+
_frame_of(name, entry, schema),
|
|
220
|
+
in_math=name in in_math,
|
|
221
|
+
description=schema.expressions[name].description,
|
|
222
|
+
)
|
|
223
|
+
for name, entry in entries.items()
|
|
224
|
+
},
|
|
225
|
+
description=schema.description,
|
|
226
|
+
)
|
|
227
|
+
if errors := emitted_name_errors(schema, program):
|
|
228
|
+
raise SchemaError('\n'.join(errors))
|
|
229
|
+
check_schema(schema, program)
|
|
230
|
+
return program
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _frame_of(name: str, entry: Named, schema: Spec) -> tuple[str, ...]:
|
|
234
|
+
"""The dims an entry is read over, in declaration order: declared for a cased entry, the body's for a plain one."""
|
|
235
|
+
if isinstance(entry.body, Cases):
|
|
236
|
+
return tuple(schema.expressions[name].dims or ())
|
|
237
|
+
carried = dims_of(entry.body, schema, f"Named expression '{name}'")
|
|
238
|
+
return tuple(d for d in schema.dimensions if d in carried)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _bound(value: float | str | None) -> Constant | Parameter | None:
|
|
242
|
+
if value is None:
|
|
243
|
+
return None
|
|
244
|
+
if isinstance(value, str):
|
|
245
|
+
return Parameter(value)
|
|
246
|
+
return Constant(value)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _assumption(name: str, block: AssumptionBlock, ns: Namespace, errors: list[str]) -> Assumption | None:
|
|
250
|
+
"""One ``assumptions:`` entry typed, or ``None`` once anything in it failed.
|
|
251
|
+
|
|
252
|
+
A predicate the connectives decide is refused: one that folds to true
|
|
253
|
+
assumes nothing, and one that folds to false refuses every dataset. A
|
|
254
|
+
variable is refused too, since an assumption is about the data and a
|
|
255
|
+
variable is what the solver decides from it.
|
|
256
|
+
"""
|
|
257
|
+
context = f"Assumption '{name}'"
|
|
258
|
+
found = len(errors)
|
|
259
|
+
holds = resolve_where_text(block.holds, ns, context, errors)
|
|
260
|
+
where = resolve_where_text(block.where, ns, f'{context}, where', errors)
|
|
261
|
+
if isinstance(holds, BooleanLiteral):
|
|
262
|
+
errors.append(_decided_assumption(context, block.holds, value=holds.value))
|
|
263
|
+
if isinstance(where, BooleanLiteral):
|
|
264
|
+
assert block.where is not None, 'a where the file did not write resolves to nothing'
|
|
265
|
+
errors.append(_decided_where(context, block.where, value=where.value))
|
|
266
|
+
for mask, part in ((holds, 'assumes'), (where, 'is checked where')):
|
|
267
|
+
if mask is None or isinstance(mask, BooleanLiteral):
|
|
268
|
+
continue
|
|
269
|
+
errors.extend(
|
|
270
|
+
f"{context}: variable '{atom.name}' stands in what the assumption {part}, and an assumption is "
|
|
271
|
+
f'about the data — a variable is what the solver decides from it. Name a parameter, or state the '
|
|
272
|
+
f'rule as a constraint.'
|
|
273
|
+
for atom in Mask(mask).atoms
|
|
274
|
+
if isinstance(atom, VariableDefined)
|
|
275
|
+
)
|
|
276
|
+
if len(errors) > found:
|
|
277
|
+
return None
|
|
278
|
+
assert holds is not None, 'a where string that read to nothing appended an error'
|
|
279
|
+
return Assumption(Mask(holds), mask_of(where), block.description)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _decided_assumption(context: str, text: str, *, value: bool) -> str:
|
|
283
|
+
"""The refusal for a predicate the connectives already decided, whose data is never read."""
|
|
284
|
+
if value:
|
|
285
|
+
return (
|
|
286
|
+
f'{context}: the predicate {text!r} folds to true, so it assumes nothing of the data. '
|
|
287
|
+
f'Delete it, or name a parameter it constrains.'
|
|
288
|
+
)
|
|
289
|
+
return (
|
|
290
|
+
f'{context}: the predicate {text!r} folds to false, so it holds on no data at all. '
|
|
291
|
+
f'Delete it, or write the predicate the data can satisfy.'
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _decided_where(context: str, text: str, *, value: bool) -> str:
|
|
296
|
+
"""The refusal for a ``where`` the connectives already decided, which narrows nothing or everything."""
|
|
297
|
+
if value:
|
|
298
|
+
return f'{context}: the where {text!r} folds to true, so it narrows nothing. Delete the where.'
|
|
299
|
+
return (
|
|
300
|
+
f'{context}: the where {text!r} folds to false, so the assumption is checked on no row. '
|
|
301
|
+
f'Delete the entry, or write the where the data can satisfy.'
|
|
302
|
+
)
|