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

Lower CmpExp between classes to __cmp call #9629

Merged
merged 1 commit into from
Jan 3, 2022

Conversation

edi33416
Copy link
Contributor

This requires druntime's PR

@dlang-bot
Copy link
Contributor

dlang-bot commented Apr 16, 2019

Thanks for your pull request and interest in making D better, @edi33416! We are looking forward to reviewing it, and you should be hearing from a maintainer soon.
Please verify that your PR follows this checklist:

  • My PR is fully covered with tests (you can see the coverage diff by visiting the details link of the codecov check)
  • My PR is as minimal as possible (smaller, focused PRs are easier to review than big ones)
  • I have provided a detailed rationale explaining my changes
  • New or modified functions have Ddoc comments (with Params: and Returns:)

Please see CONTRIBUTING.md for more information.


If you have addressed all reviews or aren't sure how to proceed, don't hesitate to ping us with a simple comment.

Bugzilla references

Your PR doesn't reference any Bugzilla issue.

If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog.

Testing this PR locally

If you don't have a local development environment setup, you can use Digger to test this PR:

dub run digger -- build "master + dmd#9629"

Copy link
Member

@wilzbach wilzbach left a comment

Choose a reason for hiding this comment

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

Can we test this with a fake object.d directly here?

// Lower to object.__cmp(e1, e2)
Expression cl = new IdentifierExp(exp.loc, Id.empty);
cl = new DotIdExp(exp.loc, cl, Id.object);
cl = new DotIdExp(exp.loc, cl, Id.__cmp);
Copy link
Member

Choose a reason for hiding this comment

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

As discussed on other PRs: don't use __ as it can lead to weird side effects - use _d_

Copy link
Contributor Author

Choose a reason for hiding this comment

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

__cmp is already defined in druntime and it’s already used in the lowering of array comparisons. This PR just uses an overload of it.

Copy link
Contributor

Choose a reason for hiding this comment

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

@wilzbach In this case the Id.__cmp is unavoidable. It's the same for the __ArrayCast identifier that I refactored in #9572. We'd have to refactor id.d and object.d to correct the issue. See how they are mixed in here: https://github.com/JinShil/dmd/blob/master/src/dmd/id.d#L26-L42

If this were a new symbol, it would be prudent to have it changed, but since this already exists, I think it would be too disruptive to include it in this PR. It could be remedied in a separate DMD and druntime PR, however.

@edi33416
Copy link
Contributor Author

Can we test this with a fake object.d directly here?

How can I do that?

@JinShil
Copy link
Contributor

JinShil commented Apr 17, 2019

Can we test this with a fake object.d directly here?

What I've been advocating (actually an idea from @ibuclaw) is to focus on the druntime PR first.

  1. The druntime PR should be dead code except for some unittests that specifically test the new implementation.
  2. After the dead code is committed to druntime, the DMD PR can officially test it.
  3. After the DMD PR is merged, a followup PR can be submitted to druntime to remove the code that the original druntime PR replaced.

So, I suggest focusing on the druntime PR first:
To test the new druntime code, add some unittests to the druntime PR that call the __cmp function directly. This can help demonstrate the druntime code is doing what it should before focusing on the DMD PR, and reduce the likelihood of a messy back-and-forth updating druntime and DMD until all problems are sorted out. See the unittests at https://github.com/dlang/druntime/blob/50d8d0910f2b886a09fa0cff4102db37e33655bc/src/object.d#L4859-L4879 for a demonstration.

Here's the workflow we followed for changing _d_arraycast to __ArrayCast:

  1. druntime PR to add object.__ArrayCast as dead code: Convert _d_arraycast to template druntime#2264
  2. DMD PR to replace call to _d_arraycast with instantiation of object__ArrayCast (object.__ArrayCast is no longer dead code): Replace call to runtime hook _d_arraycast with call to template object.__ArrayCast - Take 2 #9516
  3. druntime PR to remove old _d_arraycast: Remove _d_arraycast druntime#2535

There were a couple of patches along the way, but I think that's a good pattern to follow for this work.

Of course it would be wise for the author to test locally before submitting any PRs to reduce additional PRs being needed to patch things.

@jacob-carlborg
Copy link
Contributor

LGTM.

