Skip to content

an unknown keyword on an operator is refused twice, and the second refusal names a rewrite that does not exist #441

Description

@FBumann

Prompt: "Do a big issue about that! A bug issue i mean" — the double message seen while retiring from= on shift in #437.

Note

The following content was generated by AI.

What happened?

A call with a keyword the operator does not declare is refused by the signature line, which is right, and then refused a second time by a message about the keyword's value, which is wrong: the second line depends on what the value happens to look like, not on the mistake, and the rewrite it proposes does not exist.

objective: { sense: minimize, expression: "sum(shift(p, over=g, offset=1, edge=0, foo=g))" }
The objective: shift() expects shift(<expr>, over=<dim>, offset=<n>[, edge='wrap'|<number>][, by=<lookup>])
The objective: 'g' is a dimension, and a dimension is not a value in an expression. Dimensions appear in 'foreach:', in operator arguments (sum(x, over=g)), and in where-comparisons — to use its coordinates as data, declare a parameter over it.

The first line is the whole answer. The second says the author used a dimension as a value, which they did not; they misspelled or invented a keyword. With a list as the value the second line becomes "shift(foo=) takes a number or the name of an integer parameter. Precompute it as a parameter." — advice to precompute a parameter for a keyword that does not exist. With a number or a parameter name as the value there is no second line at all, because those happen to pass the amount check. So the second message is decided by the value's shape, which is the tell: nothing about an undeclared keyword is decided by its value.

Expected: one refusal, the signature line, for every operator and every value.

Why it is a bug by this project's rules. AGENTS.md: an error message names the rewrite — the second line names a rewrite that is not the fix, and for a typo of edge= it points the author at parameters. Nothing is guessed — a keyword the operator did not declare has no kind, and reading it as a value is a default nobody asked for.

Where. call_shape_error in src/math_spec/operators.py reports the unknown keyword correctly. _call in src/math_spec/resolution.py then types every keyword in the call regardless, and Builtin.kind_of answers 'value' for any name it does not know (its return 'value' fallthrough), so the stray keyword's value goes through _amount, which is where both second lines come from. dual is the one operator that returns after a shape error, which is why it does not do this.

Every operator that takes keywords is affected. Reproduced on main at eac1930 (0.0.0-alpha.83), so it predates #437 and the lookup work:

call second line
shift(p, over=g, offset=1, edge=0, foo=g) "'g' is a dimension, and a dimension is not a value in an expression …"
shift(p, over=g, offset=1, edge=0, foo=[g, h]) "shift(foo=) takes a number or the name of an integer parameter. Precompute it as a parameter."
shift(p, over=g, offset=1, egde='wrap') (a typo of edge) "shift(egde=) takes a number or the name of an integer parameter. Precompute it as a parameter."
sum(p, ovr=g) (a typo of over) "'g' is a dimension, and a dimension is not a value in an expression …"
at(p, by=lk, scale=2 * c) "at(scale=) takes a number or the name of an integer parameter. Precompute it as a parameter."
shift(p, over=g, offset=1, edge=0, foo=3) none — a number is a valid amount
sum(p, by=lk, weight=c) none — a parameter name is a valid amount
sum_back(p, over=g, witin=n) (a typo of within) none — n is an int parameter

The three rows with no second line are the same bug: the value was silently accepted as an amount, and a keyword that does not exist was typed as if it did.

The fix, as I would make it. Two lines and a test. Builtin.kind_of returns nothing for a keyword the operator does not declare, and _call skips those. Every other error in the same call is still reported, and dual's early return stays as it is. The test asserts, for one operator and a value of each shape above, that the refusal is exactly the signature line — it fails on the tree as it stands. It belongs in tests/test_validation.py beside the other operator refusals. A separate fix(resolution) PR against main, not part of #437.

Which operating systems have you used?

  • Linux

Version

main at eac1930 (0.0.0-alpha.83). Also present on #437's branch.

Relevant log output

Reproduction, every row of the table
from math_spec import to_spec
from math_spec.errors import LanguageError

base = {
    'dimensions': {'g': {}, 'h': {}},
    'lookups': {'lk': {'over': 'g', 'into': 'h'}},
    'parameters': {'c': {'dims': ['g']}, 'n': {'dims': [], 'dtype': 'int'}},
    'variables': {'p': {'foreach': ['g']}},
}
cases = {
    'shift, a dimension name': 'sum(shift(p, over=g, offset=1, edge=0, foo=g))',
    'shift, a list': 'sum(shift(p, over=g, offset=1, edge=0, foo=[g, h]))',
    'shift, a number': 'sum(shift(p, over=g, offset=1, edge=0, foo=3))',
    'shift, a typo of edge': "sum(shift(p, over=g, offset=1, egde='wrap'))",
    'sum, a typo of over': 'sum(sum(p, ovr=g))',
    'sum, a parameter name': 'sum(sum(p, by=lk, weight=c))',
    'at, an expression': 'sum(at(p, by=lk, scale=2 * c))',
    'sum_back, a typo of within': 'sum(sum_back(p, over=g, witin=n))',
}
for label, expr in cases.items():
    try:
        to_spec(dict(base, objective={'sense': 'minimize', 'expression': expr}))
    except LanguageError as e:
        print(f'### {label}\n{e}\n')
### shift, a dimension name
The objective: shift() expects shift(<expr>, over=<dim>, offset=<n>[, edge='wrap'|<number>][, by=<lookup>])
The objective: 'g' is a dimension, and a dimension is not a value in an expression. Dimensions appear in 'foreach:', in operator arguments (sum(x, over=g)), and in where-comparisons — to use its coordinates as data, declare a parameter over it.

### shift, a list
The objective: shift() expects shift(<expr>, over=<dim>, offset=<n>[, edge='wrap'|<number>][, by=<lookup>])
The objective: shift(foo=) takes a number or the name of an integer parameter. Precompute it as a parameter.

### shift, a number
The objective: shift() expects shift(<expr>, over=<dim>, offset=<n>[, edge='wrap'|<number>][, by=<lookup>])

### shift, a typo of edge
The objective: shift() expects shift(<expr>, over=<dim>, offset=<n>[, edge='wrap'|<number>][, by=<lookup>])
The objective: shift(egde=) takes a number or the name of an integer parameter. Precompute it as a parameter.

### sum, a typo of over
The objective: sum() expects sum(<expr>), sum(<expr>, over=<dim>) or sum(<expr>, by=<lookup>)
The objective: 'g' is a dimension, and a dimension is not a value in an expression. Dimensions appear in 'foreach:', in operator arguments (sum(x, over=g)), and in where-comparisons — to use its coordinates as data, declare a parameter over it.

### sum, a parameter name
The objective: sum() expects sum(<expr>), sum(<expr>, over=<dim>) or sum(<expr>, by=<lookup>)

### at, an expression
The objective: at() expects at(<expr>, by=<lookup>)
The objective: at(scale=) takes a number or the name of an integer parameter. Precompute it as a parameter.

### sum_back, a typo of within
The objective: sum_back() expects sum_back(<expr>, over=<dim>, within=<n|parameter>[, edge='wrap'][, by=<lookup>])

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: operatorsWhat an operator may reduce, walk, read or refusebugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions