eclipse-archived/ceylon

disallow or define semantics for "default late" values

Closed this issue · 30 comments

We know that for:

class C() {
    shared default String x = "C's value";
}

the specification = "C's value" is for C's x, and not a potential refinement of x. This can be demonstrated with the following test:

class D() extends C() {
    shared actual variable String x = "D's value";
    shared void test() {
        assert (super.x == "C's value");
    }
}

We also know that specifications (assignments) made separately from a declaration are polymorphic in nature:

class C() {
    shared default variable String x = "C's value";
    shared void update() {
        x = "newValue"; // C's x may still be "C's value"!
    }
}

This can also be demonstrated with the D test above.

However, for late values, initialization may be separated from the declaration, introducing an ambiguity for values that are also default:

class C() {
    shared default late String x;
    shared void initialize() {
        x = "C's value"; // Are we initializing C's x, or possibly assigning to
                         // a refinement of x? What if the refinement is or is not
                         // itself late or variable? What if we also mark C.x
                         // variable?

        print(x);        // This would certainly refer to a refinement
    }
}

The backends are inconsistent WRT their treatment of default late members.

I propose that default late be disallowed. Other options may be possible too (I debated them with myself here: jvasileff/ceylon-dart#11), but I'm not sure they would be worth the complexity and possibly surprising results.

So what is the proposal here? If I understand correctly:

  1. disallow default late, and
  2. (maybe) within the initializer of a class This, disallow assignment of this to a late attribute of the current class This.

Does that solve the problems here and in #4273?

Or is the proposal to:

  1. disallow assignment of this to default late, and ...

@jvasileff?

It's no more than:

I propose that default late be disallowed.

I don't see what this issue has to do with leaking of this (#4273).

The title change to "unsoundness with late values" is inconsistent with the complaint in this issue, which is:

However, for late values, initialization may be separated from the declaration, introducing an ambiguity for values that are also default:

The key is "ambiguity", not "unsoundness"

I don't see what this issue has to do with leaking of this (#4273).

Because if we're going to fix the unsoundness of late, I want to fix it all at once. And I want to see that we have a proposal that fixes all known problems.

@gavinking

I feel like #4273 is the most appropriate place for the discussion, then. This is something considerably different.

(The three issues problems are deeply connected, please note, in the sense that disallowing default late does most of the work of solving all of them.)

This is something considerably different.

Then how is it that the same change helps solve all of them?

Because if we're going to fix the unsoundness of late, I want to fix it all at once. And I want to see that we have a proposal that fixes all known problems.

Ok. If default late is disallowed, the point will be moot.

Then how is it that the same change helps solve all of them?

Disallowing default late would have no impact on leaking of this.

Then how is it that the same change helps solve all of them?

That's not true. The problem described in issue #4273 doesn't at all involve default late.

Look, if we're going to fight about stupid stuff, I'm just going to make the change that looks right to me. Otherwise, if we're going to discuss the proposed solution that is on the table above, we can discuss that.

Look, if we're going to fight about stupid stuff

I don't think anyone is fighting about anything. I'm just saying I don't think your proposed changes to fix this issue actually fix #4273. This issue is something different entirely.

I'm just saying I don't think your proposed changes to fix this issue actually fix #4273.

They don't? My proposed rule directly disallows the code in #4273! I wrote:

within the initializer of a class This, disallow assignment of this to a late attribute of the current class This.

class Foo()
{
    shared late Bar bar;
    shared void huh() => print(bar.s);
}

class Bar(Foo foo)
{
    foo.bar = this;
    foo.huh();
    shared String s = "";
}

shared void run() => Bar(Foo());

Either way, you proposed two changes, not one. Your first change is related to this issue, while your second change is related to #4273. They are different issues.

@jvasileff

The key is "ambiguity", not "unsoundness"

But—I'm not sure if you noticed—it's also unsound, because it allows an uninitialized value (a this) to be assigned to a setter function which does stuff with the this.

class Foo() {
     shared default late Bar bar;
}

class Bar(Foo foo) {
     foo.bar = this;
     shared String hello = "hello";
}

class Baz() extends Foo() {
    shared actual Bar bar => super.bar;
    assign bar { 
        super.bar = bar; 
        print(bar.hello);
    }
}

That's why all this stuff is connected. The problem is that late undermines all the guarantees about non-access to uninitialized this.

We can't really discuss any of these issues in isolation because there's almost no point solving any one of them if we don't solve all of them; the basic unsoundness due to the intersection of these two language features remains.

Alright, so here's the second rev of my proposal:

  1. disallow default late, and
  2. only allow assignment of this to a late (or even to a variable!) at the very end of the initializer of a final class.

That would guarantee that this is fully-initialized at the point of assignment.

WDYT?

My worry is it might be too heavy-handed.

Honestly I would prefer if uninitialized objects could leak via late, but that you would get an error if you accessed an uninitialized attribute. Unfortunately that imposes a much bigger cost on the runtime side.

I suppose that once a final class is fully initialized, any use of this is safe (am I missing anything?).

I suppose that once a final class is fully initialized, any use of this is safe

I suppose so too.

But the final restriction really is very heavy.

@gavinking

I think that both only allowing this to be assigned to late in a final class and disallowing default late to be too heavy-handed.

@gavinking

But the final restriction really is very heavy.

Yeah, we ended up cross-posting.

But the final restriction really is very heavy

This restriction has been on my mind for a very long time, and although I've struggled with initialization issues, I've never been inclined to assign this to a late attribute.

I would disallow all leaking of this, and wait for the complaints.

I have a complaint 😄:

class Element(shared {Element*} children) // can't be final (subtypes: `Div`, `Button`, `Span`, etc.)
{
    shared late Element? parent = null;
    
    for(value child in children)
    {
        child.parent = this;
    }
}

@Zambonifofex right. There's another issue complaining about that, opened by @sadmac7000 I believe. There are also techniques using lazy initializers.

I've never been inclined to assign this to a late attribute.

So I guess I take that back, if we define late as a silver bullet to solve initialization problems.

And I guess this is exactly why I have not been inclined to solve #4273 in the 3 years it has been open. There's probably no static-analysis based solution that isn't unacceptably restrictive.

This is a reasonable solution involving maintaining a $$isFullyInitialized$$ member for any classes which leak their this reference. But it gets a little hairy to implement in the face of inheritance.

Alright, so after reviewing the above discussion, I guess @jvasileff has more or less convinced me that it's worth outlawing default late even independently of #4273. So I've done that. I'll close this issue and reopen the ancient issue #4273.