if (t1.ty == Tclass && t2.ty == Tclass)
{
// Lower to object.__cmp(e1, e2)
Expression cl = new IdentifierExp(exp.loc, Id.empty);
Copy link
Contributor

Choose a reason for hiding this comment

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

Will this lower to object.__cmp(e1, e2) or .object.__cmp(e1, e2)? Note the leading dot.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there a .object.__cmp(e1, e2) ? I assumed that object.__cmp(e1, e2) gives the full path, so the dot(.) for "take the global __cmp(e1, e2)" wouldn't change who gets called. Am I wrong?

Copy link
Member

Choose a reason for hiding this comment

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

object.__cmp(e1, e2) could find a local class or instance object and search it for a member __cmp, but Id.empty takes care of starting the search at module level. So .object.__cmp(e1, e2) would be a more accurate comment, but that is not done anywhere else.

Copy link
Contributor

Choose a reason for hiding this comment

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

but that is not done anywhere else

It doesn't hurt to improve. I had to ask so it was not clear enough.

@@ -10225,6 +10225,24 @@ private extern (C++) final class ExpressionSemanticVisitor : Visitor
return setError();
}

if (t1.ty == Tclass && t2.ty == Tclass)
Copy link
Member

Choose a reason for hiding this comment

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

I suspect this is handled too early here. What about operator overloads, e.g. opBinary and opBinaryRight?

IMO lowerings should happen after all possible semantic errors have been reported to the user, so the user doesn't see cryptic messages referring to the lowered expressions in druntime (unless the implementation there is broken).

Copy link
Contributor

Choose a reason for hiding this comment

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

I suspect this is handled too early here. IMO lowerings should happen after all possible semantic errors have been reported to the user, so the user doesn't see cryptic messages referring to the lowered expressions in druntime (unless the implementation there is broken).

Where, specifically, do you recommend this be done? How does one know that semantic is finished and it's the proper time for lowering?

What about operator overloads, e.g. opBinary and opBinaryRight?

Can you provide a test case?

Copy link
Member

Choose a reason for hiding this comment

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

Sorry for the delay.

Where, specifically, do you recommend this be done?

I would have expected it to be close to where the array lowering is happening, i.e. after operator overload.

How does one know that semantic is finished and it's the proper time for lowering?

Yeah, hard to tell due to possibly deferred analysis. I imagine/dream that no lowerings should happen in the first 3 semantic passes (so user messages never get polluted with compiler generated identifiers and the AST can be mapped back to the source correctly). An additional pass would lower the code to library constructs and continue semantic analysis on the result, but should never generate messages if the library code isn't screwed up.

What about operator overloads, e.g. opBinary and opBinaryRight?
Can you provide a test case?

I was wrong about opBinary that cannot replace opCmp, but here's an example that gets lowered too early:

import core.stdc.stdio;

class A
{
	int a = 3;
	
	override int opCmp(Object o)
	{
		auto b = cast(B)o;
		if (!b)
			return super.opCmp(o);
		printf("comparing a=%d with b=%d\n", this.a, b.b);
		return this.a - b.b;
	}
}

class B
{
	int b = 4;
}

void main()
{
	A a = new A;
	B b = new B;
	assert(b > a);
}

I don't think it is possible for the library code to detect "rewrite 2" in https://dlang.org/spec/operatoroverloading.html#compare

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, hard to tell due to possibly deferred analysis.

Doesn't the semantic analysis end here:

Module.runDeferredSemantic3();

Or possibly here:

}

@n8sh
Copy link
Member

n8sh commented May 14, 2019

If I understand correctly, the purpose of this PR is so that comparison will work with null, right?

Copy link
Member

@andralex andralex left a comment

Choose a reason for hiding this comment

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

@rainers or @WalterBright when you're good I'm good. Please request changes/approve appropriately.

Copy link
Member

@rainers rainers left a comment

Choose a reason for hiding this comment

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

Copy link
Contributor

@RazvanN7 RazvanN7 left a comment

Choose a reason for hiding this comment

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

cc @rainers I think this is good to go

@RazvanN7 RazvanN7 added the 72h no objection -> merge The PR will be merged if there are no objections raised. label Dec 17, 2021
}
// When reversing operands of comparison operators,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this change necessary? Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I belive it is: you only need to reverse the op only when we decide to write the call exp as e2.opFunc(e1).
As it was, the op was always reversed. Imho this was a bug that just wasn’t manifesting

