Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
852052c
Add checked casting operations to ErrorBase.h
DanilaFe Feb 10, 2026
16899f8
Add initial draft of wrapping errors
DanilaFe Feb 10, 2026
e1ce15b
Add a prototype 'supports_wrap' to check if a type can be sent to Python
DanilaFe Feb 10, 2026
9d2dfe7
Start creating more specific error objects when catching errors
DanilaFe Feb 10, 2026
e27bc7d
Add initial draft of returning error info
DanilaFe Feb 10, 2026
7c1ca5a
Flesh out extraction / filtering a bit
DanilaFe Feb 10, 2026
0675e9d
Stop filtering and use placeholders instead
DanilaFe Feb 10, 2026
ccf10c5
Fail more explicitly for unwrapping error info tuples.
DanilaFe Feb 10, 2026
81b267f
Expose 'ApplicabilityResult' to Python
DanilaFe Feb 10, 2026
cfc1c8c
Bless more types for `->info()` conversion
DanilaFe Feb 10, 2026
05c1e0b
Add failure-reasons-to-string conversion
DanilaFe Feb 10, 2026
daaf409
Expose ApplicabilityResult methods
DanilaFe Feb 10, 2026
82a8ef1
Specialize error for NoMatchingCandidates
DanilaFe Feb 10, 2026
2989071
Avoid crashing for null entries in error info tuples
DanilaFe Feb 10, 2026
2b3c5cd
Add more specific location for AsWithUseExcept
DanilaFe Feb 10, 2026
b2e7012
Improve some error locations
DanilaFe Feb 10, 2026
4c640b6
Improve the display of another error
DanilaFe Feb 11, 2026
0425960
Fix support for GeneralError
DanilaFe Feb 11, 2026
9f6e21b
Preemptively add DO_NOT_DOCUMENT to ErrorBase.h's generated toBla
DanilaFe Feb 11, 2026
e2b3caa
Apply 'black'
DanilaFe Feb 11, 2026
1bf50af
Properly print empty tuples in Python
DanilaFe Feb 11, 2026
98a9522
Stop using boolean flags and just add 'transform' code.
DanilaFe Feb 11, 2026
a23cbc5
Remove yet-unnecessary `transform` implementations
DanilaFe Feb 11, 2026
6259dbc
Add tests for new locations
DanilaFe Feb 11, 2026
660a3a1
Apply 'black'
DanilaFe Feb 11, 2026
b1a7c68
Apply Jade's feedback
DanilaFe Feb 11, 2026
323fba7
Expose a CallInfo object from chapel-py
DanilaFe Feb 11, 2026
cb93f43
Allow for early returns in Error-to-diagnostic code
DanilaFe Feb 11, 2026
2393b12
Compute offset on a per-candidate basis
DanilaFe Feb 11, 2026
41b7cdb
Expand testing with new cases
DanilaFe Feb 11, 2026
5f57b79
Apply black
DanilaFe Feb 11, 2026
80c8790
Stop using 'DiagnosticRelatedInformation' since it's causing doc nons…
DanilaFe Feb 11, 2026
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
28 changes: 28 additions & 0 deletions frontend/include/chpl/framework/ErrorBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ enum ErrorType {
#undef DIAGNOSTIC_CLASS
};

/* Forward-declare subclasses of ErrorBase, so that we can use them in
ErrorBase::toBla() methods. */
class GeneralError;
#define DIAGNOSTIC_CLASS(NAME, KIND, EINFO...) class Error##NAME;
#include "chpl/framework/error-classes-list.h"
#undef DIAGNOSTIC_CLASS

