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

Make remove_linenums! more precise: remove lines from full Expr (was missing macrocalls), but _NOT_ from user's code (by preventing recursion into esc()) #31335

Open
wants to merge 15 commits into
base: master
Choose a base branch
from

Conversation

NHDaly
Copy link
Member

@NHDaly NHDaly commented Mar 13, 2019

Fix the @inferred and @test macros to stop modifying user inputs.


After several edits, this PR achieves that by making Base.remove_linenums! not recurse into esc()'d Exprs.

This PR also merges in #29619, which strengthens remove_linenums! to also remove linenumbers from macro calls.

Finally, it introduces a new comparison function, Base. isequal_nolines(a, b), which strips lineinfo nodes from both ASTs and then compares them for equality.

Fixes #31334

Currently `Base.remove_linenums!` removes line information from code
blocks, but not from macro calls, which embed line information as the
second argument in the expression. Having the line information in there
makes it difficult to test user code that performs transformations on
expressions, since the line information may differ in the input and
resulting expressions.

This change extends `Base.remove_linenums!` to handle macro call
expressions, replacing their line information with `nothing`. The result
is:

```julia
julia> :(@macro argument)
:(#= REPL[10]:1 =# @macro argument)

julia> Base.remove_linenums!(:(@macro argument))
:(@macro argument)
```
@NHDaly
Copy link
Member Author

NHDaly commented Mar 13, 2019

@vtjnash's suggestion here is probably better than what I did here.

... that said, I have always wanted something like this function for recursively replacing part of an AST that matches a pattern. But it's probably not needed for this problem.

EDIT: That solution did not work. This PR has been updated.

This makes sense because the code we introduced that we want to strip
linenumbers from and the user's code we want to `esc()` are
complementary.

 - The reason we use `Base.remove_linenums!` is to avoid injecting the linenumbers of the Test code into the user's test expressions. So we want to remove linenums from any code _iff_ it was introduced by _us_ and not the user.
 - Conversely, we want to always `esc` _any code provided by the user_ exactly once, and we _don't_ want to `esc()` our code, since we want our variable names to get gensym'd.
@NHDaly NHDaly changed the title Add Base.replace_subtree!; fix @inferred Expr inputs Fix at-inferred and at-test macros to not strip linenumbers from input Expressions. Aug 27, 2019
@NHDaly NHDaly changed the title Fix at-inferred and at-test macros to not strip linenumbers from input Expressions. Fix @inferred and @test macros to not strip linenumbers from input Expressions. Aug 27, 2019
@NHDaly NHDaly changed the title Fix @inferred and @test macros to not strip linenumbers from input Expressions. Fix @inferred and @test macros to not strip linenumbers from input Expressions: Stop remove_linenums! from recursing into esc() Aug 28, 2019
@NHDaly
Copy link
Member Author

NHDaly commented Sep 3, 2019

The trouble now is that all sorts of tests that compare Exprs will fail, since @test used to strip linenumbers and now it doesn't. Example:

julia/test/syntax.jl

Lines 46 to 47 in 445f216

@test :(module $a end) == :(module a
end)

Test Failed at /Users/sabae/buildbot/worker-tabularasa/tester_macos64/build/share/julia/test/syntax.jl:46
  Expression: $(Expr(:quote, :(module $(Expr(:$, :a))
  #= /Users/sabae/buildbot/worker-tabularasa/tester_macos64/build/share/julia/test/syntax.jl:46 =#
  #= /Users/sabae/buildbot/worker-tabularasa/tester_macos64/build/share/julia/test/syntax.jl:46 =#
  end))) == $(Expr(:quote, :(module a
  #= /Users/sabae/buildbot/worker-tabularasa/tester_macos64/build/share/julia/test/syntax.jl:46 =#
  #= /Users/sabae/buildbot/worker-tabularasa/tester_macos64/build/share/julia/test/syntax.jl:47 =#
  end)))

