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
2 changes: 2 additions & 0 deletions docs/reference/language/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ NUMBER ::= integer | float | "inf" | ".inf"
- A float may carry an exponent, as in `1e5` or `2.5e-3`.
- The same keyword twice in one call is an error.
- An expression nests at most 100 levels deep, and so does a `where:` string.
With every named expression it reads written in, an expression nests at most
300 levels deep.

## Where a product of two variables is allowed

Expand Down
3 changes: 2 additions & 1 deletion docs/reference/language/named.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ macros:
collide with a declared dimension.
- The number of arguments is checked at each call site. A cycle is reported with
its reference chain.
- Every template is parsed and name-checked at load, whether or not it is called.
- Every template is held at load to every rule a call site is, whether or not it
is called. A formal is left for the call site to bind.

Anything composed out of the [built-in operators](operators.md) belongs here.
What the language cannot express is under
Expand Down
6 changes: 3 additions & 3 deletions src/math_spec/_where_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class UnresolvedComparisonNode:
#: Every node a parsed where string is built of: the connectives and literals,
#: the unresolved leaves, and the arithmetic and the two side nodes under a
#: comparison. What the depth measurement walks.
_ParsedWhere = Predicate | UnresolvedWhereNode | ArithmeticNode | ColumnNode | KeywordNode
ParsedWhere = Predicate | UnresolvedWhereNode | ArithmeticNode | ColumnNode | KeywordNode


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -252,7 +252,7 @@ def _named_rewrite(text: str, loc: int) -> str | None:
)


