Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve error reporting of named argument mismatch #12238

Open
wants to merge 22 commits into
base: develop
Choose a base branch
from
Open
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
- [Types without constructors can be public][12052]
- Symetric, transitive and reflexive [equality for intersection types][11897]
- [IR definitions are generated by an annotation processor][11770]
- [Use fn... to reference any module function](12128)
- [Use fn... to reference any module function][12128]
- [Improve error message for mismatched named argument application][12238]

[11777]: https://github.com/enso-org/enso/pull/11777
[11600]: https://github.com/enso-org/enso/pull/11600
Expand All @@ -79,6 +80,7 @@
[11897]: https://github.com/enso-org/enso/pull/11897
[11770]: https://github.com/enso-org/enso/pull/11770
[12128]: https://github.com/enso-org/enso/pull/12128
[12238]: https://github.com/enso-org/enso/pull/12238

# Enso 2024.5

Expand Down
24 changes: 22 additions & 2 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Errors/Common.enso
Original file line number Diff line number Diff line change
Expand Up @@ -310,12 +310,32 @@ type Not_Invokable

Arguments:
- target: The called object.
Error target
- cause: Additional information about what may have caused the error.
Error target cause
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still an incompatible change for the creators of this object. To make the change compatible use:

Suggested change
Error target cause
Error target cause=Nothing

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried and unfortunately constructors of builtins do not like default arguments.

With the change above, the code

from Standard.Base import all
import Standard.Base.Errors.Common.Not_Invokable

main =
    without_cause = Not_Invokable.Error "thing"
    IO.println without_cause

still prints

Not_Invokable.Error target='thing' cause=_

I think the defaultness has to be handled in engine and we don't have capability to do that.

Technically, even with the default argument this change would still breaking as any pattern matching of Not_Invokable.Error _ also will now mismatch the amount of fields.

We could try to have 2 constructors for Not_Invokable - the old one and the new one. But I think this is complicating stuff unnecessarily. Also not sure if the builtins machinery would currently allow this, as it would no longer be a UniquelyConstructibleBuiltin.


## PRIVATE
Convert the Not_Invokable error to a human-readable format.
to_display_text : Text
to_display_text self = "Type error: expected a function, but got "+self.target.to_display_text+"."
to_display_text self =
suffix = case self.cause of
Nothing -> ""
_ -> " Caused by: "+self.cause.to_display_text
"Type error: expected a function, but got "+self.target.to_display_text+"."+suffix

@Builtin_Type
type No_Such_Argument
## PRIVATE
Indicates that an argument was passed by name, but the function being
called did not take any argument that matched that name.

Arguments:
- argument_name: The name of the argument that was not found.
Error argument_name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove ## PRIVATE use:

Suggested change
Error argument_name
private Error argument_name

possibly remove the documentation comment altogether.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is against the convention that we are using for all the errors.

I think changes in this PR should be consistent with the rest of the codebase - if we want to change the convention let's discuss and consider rewriting the whole Common.enso file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want the constructor to be private, this breaks the very simple check I've implemented like payload.cause == (No_Such_Argument.Error "foo"), at least outside of Base.

The PRIVATE is to avoid cluttering Component Browser only, I guess at some point we may remove that tag from errors.


## PRIVATE
Convert the No_Such_Argument error to a human-readable format.
to_display_text : Text
to_display_text self = "The named argument `"+self.argument_name.to_text+"` did not match any argument names. Perhaps it is misspelled?"

