Skip to content

Fix Issue 8006: Property binary assignment operators#7079

Closed
JinShil wants to merge 1 commit intodlang:masterfrom
JinShil:master
Closed

Fix Issue 8006: Property binary assignment operators#7079
JinShil wants to merge 1 commit intodlang:masterfrom
JinShil:master

Conversation

@JinShil
Copy link
Contributor

@JinShil JinShil commented Aug 11, 2017

FYI: This does not address unary assignment operators (e.g. s.x++, ++s.x). I intend to implement that in a future pull request.

DIP PR: dlang/DIPs#97
Spec change: dlang/dlang.org#1865

@dlang-bot
Copy link
Contributor

dlang-bot commented Aug 11, 2017

Thanks for your pull request, @JinShil!

Bugzilla references

Auto-close Bugzilla Severity Description
8006 enhancement Implement proper in-place-modification for properties

@JinShil
Copy link
Contributor Author

JinShil commented Aug 11, 2017

Appears to also fix Issue 16187

@JinShil JinShil changed the title Fix Issue 8006: Property binary assignment operators (Part 1) Fix Issue 8006: Property binary assignment operators Aug 11, 2017
assert(false, "Binary assignment operator was not handled.");
}

if (Expression ex = resolvePropertiesX(sc, e1, e2))
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, the backend should already handle lowering a += b into a = a + b.

Maybe all you need is the call to resolvePropertiesX() ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

resolvePropertiesX lowers e1 = e2 into e1(e2) if e1 is a property, so I need e2 to be e1 + e2 for that to work.

Currently DMD errors with e1() is not an lvalue, so, if e1 += e2 is lowered to e1 = e1 + e2 in the backend, maybe I just need to figure out how to get past that error so the expression makes it to the backend. But are you sure the backed is doing the lowering?

Copy link
Member

Choose a reason for hiding this comment

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

Actually, yes, I see the reasoning behind this. e1 = e1 + e2 won't cut the muster, as it will assign the result to a temporary. You need it to be e1(e1 + e2)

Copy link
Member

Choose a reason for hiding this comment

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

Giving this a quick test with gdc, it looks like resolvePropertiesX is changing the return type of ti.x() from int to void.
You may need to store the rhs in a temporary here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @ibuclaw. I've copied the rhs to a temporary and refactored in an attempt to consolidate code.