Can we make this kind of change and just require callers to call Base.remove_linenumbers! explicitly on their expressions before testing them? (Or maybe we can make a (shorter-named?) function that doesn't modify inputs. Maybe strip_linenumbers or without_linenumbers or something?)

It's a breaking change, but only breaking tests, which is maybe easier? It's maybe the kind of breaking where no one realllly minds, and we can just change it? (that is, I'm not sure if we actually promised not to change ASTs, and this feels kinda similar?)

The other option seems to me to somehow either change the definition of ==(::Expr, ::Expr) to remove linenums from both before comparing (Yikes!), or add another comparison function like isapprox(::Expr, ::Expr) that does that.

@KristofferC
Copy link
Sponsor Member

or add another comparison function like isapprox(::Expr, ::Expr) that does that.

FWIW, I like this.

@NHDaly
Copy link
Member Author

NHDaly commented Sep 13, 2019

or add another comparison function like isapprox(::Expr, ::Expr) that does that.

FWIW, I like this.

Same... Any suggestions on the name? isapprox doesn't seem quite right..

@DilumAluthge
Copy link
Member

Honestly, I like the idea of just defining isapprox(::Expr, ::Expr).

You can add a new function called something like compare_expr.

But if you use isapprox, you can use the Unicode .

@NHDaly
Copy link
Member Author

NHDaly commented Sep 13, 2019

Thanks Dilum! :)

Actually, after thinking about it more, this function should be for parsing ASTs, which might be expressions or might be literal values, so we should use a function that could mean something else in the case of a literal.

(Note that :(5.0) === 5.0, so we should avoid something like isapprox(:(5.0), :(5.0001)), which presumably the user would want to find out about.)

So i think this should be its own, Expr-tree-/AST- specific function.

I pushed up some commits that implements this and calls it Test.isequal_nolines, which seems explicit/clear enough. I'm also fine with compare_expr or compare_expr_nolines or compare_ast_nolines? I kind of like reusing the is-blank format that isequal and isapprox follow, but i'm happy with whatever everyone can agree on. :)

@DilumAluthge
Copy link
Member

isequal_nolines seems fine to me. I like compare_expr or compare_ast a little bit better because they are shorter. But you make a good point about isequal_nolines being consist with isequal and isapprox.

You could also do isequal_ast, which is shorter and maintans the consistency.

@NHDaly NHDaly changed the title Fix @inferred and @test macros to not strip linenumbers from input Expressions: Stop remove_linenums! from recursing into esc() Make remove_linenums! more precise: remove lines from full Expr (was missing macrocalls), but _NOT_ from user's code (by preventing recursion into esc()) Sep 17, 2019
@NHDaly
Copy link
Member Author

NHDaly commented Sep 17, 2019

(I have merged in #29619, as well, which makes remove_linenums! also strip linenumbers from macrocalls, which was missing before, by replacing the line info node with #= none:1 =#.)

@NHDaly
Copy link
Member Author

NHDaly commented Sep 17, 2019

Please take a look, I think this PR is now ready to be reviewed.

(The existing test failure appears to be a flake.)

Thanks! :)

See also: [`isapprox`](@ref) for _numerical_ approximate equality
"""
function isequal_nolines(a, b)
Base.remove_linenums!(a) == Base.remove_linenums!(b)
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

This should copy its arguments so the predicate overall is non-mutating.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oops, duh, thanks.

deepcopy() or copy()? I feel like deepcopy, but i still don't really understand the difference b/w them.

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

copy on a container makes a new container with the same elements. So copy of an array allocates exactly one new array. Exprs are more of a tree, so it copies the tree structure (Exprs and all sub-Exprs) but keeps the "leaves". So I think copy is correct here.

deepcopy attempts to copy everything reachable from an object, as if you serialized the whole thing to disk and read it back in. deepcopy is best avoided, since it violates abstraction.

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Suggested change
Base.remove_linenums!(a) == Base.remove_linenums!(b)
a = Base.remove_linenums!(copy(a))
b = Base.remove_linenums!(copy(b))
return a == b

@JeffBezanson
Copy link
Sponsor Member

I don't think excluding :escape expressions is fully correct, it just happens to work in this case. esc is just a scope marker, and it's likely we'll change how hygiene works in the future not to use it.

Did you try simply removing the remove_linenums! calls from Test and seeing if there are any ill effects? I'm hoping that will work now.

@StefanKarpinski
Copy link
Sponsor Member

Bump.

See also: [`isapprox`](@ref) for _numerical_ approximate equality
"""
function isequal_nolines(a, b)
Base.remove_linenums!(a) == Base.remove_linenums!(b)
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Suggested change
Base.remove_linenums!(a) == Base.remove_linenums!(b)
a = Base.remove_linenums!(copy(a))
b = Base.remove_linenums!(copy(b))
return a == b

# Replace line information embedded into macro calls with `nothing`
# Removing the argument entirely invalidates the expression
map!(ex.args, ex.args) do arg
arg isa LineNumberNode ? LineNumberNode(1, :none) : remove_linenums!(arg)
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

These seems controversial, since these are values, not line numbers it’s removing. We could strip the one from the first argument (replacing __source__ with nothing), but I don’t think we should modify the other argument literals in the list.

@MilesCranmer
Copy link
Sponsor Member

Friendly ping

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.

inferred macro causes :block Expr arguments to lose LineNumberNodes
8 participants