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

Introduce $*WARNINGS and ENV<RAKU_WARNINGS> #5206

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open

Conversation

lizmat
Copy link
Contributor

@lizmat lizmat commented Feb 14, 2023

Introduces 5 new classes that provide different behaviours for warnings

  • CX::Warn::Quietly - do not show warnings, similar to quietly
  • CX::Warn::Fatal - immediately throw a warning as an exception
  • CX::Warn::Verbose - immediately show warnings with complete backtrace
  • CX::Warn::Collect - collect warnings until END, then show with frequencies
  • CX::Warn::Debug - show warnings with complete backtrace and debugging info

The behaviour can be set by setting the new dynamic variable $*WARNINGS to any of these classes (CX::Warn for normal behaviour), with the exception of CX::Warn::Debug which must be instantiated.

The default behaviour of a process can be set with the RAKU_WARNINGS environment variable, which should have any of "quietly", "fatal", "verbose", or "collect" specified. No specification defaults to the current behaviour.

Custom behaviour can also be made by creating one's own CX::Warn::Foo class, which should have a WARN method taking the warning message and a Backtrace object. That method is then expected to do the right thing, and return Nil. Such custom classes can also be activated with RAKU_WARNINGS=foo.

I also tried making this a v6.e feature only, but that ran into all sorts of scoping issues. So until these are better understood, this is now implemented as a 6.c feature.

The CX::Warn::Debug class is special in that it must be instantiated, and as such cannot be activated from an environment variable. The .new method takes any number of variables and will show the name of the variable and its value every time a warning is issued.

my $foo = 42;
my $*WARNINGS = CX::Warn::Debug.new($foo);
warn "bar";

will note:

$foo = 42
bar
  in ...

@lizmat
Copy link
Contributor Author

lizmat commented Feb 14, 2023

Some examples:

  • making warnings throw
% RAKU_WARNINGS=fatal raku -e 'sub b() { a }; sub a() { warn "foo" }; a; b for ^10'
foo
  in sub a at -e line 1
  in block <unit> at -e line 1
  • making warnings verbose
% RAKU_WARNINGS=verbose raku -e 'sub b() { a }; sub a() { warn "foo" }; b'         
foo
  in sub a at -e line 1
  in sub b at -e line 1
  in block <unit> at -e line 1
  • quiet all warnings
% RAKU_WARNINGS=quietly raku -e 'sub b() { a }; sub a() { warn "foo" }; a; b for ^10'
  • collect all warning until the end
% RAKU_WARNINGS=collect raku -e 'sub b() { a }; sub a() { warn "foo" }; a; b for ^10'
10x: foo
  in sub a at -e line 1
  in sub b at -e line 1
  in block <unit> at -e line 1

1x: foo
  in sub a at -e line 1
  in block <unit> at -e line 1

@vrurg
Copy link
Member

vrurg commented Feb 14, 2023

I don't like the idea of introducing warnings which are not control exceptions. Though not postulated (or I just've never seen it specced in either way), but a warning is anticipated to be a control exception. And it has to remain such, in my view. Especially considering CX:: namespace which stands for Control eXception and this wouldn't be a good thing to contaminate it with non-exceptional entities.

Thus, I'd be for different handling of warning exceptions, not for cardinal change in their behavior.

I see the point in the feature and fatal warnings is something I did miss on a couple of occasions. But my view would be that it is to be implemented in other way around: by delegating the final handling of a waning to the CX::Warn class itself. It could, say, be method HANDLE on the class. All other CX::Warn::* would then inherit from it. The method could immediately throw for CX::Warn::Fatal, or do nothing for CX::Warn::Quiet.

Apparently, some 3rd party code might utilize non-CX behavior in WARN and do things not up to others expectations. That would be their right to do so and might even be justified, depending on their goals. But the CORE must maintain consistency.

BTW, this may need some prior discussion in problem-solving.

src/core.c/Exception.pm6 Outdated Show resolved Hide resolved
@lizmat
Copy link
Contributor Author

lizmat commented Feb 19, 2023

I don't like the idea of introducing warnings which are not control exceptions.

So why are warnings control exceptions? Historically, it seems so that they can be caught indeed. But the catching of a warning in practice is pretty complicated, needing a CONTROL block.

@lizmat
Copy link
Contributor Author

lizmat commented Feb 19, 2023

Looking at the implementation of quietly, it does:

nqp::handle(
  nqp::call(blorst),
  'WARN',
  nqp::resume(nqp::exception())
)

So it's completely compile time and not amendable. Which I find to be a disadvantage, as you might want to have warnings (not) enables in production vs development.

@vrurg
Copy link
Member

vrurg commented Feb 19, 2023

But the catching of a warning in practice is pretty complicated, needing a CONTROL block.

I don't see how is it complicated. Two more lines if CATCH and CONTROL are to be used within same block?

So it's completely compile time and not amendable.

Only from the outside of it, which is totally correct. Consider the following:

CONTROL {
    when CX::Warn {
        note "OUTSIDE: ", .raku;
        .rethrow;
    }
}

sub foo {
    CONTROL {
        when CX::Warn {
            note "INSIDE: ", .raku;
            .rethrow;
        }
    }
    warn "testing warnings";
}

quietly foo;

It does what I would expect from the code: reports INSIDE, but not OUTSIDE.

Basically, speaking of the design side, with proper separation of concerns the code which throws warnings, and the code which handles them must not depend on the kind of warning to be thrown. With the only exception, perhaps, that the throwing part might chose the particular class to be thrown, depending on context. And then we should let the warning class to determine the kind of behavior it implements. This would allow users to have their own approach if necessary without requiring any changes to the core.

The optimizer might turn the course of events inside out if it knows that external locus of control would do better in a particular situation. But this is what optimziers do – they break rules intentionally in a controllable way. :)

@lizmat
Copy link
Contributor Author

lizmat commented Feb 19, 2023

Compare:

sub foo {
    my $*WARNINGS = CX::Warn::Fatal;
    warn "testing warnings";
}

with:

sub foo {
    CONTROL {
        .throw;
    }
    warn "testing warnings";
}

Also: consider the possibility to switch on / off behaviour of warnings in a production environment. How would you do that with CONTROL blocks?

@lizmat
Copy link
Contributor Author

lizmat commented Feb 19, 2023

Come to think of it, we could also accept a Block as a valid value in $*WARNINGS:

sub foo {
    my $*WARNINGS = { .say }
    warn "foo";
}

@vrurg
Copy link
Member

vrurg commented Feb 20, 2023

What advantage would it give to us? I'd stick to the point that a warning is a control exception and nothing else.

@niner
Copy link
Collaborator

niner commented Feb 20, 2023 via email

@vrurg
Copy link
Member

vrurg commented Feb 20, 2023

Isn't that as simple as CONTROL { when CX::Warn { .gist.note unless %*ENV<PRODUCTION>; .resume } }?

What is good about this PR is the ability to suppress noise when there is an external tool which produces harmless warnings but these warnings disturb me or my tool set in one or other way.

@lizmat
Copy link
Contributor Author

lizmat commented Feb 20, 2023

I will rework this to use the CONTROL mechanism to keep compatibility

@lizmat lizmat requested review from vrurg and jnthn February 20, 2023 20:03
@vrurg
Copy link
Member

vrurg commented Feb 22, 2023

Something went very much wrong about this PR's branch.

@lizmat
Copy link
Contributor Author

lizmat commented Feb 22, 2023