/**
Parent class for all errors in Dyno.

Expand Down Expand Up @@ -152,6 +159,27 @@ class ErrorBase {
virtual void write(ErrorWriterBase& wr) const = 0;
virtual void mark(Context* context) const = 0;
virtual owned<ErrorBase> clone() const = 0;

bool isGeneralError() const { return type_ == ErrorType::General; }
const GeneralError* toGeneralError() const {
return isGeneralError() ? (const GeneralError*)(this) : nullptr;
}
GeneralError* toGeneralError() {
return isGeneralError() ? (GeneralError*)(this) : nullptr;
}

/// \cond DO_NOT_DOCUMENT
#define DIAGNOSTIC_CLASS(NAME, KIND, EINFO...) \
bool is##NAME() const { return type_ == ErrorType::NAME; } \
const Error##NAME* to##NAME() const { \
return is##NAME() ? (const Error##NAME*)(this) : nullptr; \
} \
Error##NAME* to##NAME() { \
return is##NAME() ? (Error##NAME*)(this) : nullptr; \
}
#include "chpl/framework/error-classes-list.h"
#undef DIAGNOSTIC_CLASS
/// \endcond DO_NOT_DOCUMENT
};

/* Converts a given ErrorType to its corresponding ErrorBase::Kind */
Expand Down
4 changes: 4 additions & 0 deletions frontend/include/chpl/resolution/resolution-types.h
Original file line number Diff line number Diff line change
Expand Up @@ -1451,6 +1451,8 @@ enum CandidateFailureReason {
FAIL_CANDIDATE_OTHER,
};

const char* candidateFailureReasonToString(CandidateFailureReason reason);