{
// Consider s.x += 1;
// We want to rewrite that expression to s.x(s.x() + 1);
// However, resolvePropertiesX will change the type of s.x() to void,
Copy link
Member

Choose a reason for hiding this comment

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

Thanks. Looking at this after sleeping on it, I sense that what I am see is infact memory that is being shared between both s.x() and s.x(1) calls. When the latter gets rewritten, it changes the type of the former also (oops!).

I'm just having a look now. I think this is close to being where it should be anyway, just a few nits.

* the rewritten expression if the procedure succeeds, an `ErrorExp` if the
* and error is encountered, or `null` if `e.e1` is not a `DotIdExp`.
*/
Expression binAssignRewriteProp(BinAssignExp e, Scope* sc)
Copy link
Member

Choose a reason for hiding this comment

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

This could be renamed to binSemanticProp, then you don't have to call this explicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice call! That made the code just that much more concise.

{
// https://issues.dlang.org/show_bug.cgi?id=8006

if (e.e1.op == TOKdotid)
Copy link
Member

Choose a reason for hiding this comment

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

Maybe its better to complete semantic on e1 first, then check that e1.op == TOKcall and ce.e1.type.isproperty.

Copy link
Contributor Author

@JinShil JinShil Aug 12, 2017

Choose a reason for hiding this comment

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

I'm not sure what you mean by ce. isproperty is only a field in TypeFunction, and I'm not sure how to get that. I tried a number of different things (e.g. casting to TypeFunction), but they all resulted in a segfault.

Copy link
Member

@ibuclaw ibuclaw Aug 12, 2017

Choose a reason for hiding this comment

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

After playing around, this is what I ended up with.
https://pastebin.com/iUXCpeju

Whether it is more correct than what you are doing here, I may have to leave someone else to be the judge of.

auto v = copyToTemp(0, "__tmp", e.e1);
auto de = new DeclarationExp(e.e1.loc, v);
auto ve = new VarExp(e.e1.loc, v);
Expression ea = Expression.combine(de, ve);
Copy link
Member

Choose a reason for hiding this comment

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

If you pass a copy of e1 to resolvePropertiesX, then this shouldn't be needed. Codegen looks better at least without this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do you mean by "this"? Do you mean I don't need to copy to a temporary?

@JinShil
Copy link
Contributor Author

JinShil commented Aug 12, 2017

It appears a recent merge may have broken this :(
Update: Nope I broke something somewhere.

@ibuclaw
Copy link
Member

ibuclaw commented Aug 12, 2017

It appears a recent merge may have broken this :(
Update: Nope I broke something somewhere.

I was about to say, I couldn't see anything wrong with the diff, and running it locally passes for me.

Then again, maybe you updated it just before I looked. :-)


// Consider s.x() += 1;
// We want to rewrite that expression to s.x(s.x() + 1);
if (ce.e1.type.ty == Tfunction && (cast(TypeFunction)ce.e1.type).isproperty)
Copy link
Member

Choose a reason for hiding this comment

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

Actually, I think it's best to have this check as:

auto tf = (ce.e1.type.ty) == Tfunction ? cast(TypeFunction)ce.e1.type : null;
if (tf && tf.isproperty && !tf.isref)

So that ref @property functions are not rewritten.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, that seems to work. I'll try to formulate a test case for that as well. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

@JinShil
Copy link
Contributor Author

JinShil commented Aug 12, 2017

I think I need the return type of s.x(s.x() + 1) to be the same as s.x(). Consider writeln(s.x += 1). If that gets rewritten as writeln(s.x(s.x() + 1)), it should print a number. At the moment s.x(s.x() + 1) appears to have a return type of void. Go back to copying rhs to a temporary? It seems to work.

@JinShil
Copy link
Contributor Author

JinShil commented Aug 12, 2017

At the moment s.x(s.x() + 1) appears to have a return type of void.

Though that is consistent with @property void x(int v) { mX = v; }. Perhaps the compiler should require the user to be explicit with @property int x(int v) { mX = v; return mX } if they want s.x += 1 to return a value.

@JinShil
Copy link
Contributor Author

JinShil commented Aug 13, 2017

Perhaps the compiler should require the user to be explicit with @property int x(int v) { mX = v; return mX } if they want s.x += 1 to return a value.

Added tests to ensure that if a setter @property returns a value, the resulting s.x(s.x() + 1) expression also returns a value. User will have to explicitly code this if they want that behavior.

@JinShil
Copy link
Contributor Author

JinShil commented Aug 13, 2017

Added tests for alias thised properties. I suspect making the function an overload of binSemanticProp allowed this logic to insert itself into all existing pipelines, so alias thised properties work out of the box. 😎

@JinShil
Copy link
Contributor Author

JinShil commented Aug 13, 2017

Thanks to @ibuclaw, this turned out much better than I could have hoped. I thought I was going to have to split this into a couple of different pull requests, and handle a lot of special casing, but this turned out highly cohesive, comprehensive, and surprisingly simple.

I think this is good to go.

}

/********************************************************************************
* Helper function to consolidate common code for `BinAssingExp`s.
Copy link
Member

Choose a reason for hiding this comment

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

Maybe:

Helper function to resolve @property functions in a BinAssignExp.

Also, spelling mistake in BinAssignExp

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

}

// s.x(e2x)
return resolvePropertiesX(sc, e.e1.copy(), e2x);
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps the result should be checked here, and only return if not null.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed. Done.

/*
* TEST_OUTPUT:
---
fail_compilation/test8006.d(48): Error: function test8006.TInt.x () is not callable using argument types (int)
Copy link
Member

Choose a reason for hiding this comment

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

It might be worth investigating whether we could improve the fail message here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. I looked into this and found that the error messages really need to be improved for all usages of properties, not just for the binary assignments in this pull request.

The error message situation is broader than the scope of this pull request. Simple property assignments are currently implemented and produce the same error message as above. So this pull request maintains the status quo.

The following scenarios all produce different error messages, none of which read specifically to syntax of property usage.

  • Attempting to set without a setter
  • Attempting to get without a getter
  • Attempting to set without a setter through alias this
  • Attempting to get without a getter through alias this
  • Attempting to set without a setter through static alias this
  • Attempting to get without a getter through static alias this
  • Attempting to set a property to the wrong type
  • Attempting to set a property to the wrong type through alias this
  • Attempting to set a property to the wrong type through static alias this
  • etc...

So, instead of improving the error messages in this pull request, I suggest accepting this pull request (maintaining the status quo) and improve the error messages for properties more comprehensively in a separate issue and pull request.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

... improve the error messages for properties more comprehensively in a separate issue and pull request.

Issue 5010

@ibuclaw ibuclaw requested a review from WalterBright August 13, 2017 07:55
@ibuclaw
Copy link
Member

ibuclaw commented Aug 13, 2017

I'd like to have the last say made by @WalterBright, incase there's something I might have missed.

@WalterBright
Copy link
Member

Are we really sure we want to do this? This is a significant language change with essentially no discussion about its merits.


/********************************************************************************
* Helper function to resolve `@property` functions in a `BinAssignExp`.
* It rewrites expressions of the form, for example, `s.x += 1` to `s.x(s.x() += 1)`
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 mean s.x(s.x() + 1)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Thanks!

// Consider s.x() += 1;
// We want to rewrite that expression to s.x(s.x() + 1);

// Only rewrite @property functions. Don't rewrite @ref @property functions.
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 ref?

Copy link
Contributor Author

@JinShil JinShil Aug 14, 2017

Choose a reason for hiding this comment

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

First of all, this already works today:

struct TInt
{
    int mX;

    ref @property int x()
    {
        return mX;
    }

    alias x this;
}

void main()
{
    TInt t;

    t.x += 4;
    assert(t.mX == 4);
    t.x -= 2;
    assert(t.mX == 2);
    t.x *= 4;
    assert(t.mX == 8);
    t.x /= 2;
    assert(t.mX == 4);
    t.x %= 3;
    assert(t.mX == 1);
    t.x <<= 3;
    assert(t.mX == 8);
    t.x >>= 1;
    assert(t.mX == 4);
    t.x >>>= 1;
    assert(t.mX == 2);
    t.x &= 0xF;
    assert(t.mX == 0x2);
    t.x |= 0x8;
    assert(t.mX == 0xA);
    t.x ^= 0xF;
    assert(t.mX == 0x5);
    t.x ^^= 2;
    assert(t.mX == 25);

    // same as test above, but through the `alias this`
    t = 0;
    t += 4;
    assert(t.mX == 4);
    t -= 2;
    assert(t.mX == 2);
    t *= 4;
    assert(t.mX == 8);
    t /= 2;
    assert(t.mX == 4);
    t %= 3;
    assert(t.mX == 1);
    t <<= 3;
    assert(t.mX == 8);
    t >>= 1;
    assert(t.mX == 4);
    t >>>= 1;
    assert(t.mX == 2);
    t &= 0xF;
    assert(t.mX == 0x2);
    t |= 0x8;
    assert(t.mX == 0xA);
    t ^= 0xF;
    assert(t.mX == 0x5);
    t ^^= 2;
    assert(t.mX == 25);
}

But more importantly, we found that we broke code in DUB when not excluding ref @property functions. The specific idiom that broke was added to the test suite here.

So ref @property functions were excluded in an effort to be conservative with this change, and avoid code breakage.

Copy link
Member

Choose a reason for hiding this comment

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

ok. Please add a summary of that explanation as a comment in the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please add a summary of that explanation as a comment in the code.

Done.

Copy link
Contributor

Choose a reason for hiding this comment

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

If there's a getter annotated with ref and a setter. If there's no rewrite for ref functions the setter will be bypassed.

Copy link
Contributor

Choose a reason for hiding this comment

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

You should add tests that specifically test which functions are and are not called (i.e. besides testing that the calculation is performed correctly, also test that the getter/setter is/isn't called), both for the non-ref and ref case.

e2x = new UshrExp(e.loc, e1x, e2x);
break;
default:
assert(false, "Binary assignment operator was not handled.");
Copy link
Member

Choose a reason for hiding this comment

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

Some people like assert messages, I don't. They bloat up the compiler. Leaving the message as a comment in the source code is plenty good enough.

Copy link
Member

Choose a reason for hiding this comment

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

In fact, you can just make the switch final and do away with the assert entirely!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In fact, you can just make the switch final and do away with the assert entirely!

I can't do that because the switch doesn't cover all possible Tokens.

Leaving the message as a comment in the source code is plenty good enough.

Done.

@WalterBright
Copy link
Member

The implementation itself does look nice.

e2x = new ShrExp(e.loc, e1x, e2x);
break;
case TOKushrass:
e2x = new UshrExp(e.loc, e1x, e2x);
Copy link
Member

Choose a reason for hiding this comment

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

Another thing that sprung to mind, you'll have to assign the rewrite to another variable. If resolvePropertiesX doesn't return a value for whatever reason, you don't want to assign it to the original binassignexp.

Think s.x += 1 being accidentally turned into s.x += s.x + 1

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It took me a little while to see what you meant. but I got it. Nice catch! Thanks.

@JinShil
Copy link
Contributor Author

JinShil commented Aug 14, 2017

Are we really sure we want to do this? This is a significant language change with essentially no discussion about its merits.

Honestly, it never occured to me that this would not be desired. Like the other users who commented in Issue 8006 and Issue 16187, I was a bit dismayed to find out this wasn't implemented. I was operating under the assumption that the only reason binary assignments didn't work with @property functions was because noone had gotten around to implementing them yet.

I'm willing to follow whatever process you'd like to reach a resolution. Would you like me to write a justification in this pull request, make a forum post, write a DIP?

@WalterBright
Copy link
Member

What would be best is a forum post with links to this PR and the bugzilla issues. Also, a spec PR would be needed.

BTW, I do think this is looking good.

@JinShil
Copy link
Contributor Author

JinShil commented Oct 28, 2017

You can see the change that introduced it in #5636.

Thanks! I see the utility of those functions, but what is @MartinNowak suggesting the lowering be changed to?

@JinShil
Copy link
Contributor Author

JinShil commented Nov 5, 2017

This has been discussed extensively. It's just been a few years since then and I don't even remember the conclusion anymore ;-)

That's kind of a problem, and I think the "best of" the disparate discussions should be distilled into a DIP so we all know what we're talking about.

After reviewing the DIPs and their discussions, it appears that the property debacle with UFCS, optional parens, etc... has indeed been discussed extensively, but this specific issue of binary assignment operators for properties has not. The only serious mentions seem to be the prowiki article, and DIP23.

Nevertheless, a DIP PR has been submitted at dlang/DIPs#97. At present it's still a work-in-progress, but it's coming together.

@ibuclaw
Copy link
Member

ibuclaw commented Nov 5, 2017

Thanks @JinShil. This is great stuff.

@JinShil
Copy link
Contributor Author

JinShil commented Nov 6, 2017

@JinShil
Copy link
Contributor Author

JinShil commented Nov 6, 2017

I need some help. I want to ensure that the getter and setter both contain an @property attribute. If I only have the getter, how can I get the TypeFunction of the setter?

@ibuclaw
Copy link
Member

ibuclaw commented Nov 7, 2017

If there's only a getter, wouldn't that be a readonly property?

@JinShil
Copy link
Contributor Author

JinShil commented Nov 7, 2017

If there's only a getter, wouldn't that be a readonly property?

Yes, but that's not what mean. Consider the expression e1.prop @= e2. In the binSemanticProp function I have e1.prop and e2 as two separate Expression objects. After running resolveProperties on e1.prop I get the getter for prop as a CallExp. From that CallExp I can get its TypeFunction. But how do I get the setter from e1.prop (assuming a setter may exist) and subsequently its TypeFunction?

https://github.com/dlang/phobos/blob/225ea95397f943944b2081a34d11054884085d27/std/typecons.d#L2570-L2573

Should that really be a @Property?

If there's only a getter, wouldn't that be a readonly property?

I just realized you may have been referring to the question above. The problem with that function is it has the signature of a setter (i.e. takes one argument), but is actually a getter. That is causing the algorithm in this PR to use it as a setter. The function can never be called with property syntax (e.g. no parens) so I question whether it should be marked with the @property attribute in the first place. Removing the @property attribute would allow the tests to pass.

@JinShil
Copy link
Contributor Author

JinShil commented Nov 8, 2017

I need some help. I want to ensure that the getter and setter both contain an @Property attribute. If I only have the getter, how can I get the TypeFunction of the setter?

I came up with this. I doubt it'll pass review, but it works. The meat and potatoes of the DIP is now implemented and tests are passing. I just need to redo the tests and finalize the DIP.

@JinShil
Copy link
Contributor Author

JinShil commented Nov 9, 2017

I came up with this. I doubt it'll pass review, but it works. The meat and potatoes of the DIP is now implemented and tests are passing. I just need to redo the tests and finalize the DIP.

Turned out to be moot anyway as @property function and ordinary function overloads can't coexist.

@JinShil
Copy link
Contributor Author

JinShil commented Nov 10, 2017

The implementation now matches that which is proposed in the DIP.

@ibuclaw
Copy link
Member

ibuclaw commented Jan 3, 2018

@WalterBright @andralex - are we progressing this? Is there a card for this in trello?

@JinShil
Copy link
Contributor Author

JinShil commented Jan 4, 2018

are we progressing this?

According to a comment from @mdparker on Nov 14th, 2017 we are waiting on changes to the DIP process.

@WalterBright and @andralex Can you please give @mdparker whatever support he needs to unplug the DIP queue?

Is there a card for this in trello?

I tried to add one, but I guess I don't have permission on the Trello board. I added a card here, though that probably won't do much if we don't all agree to use that GitHub feature.

@wilzbach
Copy link
Contributor

wilzbach commented Jan 4, 2018

I tried to add one, but I guess I don't have permission on the Trello board. I added a card here, though that probably won't do much if we don't all agree to use that GitHub feature.

Sent out an invitation to you, but the Trello board isn't very active and only used by Martin, so it's probably better to give the GitHub board a try.

@mdparker
Copy link
Member

mdparker commented Jan 4, 2018

@JinShil The hold up right now is me. I haven't finished revising the documentation yet. I'm almost there, though.

@JinShil
Copy link
Contributor Author

JinShil commented Nov 23, 2018

FYI. I am abandoning this implementation and the DIP. I'm afraid my future with D is uncertain at best. If anyone wants to pick of the torch, it's yours. I'll keep the PRs open for now unless someone believes they should be closed.

@12345swordy
Copy link
Contributor

cc @thewilsonator
I am interested in picking up the dip where the JinShill have left off. However I am not a compile writer so I can't guarantee to managed this pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.