def _nested(node: _ParsedWhere) -> tuple[_ParsedWhere, ...]:
def nested(node: ParsedWhere) -> tuple[ParsedWhere, ...]:
"""What a where string nests through: a connective's operands, a comparison's sides, and a call's predicate."""
if isinstance(node, UnresolvedCountNode):
return (node.call, node.value)
Expand Down Expand Up @@ -284,5 +284,5 @@ def parse_where(text: str) -> Predicate | UnresolvedWhereNode:
"""
return cast(
'Predicate | UnresolvedWhereNode',
parse_text(_WHERE_GRAMMAR, text, 'where string', _named_rewrite, _nested, _DEEP_REWRITE),
parse_text(_WHERE_GRAMMAR, text, 'where string', _named_rewrite, nested, _DEEP_REWRITE),
)
22 changes: 3 additions & 19 deletions src/math_spec/dimensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
Variable,
VariableDefined,
WindowSum,
children,
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -81,7 +82,7 @@ def dims_of(node: Expression, schema: Spec, context: str) -> frozenset[str]:
return frozenset().union(*(dims_of(region.value, schema, context) for region in node.regions))

if isinstance(node, Negate | Add | Multiply | Power | Divide):
return frozenset().union(*(dims_of(child, schema, context) for child in _operands(node)))
return frozenset().union(*(dims_of(child, schema, context) for child in children(node)))

inner = dims_of(node.operand, schema, context)
if isinstance(node, Sum):
Expand All @@ -96,16 +97,6 @@ def dims_of(node: Expression, schema: Spec, context: str) -> frozenset[str]:
assert_never(node)


def _operands(node: Negate | Add | Multiply | Power | Divide) -> tuple[Expression, ...]:
if isinstance(node, Negate):
return (node.operand,)
if isinstance(node, Add | Multiply):
return (node.left, node.right)
if isinstance(node, Power):
return (node.base, node.exponent)
return (node.numerator, node.divisor)


def _named_dims(node: Named, schema: Spec, context: str) -> frozenset[str]:
"""A cased entry's declared frame rather than the union of its arms — a narrower arm broadcasts — and a plain entry's body."""
declared = schema.expressions[node.name].dims
Expand Down Expand Up @@ -238,19 +229,12 @@ def _check_joined(call: str, use: Direction | Partition, inner: frozenset[str],
def _check_named_amount(
node: Translate | WindowSum, verb: str, inner: frozenset[str], schema: Spec, context: str
) -> None:
"""The rules that hold of an ``offset=`` or ``window=`` naming a parameter; a literal breaks none of them."""
"""The two rules of an ``offset=`` or ``window=`` naming a parameter that need the operand's dims; resolution holds it to its dtype."""
kwarg, amount = ('offset', node.offset) if isinstance(node, Translate) else ('window', node.width)
if not isinstance(amount, str):
return
words = AMOUNTS[verb]
declared = schema.parameters[amount]
if declared.dtype != 'int':
raise DimensionError(
f'{context}: {verb}({kwarg}={amount}) counts positions along '
f"'{node.along}', but '{amount}' is declared dtype: {declared.dtype}. A count of "
f'positions is integral — declare it dtype: int, which binds only an integer '
f'column, so a fractional {words.noun} has nowhere to arrive from.'
)
if node.along in declared.dims:
raise DimensionError(
f'{context}: {verb}({kwarg}={amount}) steps along '
Expand Down
3 changes: 2 additions & 1 deletion src/math_spec/exclusivity.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,8 @@ def _expression_rewrite(node: ExpressionComparison) -> str:
if number is not None and isinstance(right, Parameter):
return (
f'the literal is on the left, and a comparison is read as arithmetic there — write it as '
f'the same test the other way round, {right.name} {_FLIPPED[node.op]} {number:g}'
f'the same test the other way round, {right.name} {_FLIPPED[node.op]} '
f'{int(number) if number.is_integer() else number}'
)
return (
'it compares expressions, whose values only the data decides — compare one parameter against a '
Expand Down
8 changes: 8 additions & 0 deletions src/math_spec/expansion.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ def parse_template(name: str, macro: MacroBlock, context: str) -> ArithmeticNode


def _expand(node: ArithmeticNode, ns: Namespace, context: str, stack: tuple[str, ...]) -> ArithmeticNode:
"""*node* with every macro call under it substituted; *stack* is the macros this walk is inside.

A name a template reads is checked against the entries being resolved
here, where the macros it came through are known, so a cycle closed
through a macro is reported with the macros in its chain.
"""
if isinstance(node, NameNode) and stack and (refusal := ns.cycle(node.name, context, stack)) is not None:
raise SchemaError(refusal)
if isinstance(node, FunctionCallNode) and node.name in ns.schema.macros:
if node.name in stack:
msg = f'{context}: circular macro reference: {" -> ".join([*stack, node.name])}'
Expand Down
8 changes: 7 additions & 1 deletion src/math_spec/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1124,14 +1124,20 @@ def _sos_emitted_names(self) -> Iterator[str]:
yield from self._collisions(f"Sos '{sname}'", Emitted.of(sname, block.type).by_kind)

def _piecewise_references(self) -> Iterator[str]:
"""A curve runs along a declared dimension through values parameters carrying it, gated by a binary, masked by a bool."""
"""A curve runs along a declared dimension through numeric values parameters carrying it, gated by a binary, masked by a bool."""
for name, pw in self.piecewise.items():
context = f"piecewise '{name}'"
if pw.over not in self.dimensions:
yield undeclared_dimension('piecewise', name, pw.over)
continue
for i, link in enumerate(pw.links):
if link.values not in self.parameters:
yield f"{context}: link {i} values references undeclared parameter '{link.values}'"
elif (dtype := self.parameters[link.values].dtype) not in NUMERIC_DTYPES:
yield (
f"{context}: link {i} values parameter '{link.values}' is declared dtype: {dtype}, and a "
f'breakpoint is a number. Declare it dtype: float or int.'
)
elif pw.over not in self.parameters[link.values].dims:
yield (
f"{context}: link {i} values parameter '{link.values}' must carry dim "
Expand Down
8 changes: 6 additions & 2 deletions src/math_spec/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,12 +392,16 @@ class Named:
)


def fan_in(expression: Expression) -> FanIn:
def fan_in(expression: Expression | Named) -> FanIn:
"""How *expression*'s output rows relate to its input slots.

For the absence rules, both classes other than ``'one-to-one'`` sum
several input slots into an output row.
several input slots into an output row. A :class:`Named` answers as its
body does, so a :attr:`~math_spec.model.Spec.resolved` tree is asked as a
program's is.
"""
if isinstance(expression, Named):
return fan_in(expression.body)
if isinstance(expression, (Sum, GroupSum)):
return 'many-to-one'
if isinstance(expression, WindowSum):
Expand Down
Loading
Loading