/**
An enum that represents the reason why an actual couldn't be passed
to a formal.
Expand Down Expand Up @@ -1495,6 +1497,8 @@ enum PassingFailureReason {
FAIL_FORMAL_OTHER,
};

const char* passingFailureReasonToString(PassingFailureReason reason);

/**
Represents either a function that was accepted during call resolution, or
the reason why that function was rejected.
Expand Down
42 changes: 42 additions & 0 deletions frontend/lib/resolution/resolution-types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1287,6 +1287,48 @@ void CandidatesAndForwardingInfo::stringify(
}
}

const char* candidateFailureReasonToString(CandidateFailureReason reason) {
switch(reason) {
case FAIL_ERRORS_THROWN: return "FAIL_ERRORS_THROWN";
case FAIL_NO_DEFAULT_VALUE_FOR_GENERIC_FIELD: return "FAIL_NO_DEFAULT_VALUE_FOR_GENERIC_FIELD";
case FAIL_VARARG_MISMATCH: return "FAIL_VARARG_MISMATCH";
case FAIL_INTERFACE_NOT_TYPE_INTENT: return "FAIL_INTERFACE_NOT_TYPE_INTENT";
case FAIL_WHERE_CLAUSE: return "FAIL_WHERE_CLAUSE";
case FAIL_CANNOT_PASS: return "FAIL_CANNOT_PASS";
case FAIL_NO_TYPE_CONSTRUCTOR: return "FAIL_NO_TYPE_CONSTRUCTOR";
case FAIL_FORMAL_ACTUAL_MISMATCH: return "FAIL_FORMAL_ACTUAL_MISMATCH";
case FAIL_FORMAL_ACTUAL_MISMATCH_ITERATOR_API: return "FAIL_FORMAL_ACTUAL_MISMATCH_ITERATOR_API";
case FAIL_PARENLESS_MISMATCH: return "FAIL_PARENLESS_MISMATCH";
case FAIL_CANDIDATE_OTHER: return "FAIL_CANDIDATE_OTHER";
}
CHPL_ASSERT(false && "missing case for candidate failure reason");
return "<unknown reason>";
}

const char* passingFailureReasonToString(PassingFailureReason reason) {
switch (reason) {
case FAIL_INCOMPATIBLE_NILABILITY: return "FAIL_INCOMPATIBLE_NILABILITY";
case FAIL_INCOMPATIBLE_MGMT: return "FAIL_INCOMPATIBLE_MGMT";
case FAIL_INCOMPATIBLE_MGR: return "FAIL_INCOMPATIBLE_MGR";
case FAIL_EXPECTED_SUBTYPE: return "FAIL_EXPECTED_SUBTYPE";
case FAIL_INCOMPATIBLE_TUPLE_SIZE: return "FAIL_INCOMPATIBLE_TUPLE_SIZE";
case FAIL_INCOMPATIBLE_TUPLE_STAR: return "FAIL_INCOMPATIBLE_TUPLE_STAR";
case FAIL_CANNOT_CONVERT: return "FAIL_CANNOT_CONVERT";
case FAIL_CANNOT_INSTANTIATE: return "FAIL_CANNOT_INSTANTIATE";
case FAIL_DID_NOT_INSTANTIATE: return "FAIL_DID_NOT_INSTANTIATE";
case FAIL_TYPE_VS_NONTYPE: return "FAIL_TYPE_VS_NONTYPE";
case FAIL_NOT_PARAM: return "FAIL_NOT_PARAM";
case FAIL_MISMATCHED_PARAM: return "FAIL_MISMATCHED_PARAM";
case FAIL_UNKNOWN_ACTUAL_TYPE: return "FAIL_UNKNOWN_ACTUAL_TYPE";
case FAIL_UNKNOWN_FORMAL_TYPE: return "FAIL_UNKNOWN_FORMAL_TYPE";
case FAIL_GENERIC_TO_NONTYPE: return "FAIL_GENERIC_TO_NONTYPE";
case FAIL_NOT_EXACT_MATCH: return "FAIL_NOT_EXACT_MATCH";
case FAIL_VARARG_TQ_MISMATCH: return "FAIL_VARARG_TQ_MISMATCH";
case FAIL_FORMAL_OTHER: return "FAIL_FORMAL_OTHER";
}
CHPL_ASSERT(false && "missing case for passing failure reason");
return "<unknown reason>";
}

// Note (Daniel): the code for 'overloaded' below comes from cppreference:
// https://en.cppreference.com/w/cpp/utility/variant/visit2
Expand Down
4 changes: 4 additions & 0 deletions tools/chapel-py/src/chapel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ PyMODINIT_FUNC PyInit_core(void) {
if (ResolvedExpressionObject::ready() < 0) return nullptr;
if (MostSpecificCandidateObject::ready() < 0) return nullptr;
if (TypedSignatureObject::ready() < 0) return nullptr;
if (ApplicabilityResultObject::ready() < 0) return nullptr;
if (CallInfoObject::ready() < 0) return nullptr;

chapelModule = PyModule_Create(&ChapelModule);
if (!chapelModule) return nullptr;
Expand All @@ -81,6 +83,8 @@ PyMODINIT_FUNC PyInit_core(void) {
if (ResolvedExpressionObject::addToModule(chapelModule) < 0) return nullptr;
if (MostSpecificCandidateObject::addToModule(chapelModule) < 0) return nullptr;
if (TypedSignatureObject::addToModule(chapelModule) < 0) return nullptr;
if (ApplicabilityResultObject::addToModule(chapelModule) < 0) return nullptr;
if (CallInfoObject::addToModule(chapelModule) < 0) return nullptr;

return chapelModule;
}
Expand Down
94 changes: 92 additions & 2 deletions tools/chapel-py/src/chapel/lsp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@
and the Language Server Protocol.
"""

from lsprotocol.types import Position, Range, Diagnostic, DiagnosticSeverity
from lsprotocol.types import (
Position,
Range,
Diagnostic,
DiagnosticSeverity,
)
import typing
import chapel


def location_to_range(location) -> Range:
Expand All @@ -43,6 +50,82 @@ def location_to_range(location) -> Range:
)


def error_to_location_and_info(
error,
) -> typing.Tuple[
chapel.Location,
typing.Optional[str],
]:
location = error.location()
type_ = error.type()
new_message = None
if isinstance(error, chapel.NoMatchingCandidates):
(call, call_info, app_results, _) = error.info()

# Check if all candidates were rejected due to a particular candidate.
# In that case, highlight the specific candidate as the location of the error.
actuals_set = set()

# question args violate the correspondence between indices into the
# call and indices into the call_info. The former are needed to get
# an AST for the location, while the latter are provided by
# app_results. Thus, do not include the indexing.
if call_info.has_question_arg():
return (location, new_message)

for app_result in app_results:
idx = app_result.actual_idx()

# 'this' formals are inserted into the call info but aren't in the
# call's actual list, so the reported indices are one higher than
# we should use to index into the call's actuals.
if app_result.candidate_is_method():
idx -= 1

if app_result.candidate_failure_reason() != "FAIL_CANNOT_PASS":
idx = -1

actuals_set.add(idx)

if (
len(actuals_set) == 1
and -1 not in actuals_set
and isinstance(call, chapel.FnCall)
):
# All candidates were rejected due to the same candidate, so highlight that candidate.
location = call.actual(actuals_set.pop()).location()
assert type_ is not None
new_message = "{}: [{}]: this actual could not be passed to a corresponding formal".format(
error.kind().capitalize(), type_
)
elif isinstance(error, chapel.AsWithUseExcept):
(_, as_) = error.info()
if as_ is not None:
location = as_.location()
elif isinstance(error, chapel.TertiaryUseImportUnstable):
(_, node, _, _, _) = error.info()
if node is not None:
location = node.location()
elif isinstance(error, chapel.TupleExpansionNamedArgs):
(tup, _) = error.info()
if tup is not None:
location = tup.location()
elif isinstance(error, chapel.TupleExpansionNonTuple):
(_, tup, _) = error.info()
if tup is not None:
location = tup.location()
elif isinstance(error, chapel.MultipleQuestionArgs):
(_, arg1, arg2) = error.info()
assert arg1 is not None and arg2 is not None

# TODO: it would be nice to use additional related info to highlight
# both arguments, but we currently don't assume a URI encoding
# scheme so can't construct a Position := Tuple[URI, Range] for the related info.
location = arg2.location()

return (location, new_message)


def error_to_diagnostic(error) -> Diagnostic:
"""
Convert a Chapel error into a lsprotocol.types Diagnostic
Expand All @@ -61,9 +144,16 @@ def error_to_diagnostic(error) -> Diagnostic:
)
else:
message = "{}: {}".format(error.kind().capitalize(), error.message())

(location, new_message) = error_to_location_and_info(error)
related_info = None
if new_message is not None:
message = new_message

diagnostic = Diagnostic(
range=location_to_range(error.location()),
range=location_to_range(location),
message=message,
severity=kind_to_severity[error.kind()],
related_information=related_info,
)
return diagnostic
28 changes: 28 additions & 0 deletions tools/chapel-py/src/core-types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,10 @@ PyTypeObject* parentTypeFor(chpl::types::paramtags::ParamTag tag) {
return ParamObject::PythonType;
}

PyTypeObject* parentTypeFor(chpl::ErrorType tag) {
return ErrorObject::PythonType;
}

PyObject* wrapGeneratedType(ContextObject* context, const AstNode* node) {
PyObject* toReturn = nullptr;
if (node == nullptr) {
Expand Down Expand Up @@ -385,3 +389,27 @@ PyObject* wrapGeneratedType(ContextObject* context, const chpl::types::Param* no
Py_XDECREF(args);
return toReturn;
}

PyObject* wrapGeneratedType(ContextObject* context, const chpl::ErrorBase* node) {
PyObject* toReturn = nullptr;
if (node == nullptr) {
PyErr_SetString(PyExc_RuntimeError, "implementation attempted to wrap a null pointer");
return nullptr;
}
PyObject* args = Py_BuildValue("(O)", (PyObject*) context);
switch (node->type()) {
case chpl::ErrorType::General:
toReturn = PyObject_CallObject((PyObject*) GeneralErrorType, args);
((GeneralErrorObject*) toReturn)->parent.value_ = node->clone();
break;
#define DIAGNOSTIC_CLASS(NAME, KIND, EINFO...) \
case chpl::ErrorType::NAME: \
toReturn = PyObject_CallObject((PyObject*) NAME##Type, args); \
((NAME##Object*) toReturn)->parent.value_ = node->clone(); \
break;
#include "chpl/framework/error-classes-list.h"
#undef DIAGNOSTIC_CLASS
}
Py_XDECREF(args);
return toReturn;
}
22 changes: 22 additions & 0 deletions tools/chapel-py/src/core-types.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ struct Nilable {
PyTypeObject* parentTypeFor(chpl::uast::asttags::AstTag tag);
PyTypeObject* parentTypeFor(chpl::types::typetags::TypeTag tag);
PyTypeObject* parentTypeFor(chpl::types::paramtags::ParamTag tag);
PyTypeObject* parentTypeFor(chpl::ErrorType tag);

using LineColumnPair = std::tuple<int, int>;

Expand Down Expand Up @@ -241,6 +242,18 @@ struct TypedSignatureObject : public PythonClassWithContext<TypedSignatureObject
}
};

struct ApplicabilityResultObject : public PythonClassWithContext<ApplicabilityResultObject, chpl::resolution::ApplicabilityResult> {
static constexpr const char* QualifiedName = "chapel.ApplicabilityResult";
static constexpr const char* Name = "ApplicabilityResult";
static constexpr const char* DocStr = "The result of checking whether a particular function candidate is applicable to a call.";
};

struct CallInfoObject : public PythonClassWithContext<CallInfoObject, chpl::resolution::CallInfo> {
static constexpr const char* QualifiedName = "chapel.CallInfo";
static constexpr const char* Name = "CallInfo";
static constexpr const char* DocStr = "Information about a particular call, including the actuals usded for the resolution";
};

template<typename IntentType>
const char* intentToString(IntentType intent) {
return qualifierToString(chpl::uast::Qualifier(int(intent)));
Expand Down Expand Up @@ -269,4 +282,13 @@ PyObject* wrapGeneratedType(ContextObject* context, const chpl::types::Type* nod
*/
PyObject* wrapGeneratedType(ContextObject* context, const chpl::types::Param* node);