@Builtin_Type
type Private_Access
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from Standard.Base import all
import Standard.Base.Errors.Common.No_Such_Argument
import Standard.Base.Errors.Common.Not_Invokable
import Standard.Base.Errors.Common.Type_Error
import Standard.Base.Errors.Illegal_Argument.Illegal_Argument
import Standard.Base.Network.HTTP.Response.Response
Expand Down Expand Up @@ -61,13 +63,14 @@ type Delimited_Format
@delimiter make_file_read_delimiter_selector
@encoding Encoding.default_widget
@row_limit Rows_To_Read.default_widget
Delimited (delimiter:Text=',') (encoding:Encoding=Encoding.default) (skip_rows:Integer=0) (row_limit:Rows_To_Read=..All_Rows) (quote_style:Quote_Style=Quote_Style.With_Quotes) (headers:Headers=Headers.Detect_Headers) (value_formatter:Data_Formatter|Nothing=Data_Formatter.Value) (on_invalid_rows:Invalid_Rows=Invalid_Rows.Add_Extra_Columns) (line_endings:Line_Ending_Style|Infer=Infer) (comment_character:Text|Nothing=Nothing)
Delimited (delimiter:Text=',') (encoding:Encoding=Encoding.default) (skip_rows:Integer=0) (row_limit:Rows_To_Read=..All_Rows) (quote_style:Quote_Style=..With_Quotes) (headers:Headers=..Detect_Headers) (value_formatter:Data_Formatter|Nothing=Data_Formatter.Value) (on_invalid_rows:Invalid_Rows=..Add_Extra_Columns) (line_endings:Line_Ending_Style|Infer=Infer) (comment_character:Text|Nothing=Nothing)

## PRIVATE
Resolve an unresolved constructor to the actual type.
resolve : Function -> Delimited_Format | Nothing
resolve constructor =
Panic.catch Type_Error (constructor:Delimited_Format) _->Nothing
_catch_compatibility_changes <|
Panic.catch Type_Error (constructor:Delimited_Format) _->Nothing

## PRIVATE
ADVANCED
Expand Down Expand Up @@ -210,3 +213,13 @@ Delimited_Format.from (that : JS_Object) =
Delimited_Format.Delimited delimiter=delimiter encoding=encoding headers=headers skip_rows=skip_rows row_limit=row_limit quote_style=quote_style on_invalid_rows=on_invalid_rows
field ->
Error.throw (Illegal_Argument.Error ("The field `" ++ field ++ "` is currently not supported when deserializing the Delimited format from JSON."))

private _catch_compatibility_changes ~action =
on_not_invokable caught_panic =
# Handling the compatibility change from https://github.com/enso-org/enso/pull/12231
is_keep_invalid_rows = caught_panic.payload.cause == (No_Such_Argument.Error "keep_invalid_rows")
if is_keep_invalid_rows.not then
Panic.throw caught_panic
Error.throw (Illegal_Argument.Error "The `keep_invalid_rows` argument has been renamed to `on_invalid_rows`.")
Panic.catch Not_Invokable handler=on_not_invokable <|
action
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from Standard.Base import all
import Standard.Base.Errors.Common.No_Such_Argument
import Standard.Base.Errors.Common.Not_Invokable
import Standard.Base.Metadata.Widget
from Standard.Base.Logging import all
Expand Down Expand Up @@ -27,9 +28,21 @@ get_widget_json value call_name argument_names uuids="{}" =

read_annotation argument = Panic.catch Any handler=(log_panic argument) <|
annotation = Warning.clear <| Meta.get_annotation value call_name argument
return_target err = err.payload.target
Panic.catch Not_Invokable handler=return_target
annotation value cache=cache
case annotation of
# The annotation is a function that is taking the 'self' value and _maybe_ a cache parameter.
f : Function ->
# We pass the self value and the cache to the function.
Panic.catch Not_Invokable (f value cache=cache) caught_panic->
## If the cache argument was not expected, we ignore the problem
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you use ## instead of # here?

Suggested change
## If the cache argument was not expected, we ignore the problem
# If the cache argument was not expected, we ignore the problem

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the ## allows to write a multi-line comment, and I don't want to create lines 300+ characters long.

and return just the result of `f value` - held by
the target of the Not_Invokable error.
was_not_expecting_cache_argument = caught_panic.payload.cause == (No_Such_Argument.Error "cache")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question to myself: Why do I find this unreadable? Would following be better?

f : Function ->
    Panic.catch Not Invocable (f value cache=cache) caught_panic -> case caught.panic.payload.cause of
       No_Such_Argument.Error "cache" -> caught_panic.payload.target
       _ -> Panic.throw caught_panic

