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

Crystal parser to_s can produce invalid code #5997

Closed
anykeyh opened this issue Apr 24, 2018 · 9 comments
Closed

Crystal parser to_s can produce invalid code #5997

anykeyh opened this issue Apr 24, 2018 · 9 comments
Labels
kind:bug A bug in the code. Does not apply to documentation, specs, etc. topic:compiler

Comments

@anykeyh
Copy link

anykeyh commented Apr 24, 2018

Context

Hello, I'm playing around with the Crystal::Visitor, Parser, AST node and so.

I'm trying to implement a coverage binary which enrich the code with coverage operations.
So far, I've made a proof of concept which is working is most of the cases (note: it's a PoC, the code is ugly) : https://github.com/anykeyh/crystal-coverage.

For that, I'm using the syntax visitor and crystal parser to regenerate code with additional instructions.

I'm also expanding all the relative require to generate one file only enriched with the coverage code. I assume the exported file can be passed to crystal eval operation.

Example of code:

class Test
  def foo
    n = 1
    while (n < 10)
      n += 1
      puts "foo!"
    end
  end

  def bar
    begin
      x = 1

      x = x + 1
      raise "Code below will never be covered"

      puts "Some code below"
    rescue
    end
  end
end

test = Test.new

test.foo

Same code enriched using the coverage binary:

require "coverage/runtime"
::Coverage::File.new("spec/test.cr", "6df201f01e8cfb7f575a585521553d65",[23, 25, 3, 5, 4, 6, 12, 14, 15, 17])
#<loc:"spec/test.cr",0,0>
class Test
  def foo
    ::Coverage[0, 2] #<loc:"spec/test.cr",2,0>
        n = 1
    while (        ::Coverage[0, 4] #<loc:"spec/test.cr",3,0>
    n < 10
)
      ::Coverage[0, 3] #<loc:"spec/test.cr",4,0>
            n += 1
            ::Coverage[0, 5] #<loc:"spec/test.cr",5,0>
      puts("foo!")


    end

  end
  def bar
    ::Coverage[0, 6] #<loc:"spec/test.cr",11,0>
    begin
      x = 1
            ::Coverage[0, 7] #<loc:"spec/test.cr",13,0>
      x = x + 1

            ::Coverage[0, 8] #<loc:"spec/test.cr",14,0>
      raise("Code below will never be covered")

            ::Coverage[0, 9] #<loc:"spec/test.cr",16,0>
      puts("Some code below")

    rescue
    end
  end
end
::Coverage[0, 0] #<loc:"spec/test.cr",22,0>
test = Test.new

::Coverage[0, 1] #<loc:"spec/test.cr",24,0>
test.foo


::Coverage.get_results(Coverage::Outputter::Text.new)

Problem

Since I depend heavily on to_s of the nodes, I've noticed a case where the code outputted is not valid code:

  class Test
    def initialize(@select)
    end
  end

The AST to_s method will produce

  class Test
    def initialize(select)
      @select = select
    end
  end

Since select is a reserved keyword, the code will not be evaluable. It obviously happens for any reserved keywords.

How to reproduce

require "compiler/crystal/syntax/*"

source = <<-CRYSTAL
  class Test
    def initialize(@select)
    end
  end
CRYSTAL

astree = Crystal::Parser.parse(source)
puts astree.to_s
@RX14 RX14 added kind:bug A bug in the code. Does not apply to documentation, specs, etc. topic:compiler labels Apr 25, 2018
@asterite
Copy link
Member

asterite commented Apr 25, 2018

Yes, the shortcut @var in methods was a mistake, it also has other problems. We should probably remove it.

@straight-shoota
Copy link
Member

@asterite I think this feature is really nice and the issues are not entirely unsolvable, nor are they serious enough to make it completely fail. For #3413 it'd be relatively easy to disallow ivars as default values and the problem is solved while still being able to set ivars from method arguments.

For this issue here I'd suggest to disallow ivars as arguments that are actually a keyword (this would match with #5930 smoothly). This is a little confinement but still keeps the feature usable and productive, without many problems. At least none that I'm aware of.

@asterite
Copy link
Member

asterite commented Apr 25, 2018

Oh, actually, we should just change how they are expanded. I tried it yesterday but got flooded with "invalid memory access" after doing the change and got demotivated. It will also solve a few other problems. The idea is to expand this:

def foo(@var)
end

to this:

def foo(var __arg0)
  @var = __arg0
end

I'll try it later, I'll see if I can make it work.

@anykeyh
Copy link
Author

anykeyh commented Apr 25, 2018

@asterite

I think the fix could break some code if applied in every case (well it's not a big breaking however).

def method(@var)
  puts var #Works currently in crystal
end

Should it be transposed to __argN only when RESERVED_KEYWORDS.include?(variable_name) ?

@straight-shoota
Copy link
Member

Changing the expansion is also a viable option. However, it makes it difficult to access the argument value as local variable.

def foo(@var)
  var.do_something # => undefined local variable or method 'var'
end

This seems to be a high price compared to the alternative to simply disallow a few error-prone variable names.

@asterite
Copy link
Member

No, no, it's ok to change that behaviour:

#4332

If you have a method var, it's confusing that it would access the local variable. The fact that @var is implemented with a local variable should be transparent to the user. This is a breaking change, but a good one. Using the instance variable instead of the local variable is no problem at all.

@bcardiff
Copy link
Member

@asterite I agree that @var could hide var in the scope.
There is though some corner case regarding unions where var and @var are not interchangeable.

class Foo
  @value : Int32?
  
  def set(@value : Int32)
    pp typeof(value) # => Int32
    pp typeof(@value) # => Int32 | Nil
  end
end

Foo.new.set(4)

But then the user can remove the @ in the args and to the assignment manually. It's a good tradeoff.

@asterite
Copy link
Member

@bcardiff You are right. In fact I got it working now and I had to change such code in a few places. But yeah, I think it's a good tradeoff. Otherwise, we can close #4332 and say "it's the way the language works", which can always be good. I don't know.

@straight-shoota
Copy link
Member

straight-shoota commented Apr 25, 2018

Okay, sounds good. Accessing a ivar argument as local variable is probably a less-known feature anyway. The workaround is easy and more expressive. And avoids surprises as described in #4332.

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

No branches or pull requests

5 participants