/**
Creates a Python object of the class corresponding to the given ErrorBase*.

Note: unlike all other generated objects at the type of writing, Errors
aren't owned by the context (and borrowed by Python). Instead, they are
owned by the Python object that contains them. So, here, we clone them.
*/
PyObject* wrapGeneratedType(ContextObject* context, const chpl::ErrorBase* node);

#endif
2 changes: 1 addition & 1 deletion tools/chapel-py/src/error-tracker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ void PythonErrorHandler::report(chpl::Context* context, const chpl::ErrorBase* e
}

// There's an error list! Create an error object and store it into the list.
auto errObj = ErrorObject::create((ContextObject*) contextObject, err->clone());
auto errObj = wrapGeneratedType((ContextObject*) contextObject, err);
PyList_Append(errorLists.back(), (PyObject*) errObj);
}
2 changes: 1 addition & 1 deletion tools/chapel-py/src/error-tracker.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ struct ErrorObject : public PythonClassWithContext<ErrorObject, chpl::owned<chpl
PyType_Slot{Py_tp_str, (void*) str},
{Py_tp_repr, (void*) repr},
};
PyTypeObject* configuring = PythonClassWithContext<ErrorObject, chpl::owned<chpl::ErrorBase>>::configurePythonType(Py_TPFLAGS_DEFAULT, extraSlots);
PyTypeObject* configuring = PythonClassWithContext<ErrorObject, chpl::owned<chpl::ErrorBase>>::configurePythonType(Py_TPFLAGS_BASETYPE, extraSlots);
return configuring;
}
};
Expand Down
5 changes: 5 additions & 0 deletions tools/chapel-py/src/generated-types-list.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@
#include "chpl/types/param-classes-list.h"
#undef PARAM_NODE

#define DIAGNOSTIC_CLASS(NAME, KIND, EINFO...) GENERATED_TYPE(Error, chpl::ErrorBase, NAME, chpl::Error##NAME, chpl::ErrorType::NAME, Py_TPFLAGS_DEFAULT)
GENERATED_TYPE(Error, chpl::ErrorBase, GeneralError, chpl::GeneralError, chpl::ErrorType::General, Py_TPFLAGS_DEFAULT)
#include "chpl/framework/error-classes-list.h"
#undef DIAGNOSTIC_CLASS

#undef GENERATED_TYPE
#undef GENERATED_TYPE_BEGIN
#undef GENERATED_TYPE_END
1 change: 1 addition & 0 deletions tools/chapel-py/src/method-tables.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
#include "method-tables/param-methods.h"
#include "method-tables/type-methods.h"
#include "method-tables/uast-methods.h"
#include "method-tables/error-methods.h"

//
// Cleanup and undefine all macros
Expand Down
Loading