Maybe, but caught.panic.payload.cause is still quite long...

if was_not_expecting_cache_argument then caught_panic.payload.target else
# If this is some other error, we want to rethrow it.
Panic.throw caught_panic

# The annotation is a constant, so we just return it.
_ -> annotation

annotations = argument_names.map (arg -> [arg, read_annotation arg])
annotations.to_json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ public void invokeBuiltinWithWrongArguments_ShouldNotCrash() {
} catch (PolyglotException e) {
var panic = e.getGuestObject();
assertThat("Should be panic", panic.isException());
assertThat("Should have Type error as payload", e.getMessage(), containsString("Type error"));
assertThat(
"Should have Not_Invokable error as payload",
e.getMessage(),
containsString("Type error"));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@
import org.enso.interpreter.runtime.callable.UnresolvedSymbol;
import org.enso.interpreter.runtime.callable.argument.CallArgumentInfo;
import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.data.atom.Atom;
import org.enso.interpreter.runtime.data.atom.AtomConstructor;
import org.enso.interpreter.runtime.error.DataflowError;
import org.enso.interpreter.runtime.error.PanicException;
import org.enso.interpreter.runtime.error.PanicSentinel;
import org.enso.interpreter.runtime.state.State;
import org.enso.interpreter.runtime.warning.AppendWarningNode;
Expand Down Expand Up @@ -214,7 +212,10 @@ public Object invokeGeneric(
InvokeCallableNode.DefaultsExecutionMode defaultsExecutionMode,
InvokeCallableNode.ArgumentsExecutionMode argumentsExecutionMode,
BaseNode.TailStatus isTail) {
Atom error = EnsoContext.get(this).getBuiltins().error().makeNotInvokable(callable);
throw new PanicException(error, this);
// The IndirectInvokeCallableNode is used only from IndirectCurryNode, so it is always used for
// oversaturated arguments as long as schema.length >= 1.
boolean isEligibleForOversaturatedApplication = true;
throw InvokeCallableNode.buildNotInvokablePanicWithCause(
this, callable, isEligibleForOversaturatedApplication, schema);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.enso.interpreter.node.callable;

import com.oracle.truffle.api.CompilerAsserts;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.dsl.Bind;
import com.oracle.truffle.api.dsl.Cached;
Expand Down Expand Up @@ -105,10 +106,18 @@ public boolean shouldExecute() {

private final ArgumentsExecutionMode argumentsExecutionMode;

@CompilerDirectives.CompilationFinal(dimensions = 1)
private final CallArgumentInfo[] schema;
radeusgd marked this conversation as resolved.
Show resolved Hide resolved

private final boolean isForOversaturatedArguments;

InvokeCallableNode(
CallArgumentInfo[] schema,
DefaultsExecutionMode defaultsExecutionMode,
ArgumentsExecutionMode argumentsExecutionMode) {
ArgumentsExecutionMode argumentsExecutionMode,
boolean isForOversaturatedArguments) {
this.schema = schema;
this.isForOversaturatedArguments = isForOversaturatedArguments;
Integer thisArg = thisArgumentPosition(schema);
this.canApplyThis = thisArg != null;
this.thisArgumentPosition = thisArg == null ? -1 : thisArg;
Expand Down Expand Up @@ -166,7 +175,8 @@ public static InvokeCallableNode build(
CallArgumentInfo[] schema,
DefaultsExecutionMode defaultsExecutionMode,
ArgumentsExecutionMode argumentsExecutionMode) {
return InvokeCallableNodeGen.create(schema, defaultsExecutionMode, argumentsExecutionMode);
return InvokeCallableNodeGen.create(
schema, defaultsExecutionMode, argumentsExecutionMode, false);
}

@Specialization
Expand Down Expand Up @@ -375,8 +385,23 @@ static Object doPolyglot(
@Fallback
public Object invokeGeneric(
Object callable, VirtualFrame callerFrame, State state, Object[] arguments) {
Atom error = EnsoContext.get(this).getBuiltins().error().makeNotInvokable(callable);
throw new PanicException(error, this);
throw buildNotInvokablePanicWithCause(this, callable, isForOversaturatedArguments, schema);
}

static PanicException buildNotInvokablePanicWithCause(
Node node,
Object notCallableTarget,
boolean isForOversaturatedArguments,
CallArgumentInfo[] schema) {
boolean isMismatchedNamedArgument =
isForOversaturatedArguments && schema.length >= 1 && schema[0].isNamed();
CompilerAsserts.partialEvaluationConstant(isMismatchedNamedArgument);
var errors = EnsoContext.get(node).getBuiltins().error();
Atom cause = null;
if (isMismatchedNamedArgument) {
cause = errors.makeNoSuchArgument(schema[0].getName());
}
return new PanicException(errors.makeNotInvokableWithCause(notCallableTarget, cause), node);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ private void initializeOversaturatedCallNode(
InvokeCallableNodeGen.create(
postApplicationSchema.getOversaturatedArguments(),
defaultsExecutionMode,
argumentsExecutionMode);
argumentsExecutionMode,
true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need new boolean argument? Isn't postApplicationSchema.getOversaturatedArguments() good enough indicator that there are oversaturated arguments?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the create method/constructor does not see postApplicationSchema, it sees the result of postApplicationSchema.getOversaturatedArguments() which is just a CallArgumentInfo[] schema.

From inside of InvokeCallableNodeGen.create you can no longer tell if the arguments being passed were 'oversaturated' or 'normal' - you just get some schema, that is the same kind as in all different places where InvokeCallableNode is constructed.

That's why this argument is added to be able to distinguish the situations at call-site.

oversaturatedCallableNode.setTailStatus(getTailStatus());
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.enso.interpreter.node.expression.builtin.error;

import java.util.List;
import org.enso.interpreter.dsl.BuiltinType;
import org.enso.interpreter.node.expression.builtin.UniquelyConstructibleBuiltin;

@BuiltinType
public class NoSuchArgument extends UniquelyConstructibleBuiltin {
@Override
protected String getConstructorName() {
return "Error";
}

@Override
protected List<String> getConstructorParamNames() {
return List.of("argument_name");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ protected String getConstructorName() {

@Override
protected List<String> getConstructorParamNames() {
return List.of("target");
return List.of("target", "cause");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.enso.interpreter.node.expression.builtin.error.ModuleDoesNotExist;
import org.enso.interpreter.node.expression.builtin.error.ModuleNotInPackageError;
import org.enso.interpreter.node.expression.builtin.error.NoConversionCurrying;
import org.enso.interpreter.node.expression.builtin.error.NoSuchArgument;
import org.enso.interpreter.node.expression.builtin.error.NoSuchConversion;
import org.enso.interpreter.node.expression.builtin.error.NoSuchField;
import org.enso.interpreter.node.expression.builtin.error.NoSuchMethod;
Expand Down Expand Up @@ -61,6 +62,7 @@ public final class Error {
private final UnsupportedArgumentTypes unsupportedArgumentsError;
private final ModuleDoesNotExist moduleDoesNotExistError;
private final NotInvokable notInvokable;
private final NoSuchArgument noSuchArgument;
private final PrivateAccess privateAccessError;
private final InvalidConversionTarget invalidConversionTarget;
private final NoSuchField noSuchField;
Expand Down Expand Up @@ -100,6 +102,7 @@ public Error(Builtins builtins, EnsoContext context) {
unsupportedArgumentsError = builtins.getBuiltinType(UnsupportedArgumentTypes.class);
moduleDoesNotExistError = builtins.getBuiltinType(ModuleDoesNotExist.class);
notInvokable = builtins.getBuiltinType(NotInvokable.class);
noSuchArgument = builtins.getBuiltinType(NoSuchArgument.class);
privateAccessError = builtins.getBuiltinType(PrivateAccess.class);
invalidConversionTarget = builtins.getBuiltinType(InvalidConversionTarget.class);
noSuchField = builtins.getBuiltinType(NoSuchField.class);
Expand Down Expand Up @@ -312,7 +315,30 @@ public Atom makeModuleDoesNotExistError(String name) {
* @return a not invokable error
*/
public Atom makeNotInvokable(Object target) {
return notInvokable.newInstance(target);
return notInvokable.newInstance(target, context.getNothing());
}

/**
* @param target the target attempted to be invoked
* @param cause additional information on what caused the error
* @return a not invokable error
*/
public Atom makeNotInvokableWithCause(Object target, Object cause) {
if (cause == null) {
cause = context.getNothing();
}
return notInvokable.newInstance(target, cause);
}

/**
* Constructs an error that indicates that a named argument application could not find a matching
* parameter.
*
* @param argumentName name of the named argument being applied
* @return a no such argument error
*/
public Atom makeNoSuchArgument(String argumentName) {
return noSuchArgument.newInstance(Text.create(argumentName));
}

/**
Expand Down
22 changes: 21 additions & 1 deletion test/Base_Tests/src/Semantic/Default_Args_Spec.enso
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from Standard.Base import all
import Standard.Base.Errors.Common.No_Such_Argument
import Standard.Base.Errors.Common.Not_Invokable

from Standard.Test import all

Expand Down Expand Up @@ -86,8 +88,26 @@ add_specs suite_builder =
v1.at 2 . should_equal h.y
v1.at 3 . should_equal h.y

group_builder.specify "should return a helpful error when applying a named argument that does not match" <|
f a='a' b='b' c='c' =
[a, b, c]

r1 = Test.expect_panic Not_Invokable (f y='Y')
r1.to_display_text . should_contain "The named argument `y` did not match any argument names. Perhaps it is misspelled?"
r1.cause.should_equal (No_Such_Argument.Error "y")

g a=1 = [a]
r2 = Test.expect_panic Not_Invokable (g x=2)
r2.to_display_text . should_contain "The named argument `x` did not match any argument names. Perhaps it is misspelled?"
r2.cause.should_equal (No_Such_Argument.Error "x")

group_builder.specify "should return Not_Invokable if a non-function is called" <|
not_f = 42 : Any
Test.expect_panic Not_Invokable (not_f 1)
r2 = Test.expect_panic Not_Invokable (not_f x=1)
r2.cause.should_equal Nothing

main filter=Nothing =
suite = Test.build suite_builder->
add_specs suite_builder
suite.run_with_filter filter

6 changes: 6 additions & 0 deletions test/Table_Tests/src/IO/Delimited_Read_Spec.enso
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,12 @@ add_specs suite_builder =
r3.at 'b' . to_vector . should_equal ['2', '0', '5']
r3.at 'c' . to_vector . should_equal ['3', Nothing, '6']

group_builder.specify "should offer a helpful error when using the old argument name" <|
r1 = Data.read (enso_project.data / "varying_rows.csv") (..Delimited "," headers=True keep_invalid_rows=False value_formatter=Nothing)
r1.should_fail_with Illegal_Argument
r1.to_display_text.should_contain "keep_invalid_rows"
r1.to_display_text.should_contain "on_invalid_rows"

group_builder.specify "should aggregate invalid rows over some limit" <|
action on_problems =
Data.read (enso_project.data / "many_invalid_rows.csv") (..Delimited "," headers=True on_invalid_rows=False value_formatter=Nothing) on_problems
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,16 @@ type Type_Error

@Builtin_Type
type Not_Invokable
Error target
Error target cause

to_display_text self = "Type error: expected a function, but got "+self.target.to_display_text+"."

@Builtin_Type
type No_Such_Argument
Error argument_name

to_display_text self = "The named argument `"+self.argument_name.to_text+"` did not match any argument names. Perhaps it is misspelled?"
radeusgd marked this conversation as resolved.
Show resolved Hide resolved

@Builtin_Type
type Compile_Error
Error message
Expand Down
Loading