src/dmd/expressionsem.d Outdated Show resolved Hide resolved
@rainers
Copy link
Member

rainers commented Dec 18, 2021

cc @rainers I think this is good to go

The global symbol lookup seems fine now, but none of the changes seem to be covered by the test suite. As the druntime PR is merged, adding tests should be possible now.

@RazvanN7
Copy link
Contributor

@rainers Hmm, I was going to write that since this implementation is now used for class comparison, the fact that the tests are passing shows that it should be correct. My expectation wass that class comparisons are thoroughly tested in the test suite, but upon a transitory look, I was surprised to see how little such tests are employed. @edi33416 could you please add tests as to cover the added code?

@@ -11343,13 +11344,48 @@ private extern (C++) final class ExpressionSemanticVisitor : Visitor
}
if (e.op == EXP.call)
{

if (t1.ty == Tclass && t2.ty == Tclass)
Copy link
Member

Choose a reason for hiding this comment

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

I think you also have to verify that the call is actually to Object.opCmp, as it can be redirected to other functions aswell, e.g.

class A
{
    int x;
    this(int a) { x = a; }
    
    alias opCmp = Object.opCmp;
    alias opCmp = my_cmp;
    
    final int my_cmp(A a)
    {
        return x - a.x;
    }
}

int main()
{
    auto a = new A(1);    
    return a < a;
} 

The comparison currently results in a non-virtual call to my_cmp. Does the lowered template do the same?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The lowered template doest the same. I don't think the lowering should change the current behaviour.
Why do you want the behaviour to be different?

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 want the behaviour to be different?

I don't want observable changes, and AFAICT the actual call stays the same - that's good. But the template adds some additional checks that are not there so far. This might duplicate null checks in existing custom implementations of opCmp, or it might work differently. The spec doesn't seem to cover the comparison to null. I'm not against adding the checks, just saying they should be added to the spec (similar to opEquals) if the change is deliberate.

BTW: dmd is unable to inline the __cmp template, it can if it is written as return lhs is rhs ? 0 : lhs is null ? -1 : rhs is null ? 1 : lhs.opCmp(rhs);. dmd doesn't eliminate unnecessary checks, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why do you want the behaviour to be different?

I don't want observable changes, and AFAICT the actual call stays the same - that's good. But the template adds some additional checks that are not there so far. This might duplicate null checks in existing custom implementations of opCmp, or it might work differently. The spec doesn't seem to cover the comparison to null. I'm not against adding the checks, just saying they should be added to the spec (similar to opEquals) if the change is deliberate.

The change is deliberate. I’ll update the spec to mention the changes.

BTW: dmd is unable to inline the __cmp template, it can if it is written as return lhs is rhs ? 0 : lhs is null ? -1 : rhs is null ? 1 : lhs.opCmp(rhs);. dmd doesn't eliminate unnecessary checks, though.

Hmm, rewritting it this way would make the code much harder to understand. Imho, we shouldn’t make the code harder to understand just so we improve dmd’s inlining. If this also affects ldc and gdc (haven’t checked yet) then we should modify the call.

I think the issue at hand is related to how dmd is able to inline such constructs. I think that should be a separate improvement done in dmd, as it will benefit other existing user code bases.

@RazvanN7
Copy link
Contributor

@rainers Is this good to go?

Copy link
Member

@rainers rainers left a comment

Choose a reason for hiding this comment

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

Still not very happy with replacing any call with the template (see ExpressionSemanticVisitor.visit(CallExp e) for what might happen to the expression), but the existing code assumes the same. So probably good enough.

BTW: it's a bit strange that comparison to null is supported by the template, but forbidden when explicit null is used (see error message a couple of lines above). Will this restriction be removed, too?

@12345swordy
Copy link
Contributor

Still not very happy with replacing any call with the template (see ExpressionSemanticVisitor.visit(CallExp e) for what might happen to the expression), but the existing code assumes the same. So probably good enough.

BTW: it's a bit strange that comparison to null is supported by the template, but forbidden when explicit null is used (see error message a couple of lines above). Will this restriction be removed, too?

I hope so, as the current restriction doesn't make any sense.

@RazvanN7 RazvanN7 merged commit 9bd87f7 into dlang:master Jan 3, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
72h no objection -> merge The PR will be merged if there are no objections raised.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants