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

More accurate macro errors #13260

Merged

Conversation

Blacksmoke16
Copy link
Member

@Blacksmoke16 Blacksmoke16 commented Apr 1, 2023

  • Tweak how macro errors are treated to retain original location
  • Add test coverage for these cases

Resolves #7147

Top level raise in macro

macro macro_raise
  {% raise "OH NO" %}
end

macro_raise

Before:

$ crystal run --error-trace test.cr 
In test.cr:5:1

 5 | macro_raise
     ^----------
Error: OH NO

After:

$ ccrystal run --error-trace test.cr 
In test.cr:2:6

 2 | {% raise "OH NO" %}
        ^----
Error: OH NO

In test.cr:5:1

 5 | macro_raise
     ^----------
Error: OH NO

Node raise in macro

macro macro_raise_on(arg)
  {% arg.raise "OH NO" %}
end

macro_raise_on 123

Before:

$ crystal run --error-trace test.cr 
In test.cr:5:1

 5 | macro_raise_on 123
     ^-------------
Error: OH NO

After:

$ ccrystal run --error-trace test.cr 
In test.cr:5:1

 5 | macro_raise_on 123
     ^-------------
Error: OH NO

In test.cr:5:16

 5 | macro_raise_on 123
                    ^
Error: OH NO

This one is a bit weird, but I didn't think it was worth trying to figure out how to remove the first frame in only this single context.

Node raise in macro missing location

macro macro_raise_on(arg)
  {% arg.raise "foo" %}
end

macro_raise_on :this

Before:

$ crystal run --error-trace test.cr 
In test.cr:5:1

 5 | macro_raise_on :this
     ^-------------
Error: foo

After:

$ ccrystal run --error-trace test.cr 
In test.cr:5:1

 5 | macro_raise_on :this
     ^-------------
Error: foo

Node raise in complex/nested macro call

class Child
  def self.value : Nil
  end
end

module ExampleModule
  macro calculate_value
    {% begin %}
      {%
        if method = Child.class.methods.find &.name.stringify.==("value")
          method.raise "Value method must be an instance method."
        else
          raise "BUG: Didn't find value method."
        end
      %}
    {% end %}
  end

  class_getter value : Nil do
    calculate_value
  end
end

ExampleModule.value

Before:

$ crystal run --error-trace test.cr 
In test.cr:24:15

 24 | ExampleModule.value
                    ^----
Error: Value method must be an instance method.

After:

$ ccrystal run --error-trace test.cr 
In test.cr:24:15

 24 | ExampleModule.value
                    ^----
Error: Value method must be an instance method.

In test.cr:20:5

 20 | calculate_value
      ^--------------
Error: Value method must be an instance method.

In test.cr:2:12

 2 | def self.value : Nil
              ^----
Error: Value method must be an instance method.

Top level raise in method

def foo(x)
  {% raise "OH NO" %}
end

foo 1

Before:

$ crystal run --error-trace test.cr 
In test.cr:5:1

 5 | foo 1
     ^--
Error: OH NO

After:

$ ccrystal run --error-trace test.cr 
In test.cr:2:6

 2 | {% raise "OH NO" %}
        ^----
Error: OH NO

In test.cr:5:1

 5 | foo 1
     ^--
Error: OH NO

@Blacksmoke16 Blacksmoke16 added kind:bug A bug in the code. Does not apply to documentation, specs, etc. topic:compiler labels Apr 1, 2023
Fix bug within interpreter I ran into while testing
@@ -1871,7 +1871,7 @@ class Crystal::Repl::Compiler < Crystal::Visitor
begin
create_compiled_def(node, target_def)
rescue ex : Crystal::TypeException
node.raise ex, inner: ex
node.raise ex.message, inner: ex
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 think manually creating the type exceptions via .for_node changed something that caused these calls to be taken into consideration and error due to the first arg expecting a string not the exception instance.

msg = args.map do |arg|
arg.accept interpreter
interpreter.last.to_macro_id
end
msg = msg.join " "

node.raise msg, exception_type: Crystal::MacroRaiseException
node.raise msg, exception_type: exception_type
Copy link
Member Author

Choose a reason for hiding this comment

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

Allows differentiating a node specific versus top level raise to make the logic simpler as each case is slightly different in how it should be ordered.

Copy link
Member

@straight-shoota straight-shoota left a comment

Choose a reason for hiding this comment

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

Looks good. I'm only a bit unsure about the representation of TopLevelMacroRaiseException. Not entirely sure why. Maybe because I think {% foo.raise "" %} and {% raise "" %} should produce the same concept of an exception? I suppose an alternative implementation would be to have a property in MacroRaiseException to signal whether the raise call has a target or not.
I'm definitely fine with taking it as is, just leaving this as food for thought.

def self.value : Nil
ex.to_s.should contain "OH NO"
ex.to_s.should contain "error in line 5"
ex.to_s.scan("error in line").size.should eq 2
Copy link
Member

Choose a reason for hiding this comment

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

Why not check for the concrete line number ("error in line 2"?) here like above?

Copy link
Member Author

Choose a reason for hiding this comment

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

In this particular case, the trace looks like:

$ ./bin/crystal run --error-trace test.cr 
In test.cr:5:1

 5 | macro_raise_on 123
     ^-------------
Error: OH NO


In test.cr:5:16

 5 | macro_raise_on 123
                    ^
Error: OH NO

From the OP:

This one is a bit weird, but I didn't think it was worth trying to figure out how to remove the first frame in only this single context.

So the spec is basically asserting there are 2 frames on the same line, unlike the previous example which had two frames on two different lines.

spec/compiler/semantic/macro_spec.cr Outdated Show resolved Hide resolved
@straight-shoota straight-shoota added this to the 1.9.0 milestone Apr 26, 2023
@straight-shoota straight-shoota merged commit b5450a2 into crystal-lang:master Apr 27, 2023
@Blacksmoke16 Blacksmoke16 deleted the more-accurate-macro-errors branch April 27, 2023 12:36
@asterite
Copy link
Member

asterite commented Jul 4, 2023

Note that the frame for raise should never be shown. I implemented it that way so that a method call could produce a error just like the compiler can produce an error for it (for example, wrong number of arguments). Pointing to raise makes the error a bit more obscure, and kind of loses the point I had in mind.

The other changes look good, though!

@straight-shoota
Copy link
Member

Yeah, that's an important point. I think this change in its entirety is still a good move forward. No existing specs are broken, so the error attribution scope was never specified in code. And as we can see from the examples in #7147 there were quite some inconsistencies.
Now we have a more clearly defined base behaviour. We can use that to adjust the error reporting for better usability in future changes.

Showing the frame of the raise call probably won't hurt anyone in the mean time.
Do you agree that we can leave this change in for 1.9 (it really is quite an improvement) and do some fine tuning afterwards?

@asterite
Copy link
Member

asterite commented Jul 5, 2023

Of course! I was just mentioning the intention behind not showing raise in the frame, but the overall change is better than that lost feature. And that feature isn't that important either.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind:bug A bug in the code. Does not apply to documentation, specs, etc. topic:compiler
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Macro raise doesn't keep location
3 participants