Skip to content

feat[next]: friendly errors for more invalid DSL programs#2661

Closed
havogt wants to merge 11 commits into
GridTools:mainfrom
havogt:next_friendly_errors_coverage
Closed

feat[next]: friendly errors for more invalid DSL programs#2661
havogt wants to merge 11 commits into
GridTools:mainfrom
havogt:next_friendly_errors_coverage

Conversation

@havogt

@havogt havogt commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #2655. Extends the structured DSLError diagnostics to more
invalid DSL programs, turning a range of internal crashes into actionable
user-facing errors and improving several existing messages.

New or clearer diagnostics for:

  • global / nonlocal statements (previously crashed SingleAssignTargetPass
    with an AttributeError)
  • invalid attribute access in field operators, with "Did you mean ...?"
    suggestions
  • invalid subscripts and dimension indices
  • unresolvable or invalid type annotations
  • invalid statements and calls in programs
  • invalid arguments at call time
  • misuse of signatures, constants, connectivities, offset providers,
    dtypes, and constructors

Other changes:

  • eve: use an identity check for None in infer_type, which avoids an
    ambiguous truth-value error on array-like values
  • reformat the comparison-chain error into message + hint
  • lint and typing cleanup for the diagnostics code paths

Notes:

  • branched on top of main after feat[next]: Friendly error messages #2655 merged
  • the private helper _did_you_mean is promoted to a public, exported
    errors.did_you_mean so the attribute-access diagnostic can reuse it
  • UI-level test coverage added in test_diagnostic_messages.py

havogt added 11 commits June 16, 2026 18:04
'global x' inside a field operator crashed SingleAssignTargetPass with
AttributeError ('str' object has no attribute '_fields') because the
pass visited the plain-string 'names' field of the statement. Skip
non-AST field values in NodeYielder.generic_visit so the statement
reaches the dialect parser, and register friendly names + hints for
'global'/'nonlocal' there.
Attribute accesses like 'a.T', 'a.transpose()' or 'np.sin(a)' leaked
raw AttributeError/ValueError from FOAST type deduction. Guard
visit_Attribute: report nonexistent attributes (with a NumPy-specific
note for fields/scalars and 'did you mean' for named collections),
nonexistent namespace members, and namespace members whose value has no
DSL type (with a hint towards GT4Py built-ins). Also publish the
did_you_mean helper for reuse.
Three crashes in FOAST type deduction become diagnostics:
- 'field[3]' leaked AttributeError ('ScalarType' has no 'dim'); now
  explains that absolute indexing is unavailable and points to field
  offsets / local-dimension indices.
- 'tuple[5]' on a 2-tuple leaked IndexError; now reports the index and
  the tuple size.
- 'IDim(3)' on a non-local dimension crashed an assert; now explains
  that only local-dimension indices can be constructed.
Three annotation failure modes leaked raw Python exceptions:
- annotations that typing.get_type_hints cannot resolve (invalid
  forward-reference strings, undefined names) raised SyntaxError or
  NameError; now wrapped into a DSLError pointing at the function.
- parameter annotations that have no GT4Py type (e.g. 'a: list',
  'inp: gtx.Field' without arguments) raised ValueError from type
  translation; now InvalidParameterAnnotationError, with the underlying
  reason as a note and a hint showing valid annotations.
- annotated assignments inside an operator ('b: gtx.Field[...] = a')
  raised NameError when the annotation used names not visible inside
  the function; now a DSLError explaining what names are available.
Three program-level crashes become diagnostics:
- a bare expression statement ('a + a') leaked a TypeError from IR node
  validation; now explains that program statements must be operator
  calls.
- calling a '@program' from another program crashed with an
  AssertionError; now a DSLError suggesting to call the field operators
  directly or compose programs in Python.
- referencing a plain (undecorated) Python function leaked a ValueError
  from type translation; now a DSLTypeError hinting at
  '@field_operator'/'@scan_operator'. Other non-translatable closure
  variables in programs get the same wrapping as in field operators.
DSLTypeError now forwards the structured diagnostic payload
(label/notes/hints) to DSLError.
The message started with a newline and embedded a multi-line code block,
which rendered awkwardly; the replacement suggestion is now a hint in
the structured diagnostics format.
'value in (None, type(None))' invokes '__eq__', which fails for values
with non-boolean equality such as NumPy arrays ('The truth value of an
array ... is ambiguous'). Compare by identity instead.
Direct calls to field operators and scan operators ('as program') now
validate their arguments the same way program calls do, instead of
crashing deep inside the embedded execution or backend:
- non-GT4Py argument types (e.g. raw NumPy arrays) report which
  argument is wrong and hint at 'gtx.as_field' (also when calling
  programs, where this previously crashed in type inference);
- wrong dimensions/dtypes/argument counts raise 'Invalid argument
  types in call to ...' listing the mismatches;
- a wrong 'out' argument reports the expected vs actual type;
- an invalid 'domain' argument explains the expected mapping form.
Also turned into diagnostics: missing/invalid offset-provider entries
when applying a field offset (was KeyError/NotImplementedError),
unsupported scan-operator attributes like 'init=np.zeros(...)' (was a
crash in fingerprinting), string keys in 'domain' dicts (was an IR
validation TypeError), nested tuple unpacking (was AttributeError) and
invalid literals in type constructors like 'int32("abc")' (was a
ValueError at execution time).
…vity misuse

- Unsupported parameter-list features (keyword-only, positional-only,
  '*args', '**kwargs', defaults) were silently dropped, leading to
  misleading 'Undeclared symbol' errors; they now raise
  UnsupportedPythonFeatureError pointing at the parameter.
- Out-of-range integer literals reported 'Constants of type <class
  'int'> are not permitted'; the underlying reason is now a note.
- Calling a '@program' inside a field operator dumped the whole
  'ProgramType(...)' spec; now a one-line explanation with a hint.
- Unstructured offsets: a missing offset-provider entry leaked KeyError
  and a raw neighbor-table (NumPy array) entry leaked
  NotImplementedError from 'as_connectivity_field'; both are now
  diagnostics, the latter hinting at 'gtx.as_connectivity'.
…tructor misuse

- 'offset_provider' arguments that are not a mapping (e.g. a list of
  pairs or a plain string) were silently accepted and only crashed (or
  worse, went unnoticed) when an offset was looked up; programs and
  direct operator calls now reject them upfront with a DSLTypeError.
- 'as_field' with an unsupported dtype (e.g. float16) crashed an
  assert; now a ValueError naming the dtype.
- 'as_field' with string dimensions (e.g. ['IDim']) failed with a
  baffling "''D'' cannot be interpreted as 'UnitRange'"; now a
  TypeError explaining dimensions must be 'Dimension' objects.
- 'as_connectivity' with a non-integral neighbor table crashed an
  assert; now a ValueError naming the dtype.
- 'with_backend' with a non-backend object failed later with
  AttributeError; now a TypeError at the call.
ruff (imports, noqa for deliberately-dead DSL test code, raw regex
patterns), ruff-format, and mypy annotations for the new diagnostics
code paths.
@havogt

havogt commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by a split into three smaller PRs (converted to draft; will close once those land):

Together #2663 + #2664 + #2662 reproduce this branch's src exactly. Please review the splits instead of this PR.

@havogt havogt closed this Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant