Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/math_spec/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,25 @@ def _reject_unknown_keys(cls, data: Any) -> Any:
return data


#: The dtype a dimension index may declare (the declaration rules).
#: The dtype a dimension index may declare (the declaration rules), and what
#: its labels are. ``datetime`` is a dimension's alone — labels on a timeline
#: order and compare, where a *value* of that type is a moment nothing
#: computes with.
DimensionDtype = Literal['float', 'int', 'str', 'datetime']

#: The dtype a parameter may declare (the declaration rules), and what its bound
#: column must be.
#: column must be. ``bool`` is a parameter's alone — a value column may be a
#: flag a mask reads, where a label set of two members is a dimension nothing
#: indexes by.
ParameterDtype = Literal['float', 'int', 'bool', 'str']

#: The domain a variable may declare.
VariableDomain = Literal['continuous', 'integer', 'binary']

#: What a masked variable's non-existence *means*.
#: What a masked variable's non-existence *means* where it does not exist.
#: ``undefined`` is the absence rules' default — a term carrying it takes its
#: row. ``zero`` says the quantity *is* zero there, so the term contributes
#: nothing and the row stands.
VariableAbsence = Literal['undefined', 'zero']

#: Which way an objective is optimised (the declaration rules).
Expand Down
45 changes: 26 additions & 19 deletions src/math_spec/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
*means*, with macros expanded, names typed, operators resolved to nodes and
every dim rule already checked. Consumers dispatch on these nodes and read
them; nothing here is built by hand, so what ships beside the nodes is the
walk (:func:`children`), not builders.
walk (:func:`children`), not builders. A program is trusted by construction:
:func:`~math_spec.lowering.to_program` is the only thing that builds one, and
nothing checks one assembled by hand. The language's refusals happen at load,
where the file and its author are, and a program put together some other way
is outside that guarantee rather than inside a pass restating it.

What a consumer needs from this module falls in three, and only the middle
one has to be *called* to be got right:
Expand All @@ -32,7 +36,10 @@
A mask is the language's own resolved ``where`` node
(:mod:`math_spec.where_parser`) rather than a second set spelling the same
predicates — one home, so the two cannot come to disagree about what a
comparison is.
comparison is. The declaration vocabularies are the language's own for the
same reason (:mod:`math_spec.model`): a ``dtype``, a domain and an absence
reading cross into a program by a cast, and a member added to one spelling
alone would arrive as a string no consumer's branch recognises.

Frozen dataclasses only — no execution logic, and nothing imported from a
consumer.
Expand All @@ -50,6 +57,7 @@
from types import MappingProxyType
from typing import TYPE_CHECKING, Literal, NamedTuple, assert_never, get_args

import math_spec.model as _model
from math_spec.errors import did_you_mean

if TYPE_CHECKING:
Expand Down Expand Up @@ -133,23 +141,22 @@
#: and hears about it when the language admits another.
QUADRATIC_POSITIONS = frozenset(get_args(QuadraticPosition))
ComparisonOperator = Literal['==', '!=', '<=', '>=', '<', '>']
VariableType = Literal['continuous', 'binary', 'integer']

#: What a masked variable's non-existence means where it does not exist.
#: ``undefined`` is the absence rules' default — a term carrying it takes its
#: row. ``zero`` says the quantity *is* zero there, so the term contributes
#: nothing and the row stands.
VariableAbsence = Literal['undefined', 'zero']

#: What a dimension's labels are. ``datetime`` is a dimension's alone — labels
#: on a timeline order and compare, where a *value* of that type is a moment
#: nothing computes with.
DimensionDtype = Literal['float', 'int', 'str', 'datetime']

#: What a parameter's values are. ``bool`` is a parameter's alone — a value
#: column may be a flag a mask reads, where a label set of two members is a
#: dimension nothing indexes by.
ParameterDtype = Literal['float', 'int', 'bool', 'str']

#: What a dimension's labels are — the language's own vocabulary
#: (:data:`~math_spec.model.DimensionDtype`), under the name a consumer reads
#: it by.
DimensionDtype = _model.DimensionDtype

#: What a parameter's values are (:data:`~math_spec.model.ParameterDtype`).
ParameterDtype = _model.ParameterDtype

#: What a masked variable's non-existence means
#: (:data:`~math_spec.model.VariableAbsence`).
VariableAbsence = _model.VariableAbsence

#: A variable's domain (:data:`~math_spec.model.VariableDomain`), under the
#: name this module's field carries.
VariableType = _model.VariableDomain


# --------------------------------------------------------------------------
Expand Down
31 changes: 31 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
BinaryOperatorNode,
ComparisonNode,
FunctionCallNode,
NameListNode,
NameNode,
NumberNode,
UnaryOperatorNode,
Expand Down Expand Up @@ -103,6 +104,36 @@ def test_a_keyword_given_twice_is_refused_not_overwritten():
parse_expression('sum(p, over=snapshot, over=generator)')


def test_a_list_of_names_is_a_kwarg_value():
"""`by=[a, b]` is one value, so the operator reads one grouping and not two."""
node = parse_expression('sum(p, by=[a, b])')
assert node.kwargs['by'] == NameListNode(('a', 'b'))


@pytest.mark.parametrize(
'text',
[
pytest.param('sum(p, by=[a,])', id='a-trailing-comma'),
pytest.param('sum(p, by=[])', id='no-names-at-all'),
pytest.param('sum(p, by=[a b])', id='a-missing-comma'),
pytest.param('sum(p, by=[a)', id='an-unclosed-bracket'),
pytest.param('sum([p], over=g)', id='a-positional-argument'),
pytest.param('p + [c]', id='a-term'),
pytest.param('[a, b]', id='the-whole-expression'),
],
)
def test_a_list_the_grammar_cannot_read_is_refused_at_load(text):
"""A list is a kwarg value and nothing else, and the last three say so.

Which is a claim about the *grammar*: a list admitted as a term would be
a second thing `[a, b]` could mean, and one read past a missing comma
would be a grouping the file does not write. Neither is decidable later —
a parse is what every consumer starts from.
"""
with pytest.raises(SchemaError, match='Failed to parse expression'):
parse_expression(text)


@pytest.mark.parametrize(
('text', 'value'),
[('1e5', 1e5), ('2.5E-3', 2.5e-3), ('1e+3', 1e3), ('7.e2', 700.0)],
Expand Down
Loading