I thought I had just rebased it :-(

 CX::Warn::Quietly - do not show warnings, similar to quietly
 CX::Warn::Fatal - immediately throw a warning as an exception
 CX::Warn::Verbose - immediately show warnings with complete backtrace
 CX::Warn::Collect - collect warnings until END, then show with frequencies
 CX::Warn::Debug - show warnings with complete backtrace and debugging info

The behaviour can be set by setting the new dynamic variable $*WARNINGS to
any of these classes (CX::Warn for normal behaviour), with the exception of
CX::Warn::Debug which *must* be instantiated.

The default behaviour of a process can be set with the RAKU_WARNINGS
environment variable, which should have any of "quietly", "fatal", "verbose",
or "collect" specified. No specification defaults to the current behaviour.

Custom behaviour can also be made by creating one's own CX::Warn::Foo class,
which should have a WARN method taking the warning message and a Backtrace
object. That method is then expected to do the right thing, and return Nil.
Such custom classes can also be activated with RAKU_WARNINGS=foo.

I also tried making this a v6.e feature only, but that ran into all sorts of
scoping issues. So until these are better understood, this is now implemented
as a 6.c feature.

The CX::Warn::Debug is special in that it must be instantiated, and as
such cannot be activated from an environment variable.  The .new method
takes any number of *variables* and will show the name of the variable and
its value every time a warning is issued.

    my $foo = 42;
    my $*WARNINGS = CX::Warn::Debug.new($foo);
    warn "bar";

will note:

    $foo = 42
    bar
      in ...
@lizmat
Copy link
Contributor Author

lizmat commented Feb 22, 2023

I've recreated the branch and added a CX::Warn::Debug class.

The Callable is given the message, and is supposed to return a string
to be used instead of the message.  Otherwise works as ::Verbose.

my $*WARNINGS = { "foo: $_" }
warn "bar";
 # foo: bar
 #   in ...
@vrurg
Copy link
Member

vrurg commented Feb 22, 2023

I still disagree with the course taken. To sum up, what worries me are:

  1. Namespace inconsistency. CX stands for Control eXception, but the new classes are not X::Control. They are not even exceptions.
  2. Complications for user code to handle warnings depending on what $*WARNINGS is set to. Instead of simply dispatching over exception type a handler would have to analyze on only the exception itself but $*WARNINGS itself.
  3. I don't really see a use in supporting callables except for extra code to handle them. Same functionality could be achieved by a custom class, inheriting from CX::Warn.

I stick to the point that $*WARNINGS must be processed by sub EXCEPTION. Each CX::Warn:: must be a child of CX::Warn and be (re-)throwabable by user code. print_control would only invoke .WARN and doesn't care about what it does but keep in mind that the method may not return.

CONTROL {
    when CX::Warn::Debug { ... }
    when CX::Warn::Fatal { ...; .rethrow }
}
class MyApp::CX::Warn::Logging is CX::Warn {
    method WARN {
        $logger.log: $.message;
        nextsame; # Let the default warning do its job
    }
}

}
my class CX::Proceed does X::Control {
method message() { "<proceed control exception>" }
my class CX::Warn::Quietly {
Copy link
Member

Choose a reason for hiding this comment

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

This and the remaining CX::Warn:: must be is CX::Warn or we may cause problems to code which doesn't care about the kind of warning.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I just realized that they're not to be thrown. That's not what I considered. Need to think more about it.

Copy link
Member

Choose a reason for hiding this comment

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

Preliminary, it looks like the right approach would be to have them inherit from CX::Warn, that's for sure. Then sub EXCEPTION must chose a wrapper class depending on $*WARNINGS. Otherwise any use code wanting to react to the kind of warnings being currently used would have to pick into the variable and analyze its content instead of simply multidispatching on CX::Warn:: type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How does &EXCEPTION even enter into this?

The CX::Warn::xxx classes are not to be thrown. The reason I chose their name that way, was a. to prevent more global name pollution. I guess they could well be called Warnings::Quietly, Warnings::Fatal, etc. Also, there's a bit of prior art with $*UPGRADE-RAT which currently takes the CX::Warn class to warn.

To re-iterate: the only time when $*WARNINGS is checked, is when a warning has already been thrown, and we're about to print the warning on STDERR. Only THEN does it act differently (potentially) from before.

If your main beef is with the name of the CX::Warn::xxx, I can change them

Copy link
Member

Choose a reason for hiding this comment

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

To re-iterate: the only time when $*WARNINGS is checked, is when a warning has already been thrown, and we're about to print the warning on STDERR. Only THEN does it act differently (potentially) from before.

This is the key point I consider wrong.

Once again, the idea is to let user code decide their behavior depending on the current kind of warning. Best this is done if they can multi-dispatch over warning type object. To have this working correctly, as I stated, all of them must inherit from CX::Warn and conversion from VM-level control exception into a HLL CX::Warn instance must take place in EXCEPTION. In this case all print_control would have to do is invoke WARN method on a received CX::Warn instance. And when done this way the code in my examples would become possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But, a warn "foo" doesn't call EXCEPTION at all???? Only if you have a CONTROL { } block active, does it run EXCEPTION?

Copy link
Member

Choose a reason for hiding this comment

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

Actually, true. I missed this point. But this changes little because print_control can use it too if what it received is not a X::Control instance.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure what you meant there, but that's the reason for me to put the check on $*WARNINGS in print_control.

Now, if you find the CX::Warn class naming confusing, I can change that.

Copy link
Member

Choose a reason for hiding this comment

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

No, it's not about the naming. It is about handling by user code would that be necessary. It is about being able to multi-dispatch within CONTROL on a caught warning exception.

Simple, in your implementation print_control reads $*WARNINGS and interprets it. EXCEPTION behavior is unchanged, it creates a CX::Warn for user when needed.

I propose to teach EXCEPTION to read and interpret $*WARNINGS. print_control might use this ability to get a CX::Warn to call WARN upon. This way $*WARNINGS interpretation is localized in one place, but the result of it can be used by either user's CONTROL or by print_control equally well.

Copy link
Member

Choose a reason for hiding this comment

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

And, BTW, I don't want them to be renamed. :)

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.

None yet

4 participants